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"; import { resolveTemplate, translator, type Template } from "@solid-primitives/i18n"; async function synchronised( name: string, callback: () => Promise | void, ): Promise { await navigator.locks.request(name, callback); } 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); } const DateFnLocaleCx = /* __@PURE__ */createContext>(() => enGB); const cachedDateFnLocale: Record = { 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 { 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 ( {props.children} ); }; /** * Get the {@link Locale} object for date-fns. * * This function must be using in {@link DateFnScope} * * @returns Accessor for Locale */ export function useDateFnLocale(): Accessor { const cx = useContext(DateFnLocaleCx); return cx; } export function useLanguage() { const settings = useStore($settings); return () => settings().language || autoMatchLangTag(); } type ImportFn = (name: string) => Promise<{default: T}> type ImportedModule = F extends ImportFn ? T: never type MergedImportedModule = T extends [] ? {} : T extends [infer I] ? ImportedModule : T extends [infer I, ...infer J] ? ImportedModule & MergedImportedModule : never export function createStringResource< T extends ImportFn | undefined>>[], >(...importFns: T) { const language = useLanguage(); const cache: Record> = {}; 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 = Object.assign({}, ...results) cache[nlang] = merged; return merged; }, ); } export function createTranslator | undefined>>[],>(...importFns: T) { const res = createStringResource(...importFns) return [translator(res[0], resolveTemplate), res] as const }