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 { 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 { const { dateFn } = useAppLocale(); return dateFn; } export function createCurrentLanguage() { 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 = createCurrentLanguage(); 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< T extends ImportFn | 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(); 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, ); }