"use client"; /** * Browser-side locale store + React bindings. * * English is the default and the source of truth; Czech is a full translation. * Only *interface* copy lives in the dictionaries — your notes and the generated * decks always keep their own language (that is a core feature, not a setting). * * The dictionaries themselves live in ./i18n-dict so the server can translate * its API messages into the same language. */ import { useSyncExternalStore } from "react"; import { DEFAULT_LOCALE, LOCALE_HEADER, LOCALES, normaliseLocale, translate, translatePlural, type Locale, type Vars, } from "./i18n-dict"; export { LOCALES, DEFAULT_LOCALE, LOCALE_HEADER, translate, translatePlural }; export type { Locale }; const STORAGE_KEY = "freezmo.locale.v1"; let cached: Locale | null = null; const listeners = new Set<() => void>(); function read(): Locale { if (typeof window === "undefined") return DEFAULT_LOCALE; try { return normaliseLocale(window.localStorage.getItem(STORAGE_KEY)); } catch { return DEFAULT_LOCALE; } } function emit(): void { listeners.forEach((listener) => listener()); } function onStorage(event: StorageEvent): void { if (event.key === STORAGE_KEY) { cached = read(); emit(); } } export const localeStore = { subscribe(listener: () => void): () => void { if (listeners.size === 0) window.addEventListener("storage", onStorage); listeners.add(listener); return () => { listeners.delete(listener); if (listeners.size === 0) window.removeEventListener("storage", onStorage); }; }, getSnapshot(): Locale { if (cached === null) cached = read(); return cached; }, getServerSnapshot(): Locale { return DEFAULT_LOCALE; }, }; export function getLocale(): Locale { return localeStore.getSnapshot(); } export function setLocale(locale: Locale): void { cached = normaliseLocale(locale); try { window.localStorage.setItem(STORAGE_KEY, cached); } catch { /* private mode */ } try { document.documentElement.lang = cached; } catch { /* SSR */ } emit(); } /** React hook: re-renders when the locale changes. */ export function useLocale(): Locale { return useSyncExternalStore(localeStore.subscribe, localeStore.getSnapshot, localeStore.getServerSnapshot); } /** Translate a key for the current locale. */ export function t(key: string, vars?: Vars): string { return translate(localeStore.getSnapshot(), key, vars); } /** Plural-aware translate for the current locale. */ export function plural(n: number, base: string, vars?: Vars): string { return translatePlural(localeStore.getSnapshot(), n, base, vars); } /** Header handed to our own API routes so they answer in the chosen language. */ export function localeHeader(): Record { return { [LOCALE_HEADER]: localeStore.getSnapshot() }; } /** * Convenience hook for components. * * `t` / `plural` are bound to the locale React is currently rendering with * (the server snapshot during hydration, then the stored choice), so the first * client render always matches the server HTML. Use the module-level `t` only * in event handlers and non-React helpers. */ export function useI18n() { const locale = useLocale(); return { locale, setLocale, t: (key: string, vars?: Vars) => translate(locale, key, vars), plural: (n: number, base: string, vars?: Vars) => translatePlural(locale, n, base, vars), }; }