first prototype of i18n system
This commit is contained in:
		
							parent
							
								
									d3a7602fb5
								
							
						
					
					
						commit
						b4fa751345
					
				
					 9 changed files with 348 additions and 20 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",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/App.tsx
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								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()}>
 | 
				
			||||||
 | 
					        <DateFnScope>
 | 
				
			||||||
          <ClientProvider value={clientStore}>
 | 
					          <ClientProvider value={clientStore}>
 | 
				
			||||||
            <Routing />
 | 
					            <Routing />
 | 
				
			||||||
          </ClientProvider>
 | 
					          </ClientProvider>
 | 
				
			||||||
 | 
					        </DateFnScope>
 | 
				
			||||||
      </ThemeProvider>
 | 
					      </ThemeProvider>
 | 
				
			||||||
    </ErrorBoundary>
 | 
					    </ErrorBoundary>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										170
									
								
								src/platform/i18n.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/platform/i18n.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,170 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ParentComponent,
 | 
				
			||||||
 | 
					  createContext,
 | 
				
			||||||
 | 
					  createMemo,
 | 
				
			||||||
 | 
					  createRenderEffect,
 | 
				
			||||||
 | 
					  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 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"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 = 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();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createStringResource<
 | 
				
			||||||
 | 
					  M extends Record<string, string | Template<any>>,
 | 
				
			||||||
 | 
					>(importFn: (code: string) => Promise<{ default: M }>) {
 | 
				
			||||||
 | 
					  const language = useLanguage();
 | 
				
			||||||
 | 
					  const cache: Record<string, M | undefined> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return createResource(
 | 
				
			||||||
 | 
					    () => [language()] as const,
 | 
				
			||||||
 | 
					    async ([nlang]) => {
 | 
				
			||||||
 | 
					      if (cache[nlang]) {
 | 
				
			||||||
 | 
					        return cache[nlang];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { default: dict } = await importFn(`${nlang}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return dict;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -7,32 +7,50 @@ 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,
 | 
				
			||||||
 | 
					  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, intlFormat } 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,
 | 
				
			||||||
 | 
					  createStringResource,
 | 
				
			||||||
 | 
					  SUPPORTED_LANGS,
 | 
				
			||||||
 | 
					  SUPPORTED_REGIONS,
 | 
				
			||||||
 | 
					  useDateFnLocale,
 | 
				
			||||||
 | 
					} from "../platform/i18n.jsx";
 | 
				
			||||||
 | 
					import { resolveTemplate, translator } from "@solid-primitives/i18n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Settings: ParentComponent = () => {
 | 
					const Settings: ParentComponent = () => {
 | 
				
			||||||
 | 
					  const [strings] = createStringResource(
 | 
				
			||||||
 | 
					    (code) => import(`./i18n/${code}.json`),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const t = translator(strings, resolveTemplate);
 | 
				
			||||||
  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 [profiles] = useSignedInProfiles();
 | 
					  const [profiles] = useSignedInProfiles();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,7 +78,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,16 +86,16 @@ 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>
 | 
					            <ListItem>
 | 
				
			||||||
              <ListItemText>All Notifications</ListItemText>
 | 
					              <ListItemText>{t("All Notifications")}</ListItemText>
 | 
				
			||||||
              <ListItemSecondaryAction>
 | 
					              <ListItemSecondaryAction>
 | 
				
			||||||
                <Switch value={false} />
 | 
					                <Switch value={false} />
 | 
				
			||||||
              </ListItemSecondaryAction>
 | 
					              </ListItemSecondaryAction>
 | 
				
			||||||
            </ListItem>
 | 
					            </ListItem>
 | 
				
			||||||
            <Divider />
 | 
					            <Divider />
 | 
				
			||||||
            <ListItem>
 | 
					            <ListItem>
 | 
				
			||||||
              <ListItemText>Sign in...</ListItemText>
 | 
					              <ListItemText>{t("Sign in...")}</ListItemText>
 | 
				
			||||||
            </ListItem>
 | 
					            </ListItem>
 | 
				
			||||||
            <Divider />
 | 
					            <Divider />
 | 
				
			||||||
          </ul>
 | 
					          </ul>
 | 
				
			||||||
| 
						 | 
					@ -101,7 +119,7 @@ const Settings: ParentComponent = () => {
 | 
				
			||||||
          </For>
 | 
					          </For>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
          <ListSubheader>Reading</ListSubheader>
 | 
					          <ListSubheader>{t("Reading")}</ListSubheader>
 | 
				
			||||||
          <ListItem>
 | 
					          <ListItem>
 | 
				
			||||||
            <ListItemText secondary="Regular">Fonts</ListItemText>
 | 
					            <ListItemText secondary="Regular">Fonts</ListItemText>
 | 
				
			||||||
          </ListItem>
 | 
					          </ListItem>
 | 
				
			||||||
| 
						 | 
					@ -114,8 +132,8 @@ 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} />
 | 
				
			||||||
| 
						 | 
					@ -124,24 +142,93 @@ const Settings: ParentComponent = () => {
 | 
				
			||||||
          <Divider />
 | 
					          <Divider />
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
          <ListSubheader>This Application</ListSubheader>
 | 
					          <ListSubheader>{t("This Application")}</ListSubheader>
 | 
				
			||||||
          <ListItem>
 | 
					          <ListItem>
 | 
				
			||||||
            <ListItemText secondary="Comformtable tooting experience.">
 | 
					            <ListItemIcon>
 | 
				
			||||||
              About Tutu
 | 
					              <TranslateIcon />
 | 
				
			||||||
 | 
					            </ListItemIcon>
 | 
				
			||||||
 | 
					            <ListItemText>{t("Language")}</ListItemText>
 | 
				
			||||||
 | 
					            <ListItemSecondaryAction>
 | 
				
			||||||
 | 
					              <NativeSelect
 | 
				
			||||||
 | 
					                value={settings$().language || "xauto"}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  $settings.setKey(
 | 
				
			||||||
 | 
					                    "language",
 | 
				
			||||||
 | 
					                    e.currentTarget.value === "xauto"
 | 
				
			||||||
 | 
					                      ? undefined
 | 
				
			||||||
 | 
					                      : e.currentTarget.value,
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <option value={"xauto"}>
 | 
				
			||||||
 | 
					                  {t("lang.auto", {
 | 
				
			||||||
 | 
					                    detected: t("lang." + autoMatchLangTag()),
 | 
				
			||||||
 | 
					                  })}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					                <For each={SUPPORTED_LANGS}>
 | 
				
			||||||
 | 
					                  {(code) => <option value={code}>{t("lang." + code)}</option>}
 | 
				
			||||||
 | 
					                </For>
 | 
				
			||||||
 | 
					              </NativeSelect>
 | 
				
			||||||
 | 
					            </ListItemSecondaryAction>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <Divider />
 | 
				
			||||||
 | 
					          <ListItem>
 | 
				
			||||||
 | 
					            <ListItemIcon>
 | 
				
			||||||
 | 
					              <PublicIcon />
 | 
				
			||||||
 | 
					            </ListItemIcon>
 | 
				
			||||||
 | 
					            <ListItemText>{t("Region")}</ListItemText>
 | 
				
			||||||
 | 
					            <ListItemSecondaryAction>
 | 
				
			||||||
 | 
					              <NativeSelect
 | 
				
			||||||
 | 
					                value={settings$().region}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  $settings.setKey(
 | 
				
			||||||
 | 
					                    "region",
 | 
				
			||||||
 | 
					                    e.currentTarget.value === "xauto"
 | 
				
			||||||
 | 
					                      ? undefined
 | 
				
			||||||
 | 
					                      : e.currentTarget.value,
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <option value={"xauto"}>
 | 
				
			||||||
 | 
					                  {t("region.auto", {
 | 
				
			||||||
 | 
					                    detected: t("region." + autoMatchRegion()),
 | 
				
			||||||
 | 
					                  })}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					                <For each={SUPPORTED_REGIONS}>
 | 
				
			||||||
 | 
					                  {(code) => (
 | 
				
			||||||
 | 
					                    <option value={code}>{t("region." + code)}</option>
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </For>
 | 
				
			||||||
 | 
					              </NativeSelect>
 | 
				
			||||||
 | 
					            </ListItemSecondaryAction>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <Divider />
 | 
				
			||||||
 | 
					          <ListItem>
 | 
				
			||||||
 | 
					            <ListItemText secondary={t("About Tutu.2nd")}>
 | 
				
			||||||
 | 
					              {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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/settings/i18n/en.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/settings/i18n/en.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "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}})",
 | 
				
			||||||
 | 
					  "lang.zh-Hans": "中文(简体)",
 | 
				
			||||||
 | 
					  "lang.en": "English",
 | 
				
			||||||
 | 
					  "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"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/settings/i18n/zh-Hans.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/settings/i18n/zh-Hans.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "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}})",
 | 
				
			||||||
 | 
					  "lang.zh-Hans": "中文(简体)",
 | 
				
			||||||
 | 
					  "lang.en": "English",
 | 
				
			||||||
 | 
					  "region.auto": "自动({{detected}})",
 | 
				
			||||||
 | 
					  "region.en_GB": "英国和苏格兰(英语)",
 | 
				
			||||||
 | 
					  "region.en_US": "美国(英语)",
 | 
				
			||||||
 | 
					  "region.zh_CN": "中国大陆(中文)",
 | 
				
			||||||
 | 
					  "datefmt": "yyyy年MM月dd日"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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>(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue