tutu/src/platform/i18n.tsx

189 lines
5 KiB
TypeScript
Raw Normal View History

2024-09-26 09:23:40 +00:00
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";
2024-09-26 11:42:44 +00:00
import { resolveTemplate, translator, type Template } from "@solid-primitives/i18n";
2024-09-26 09:23:40 +00:00
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 = /* __@PURE__ */createContext<Accessor<Locale>>(() => enGB);
2024-09-26 09:23:40 +00:00
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();
}
type ImportFn<T> = (name: string) => Promise<{default: T}>
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
2024-09-26 09:23:40 +00:00
export function createStringResource<
T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) {
2024-09-26 09:23:40 +00:00
const language = useLanguage();
const cache: Record<string, MergedImportedModule<T>> = {};
2024-09-26 09:23:40 +00:00
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<T> = Object.assign({}, ...results)
cache[nlang] = merged;
2024-09-26 09:23:40 +00:00
return merged;
2024-09-26 09:23:40 +00:00
},
);
}
2024-09-26 11:42:44 +00:00
export function createTranslator<T extends ImportFn<Record<string, string | Template<any> | undefined>>[],>(...importFns: T) {
const res = createStringResource(...importFns)
return [translator(res[0], resolveTemplate), res] as const
}