tutu/src/platform/i18n.tsx
2024-11-23 23:39:52 +08:00

184 lines
4.5 KiB
TypeScript

import {
catchError,
createContext,
createMemo,
createResource,
useContext,
} from "solid-js";
import { match } from "@formatjs/intl-localematcher";
import { Accessor } 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";
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);
}
export function autoMatchRegion() {
const specifiers = navigator.languages.map((x) => x.split("-"));
for (const s of specifiers) {
if (s.length === 1) {
const lang = s[0];
for (const available of SUPPORTED_REGIONS) {
if (available.toLowerCase().startsWith(lang.toLowerCase())) {
return available;
}
}
} else if (s.length === 2) {
const [lang, region] = s;
for (const available of SUPPORTED_REGIONS) {
if (available.toLowerCase() === `${lang}_${region}`.toLowerCase()) {
return available;
}
}
}
}
return "en_GB";
}
export function createCurrentRegion() {
const appSettings = useStore($settings);
return createMemo(
() => {
const settings = appSettings();
if (typeof settings.region !== "undefined") {
return settings.region;
} else {
return autoMatchRegion();
}
},
"en_GB",
{ name: "region" },
);
}
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 enGB;
case "zh_cn":
return (await import("date-fns/locale/zh-CN")).zhCN;
default:
throw new TypeError(`unsupported tag "${tag}"`);
}
}
/**
* 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 { dateFn } = useAppLocale();
return dateFn;
}
export function createCurrentLanguage() {
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;
export function createStringResource<
T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) {
const language = createCurrentLanguage();
const cache: Record<string, MergedImportedModule<T>> = {};
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;
return merged;
},
);
}
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;
}
export type AppLocale = {
dateFn: () => Locale;
language: () => string;
region: () => string;
};
const AppLocaleContext = /* @__PURE__ */ createContext<AppLocale>();
export const AppLocaleProvider = AppLocaleContext.Provider;
export function useAppLocale() {
const l = useContext(AppLocaleContext);
if (!l) {
throw new TypeError("app locale not found");
}
return l;
}
export function createDateFnLocaleResource(region: () => string) {
const [localeUncaught] = createResource(
region,
async (region) => {
return await importDateFnLocale(region);
},
{ initialValue: enGB },
);
return createMemo(
() =>
catchError(localeUncaught, (reason) => {
console.error("fetch date-fns locale", reason);
}) ?? enGB,
);
}