diff --git a/bun.lockb b/bun.lockb index 25e9097..024e997 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 700f2f4..b638162 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,9 @@ "wrangler": "^3.78.2" }, "dependencies": { - "@formatjs/intl-localematcher": "^0.5.4", "@nanostores/persistent": "^0.10.2", "@nanostores/solid": "^0.4.2", "@solid-primitives/event-listener": "^2.3.3", - "@solid-primitives/i18n": "^2.1.1", "@solid-primitives/intersection-observer": "^2.1.6", "@solid-primitives/resize-observer": "^2.0.26", "@solidjs/router": "^0.14.5", @@ -42,7 +40,6 @@ "date-fns": "^3.6.0", "fast-average-color": "^9.4.0", "hammerjs": "^2.0.8", - "iso-639-1": "^3.1.3", "masto": "^6.8.0", "nanostores": "^0.11.3", "solid-js": "^1.8.22", diff --git a/src/App.tsx b/src/App.tsx index 6ed8a9b..c3573a5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,6 @@ import { createSignal, ErrorBoundary, lazy, - onCleanup, } from "solid-js"; import { useRootTheme } from "./material/mui.js"; import { @@ -16,7 +15,6 @@ 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( @@ -49,7 +47,6 @@ const App: Component = () => { const theme = useRootTheme(); const accts = useStore($accounts); const clientStore = createSignal([]); - const lang = useLanguage(); createRenderEffect(() => { const [, setClients] = clientStore; @@ -58,16 +55,6 @@ 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 ( @@ -78,11 +65,9 @@ const App: Component = () => { }} > - - - - - + + + ); diff --git a/src/masto/acct.ts b/src/masto/acct.ts index ff9d488..97bbcec 100644 --- a/src/masto/acct.ts +++ b/src/masto/acct.ts @@ -12,16 +12,11 @@ export function useSignedInProfiles() { }); return [ () => { - try { - const value = accessor(); - if (value) { - return value; - } - } catch (reason) { - console.error("useSignedInProfiles: update acct info failed", reason); + const value = accessor(); + if (!value) { + return sessions().map((x) => ({ ...x, inf: x.account.inf })); } - - return sessions().map((x) => ({ ...x, inf: x.account.inf })); + return value; }, tools, ] as const; diff --git a/src/platform/i18n.tsx b/src/platform/i18n.tsx deleted file mode 100644 index b31d888..0000000 --- a/src/platform/i18n.tsx +++ /dev/null @@ -1,188 +0,0 @@ -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, -): Promise { - 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>(() => enGB); - -const cachedDateFnLocale: Record = { - 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 { - 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 ( - - {props.children} - - ); -}; - -/** - * Get the {@link Locale} object for date-fns. - * - * This function must be using in {@link DateFnScope} - * - * @returns Accessor for Locale - */ -export function useDateFnLocale(): Accessor { - const cx = useContext(DateFnLocaleCx); - return cx; -} - -export function useLanguage() { - const settings = useStore($settings); - return () => settings().language || autoMatchLangTag(); -} - -type ImportFn = (name: string) => Promise<{default: T}> - -type ImportedModule = F extends ImportFn ? T: never - -type MergedImportedModule = - T extends [] ? {} : - T extends [infer I] ? ImportedModule : - T extends [infer I, ...infer J] ? ImportedModule & MergedImportedModule : never - -export function createStringResource< - T extends ImportFn | undefined>>[], ->(...importFns: T) { - const language = useLanguage(); - const cache: Record> = {}; - - 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 = Object.assign({}, ...results) - - cache[nlang] = merged; - - return merged; - }, - ); -} - -export function createTranslator | undefined>>[],>(...importFns: T) { - const res = createStringResource(...importFns) - - return [translator(res[0], resolveTemplate), res] as const -} diff --git a/src/settings/ChooseLang.tsx b/src/settings/ChooseLang.tsx deleted file mode 100644 index ac03c7b..0000000 --- a/src/settings/ChooseLang.tsx +++ /dev/null @@ -1,116 +0,0 @@ -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; -}; - -const ChooseLang: Component = (props) => { - const [t] = createTranslator( - () => import("./i18n/lang-names.json"), - (code) => - import(`./i18n/${code}.json`) as Promise<{ - default: Record & { - ["lang.auto"]: Template<{ detected: string }>; - }; - }>, - ); - - const unsupportedLangCodes = createMemo(() => { - return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x)); - }); - - const matchedLangCode = createMemo(() => autoMatchLangTag()); - - return ( - - - - - - {t("Choose Language")} - - - } - > - - { - props.onCodeChange(props.code ? undefined : matchedLangCode()); - }} - > - - {t("lang.auto", { - detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(), - })} - - - - - - {t("Supported")}}> - - {(code) => ( - - {t(`lang.${code}`)} - - - - - )} - - - - {t("Unsupported")}}> - - {(code) => ( - - {iso639_1.getNativeName(code)} - - )} - - - - - ); -}; - -export default ChooseLang; diff --git a/src/settings/ChooseRegion.tsx b/src/settings/ChooseRegion.tsx deleted file mode 100644 index 77a969e..0000000 --- a/src/settings/ChooseRegion.tsx +++ /dev/null @@ -1,106 +0,0 @@ -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; -}; - -const ChooseRegion: Component = (props) => { - const [t] = createTranslator( - () => import("./i18n/lang-names.json"), - (code) => - import(`./i18n/${code}.json`) as Promise<{ - default: Record & { - ["lang.auto"]: Template<{ detected: string }>; - }; - }>, - ); - - const unsupportedLangCodes = createMemo(() => { - return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x)); - }); - - const matchedRegionCode = createMemo(() => autoMatchRegion()); - - return ( - - - - - - {t("Choose Language")} - - - } - > - - { - props.onCodeChange(props.code ? undefined : matchedRegionCode()); - }} - > - - {t("region.auto", { - detected: t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(), - })} - - - - - - - {t("Supported")}}> - - {(code) => ( - - {t(`region.${code}`)} - - - - - )} - - - - - ); -}; - -export default ChooseRegion; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index ad9f66b..9eebcac 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,4 +1,4 @@ -import { createSignal, For, Show, type ParentComponent } from "solid-js"; +import { For, Show, type ParentComponent } from "solid-js"; import Scaffold from "../material/Scaffold.js"; import { AppBar, @@ -7,63 +7,32 @@ import { List, ListItem, ListItemButton, - ListItemIcon, ListItemSecondaryAction, ListItemText, ListSubheader, - NativeSelect, Switch, Toolbar, } from "@suid/material"; import { Close as CloseIcon, - Logout, - 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 { format } from "date-fns"; +import { intlFormat } from "date-fns"; import { useStore } from "@nanostores/solid"; import { $settings } from "./stores.js"; 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; const Settings: ParentComponent = () => { - const [t] = createTranslator( - (code) => - import(`./i18n/${code}.json`) as Promise<{ - default: Strings; - }>, - () => import(`./i18n/lang-names.json`), - ); const navigate = useNavigate(); const settings$ = useStore($settings); const { needRefresh: [needRefresh], } = useRegisterSW(); - const dateFnLocale = useDateFnLocale(); - const [langPickerOpen, setLangPickerOpen] = createSignal(false); - const [regionPickerOpen, setRegionPickerOpen] = createSignal(false); const [profiles] = useSignedInProfiles(); @@ -91,7 +60,7 @@ const Settings: ParentComponent = () => { - {t("Settings")} + Settings } @@ -99,35 +68,32 @@ const Settings: ParentComponent = () => {
    • - {t("Accounts")} - - {t("All Notifications")} + Accounts + + All Notifications - + - + - - {t("Sign in...")} - + + Sign in... +
    {({ account: acct, inf }) => (
      {`@${inf?.username ?? "..."}@${new URL(acct.site).host}`} - - {t("Notifications")} + + Notifications - + - + - - - - {t("Sign out")} + Sign out
    @@ -135,8 +101,12 @@ const Settings: ParentComponent = () => {
  • - {t("Reading")} - Reading + + Fonts + + + $settings.setKey( "prefetchTootsDisabled", @@ -144,93 +114,34 @@ const Settings: ParentComponent = () => { ) } > - - {t("Prefetch Toots")} + + Prefetch Toots - +
  • - {t("This Application")} - - - - - - {t("Language")} - - - - $settings.setKey("language", nval)} - onClose={[setLangPickerOpen, false]} - /> - - - - - - - - {t("Region")} - - - - $settings.setKey("region", nval)} - onClose={[setRegionPickerOpen, false]} - /> - - + This Application - - {t("About Tutu")} + + About Tutu - {needRefresh() ? t("updates.ready") : t("updates.no")} + {needRefresh() + ? "An update is ready, restart the Tutu to apply" + : "No updates"} - window.location.reload()} - > + window.location.reload()}> diff --git a/src/settings/i18n/en.json b/src/settings/i18n/en.json deleted file mode 100644 index 095bc04..0000000 --- a/src/settings/i18n/en.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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" -} \ No newline at end of file diff --git a/src/settings/i18n/lang-names.json b/src/settings/i18n/lang-names.json deleted file mode 100644 index 6d250bd..0000000 --- a/src/settings/i18n/lang-names.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "lang.zh-Hans": "中文(简体)", - "lang.en": "English" -} \ No newline at end of file diff --git a/src/settings/i18n/zh-Hans.json b/src/settings/i18n/zh-Hans.json deleted file mode 100644 index 8fd70d1..0000000 --- a/src/settings/i18n/zh-Hans.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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": "选择区域" -} \ No newline at end of file diff --git a/src/settings/stores.ts b/src/settings/stores.ts index 151f449..25f6484 100644 --- a/src/settings/stores.ts +++ b/src/settings/stores.ts @@ -3,8 +3,6 @@ import { persistentMap } from "@nanostores/persistent"; type Settings = { onGoingOAuth2Process?: string; prefetchTootsDisabled?: boolean; - language?: string; - region?: string; }; export const $settings = persistentMap( diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index b83de4d..c09023d 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -10,8 +10,7 @@ import { children, Suspense, Match, - Switch as JsSwitch, - ErrorBoundary + Switch as JsSwitch } from "solid-js"; import { useDocumentTitle } from "../utils"; import { type mastodon } from "masto"; @@ -126,9 +125,7 @@ const TimelinePanel: Component<{ }; return ( - { - return

    Oops: {String(err)}

    - }}> + <> -
    + ); }; diff --git a/src/timelines/RegularToot.tsx b/src/timelines/RegularToot.tsx index 30376de..97178c3 100644 --- a/src/timelines/RegularToot.tsx +++ b/src/timelines/RegularToot.tsx @@ -36,7 +36,6 @@ 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; @@ -171,7 +170,6 @@ function TootActionGroup( function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) { const toot = () => props.status; - const dateFnLocale = useDateFnLocale() return (
    @@ -189,7 +187,7 @@ function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) { }} /> @{toot().account.username}@{new URL(toot().account.url).hostname} diff --git a/tsconfig.json b/tsconfig.json index 6cc68b0..ccd018b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,5 @@ "types": ["vite/client", "vite-plugin-pwa/solid"], "noEmit": true, "isolatedModules": true, - "resolveJsonModule": true, } }