first prototype of i18n system
This commit is contained in:
		
							parent
							
								
									70de83a24a
								
							
						
					
					
						commit
						4f8b31ca31
					
				
					 9 changed files with 348 additions and 20 deletions
				
			
		
							
								
								
									
										21
									
								
								src/App.tsx
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								src/App.tsx
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -6,6 +6,7 @@ import {
 | 
			
		|||
  createSignal,
 | 
			
		||||
  ErrorBoundary,
 | 
			
		||||
  lazy,
 | 
			
		||||
  onCleanup,
 | 
			
		||||
} from "solid-js";
 | 
			
		||||
import { useRootTheme } from "./material/mui.js";
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +16,7 @@ import {
 | 
			
		|||
} from "./masto/clients.js";
 | 
			
		||||
import { $accounts } from "./accounts/stores.js";
 | 
			
		||||
import { useStore } from "@nanostores/solid";
 | 
			
		||||
import { DateFnScope, useLanguage } from "./platform/i18n.jsx";
 | 
			
		||||
 | 
			
		||||
const AccountSignIn = lazy(() => import("./accounts/SignIn.js"));
 | 
			
		||||
const AccountMastodonOAuth2Callback = lazy(
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +49,7 @@ const App: Component = () => {
 | 
			
		|||
  const theme = useRootTheme();
 | 
			
		||||
  const accts = useStore($accounts);
 | 
			
		||||
  const clientStore = createSignal<Session[]>([]);
 | 
			
		||||
  const lang = useLanguage();
 | 
			
		||||
 | 
			
		||||
  createRenderEffect(() => {
 | 
			
		||||
    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"));
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -65,9 +78,11 @@ const App: Component = () => {
 | 
			
		|||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <ThemeProvider theme={theme()}>
 | 
			
		||||
        <ClientProvider value={clientStore}>
 | 
			
		||||
          <Routing />
 | 
			
		||||
        </ClientProvider>
 | 
			
		||||
        <DateFnScope>
 | 
			
		||||
          <ClientProvider value={clientStore}>
 | 
			
		||||
            <Routing />
 | 
			
		||||
          </ClientProvider>
 | 
			
		||||
        </DateFnScope>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </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,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  ListItemButton,
 | 
			
		||||
  ListItemIcon,
 | 
			
		||||
  ListItemSecondaryAction,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  ListSubheader,
 | 
			
		||||
  NativeSelect,
 | 
			
		||||
  Switch,
 | 
			
		||||
  Toolbar,
 | 
			
		||||
} from "@suid/material";
 | 
			
		||||
import {
 | 
			
		||||
  Close as CloseIcon,
 | 
			
		||||
  Public as PublicIcon,
 | 
			
		||||
  Refresh as RefreshIcon,
 | 
			
		||||
  Translate as TranslateIcon,
 | 
			
		||||
} from "@suid/icons-material";
 | 
			
		||||
import { useNavigate } from "@solidjs/router";
 | 
			
		||||
import { Title } from "../material/typography.jsx";
 | 
			
		||||
import { css } from "solid-styled";
 | 
			
		||||
import { useSignedInProfiles } from "../masto/acct.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 { $settings } from "./stores.js";
 | 
			
		||||
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 [strings] = createStringResource(
 | 
			
		||||
    (code) => import(`./i18n/${code}.json`),
 | 
			
		||||
  );
 | 
			
		||||
  const t = translator(strings, resolveTemplate);
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const settings$ = useStore($settings);
 | 
			
		||||
  const {
 | 
			
		||||
    needRefresh: [needRefresh],
 | 
			
		||||
  } = useRegisterSW();
 | 
			
		||||
  const dateFnLocale = useDateFnLocale();
 | 
			
		||||
 | 
			
		||||
  const [profiles] = useSignedInProfiles();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +78,7 @@ const Settings: ParentComponent = () => {
 | 
			
		|||
            <IconButton color="inherit" onClick={[navigate, -1]} disableRipple>
 | 
			
		||||
              <CloseIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
            <Title>Settings</Title>
 | 
			
		||||
            <Title>{t("Settings")}</Title>
 | 
			
		||||
          </Toolbar>
 | 
			
		||||
        </AppBar>
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -68,16 +86,16 @@ const Settings: ParentComponent = () => {
 | 
			
		|||
      <List class="setting-list" use:solid-styled>
 | 
			
		||||
        <li>
 | 
			
		||||
          <ul>
 | 
			
		||||
            <ListSubheader>Accounts</ListSubheader>
 | 
			
		||||
            <ListSubheader>{t("Accounts")}</ListSubheader>
 | 
			
		||||
            <ListItem>
 | 
			
		||||
              <ListItemText>All Notifications</ListItemText>
 | 
			
		||||
              <ListItemText>{t("All Notifications")}</ListItemText>
 | 
			
		||||
              <ListItemSecondaryAction>
 | 
			
		||||
                <Switch value={false} />
 | 
			
		||||
              </ListItemSecondaryAction>
 | 
			
		||||
            </ListItem>
 | 
			
		||||
            <Divider />
 | 
			
		||||
            <ListItem>
 | 
			
		||||
              <ListItemText>Sign in...</ListItemText>
 | 
			
		||||
              <ListItemText>{t("Sign in...")}</ListItemText>
 | 
			
		||||
            </ListItem>
 | 
			
		||||
            <Divider />
 | 
			
		||||
          </ul>
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +119,7 @@ const Settings: ParentComponent = () => {
 | 
			
		|||
          </For>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
          <ListSubheader>Reading</ListSubheader>
 | 
			
		||||
          <ListSubheader>{t("Reading")}</ListSubheader>
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <ListItemText secondary="Regular">Fonts</ListItemText>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
| 
						 | 
				
			
			@ -114,8 +132,8 @@ const Settings: ParentComponent = () => {
 | 
			
		|||
              )
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            <ListItemText secondary="Tutu will download toots before you scroll to the position.">
 | 
			
		||||
              Prefetch Toots
 | 
			
		||||
            <ListItemText secondary={t("Prefetch Toots.2nd")}>
 | 
			
		||||
              {t("Prefetch Toots")}
 | 
			
		||||
            </ListItemText>
 | 
			
		||||
            <ListItemSecondaryAction>
 | 
			
		||||
              <Switch checked={!settings$().prefetchTootsDisabled} />
 | 
			
		||||
| 
						 | 
				
			
			@ -124,24 +142,93 @@ const Settings: ParentComponent = () => {
 | 
			
		|||
          <Divider />
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
          <ListSubheader>This Application</ListSubheader>
 | 
			
		||||
          <ListSubheader>{t("This Application")}</ListSubheader>
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <ListItemText secondary="Comformtable tooting experience.">
 | 
			
		||||
              About Tutu
 | 
			
		||||
            <ListItemIcon>
 | 
			
		||||
              <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>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <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()
 | 
			
		||||
                ? "An update is ready, restart the Tutu to apply"
 | 
			
		||||
                : "No updates"}
 | 
			
		||||
              {needRefresh() ? t("updates.ready") : t("updates.no")}
 | 
			
		||||
            </ListItemText>
 | 
			
		||||
            <Show when={needRefresh()}>
 | 
			
		||||
              <ListItemSecondaryAction>
 | 
			
		||||
                <IconButton aria-label="Restart Now" onClick={() => window.location.reload()}>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  aria-label="Restart Now"
 | 
			
		||||
                  onClick={() => window.location.reload()}
 | 
			
		||||
                >
 | 
			
		||||
                  <RefreshIcon />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
              </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 = {
 | 
			
		||||
  onGoingOAuth2Process?: string;
 | 
			
		||||
  prefetchTootsDisabled?: boolean;
 | 
			
		||||
  language?: string;
 | 
			
		||||
  region?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const $settings = persistentMap<Settings>(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,7 @@ import Button from "../material/Button.js";
 | 
			
		|||
import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
 | 
			
		||||
import { FastAverageColor } from "fast-average-color";
 | 
			
		||||
import Color from "colorjs.io";
 | 
			
		||||
import { useDateFnLocale } from "../platform/i18n";
 | 
			
		||||
 | 
			
		||||
type TootContentViewProps = {
 | 
			
		||||
  source?: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -170,6 +171,7 @@ function TootActionGroup<T extends mastodon.v1.Status>(
 | 
			
		|||
 | 
			
		||||
function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
 | 
			
		||||
  const toot = () => props.status;
 | 
			
		||||
  const dateFnLocale = useDateFnLocale()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div class={tootStyle.tootAuthorGrp}>
 | 
			
		||||
| 
						 | 
				
			
			@ -187,7 +189,7 @@ function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) {
 | 
			
		|||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <time datetime={toot().createdAt}>
 | 
			
		||||
          {formatRelative(toot().createdAt, props.now)}
 | 
			
		||||
          {formatRelative(toot().createdAt, props.now, {locale: dateFnLocale()})}
 | 
			
		||||
        </time>
 | 
			
		||||
        <span>
 | 
			
		||||
          @{toot().account.username}@{new URL(toot().account.url).hostname}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue