Merge pull request 'I18N support' (#22) from feat-i18n into master
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				/ depoly (push) Successful in 1m12s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	/ depoly (push) Successful in 1m12s
				
			Reviewed-on: https://code.lightstands.xyz///Rubicon/tutu/pulls/22
This commit is contained in:
		
						commit
						69f7f37a2c
					
				
					 15 changed files with 638 additions and 44 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -27,9 +27,11 @@
 | 
				
			||||||
    "wrangler": "^3.78.2"
 | 
					    "wrangler": "^3.78.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@formatjs/intl-localematcher": "^0.5.4",
 | 
				
			||||||
    "@nanostores/persistent": "^0.10.2",
 | 
					    "@nanostores/persistent": "^0.10.2",
 | 
				
			||||||
    "@nanostores/solid": "^0.4.2",
 | 
					    "@nanostores/solid": "^0.4.2",
 | 
				
			||||||
    "@solid-primitives/event-listener": "^2.3.3",
 | 
					    "@solid-primitives/event-listener": "^2.3.3",
 | 
				
			||||||
 | 
					    "@solid-primitives/i18n": "^2.1.1",
 | 
				
			||||||
    "@solid-primitives/intersection-observer": "^2.1.6",
 | 
					    "@solid-primitives/intersection-observer": "^2.1.6",
 | 
				
			||||||
    "@solid-primitives/resize-observer": "^2.0.26",
 | 
					    "@solid-primitives/resize-observer": "^2.0.26",
 | 
				
			||||||
    "@solidjs/router": "^0.14.5",
 | 
					    "@solidjs/router": "^0.14.5",
 | 
				
			||||||
| 
						 | 
					@ -40,6 +42,7 @@
 | 
				
			||||||
    "date-fns": "^3.6.0",
 | 
					    "date-fns": "^3.6.0",
 | 
				
			||||||
    "fast-average-color": "^9.4.0",
 | 
					    "fast-average-color": "^9.4.0",
 | 
				
			||||||
    "hammerjs": "^2.0.8",
 | 
					    "hammerjs": "^2.0.8",
 | 
				
			||||||
 | 
					    "iso-639-1": "^3.1.3",
 | 
				
			||||||
    "masto": "^6.8.0",
 | 
					    "masto": "^6.8.0",
 | 
				
			||||||
    "nanostores": "^0.11.3",
 | 
					    "nanostores": "^0.11.3",
 | 
				
			||||||
    "solid-js": "^1.8.22",
 | 
					    "solid-js": "^1.8.22",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/App.tsx
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								src/App.tsx
									
										
									
									
									
								
							| 
						 | 
					@ -6,6 +6,7 @@ import {
 | 
				
			||||||
  createSignal,
 | 
					  createSignal,
 | 
				
			||||||
  ErrorBoundary,
 | 
					  ErrorBoundary,
 | 
				
			||||||
  lazy,
 | 
					  lazy,
 | 
				
			||||||
 | 
					  onCleanup,
 | 
				
			||||||
} from "solid-js";
 | 
					} from "solid-js";
 | 
				
			||||||
import { useRootTheme } from "./material/mui.js";
 | 
					import { useRootTheme } from "./material/mui.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -15,6 +16,7 @@ import {
 | 
				
			||||||
} from "./masto/clients.js";
 | 
					} from "./masto/clients.js";
 | 
				
			||||||
import { $accounts } from "./accounts/stores.js";
 | 
					import { $accounts } from "./accounts/stores.js";
 | 
				
			||||||
import { useStore } from "@nanostores/solid";
 | 
					import { useStore } from "@nanostores/solid";
 | 
				
			||||||
 | 
					import { DateFnScope, useLanguage } from "./platform/i18n.jsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
 | 
					const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
 | 
				
			||||||
const AccountMastodonOAuth2Callback = lazy(
 | 
					const AccountMastodonOAuth2Callback = lazy(
 | 
				
			||||||
| 
						 | 
					@ -47,6 +49,7 @@ const App: Component = () => {
 | 
				
			||||||
  const theme = useRootTheme();
 | 
					  const theme = useRootTheme();
 | 
				
			||||||
  const accts = useStore($accounts);
 | 
					  const accts = useStore($accounts);
 | 
				
			||||||
  const clientStore = createSignal<Session[]>([]);
 | 
					  const clientStore = createSignal<Session[]>([]);
 | 
				
			||||||
 | 
					  const lang = useLanguage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createRenderEffect(() => {
 | 
					  createRenderEffect(() => {
 | 
				
			||||||
    const [, setClients] = clientStore;
 | 
					    const [, setClients] = clientStore;
 | 
				
			||||||
| 
						 | 
					@ -55,6 +58,16 @@ const App: Component = () => {
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  createRenderEffect(() => {
 | 
				
			||||||
 | 
					    const root = document.querySelector(":root")!;
 | 
				
			||||||
 | 
					    root.setAttribute("lang", lang());
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onCleanup(() => {
 | 
				
			||||||
 | 
					    const root = document.querySelector(":root")!;
 | 
				
			||||||
 | 
					    root.removeAttribute("lang");
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
 | 
					  const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
| 
						 | 
					@ -65,9 +78,11 @@ const App: Component = () => {
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <ThemeProvider theme={theme()}>
 | 
					      <ThemeProvider theme={theme()}>
 | 
				
			||||||
        <ClientProvider value={clientStore}>
 | 
					        <DateFnScope>
 | 
				
			||||||
          <Routing />
 | 
					          <ClientProvider value={clientStore}>
 | 
				
			||||||
        </ClientProvider>
 | 
					            <Routing />
 | 
				
			||||||
 | 
					          </ClientProvider>
 | 
				
			||||||
 | 
					        </DateFnScope>
 | 
				
			||||||
      </ThemeProvider>
 | 
					      </ThemeProvider>
 | 
				
			||||||
    </ErrorBoundary>
 | 
					    </ErrorBoundary>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,11 +12,16 @@ export function useSignedInProfiles() {
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  return [
 | 
					  return [
 | 
				
			||||||
    () => {
 | 
					    () => {
 | 
				
			||||||
      const value = accessor();
 | 
					      try {
 | 
				
			||||||
      if (!value) {
 | 
					        const value = accessor();
 | 
				
			||||||
        return sessions().map((x) => ({ ...x, inf: x.account.inf }));
 | 
					        if (value) {
 | 
				
			||||||
 | 
					          return value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (reason) {
 | 
				
			||||||
 | 
					        console.error("useSignedInProfiles: update acct info failed", reason);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return value;
 | 
					
 | 
				
			||||||
 | 
					      return sessions().map((x) => ({ ...x, inf: x.account.inf }));
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    tools,
 | 
					    tools,
 | 
				
			||||||
  ] as const;
 | 
					  ] as const;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										188
									
								
								src/platform/i18n.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/platform/i18n.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,188 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ParentComponent,
 | 
				
			||||||
 | 
					  createContext,
 | 
				
			||||||
 | 
					  createMemo,
 | 
				
			||||||
 | 
					  createResource,
 | 
				
			||||||
 | 
					  useContext,
 | 
				
			||||||
 | 
					} from "solid-js";
 | 
				
			||||||
 | 
					import { match } from "@formatjs/intl-localematcher";
 | 
				
			||||||
 | 
					import { Accessor, createEffect, createSignal } from "solid-js";
 | 
				
			||||||
 | 
					import { $settings } from "../settings/stores";
 | 
				
			||||||
 | 
					import { enGB } from "date-fns/locale/en-GB";
 | 
				
			||||||
 | 
					import { useStore } from "@nanostores/solid";
 | 
				
			||||||
 | 
					import type { Locale } from "date-fns";
 | 
				
			||||||
 | 
					import { resolveTemplate, translator, type Template } from "@solid-primitives/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function synchronised(
 | 
				
			||||||
 | 
					  name: string,
 | 
				
			||||||
 | 
					  callback: () => Promise<void> | void,
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
 | 
					  await navigator.locks.request(name, callback);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SUPPORTED_LANGS = ["en", "zh-Hans"] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DEFAULT_LANG = "en";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Decide the using language for the user.
 | 
				
			||||||
 | 
					 * @returns the selected language tag
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function autoMatchLangTag() {
 | 
				
			||||||
 | 
					  return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DateFnLocaleCx = /* __@PURE__ */createContext<Accessor<Locale>>(() => enGB);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const cachedDateFnLocale: Record<string, Locale> = {
 | 
				
			||||||
 | 
					  enGB,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function autoMatchRegion() {
 | 
				
			||||||
 | 
					  const regions = navigator.languages
 | 
				
			||||||
 | 
					    .map((x) => {
 | 
				
			||||||
 | 
					      const parts = x.split("_");
 | 
				
			||||||
 | 
					      if (parts.length > 1) {
 | 
				
			||||||
 | 
					        return parts[1];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return undefined;
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .filter((x): x is string => !!x);
 | 
				
			||||||
 | 
					  for (const r of regions) {
 | 
				
			||||||
 | 
					    for (const available of SUPPORTED_REGIONS) {
 | 
				
			||||||
 | 
					      if (available.toLowerCase().endsWith(r.toLowerCase())) {
 | 
				
			||||||
 | 
					        return available;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return "en_GB";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useRegion() {
 | 
				
			||||||
 | 
					  const appSettings = useStore($settings);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return createMemo(() => {
 | 
				
			||||||
 | 
					    const settings = appSettings();
 | 
				
			||||||
 | 
					    if (typeof settings.region !== "undefined") {
 | 
				
			||||||
 | 
					      return settings.region;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return autoMatchRegion();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function importDateFnLocale(tag: string): Promise<Locale> {
 | 
				
			||||||
 | 
					  switch (tag.toLowerCase()) {
 | 
				
			||||||
 | 
					    case "en_us":
 | 
				
			||||||
 | 
					      return (await import("date-fns/locale/en-US")).enUS;
 | 
				
			||||||
 | 
					    case "en_gb":
 | 
				
			||||||
 | 
					      return (await import("date-fns/locale/en-GB")).enGB;
 | 
				
			||||||
 | 
					    case "zh_cn":
 | 
				
			||||||
 | 
					      return (await import("date-fns/locale/zh-CN")).zhCN;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      throw new TypeError(`unsupported tag "${tag}"`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Provides runtime values and fetch dependencies for date-fns locale
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const DateFnScope: ParentComponent = (props) => {
 | 
				
			||||||
 | 
					  const [dateFnLocale, setDateFnLocale] = createSignal(enGB);
 | 
				
			||||||
 | 
					  const region = useRegion();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  createEffect(() => {
 | 
				
			||||||
 | 
					    const dateFnLocaleName = region();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (cachedDateFnLocale[dateFnLocaleName]) {
 | 
				
			||||||
 | 
					      setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      synchronised("i18n-wrapper-load-date-fns-locale", async () => {
 | 
				
			||||||
 | 
					        if (cachedDateFnLocale[dateFnLocaleName]) {
 | 
				
			||||||
 | 
					          setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const target = `date-fns/locale/${dateFnLocaleName}`;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const mod = await importDateFnLocale(dateFnLocaleName);
 | 
				
			||||||
 | 
					          cachedDateFnLocale[dateFnLocaleName] = mod;
 | 
				
			||||||
 | 
					          setDateFnLocale(mod);
 | 
				
			||||||
 | 
					        } catch (reason) {
 | 
				
			||||||
 | 
					          console.error(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              act: "load-date-fns-locale",
 | 
				
			||||||
 | 
					              stat: "failed",
 | 
				
			||||||
 | 
					              reason,
 | 
				
			||||||
 | 
					              target,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "failed to load date-fns locale",
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <DateFnLocaleCx.Provider value={dateFnLocale}>
 | 
				
			||||||
 | 
					      {props.children}
 | 
				
			||||||
 | 
					    </DateFnLocaleCx.Provider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get the {@link Locale} object for date-fns.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This function must be using in {@link DateFnScope}
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @returns Accessor for Locale
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function useDateFnLocale(): Accessor<Locale> {
 | 
				
			||||||
 | 
					  const cx = useContext(DateFnLocaleCx);
 | 
				
			||||||
 | 
					  return cx;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useLanguage() {
 | 
				
			||||||
 | 
					  const settings = useStore($settings);
 | 
				
			||||||
 | 
					  return () => settings().language || autoMatchLangTag();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ImportFn<T> = (name: string) => Promise<{default: T}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ImportedModule<F> = F extends ImportFn<infer T> ? T: never
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MergedImportedModule<T> =
 | 
				
			||||||
 | 
					  T extends [] ? {} :
 | 
				
			||||||
 | 
					  T extends [infer I] ? ImportedModule<I> :
 | 
				
			||||||
 | 
					  T extends [infer I, ...infer J] ? ImportedModule<I> & MergedImportedModule<J> : never
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createStringResource<
 | 
				
			||||||
 | 
					  T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
 | 
				
			||||||
 | 
					>(...importFns: T) {
 | 
				
			||||||
 | 
					  const language = useLanguage();
 | 
				
			||||||
 | 
					  const cache: Record<string, MergedImportedModule<T>> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return createResource(
 | 
				
			||||||
 | 
					    () => [language()] as const,
 | 
				
			||||||
 | 
					    async ([nlang]) => {
 | 
				
			||||||
 | 
					      if (cache[nlang]) {
 | 
				
			||||||
 | 
					        return cache[nlang];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const results = await Promise.all(importFns.map(x => x(nlang).then(v => v.default)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const merged: MergedImportedModule<T> = Object.assign({}, ...results)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      cache[nlang] = merged;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return merged;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createTranslator<T extends ImportFn<Record<string, string | Template<any> | undefined>>[],>(...importFns: T) {
 | 
				
			||||||
 | 
					  const res = createStringResource(...importFns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [translator(res[0], resolveTemplate), res] as const
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										116
									
								
								src/settings/ChooseLang.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/settings/ChooseLang.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,116 @@
 | 
				
			||||||
 | 
					import { createMemo, For, type Component, type JSX } from "solid-js";
 | 
				
			||||||
 | 
					import Scaffold from "../material/Scaffold";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AppBar,
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					  List,
 | 
				
			||||||
 | 
					  ListItem,
 | 
				
			||||||
 | 
					  ListItemButton,
 | 
				
			||||||
 | 
					  ListItemSecondaryAction,
 | 
				
			||||||
 | 
					  ListItemText,
 | 
				
			||||||
 | 
					  ListSubheader,
 | 
				
			||||||
 | 
					  Radio,
 | 
				
			||||||
 | 
					  Switch,
 | 
				
			||||||
 | 
					  Toolbar,
 | 
				
			||||||
 | 
					} from "@suid/material";
 | 
				
			||||||
 | 
					import { Close as CloseIcon } from "@suid/icons-material";
 | 
				
			||||||
 | 
					import iso639_1 from "iso-639-1";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  autoMatchLangTag,
 | 
				
			||||||
 | 
					  createTranslator,
 | 
				
			||||||
 | 
					  SUPPORTED_LANGS,
 | 
				
			||||||
 | 
					} from "../platform/i18n";
 | 
				
			||||||
 | 
					import { Title } from "../material/typography";
 | 
				
			||||||
 | 
					import type { Template } from "@solid-primitives/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChooseLangProps = {
 | 
				
			||||||
 | 
					  code?: string;
 | 
				
			||||||
 | 
					  onCodeChange: (ncode?: string) => void;
 | 
				
			||||||
 | 
					  onClose?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ChooseLang: Component<ChooseLangProps> = (props) => {
 | 
				
			||||||
 | 
					  const [t] = createTranslator(
 | 
				
			||||||
 | 
					    () => import("./i18n/lang-names.json"),
 | 
				
			||||||
 | 
					    (code) =>
 | 
				
			||||||
 | 
					      import(`./i18n/${code}.json`) as Promise<{
 | 
				
			||||||
 | 
					        default: Record<string, string | undefined> & {
 | 
				
			||||||
 | 
					          ["lang.auto"]: Template<{ detected: string }>;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }>,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const unsupportedLangCodes = createMemo(() => {
 | 
				
			||||||
 | 
					    return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const matchedLangCode = createMemo(() => autoMatchLangTag());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Scaffold
 | 
				
			||||||
 | 
					      topbar={
 | 
				
			||||||
 | 
					        <AppBar position="static">
 | 
				
			||||||
 | 
					          <Toolbar
 | 
				
			||||||
 | 
					            variant="dense"
 | 
				
			||||||
 | 
					            sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <IconButton color="inherit" onClick={props.onClose} disableRipple>
 | 
				
			||||||
 | 
					              <CloseIcon />
 | 
				
			||||||
 | 
					            </IconButton>
 | 
				
			||||||
 | 
					            <Title>{t("Choose Language")}</Title>
 | 
				
			||||||
 | 
					          </Toolbar>
 | 
				
			||||||
 | 
					        </AppBar>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <List
 | 
				
			||||||
 | 
					        sx={{
 | 
				
			||||||
 | 
					          paddingBottom: "var(--safe-area-inset-bottom, 0)",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ListItemButton
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            props.onCodeChange(props.code ? undefined : matchedLangCode());
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ListItemText>
 | 
				
			||||||
 | 
					            {t("lang.auto", {
 | 
				
			||||||
 | 
					              detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(),
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
 | 
					          </ListItemText>
 | 
				
			||||||
 | 
					          <ListItemSecondaryAction>
 | 
				
			||||||
 | 
					            <Switch checked={typeof props.code === "undefined"} />
 | 
				
			||||||
 | 
					          </ListItemSecondaryAction>
 | 
				
			||||||
 | 
					        </ListItemButton>
 | 
				
			||||||
 | 
					        <List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
 | 
				
			||||||
 | 
					          <For each={SUPPORTED_LANGS}>
 | 
				
			||||||
 | 
					            {(code) => (
 | 
				
			||||||
 | 
					              <ListItemButton
 | 
				
			||||||
 | 
					                disabled={typeof props.code === "undefined"}
 | 
				
			||||||
 | 
					                onClick={[props.onCodeChange, code]}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <ListItemText>{t(`lang.${code}`)}</ListItemText>
 | 
				
			||||||
 | 
					                <ListItemSecondaryAction>
 | 
				
			||||||
 | 
					                  <Radio
 | 
				
			||||||
 | 
					                    checked={props.code === code || (props.code === undefined && matchedLangCode() == code)}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </ListItemSecondaryAction>
 | 
				
			||||||
 | 
					              </ListItemButton>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </For>
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}>
 | 
				
			||||||
 | 
					          <For each={unsupportedLangCodes()}>
 | 
				
			||||||
 | 
					            {(code) => (
 | 
				
			||||||
 | 
					              <ListItem>
 | 
				
			||||||
 | 
					                <ListItemText>{iso639_1.getNativeName(code)}</ListItemText>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </For>
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					      </List>
 | 
				
			||||||
 | 
					    </Scaffold>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ChooseLang;
 | 
				
			||||||
							
								
								
									
										106
									
								
								src/settings/ChooseRegion.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/settings/ChooseRegion.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,106 @@
 | 
				
			||||||
 | 
					import { createMemo, For, type Component, type JSX } from "solid-js";
 | 
				
			||||||
 | 
					import Scaffold from "../material/Scaffold";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AppBar,
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					  List,
 | 
				
			||||||
 | 
					  ListItemButton,
 | 
				
			||||||
 | 
					  ListItemSecondaryAction,
 | 
				
			||||||
 | 
					  ListItemText,
 | 
				
			||||||
 | 
					  ListSubheader,
 | 
				
			||||||
 | 
					  Radio,
 | 
				
			||||||
 | 
					  Switch,
 | 
				
			||||||
 | 
					  Toolbar,
 | 
				
			||||||
 | 
					} from "@suid/material";
 | 
				
			||||||
 | 
					import { Close as CloseIcon } from "@suid/icons-material";
 | 
				
			||||||
 | 
					import iso639_1 from "iso-639-1";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  autoMatchRegion,
 | 
				
			||||||
 | 
					  createTranslator,
 | 
				
			||||||
 | 
					  SUPPORTED_REGIONS,
 | 
				
			||||||
 | 
					} from "../platform/i18n";
 | 
				
			||||||
 | 
					import { Title } from "../material/typography";
 | 
				
			||||||
 | 
					import type { Template } from "@solid-primitives/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChooseRegionProps = {
 | 
				
			||||||
 | 
					  code?: string;
 | 
				
			||||||
 | 
					  onCodeChange: (ncode?: string) => void;
 | 
				
			||||||
 | 
					  onClose?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ChooseRegion: Component<ChooseRegionProps> = (props) => {
 | 
				
			||||||
 | 
					  const [t] = createTranslator(
 | 
				
			||||||
 | 
					    () => import("./i18n/lang-names.json"),
 | 
				
			||||||
 | 
					    (code) =>
 | 
				
			||||||
 | 
					      import(`./i18n/${code}.json`) as Promise<{
 | 
				
			||||||
 | 
					        default: Record<string, string | undefined> & {
 | 
				
			||||||
 | 
					          ["lang.auto"]: Template<{ detected: string }>;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }>,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const unsupportedLangCodes = createMemo(() => {
 | 
				
			||||||
 | 
					    return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const matchedRegionCode = createMemo(() => autoMatchRegion());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Scaffold
 | 
				
			||||||
 | 
					      topbar={
 | 
				
			||||||
 | 
					        <AppBar position="static">
 | 
				
			||||||
 | 
					          <Toolbar
 | 
				
			||||||
 | 
					            variant="dense"
 | 
				
			||||||
 | 
					            sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <IconButton color="inherit" onClick={props.onClose} disableRipple>
 | 
				
			||||||
 | 
					              <CloseIcon />
 | 
				
			||||||
 | 
					            </IconButton>
 | 
				
			||||||
 | 
					            <Title>{t("Choose Language")}</Title>
 | 
				
			||||||
 | 
					          </Toolbar>
 | 
				
			||||||
 | 
					        </AppBar>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <List
 | 
				
			||||||
 | 
					        sx={{
 | 
				
			||||||
 | 
					          paddingBottom: "var(--safe-area-inset-bottom, 0)",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ListItemButton
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            props.onCodeChange(props.code ? undefined : matchedRegionCode());
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ListItemText>
 | 
				
			||||||
 | 
					            {t("region.auto", {
 | 
				
			||||||
 | 
					              detected: t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(),
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
 | 
					          </ListItemText>
 | 
				
			||||||
 | 
					          <ListItemSecondaryAction>
 | 
				
			||||||
 | 
					            <Switch checked={typeof props.code === "undefined"} />
 | 
				
			||||||
 | 
					          </ListItemSecondaryAction>
 | 
				
			||||||
 | 
					        </ListItemButton>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}>
 | 
				
			||||||
 | 
					          <For each={SUPPORTED_REGIONS}>
 | 
				
			||||||
 | 
					            {(code) => (
 | 
				
			||||||
 | 
					              <ListItemButton
 | 
				
			||||||
 | 
					                disabled={typeof props.code === "undefined"}
 | 
				
			||||||
 | 
					                onClick={[props.onCodeChange, code]}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <ListItemText>{t(`region.${code}`)}</ListItemText>
 | 
				
			||||||
 | 
					                <ListItemSecondaryAction>
 | 
				
			||||||
 | 
					                  <Radio
 | 
				
			||||||
 | 
					                    checked={props.code === code || (props.code === undefined && matchedRegionCode() == code)}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </ListItemSecondaryAction>
 | 
				
			||||||
 | 
					              </ListItemButton>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </For>
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					      </List>
 | 
				
			||||||
 | 
					    </Scaffold>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ChooseRegion;
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { For, Show, type ParentComponent } from "solid-js";
 | 
					import { createSignal, For, Show, type ParentComponent } from "solid-js";
 | 
				
			||||||
import Scaffold from "../material/Scaffold.js";
 | 
					import Scaffold from "../material/Scaffold.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AppBar,
 | 
					  AppBar,
 | 
				
			||||||
| 
						 | 
					@ -7,32 +7,63 @@ import {
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
  ListItem,
 | 
					  ListItem,
 | 
				
			||||||
  ListItemButton,
 | 
					  ListItemButton,
 | 
				
			||||||
 | 
					  ListItemIcon,
 | 
				
			||||||
  ListItemSecondaryAction,
 | 
					  ListItemSecondaryAction,
 | 
				
			||||||
  ListItemText,
 | 
					  ListItemText,
 | 
				
			||||||
  ListSubheader,
 | 
					  ListSubheader,
 | 
				
			||||||
 | 
					  NativeSelect,
 | 
				
			||||||
  Switch,
 | 
					  Switch,
 | 
				
			||||||
  Toolbar,
 | 
					  Toolbar,
 | 
				
			||||||
} from "@suid/material";
 | 
					} from "@suid/material";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Close as CloseIcon,
 | 
					  Close as CloseIcon,
 | 
				
			||||||
 | 
					  Logout,
 | 
				
			||||||
 | 
					  Public as PublicIcon,
 | 
				
			||||||
  Refresh as RefreshIcon,
 | 
					  Refresh as RefreshIcon,
 | 
				
			||||||
 | 
					  Translate as TranslateIcon,
 | 
				
			||||||
} from "@suid/icons-material";
 | 
					} from "@suid/icons-material";
 | 
				
			||||||
import { useNavigate } from "@solidjs/router";
 | 
					import { useNavigate } from "@solidjs/router";
 | 
				
			||||||
import { Title } from "../material/typography.jsx";
 | 
					import { Title } from "../material/typography.jsx";
 | 
				
			||||||
import { css } from "solid-styled";
 | 
					import { css } from "solid-styled";
 | 
				
			||||||
import { useSignedInProfiles } from "../masto/acct.js";
 | 
					import { useSignedInProfiles } from "../masto/acct.js";
 | 
				
			||||||
import { signOut, type Account } from "../accounts/stores.js";
 | 
					import { signOut, type Account } from "../accounts/stores.js";
 | 
				
			||||||
import { intlFormat } from "date-fns";
 | 
					import { format } from "date-fns";
 | 
				
			||||||
import { useStore } from "@nanostores/solid";
 | 
					import { useStore } from "@nanostores/solid";
 | 
				
			||||||
import { $settings } from "./stores.js";
 | 
					import { $settings } from "./stores.js";
 | 
				
			||||||
import { useRegisterSW } from "virtual:pwa-register/solid";
 | 
					import { useRegisterSW } from "virtual:pwa-register/solid";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  autoMatchLangTag,
 | 
				
			||||||
 | 
					  autoMatchRegion,
 | 
				
			||||||
 | 
					  createTranslator,
 | 
				
			||||||
 | 
					  SUPPORTED_LANGS,
 | 
				
			||||||
 | 
					  SUPPORTED_REGIONS,
 | 
				
			||||||
 | 
					  useDateFnLocale,
 | 
				
			||||||
 | 
					} from "../platform/i18n.jsx";
 | 
				
			||||||
 | 
					import { type Template } from "@solid-primitives/i18n";
 | 
				
			||||||
 | 
					import BottomSheet from "../material/BottomSheet.jsx";
 | 
				
			||||||
 | 
					import ChooseLang from "./ChooseLang.jsx";
 | 
				
			||||||
 | 
					import ChooseRegion from "./ChooseRegion.jsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Strings = {
 | 
				
			||||||
 | 
					  ["lang.auto"]: Template<{ detected: string }>;
 | 
				
			||||||
 | 
					} & Record<string, string | undefined>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Settings: ParentComponent = () => {
 | 
					const Settings: ParentComponent = () => {
 | 
				
			||||||
 | 
					  const [t] = createTranslator(
 | 
				
			||||||
 | 
					    (code) =>
 | 
				
			||||||
 | 
					      import(`./i18n/${code}.json`) as Promise<{
 | 
				
			||||||
 | 
					        default: Strings;
 | 
				
			||||||
 | 
					      }>,
 | 
				
			||||||
 | 
					    () => import(`./i18n/lang-names.json`),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const settings$ = useStore($settings);
 | 
					  const settings$ = useStore($settings);
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    needRefresh: [needRefresh],
 | 
					    needRefresh: [needRefresh],
 | 
				
			||||||
  } = useRegisterSW();
 | 
					  } = useRegisterSW();
 | 
				
			||||||
 | 
					  const dateFnLocale = useDateFnLocale();
 | 
				
			||||||
 | 
					  const [langPickerOpen, setLangPickerOpen] = createSignal(false);
 | 
				
			||||||
 | 
					  const [regionPickerOpen, setRegionPickerOpen] = createSignal(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [profiles] = useSignedInProfiles();
 | 
					  const [profiles] = useSignedInProfiles();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,7 +91,7 @@ const Settings: ParentComponent = () => {
 | 
				
			||||||
            <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
 | 
					            <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
 | 
				
			||||||
              <CloseIcon />
 | 
					              <CloseIcon />
 | 
				
			||||||
            </IconButton>
 | 
					            </IconButton>
 | 
				
			||||||
            <Title>Settings</Title>
 | 
					            <Title>{t("Settings")}</Title>
 | 
				
			||||||
          </Toolbar>
 | 
					          </Toolbar>
 | 
				
			||||||
        </AppBar>
 | 
					        </AppBar>
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -68,32 +99,35 @@ const Settings: ParentComponent = () => {
 | 
				
			||||||
      <List class="setting-list" use:solid-styled>
 | 
					      <List class="setting-list" use:solid-styled>
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
          <ul>
 | 
					          <ul>
 | 
				
			||||||
            <ListSubheader>Accounts</ListSubheader>
 | 
					            <ListSubheader>{t("Accounts")}</ListSubheader>
 | 
				
			||||||
            <ListItem>
 | 
					            <ListItemButton disabled>
 | 
				
			||||||
              <ListItemText>All Notifications</ListItemText>
 | 
					              <ListItemText>{t("All Notifications")}</ListItemText>
 | 
				
			||||||
              <ListItemSecondaryAction>
 | 
					              <ListItemSecondaryAction>
 | 
				
			||||||
                <Switch value={false} />
 | 
					                <Switch value={false} disabled />
 | 
				
			||||||
              </ListItemSecondaryAction>
 | 
					              </ListItemSecondaryAction>
 | 
				
			||||||
            </ListItem>
 | 
					            </ListItemButton>
 | 
				
			||||||
            <Divider />
 | 
					            <Divider />
 | 
				
			||||||
            <ListItem>
 | 
					            <ListItemButton disabled>
 | 
				
			||||||
              <ListItemText>Sign in...</ListItemText>
 | 
					              <ListItemText>{t("Sign in...")}</ListItemText>
 | 
				
			||||||
            </ListItem>
 | 
					            </ListItemButton>
 | 
				
			||||||
            <Divider />
 | 
					            <Divider />
 | 
				
			||||||
          </ul>
 | 
					          </ul>
 | 
				
			||||||
          <For each={profiles()}>
 | 
					          <For each={profiles()}>
 | 
				
			||||||
            {({ account: acct, inf }) => (
 | 
					            {({ account: acct, inf }) => (
 | 
				
			||||||
              <ul data-site={acct.site} data-username={inf?.username}>
 | 
					              <ul data-site={acct.site} data-username={inf?.username}>
 | 
				
			||||||
                <ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
 | 
					                <ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
 | 
				
			||||||
                <ListItem>
 | 
					                <ListItemButton disabled>
 | 
				
			||||||
                  <ListItemText>Notifications</ListItemText>
 | 
					                  <ListItemText>{t("Notifications")}</ListItemText>
 | 
				
			||||||
                  <ListItemSecondaryAction>
 | 
					                  <ListItemSecondaryAction>
 | 
				
			||||||
                    <Switch value={false} />
 | 
					                    <Switch value={false} disabled />
 | 
				
			||||||
                  </ListItemSecondaryAction>
 | 
					                  </ListItemSecondaryAction>
 | 
				
			||||||
                </ListItem>
 | 
					                </ListItemButton>
 | 
				
			||||||
                <Divider />
 | 
					                <Divider />
 | 
				
			||||||
                <ListItemButton onClick={[doSignOut, acct]}>
 | 
					                <ListItemButton onClick={[doSignOut, acct]}>
 | 
				
			||||||
                  <ListItemText>Sign out</ListItemText>
 | 
					                  <ListItemIcon>
 | 
				
			||||||
 | 
					                    <Logout />
 | 
				
			||||||
 | 
					                  </ListItemIcon>
 | 
				
			||||||
 | 
					                  <ListItemText>{t("Sign out")}</ListItemText>
 | 
				
			||||||
                </ListItemButton>
 | 
					                </ListItemButton>
 | 
				
			||||||
                <Divider />
 | 
					                <Divider />
 | 
				
			||||||
              </ul>
 | 
					              </ul>
 | 
				
			||||||
| 
						 | 
					@ -101,12 +135,8 @@ const Settings: ParentComponent = () => {
 | 
				
			||||||
          </For>
 | 
					          </For>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
          <ListSubheader>Reading</ListSubheader>
 | 
					          <ListSubheader>{t("Reading")}</ListSubheader>
 | 
				
			||||||
          <ListItem>
 | 
					          <ListItemButton
 | 
				
			||||||
            <ListItemText secondary="Regular">Fonts</ListItemText>
 | 
					 | 
				
			||||||
          </ListItem>
 | 
					 | 
				
			||||||
          <Divider />
 | 
					 | 
				
			||||||
          <ListItem
 | 
					 | 
				
			||||||
            onClick={(e) =>
 | 
					            onClick={(e) =>
 | 
				
			||||||
              $settings.setKey(
 | 
					              $settings.setKey(
 | 
				
			||||||
                "prefetchTootsDisabled",
 | 
					                "prefetchTootsDisabled",
 | 
				
			||||||
| 
						 | 
					@ -114,34 +144,93 @@ const Settings: ParentComponent = () => {
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <ListItemText secondary="Tutu will download toots before you scroll to the position.">
 | 
					            <ListItemText secondary={t("Prefetch Toots.2nd")}>
 | 
				
			||||||
              Prefetch Toots
 | 
					              {t("Prefetch Toots")}
 | 
				
			||||||
            </ListItemText>
 | 
					            </ListItemText>
 | 
				
			||||||
            <ListItemSecondaryAction>
 | 
					            <ListItemSecondaryAction>
 | 
				
			||||||
              <Switch checked={!settings$().prefetchTootsDisabled} />
 | 
					              <Switch checked={!settings$().prefetchTootsDisabled} />
 | 
				
			||||||
            </ListItemSecondaryAction>
 | 
					            </ListItemSecondaryAction>
 | 
				
			||||||
          </ListItem>
 | 
					          </ListItemButton>
 | 
				
			||||||
          <Divider />
 | 
					          <Divider />
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
          <ListSubheader>This Application</ListSubheader>
 | 
					          <ListSubheader>{t("This Application")}</ListSubheader>
 | 
				
			||||||
 | 
					          <ListItemButton onClick={[setLangPickerOpen, true]}>
 | 
				
			||||||
 | 
					            <ListItemIcon>
 | 
				
			||||||
 | 
					              <TranslateIcon />
 | 
				
			||||||
 | 
					            </ListItemIcon>
 | 
				
			||||||
 | 
					            <ListItemText
 | 
				
			||||||
 | 
					              secondary={
 | 
				
			||||||
 | 
					                settings$().language === undefined
 | 
				
			||||||
 | 
					                  ? t("lang.auto", {
 | 
				
			||||||
 | 
					                      detected:
 | 
				
			||||||
 | 
					                        t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(),
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                  : t("lang." + settings$().language)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {t("Language")}
 | 
				
			||||||
 | 
					            </ListItemText>
 | 
				
			||||||
 | 
					          </ListItemButton>
 | 
				
			||||||
 | 
					          <BottomSheet open={langPickerOpen()}>
 | 
				
			||||||
 | 
					            <ChooseLang
 | 
				
			||||||
 | 
					              code={settings$().language}
 | 
				
			||||||
 | 
					              onCodeChange={(nval) => $settings.setKey("language", nval)}
 | 
				
			||||||
 | 
					              onClose={[setLangPickerOpen, false]}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </BottomSheet>
 | 
				
			||||||
 | 
					          <Divider />
 | 
				
			||||||
 | 
					          <ListItemButton onClick={[setRegionPickerOpen, true]}>
 | 
				
			||||||
 | 
					            <ListItemIcon>
 | 
				
			||||||
 | 
					              <PublicIcon />
 | 
				
			||||||
 | 
					            </ListItemIcon>
 | 
				
			||||||
 | 
					            <ListItemText
 | 
				
			||||||
 | 
					              secondary={
 | 
				
			||||||
 | 
					                settings$().region === undefined
 | 
				
			||||||
 | 
					                  ? t("region.auto", {
 | 
				
			||||||
 | 
					                      detected:
 | 
				
			||||||
 | 
					                        t("region." + autoMatchRegion()) ?? autoMatchRegion(),
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                  : t("region." + settings$().region)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {t("Region")}
 | 
				
			||||||
 | 
					            </ListItemText>
 | 
				
			||||||
 | 
					          </ListItemButton>
 | 
				
			||||||
 | 
					          <BottomSheet open={regionPickerOpen()}>
 | 
				
			||||||
 | 
					            <ChooseRegion
 | 
				
			||||||
 | 
					              code={settings$().region}
 | 
				
			||||||
 | 
					              onCodeChange={(nval) => $settings.setKey("region", nval)}
 | 
				
			||||||
 | 
					              onClose={[setRegionPickerOpen, false]}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </BottomSheet>
 | 
				
			||||||
 | 
					          <Divider />
 | 
				
			||||||
          <ListItem>
 | 
					          <ListItem>
 | 
				
			||||||
            <ListItemText secondary="Comformtable tooting experience.">
 | 
					            <ListItemText secondary={t("About Tutu.2nd")}>
 | 
				
			||||||
              About Tutu
 | 
					              {t("About Tutu")}
 | 
				
			||||||
            </ListItemText>
 | 
					            </ListItemText>
 | 
				
			||||||
          </ListItem>
 | 
					          </ListItem>
 | 
				
			||||||
          <Divider />
 | 
					          <Divider />
 | 
				
			||||||
          <ListItem>
 | 
					          <ListItem>
 | 
				
			||||||
            <ListItemText
 | 
					            <ListItemText
 | 
				
			||||||
              secondary={`Using v${import.meta.env.PACKAGE_VERSION} (built on ${intlFormat(import.meta.env.BUILT_AT)}, ${import.meta.env.MODE})`}
 | 
					              secondary={t("version", {
 | 
				
			||||||
 | 
					                packageVersion: import.meta.env.PACKAGE_VERSION,
 | 
				
			||||||
 | 
					                builtAt: format(
 | 
				
			||||||
 | 
					                  import.meta.env.BUILT_AT,
 | 
				
			||||||
 | 
					                  t("datefmt") || "yyyy/MM/dd",
 | 
				
			||||||
 | 
					                  { locale: dateFnLocale() },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                buildMode: import.meta.env.MODE,
 | 
				
			||||||
 | 
					              })}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              {needRefresh()
 | 
					              {needRefresh() ? t("updates.ready") : t("updates.no")}
 | 
				
			||||||
                ? "An update is ready, restart the Tutu to apply"
 | 
					 | 
				
			||||||
                : "No updates"}
 | 
					 | 
				
			||||||
            </ListItemText>
 | 
					            </ListItemText>
 | 
				
			||||||
            <Show when={needRefresh()}>
 | 
					            <Show when={needRefresh()}>
 | 
				
			||||||
              <ListItemSecondaryAction>
 | 
					              <ListItemSecondaryAction>
 | 
				
			||||||
                <IconButton aria-label="Restart Now" onClick={() => window.location.reload()}>
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  aria-label="Restart Now"
 | 
				
			||||||
 | 
					                  onClick={() => window.location.reload()}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
                  <RefreshIcon />
 | 
					                  <RefreshIcon />
 | 
				
			||||||
                </IconButton>
 | 
					                </IconButton>
 | 
				
			||||||
              </ListItemSecondaryAction>
 | 
					              </ListItemSecondaryAction>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/settings/i18n/en.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/settings/i18n/en.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "Settings": "Settings",
 | 
				
			||||||
 | 
					  "Accounts": "Accounts",
 | 
				
			||||||
 | 
					  "All Notifications": "All Notifications",
 | 
				
			||||||
 | 
					  "Sign in...": "Sign in...",
 | 
				
			||||||
 | 
					  "Reading": "Reading",
 | 
				
			||||||
 | 
					  "Prefetch Toots": "Prefetch Toots",
 | 
				
			||||||
 | 
					  "Prefetch Toots.2nd": "Tutu will download the toots before you need.",
 | 
				
			||||||
 | 
					  "This Application": "This Application",
 | 
				
			||||||
 | 
					  "About Tutu": "About Tutu",
 | 
				
			||||||
 | 
					  "About Tutu.2nd": "Comfortable tooting experience.",
 | 
				
			||||||
 | 
					  "updates.ready": "An update is ready, restart the Tutu to apply",
 | 
				
			||||||
 | 
					  "updates.no": "No updates",
 | 
				
			||||||
 | 
					  "version": "Using v{{packageVersion}} (built on {{builtAt}}, {{buildMode}})",
 | 
				
			||||||
 | 
					  "Language": "Language",
 | 
				
			||||||
 | 
					  "Region": "Region",
 | 
				
			||||||
 | 
					  "lang.auto": "(Auto) {{detected}}",
 | 
				
			||||||
 | 
					  "region.auto": "(Auto) {{detected}}",
 | 
				
			||||||
 | 
					  "region.en_GB": "Great Britan (English)",
 | 
				
			||||||
 | 
					  "region.en_US": "United States (English)",
 | 
				
			||||||
 | 
					  "region.zh_CN": "China Mainland (Chinese)",
 | 
				
			||||||
 | 
					  "datefmt": "yyyy/MM/dd",
 | 
				
			||||||
 | 
					  "Sign out": "Sign out",
 | 
				
			||||||
 | 
					  "Notifications": "Notifications",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "Choose Language": "Choose Language",
 | 
				
			||||||
 | 
					  "Supported": "Supported",
 | 
				
			||||||
 | 
					  "Unsupported": "Unsupported",
 | 
				
			||||||
 | 
					  "Choose Region": "Choose Region"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/settings/i18n/lang-names.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/settings/i18n/lang-names.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "lang.zh-Hans": "中文(简体)",
 | 
				
			||||||
 | 
					  "lang.en": "English"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/settings/i18n/zh-Hans.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/settings/i18n/zh-Hans.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "Settings": "设置",
 | 
				
			||||||
 | 
					  "Accounts": "所有账户",
 | 
				
			||||||
 | 
					  "All Notifications": "所有通知",
 | 
				
			||||||
 | 
					  "Sign in...": "登录新账户...",
 | 
				
			||||||
 | 
					  "Reading": "阅读",
 | 
				
			||||||
 | 
					  "Prefetch Toots": "提前下载嘟文",
 | 
				
			||||||
 | 
					  "Prefetch Toots.2nd": "图图会在你可能需要的时候提前下载嘟文。",
 | 
				
			||||||
 | 
					  "This Application": "本应用",
 | 
				
			||||||
 | 
					  "About Tutu": "关于图图",
 | 
				
			||||||
 | 
					  "About Tutu.2nd": "舒服地刷嘟。",
 | 
				
			||||||
 | 
					  "updates.ready": "更新已准备好,下次开启会启动新版本",
 | 
				
			||||||
 | 
					  "updates.no": "已是最新版本",
 | 
				
			||||||
 | 
					  "version": "正在使用 v{{packageVersion}} ({{builtAt}}构建, {{buildMode}})",
 | 
				
			||||||
 | 
					  "Language": "语言",
 | 
				
			||||||
 | 
					  "Region": "区域",
 | 
				
			||||||
 | 
					  "lang.auto": "(自动){{detected}}",
 | 
				
			||||||
 | 
					  "region.auto": "(自动){{detected}}",
 | 
				
			||||||
 | 
					  "region.en_GB": "英国和苏格兰(英语)",
 | 
				
			||||||
 | 
					  "region.en_US": "美国(英语)",
 | 
				
			||||||
 | 
					  "region.zh_CN": "中国大陆(中文)",
 | 
				
			||||||
 | 
					  "datefmt": "yyyy年MM月dd日",
 | 
				
			||||||
 | 
					  "Sign out": "登出此账户",
 | 
				
			||||||
 | 
					  "Notifications": "通知",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "Choose Language": "选择语言",
 | 
				
			||||||
 | 
					  "Supported": "已支持",
 | 
				
			||||||
 | 
					  "Unsupported": "尚未支持",
 | 
				
			||||||
 | 
					  "Choose Region": "选择区域"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,8 @@ import { persistentMap } from "@nanostores/persistent";
 | 
				
			||||||
type Settings = {
 | 
					type Settings = {
 | 
				
			||||||
  onGoingOAuth2Process?: string;
 | 
					  onGoingOAuth2Process?: string;
 | 
				
			||||||
  prefetchTootsDisabled?: boolean;
 | 
					  prefetchTootsDisabled?: boolean;
 | 
				
			||||||
 | 
					  language?: string;
 | 
				
			||||||
 | 
					  region?: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const $settings = persistentMap<Settings>(
 | 
					export const $settings = persistentMap<Settings>(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,8 @@ import {
 | 
				
			||||||
  children,
 | 
					  children,
 | 
				
			||||||
  Suspense,
 | 
					  Suspense,
 | 
				
			||||||
  Match,
 | 
					  Match,
 | 
				
			||||||
  Switch as JsSwitch
 | 
					  Switch as JsSwitch,
 | 
				
			||||||
 | 
					  ErrorBoundary
 | 
				
			||||||
} from "solid-js";
 | 
					} from "solid-js";
 | 
				
			||||||
import { useDocumentTitle } from "../utils";
 | 
					import { useDocumentTitle } from "../utils";
 | 
				
			||||||
import { type mastodon } from "masto";
 | 
					import { type mastodon } from "masto";
 | 
				
			||||||
| 
						 | 
					@ -125,7 +126,9 @@ const TimelinePanel: Component<{
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <ErrorBoundary fallback={(err, reset) => {
 | 
				
			||||||
 | 
					      return <p>Oops: {String(err)}</p>
 | 
				
			||||||
 | 
					    }}>
 | 
				
			||||||
      <PullDownToRefresh
 | 
					      <PullDownToRefresh
 | 
				
			||||||
        linkedElement={scrollLinked()}
 | 
					        linkedElement={scrollLinked()}
 | 
				
			||||||
        loading={snapshot.loading}
 | 
					        loading={snapshot.loading}
 | 
				
			||||||
| 
						 | 
					@ -202,7 +205,7 @@ const TimelinePanel: Component<{
 | 
				
			||||||
          </Match>
 | 
					          </Match>
 | 
				
			||||||
        </JsSwitch>
 | 
					        </JsSwitch>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </>
 | 
					    </ErrorBoundary>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,7 @@ import Button from "../material/Button.js";
 | 
				
			||||||
import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
 | 
					import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
 | 
				
			||||||
import { FastAverageColor } from "fast-average-color";
 | 
					import { FastAverageColor } from "fast-average-color";
 | 
				
			||||||
import Color from "colorjs.io";
 | 
					import Color from "colorjs.io";
 | 
				
			||||||
 | 
					import { useDateFnLocale } from "../platform/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TootContentViewProps = {
 | 
					type TootContentViewProps = {
 | 
				
			||||||
  source?: string;
 | 
					  source?: string;
 | 
				
			||||||
| 
						 | 
					@ -170,6 +171,7 @@ function TootActionGroup<T extends mastodon.v1.Status>(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
 | 
					function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
 | 
				
			||||||
  const toot = () => props.status;
 | 
					  const toot = () => props.status;
 | 
				
			||||||
 | 
					  const dateFnLocale = useDateFnLocale()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div class={tootStyle.tootAuthorGrp}>
 | 
					    <div class={tootStyle.tootAuthorGrp}>
 | 
				
			||||||
| 
						 | 
					@ -187,7 +189,7 @@ function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <time datetime={toot().createdAt}>
 | 
					        <time datetime={toot().createdAt}>
 | 
				
			||||||
          {formatRelative(toot().createdAt, props.now)}
 | 
					          {formatRelative(toot().createdAt, props.now, {locale: dateFnLocale()})}
 | 
				
			||||||
        </time>
 | 
					        </time>
 | 
				
			||||||
        <span>
 | 
					        <span>
 | 
				
			||||||
          @{toot().account.username}@{new URL(toot().account.url).hostname}
 | 
					          @{toot().account.username}@{new URL(toot().account.url).hostname}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,5 +11,6 @@
 | 
				
			||||||
    "types": ["vite/client", "vite-plugin-pwa/solid"],
 | 
					    "types": ["vite/client", "vite-plugin-pwa/solid"],
 | 
				
			||||||
    "noEmit": true,
 | 
					    "noEmit": true,
 | 
				
			||||||
    "isolatedModules": true,
 | 
					    "isolatedModules": true,
 | 
				
			||||||
 | 
					    "resolveJsonModule": true,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue