import { createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js" import type { ParentComponent } from "solid-js" import { useConfig } from "../../stores/preferences" import { enMessages } from "./messages/en" import { esMessages } from "./messages/es" import { frMessages } from "./messages/fr" import { ruMessages } from "./messages/ru" import { jaMessages } from "./messages/ja" import { zhHansMessages } from "./messages/zh-Hans" type Messages = Record export type TranslateParams = Record export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans" const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const const messagesByLocale: Record = { en: enMessages, es: esMessages, fr: frMessages, ru: ruMessages, ja: jaMessages, "zh-Hans": zhHansMessages, } function normalizeLocaleTag(value: string): string { return value.trim().replace(/_/g, "-") } function matchSupportedLocale(value: string | undefined): Locale | null { if (!value) return null const normalized = normalizeLocaleTag(value) const lower = normalized.toLowerCase() const supportedLower = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale])) const exact = supportedLower.get(lower) if (exact) return exact const parts = lower.split("-") const base = parts[0] if (!base) return null if (base === "zh") { const zhHans = supportedLower.get("zh-hans") return zhHans ?? null } const baseMatch = supportedLower.get(base) return baseMatch ?? null } function detectNavigatorLocale(): Locale | null { if (typeof navigator === "undefined") return null const candidates = Array.isArray(navigator.languages) && navigator.languages.length > 0 ? navigator.languages : navigator.language ? [navigator.language] : [] for (const candidate of candidates) { const match = matchSupportedLocale(candidate) if (match) return match } return null } function interpolate(template: string, params?: Record): string { if (!params) return template return template.replace(/\{(\w+)\}/g, (_match, key: string) => { const value = params[key] return value === undefined || value === null ? "" : String(value) }) } function translateFrom(messages: Messages, key: string, params?: TranslateParams): string { const current = messages[key] const fallback = enMessages[key as keyof typeof enMessages] const template = current ?? fallback ?? key return interpolate(template, params) } const [globalRevision, setGlobalRevision] = createSignal(0) const initialGlobalLocale: Locale = detectNavigatorLocale() ?? "en" let globalMessages: Messages = messagesByLocale[initialGlobalLocale] export function tGlobal(key: string, params?: TranslateParams): string { globalRevision() return translateFrom(globalMessages, key, params) } export interface I18nContextValue { locale: () => Locale t: (key: string, params?: TranslateParams) => string } const I18nContext = createContext() export const I18nProvider: ParentComponent = (props) => { const { preferences } = useConfig() const [detectedLocale, setDetectedLocale] = createSignal("en") const previousMessages = globalMessages onMount(() => { const detected = detectNavigatorLocale() if (detected) setDetectedLocale(detected) }) const locale = createMemo(() => { const configured = matchSupportedLocale(preferences().locale) return configured ?? detectedLocale() ?? "en" }) const messages = createMemo(() => messagesByLocale[locale()]) function t(key: string, params?: TranslateParams): string { return translateFrom(messages(), key, params) } createEffect(() => { globalMessages = messages() setGlobalRevision((value) => value + 1) }) onCleanup(() => { globalMessages = previousMessages setGlobalRevision((value) => value + 1) }) const value: I18nContextValue = { locale, t, } return {props.children} } export function useI18n(): I18nContextValue { const context = useContext(I18nContext) if (!context) { throw new Error("useI18n must be used within I18nProvider") } return context }