I18N support #22

Merged
Rubicon merged 5 commits from feat-i18n into master 2024-09-27 06:20:32 +00:00
6 changed files with 58 additions and 32 deletions
Showing only changes of commit c49ae6fe0a - Show all commits

View file

@ -35,7 +35,7 @@ export function autoMatchLangTag() {
return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG); return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG);
} }
const DateFnLocaleCx = createContext<Accessor<Locale>>(() => enGB); const DateFnLocaleCx = /* __@PURE__ */createContext<Accessor<Locale>>(() => enGB);
const cachedDateFnLocale: Record<string, Locale> = { const cachedDateFnLocale: Record<string, Locale> = {
enGB, enGB,
@ -149,11 +149,20 @@ export function useLanguage() {
return () => settings().language || autoMatchLangTag(); return () => settings().language || autoMatchLangTag();
} }
type ImportFn<T> = (name: string) => Promise<{default: T}>
Rubicon marked this conversation as resolved Outdated

That's a needed feature: merging multiple resource into one.
Like:

createStringResource((code) => import(`./i18n/${code}.json`), (code) => import(`./i18n/${code}.js`))

Such feature can help us to reuse some common strings.

That's a needed feature: merging multiple resource into one. Like: ```js createStringResource((code) => import(`./i18n/${code}.json`), (code) => import(`./i18n/${code}.js`)) ``` Such feature can help us to reuse some common strings.
type ImportedModule<F> = F extends ImportFn<infer T> ? T: never
type MergedImportedModule<T> =
T extends [] ? {} :
T extends [infer I] ? ImportedModule<I> :
T extends [infer I, ...infer J] ? ImportedModule<I> & MergedImportedModule<J> : never
export function createStringResource< export function createStringResource<
M extends Record<string, string | Template<any>>, T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(importFn: (code: string) => Promise<{ default: M }>) { >(...importFns: T) {
const language = useLanguage(); const language = useLanguage();
const cache: Record<string, M | undefined> = {}; const cache: Record<string, MergedImportedModule<T>> = {};
return createResource( return createResource(
() => [language()] as const, () => [language()] as const,
@ -162,9 +171,13 @@ export function createStringResource<
return cache[nlang]; return cache[nlang];
} }
const { default: dict } = await importFn(`${nlang}`); const results = await Promise.all(importFns.map(x => x(nlang).then(v => v.default)))
return dict; const merged: MergedImportedModule<T> = Object.assign({}, ...results)
cache[nlang] = merged;
return merged;
}, },
); );
} }

View file

@ -17,6 +17,7 @@ import {
} from "@suid/material"; } from "@suid/material";
import { import {
Close as CloseIcon, Close as CloseIcon,
Logout,
Public as PublicIcon, Public as PublicIcon,
Refresh as RefreshIcon, Refresh as RefreshIcon,
Translate as TranslateIcon, Translate as TranslateIcon,
@ -38,11 +39,19 @@ import {
SUPPORTED_REGIONS, SUPPORTED_REGIONS,
useDateFnLocale, useDateFnLocale,
} from "../platform/i18n.jsx"; } from "../platform/i18n.jsx";
import { resolveTemplate, translator } from "@solid-primitives/i18n"; import { resolveTemplate, translator, type Template } from "@solid-primitives/i18n";
type Strings = {
["lang.auto"]: Template<{detected: string}>
} & Record<string, string | undefined>
Rubicon marked this conversation as resolved Outdated

I hope we can use one call to create this instead of two. This use will be very common.

I hope we can use one call to create this instead of two. This use will be very common.
const Settings: ParentComponent = () => { const Settings: ParentComponent = () => {
const [strings] = createStringResource( const [strings] = createStringResource(
(code) => import(`./i18n/${code}.json`), (code) =>
import(`./i18n/${code}.json`) as Promise<{
default: Strings;
}>,
() => import(`./i18n/lang-names.json`)
); );
const t = translator(strings, resolveTemplate); const t = translator(strings, resolveTemplate);
const navigate = useNavigate(); const navigate = useNavigate();
@ -87,31 +96,34 @@ const Settings: ParentComponent = () => {
<li> <li>
<ul> <ul>
<ListSubheader>{t("Accounts")}</ListSubheader> <ListSubheader>{t("Accounts")}</ListSubheader>
<ListItem> <ListItemButton disabled>
<ListItemText>{t("All Notifications")}</ListItemText> <ListItemText>{t("All Notifications")}</ListItemText>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch value={false} /> <Switch value={false} disabled/>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItemButton>
<Divider /> <Divider />
<ListItem> <ListItemButton disabled>
<ListItemText>{t("Sign in...")}</ListItemText> <ListItemText>{t("Sign in...")}</ListItemText>
</ListItem> </ListItemButton>
<Divider /> <Divider />
</ul> </ul>
<For each={profiles()}> <For each={profiles()}>
{({ account: acct, inf }) => ( {({ account: acct, inf }) => (
<ul data-site={acct.site} data-username={inf?.username}> <ul data-site={acct.site} data-username={inf?.username}>
<ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader> <ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader>
<ListItem> <ListItemButton disabled>
<ListItemText>Notifications</ListItemText> <ListItemText>{t("Notifications")}</ListItemText>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch value={false} /> <Switch value={false} disabled/>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItemButton>
<Divider /> <Divider />
<ListItemButton onClick={[doSignOut, acct]}> <ListItemButton onClick={[doSignOut, acct]}>
<ListItemText>Sign out</ListItemText> <ListItemIcon>
<Logout/>
</ListItemIcon>
<ListItemText>{t("Sign out")}</ListItemText>
</ListItemButton> </ListItemButton>
<Divider /> <Divider />
</ul> </ul>
@ -120,11 +132,7 @@ const Settings: ParentComponent = () => {
</li> </li>
<li> <li>
<ListSubheader>{t("Reading")}</ListSubheader> <ListSubheader>{t("Reading")}</ListSubheader>
<ListItem> <ListItemButton
<ListItemText secondary="Regular">Fonts</ListItemText>
</ListItem>
<Divider />
<ListItem
onClick={(e) => onClick={(e) =>
$settings.setKey( $settings.setKey(
"prefetchTootsDisabled", "prefetchTootsDisabled",
@ -138,7 +146,7 @@ const Settings: ParentComponent = () => {
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch checked={!settings$().prefetchTootsDisabled} /> <Switch checked={!settings$().prefetchTootsDisabled} />
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItemButton>
<Divider /> <Divider />
</li> </li>
<li> <li>
@ -162,7 +170,7 @@ const Settings: ParentComponent = () => {
> >
<option value={"xauto"}> <option value={"xauto"}>
{t("lang.auto", { {t("lang.auto", {
detected: t("lang." + autoMatchLangTag()), detected: t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(),
})} })}
</option> </option>
<For each={SUPPORTED_LANGS}> <For each={SUPPORTED_LANGS}>
@ -191,7 +199,7 @@ const Settings: ParentComponent = () => {
> >
<option value={"xauto"}> <option value={"xauto"}>
{t("region.auto", { {t("region.auto", {
detected: t("region." + autoMatchRegion()), detected: t("region." + autoMatchRegion()) ?? autoMatchRegion(),
})} })}
</option> </option>
<For each={SUPPORTED_REGIONS}> <For each={SUPPORTED_REGIONS}>

View file

@ -15,11 +15,11 @@
"Language": "Language", "Language": "Language",
"Region": "Region", "Region": "Region",
"lang.auto": "Auto({{detected}})", "lang.auto": "Auto({{detected}})",
"lang.zh-Hans": "中文(简体)",
"lang.en": "English",
"region.auto": "Auto({{detected}})", "region.auto": "Auto({{detected}})",
"region.en_GB": "Great Britan (English)", "region.en_GB": "Great Britan (English)",
"region.en_US": "United States (English)", "region.en_US": "United States (English)",
"region.zh_CN": "China Mainland (Chinese)", "region.zh_CN": "China Mainland (Chinese)",
"datefmt": "yyyy/MM/dd" "datefmt": "yyyy/MM/dd",
"Sign out": "Sign out",
"Notifications": "Notifications"
} }

View file

@ -0,0 +1,4 @@
{
"lang.zh-Hans": "中文(简体)",
"lang.en": "English"
}

View file

@ -15,11 +15,11 @@
"Language": "语言", "Language": "语言",
"Region": "区域", "Region": "区域",
"lang.auto": "自动({{detected}}", "lang.auto": "自动({{detected}}",
"lang.zh-Hans": "中文(简体)",
"lang.en": "English",
"region.auto": "自动({{detected}}", "region.auto": "自动({{detected}}",
"region.en_GB": "英国和苏格兰(英语)", "region.en_GB": "英国和苏格兰(英语)",
"region.en_US": "美国(英语)", "region.en_US": "美国(英语)",
"region.zh_CN": "中国大陆(中文)", "region.zh_CN": "中国大陆(中文)",
"datefmt": "yyyy年MM月dd日" "datefmt": "yyyy年MM月dd日",
"Sign out": "登出此账户",
"Notifications": "通知"
} }

View file

@ -11,5 +11,6 @@
"types": ["vite/client", "vite-plugin-pwa/solid"], "types": ["vite/client", "vite-plugin-pwa/solid"],
"noEmit": true, "noEmit": true,
"isolatedModules": true, "isolatedModules": true,
"resolveJsonModule": true,
} }
} }