diff --git a/bun.lockb b/bun.lockb index 8230f5f..25e9097 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 8cc3324..700f2f4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "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/masto/acct.ts b/src/masto/acct.ts index 97bbcec..ff9d488 100644 --- a/src/masto/acct.ts +++ b/src/masto/acct.ts @@ -12,11 +12,16 @@ export function useSignedInProfiles() { }); return [ () => { - const value = accessor(); - if (!value) { - return sessions().map((x) => ({ ...x, inf: x.account.inf })); + try { + const value = accessor(); + 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, ] as const; diff --git a/src/platform/i18n.tsx b/src/platform/i18n.tsx index a43a574..b31d888 100644 --- a/src/platform/i18n.tsx +++ b/src/platform/i18n.tsx @@ -20,9 +20,9 @@ async function synchronised( await navigator.locks.request(name, callback); } -export const SUPPORTED_LANGS = ["en", "zh-Hans"]; +export const SUPPORTED_LANGS = ["en", "zh-Hans"] as const; -export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"]; +export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"] as const; const DEFAULT_LANG = "en"; diff --git a/src/settings/ChooseLang.tsx b/src/settings/ChooseLang.tsx new file mode 100644 index 0000000..96a8db9 --- /dev/null +++ b/src/settings/ChooseLang.tsx @@ -0,0 +1,117 @@ +import { createMemo, For, type Component, type JSX } from "solid-js"; +import Scaffold from "../material/Scaffold"; +import { + AppBar, + Checkbox, + 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 new file mode 100644 index 0000000..53302d8 --- /dev/null +++ b/src/settings/ChooseRegion.tsx @@ -0,0 +1,110 @@ +import { createMemo, For, type Component, type JSX } from "solid-js"; +import Scaffold from "../material/Scaffold"; +import { + AppBar, + Checkbox, + 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, + autoMatchRegion, + createTranslator, + SUPPORTED_LANGS, + 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 bc35b7d..ad9f66b 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -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 { AppBar, @@ -40,10 +40,13 @@ import { 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 + ["lang.auto"]: Template<{ detected: string }>; +} & Record; const Settings: ParentComponent = () => { const [t] = createTranslator( @@ -51,7 +54,7 @@ const Settings: ParentComponent = () => { import(`./i18n/${code}.json`) as Promise<{ default: Strings; }>, - () => import(`./i18n/lang-names.json`) + () => import(`./i18n/lang-names.json`), ); const navigate = useNavigate(); const settings$ = useStore($settings); @@ -59,6 +62,8 @@ const Settings: ParentComponent = () => { needRefresh: [needRefresh], } = useRegisterSW(); const dateFnLocale = useDateFnLocale(); + const [langPickerOpen, setLangPickerOpen] = createSignal(false); + const [regionPickerOpen, setRegionPickerOpen] = createSignal(false); const [profiles] = useSignedInProfiles(); @@ -98,7 +103,7 @@ const Settings: ParentComponent = () => { {t("All Notifications")} - + @@ -114,13 +119,13 @@ const Settings: ParentComponent = () => { {t("Notifications")} - + - + {t("Sign out")} @@ -150,65 +155,55 @@ const Settings: ParentComponent = () => {
  • {t("This Application")} - + - {t("Language")} - - { - $settings.setKey( - "language", - e.currentTarget.value === "xauto" - ? undefined - : e.currentTarget.value, - ); - }} - > - - - {(code) => } - - - - + + {t("Language")} + + + + $settings.setKey("language", nval)} + onClose={[setLangPickerOpen, false]} + /> + - + - {t("Region")} - - { - $settings.setKey( - "region", - e.currentTarget.value === "xauto" - ? undefined - : e.currentTarget.value, - ); - }} - > - - - {(code) => ( - - )} - - - - + + {t("Region")} + + + + $settings.setKey("region", nval)} + onClose={[setRegionPickerOpen, false]} + /> + diff --git a/src/settings/i18n/en.json b/src/settings/i18n/en.json index 1149933..095bc04 100644 --- a/src/settings/i18n/en.json +++ b/src/settings/i18n/en.json @@ -14,12 +14,17 @@ "version": "Using v{{packageVersion}} (built on {{builtAt}}, {{buildMode}})", "Language": "Language", "Region": "Region", - "lang.auto": "Auto({{detected}})", - "region.auto": "Auto({{detected}})", + "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" + "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/zh-Hans.json b/src/settings/i18n/zh-Hans.json index 4d046df..8fd70d1 100644 --- a/src/settings/i18n/zh-Hans.json +++ b/src/settings/i18n/zh-Hans.json @@ -1,6 +1,6 @@ { "Settings": "设置", - "Accounts": "所有账号", + "Accounts": "所有账户", "All Notifications": "所有通知", "Sign in...": "登录新账户...", "Reading": "阅读", @@ -14,12 +14,17 @@ "version": "正在使用 v{{packageVersion}} ({{builtAt}}构建, {{buildMode}})", "Language": "语言", "Region": "区域", - "lang.auto": "自动({{detected}})", - "region.auto": "自动({{detected}})", + "lang.auto": "(自动){{detected}}", + "region.auto": "(自动){{detected}}", "region.en_GB": "英国和苏格兰(英语)", "region.en_US": "美国(英语)", "region.zh_CN": "中国大陆(中文)", "datefmt": "yyyy年MM月dd日", "Sign out": "登出此账户", - "Notifications": "通知" + "Notifications": "通知", + + "Choose Language": "选择语言", + "Supported": "已支持", + "Unsupported": "尚未支持", + "Choose Region": "选择区域" } \ No newline at end of file diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index c09023d..b83de4d 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -10,7 +10,8 @@ import { children, Suspense, Match, - Switch as JsSwitch + Switch as JsSwitch, + ErrorBoundary } from "solid-js"; import { useDocumentTitle } from "../utils"; import { type mastodon } from "masto"; @@ -125,7 +126,9 @@ const TimelinePanel: Component<{ }; return ( - <> + { + return

    Oops: {String(err)}

    + }}> - +
    ); };