first prototype of i18n system
This commit is contained in:
parent
d3a7602fb5
commit
b4fa751345
9 changed files with 348 additions and 20 deletions
170
src/platform/i18n.tsx
Normal file
170
src/platform/i18n.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
import {
|
||||
ParentComponent,
|
||||
createContext,
|
||||
createMemo,
|
||||
createRenderEffect,
|
||||
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";
|
||||
import type { Template } from "@solid-primitives/i18n";
|
||||
|
||||
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 = createContext<Accessor<Locale>>(() => enGB);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export function createStringResource<
|
||||
M extends Record<string, string | Template<any>>,
|
||||
>(importFn: (code: string) => Promise<{ default: M }>) {
|
||||
const language = useLanguage();
|
||||
const cache: Record<string, M | undefined> = {};
|
||||
|
||||
return createResource(
|
||||
() => [language()] as const,
|
||||
async ([nlang]) => {
|
||||
if (cache[nlang]) {
|
||||
return cache[nlang];
|
||||
}
|
||||
|
||||
const { default: dict } = await importFn(`${nlang}`);
|
||||
|
||||
return dict;
|
||||
},
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue