2024-09-26 17:23:40 +08: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-11-04 00:13:21 +08:00
|
|
|
import {
|
|
|
|
resolveTemplate,
|
|
|
|
translator,
|
|
|
|
type Template,
|
|
|
|
} from "@solid-primitives/i18n";
|
2024-09-26 17:23:40 +08:00
|
|
|
|
|
|
|
async function synchronised(
|
|
|
|
name: string,
|
|
|
|
callback: () => Promise<void> | void,
|
|
|
|
): Promise<void> {
|
|
|
|
await navigator.locks.request(name, callback);
|
|
|
|
}
|
|
|
|
|
2024-09-27 14:15:34 +08:00
|
|
|
export const SUPPORTED_LANGS = ["en", "zh-Hans"] as const;
|
2024-09-26 17:23:40 +08:00
|
|
|
|
2024-09-27 14:15:34 +08:00
|
|
|
export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"] as const;
|
2024-09-26 17:23:40 +08:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
const DateFnLocaleCx = /* __@PURE__ */ createContext<Accessor<Locale>>(
|
|
|
|
() => enGB,
|
|
|
|
);
|
2024-09-26 17:23:40 +08:00
|
|
|
|
|
|
|
const cachedDateFnLocale: Record<string, Locale> = {
|
|
|
|
enGB,
|
|
|
|
};
|
|
|
|
|
|
|
|
export function autoMatchRegion() {
|
2024-11-04 00:13:21 +08:00
|
|
|
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;
|
|
|
|
}
|
2024-09-26 17:23:40 +08:00
|
|
|
}
|
2024-11-04 00:13:21 +08:00
|
|
|
} else if (s.length === 2) {
|
|
|
|
const [lang, region] = s[1];
|
|
|
|
for (const available of SUPPORTED_REGIONS) {
|
|
|
|
if (available.toLowerCase() === `${lang}_${region}`.toLowerCase()) {
|
|
|
|
return available;
|
|
|
|
}
|
2024-09-26 17:23:40 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-11-04 00:13:21 +08:00
|
|
|
|
2024-09-26 17:23:40 +08:00
|
|
|
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":
|
2024-10-22 17:48:17 +08:00
|
|
|
return enGB;
|
2024-09-26 17:23:40 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
type ImportFn<T> = (name: string) => Promise<{ default: T }>;
|
2024-09-26 19:36:46 +08:00
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
type ImportedModule<F> = F extends ImportFn<infer T> ? T : never;
|
2024-09-26 19:36:46 +08:00
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
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 19:36:46 +08:00
|
|
|
|
2024-09-26 17:23:40 +08:00
|
|
|
export function createStringResource<
|
2024-09-26 19:36:46 +08:00
|
|
|
T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
|
|
|
|
>(...importFns: T) {
|
2024-09-26 17:23:40 +08:00
|
|
|
const language = useLanguage();
|
2024-09-26 19:36:46 +08:00
|
|
|
const cache: Record<string, MergedImportedModule<T>> = {};
|
2024-09-26 17:23:40 +08:00
|
|
|
|
|
|
|
return createResource(
|
|
|
|
() => [language()] as const,
|
|
|
|
async ([nlang]) => {
|
|
|
|
if (cache[nlang]) {
|
|
|
|
return cache[nlang];
|
|
|
|
}
|
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
const results = await Promise.all(
|
|
|
|
importFns.map((x) => x(nlang).then((v) => v.default)),
|
|
|
|
);
|
2024-09-26 19:36:46 +08:00
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
const merged: MergedImportedModule<T> = Object.assign({}, ...results);
|
2024-09-26 19:36:46 +08:00
|
|
|
|
|
|
|
cache[nlang] = merged;
|
2024-09-26 17:23:40 +08:00
|
|
|
|
2024-09-26 19:36:46 +08:00
|
|
|
return merged;
|
2024-09-26 17:23:40 +08:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2024-09-26 19:42:44 +08:00
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
export function createTranslator<
|
|
|
|
T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
|
|
|
|
>(...importFns: T) {
|
|
|
|
const res = createStringResource(...importFns);
|
2024-09-26 19:42:44 +08:00
|
|
|
|
2024-11-04 00:13:21 +08:00
|
|
|
return [translator(res[0], resolveTemplate), res] as const;
|
2024-09-26 19:42:44 +08:00
|
|
|
}
|