import { createContext, useContext, useEffect, useMemo, type ReactNode, } from "react"; import { useSettingsStore, type Lang } from "@/lib/settings"; import { ar } from "@/lib/translations/ar"; import { en } from "@/lib/translations/en"; import { fr } from "@/lib/translations/fr"; export type Vars = Record; /** * Lightweight i18n. No external dependency: three languages, dot-path keys, * `{var}` interpolation, and a `one`/`other`/`zero` plural convention that * covers the few counts this app shows (minutes, seats). */ const DICTS: Record> = { en, ar, fr }; const interpolate = (text: string, vars?: Vars): string => vars ? text.replace(/\{(\w+)\}/g, (_, key: string) => String(vars[key] ?? "")) : text; const readPath = (dict: Record, path: string): unknown => path.split(".").reduce( (acc, segment) => acc && typeof acc === "object" ? (acc as Record)[segment] : undefined, dict, ); const translate = ( lang: Lang, key: string, vars?: Vars, count?: number, ): string => { const dict = DICTS[lang] ?? en; if (count !== undefined) { const pluralSuffix = count === 0 ? "zero" : count === 1 ? "one" : "other"; const pluralValue = readPath(dict, `${key}.${pluralSuffix}`); if (typeof pluralValue === "string") { return interpolate(pluralValue, { n: count, ...vars }); } } const value = readPath(dict, key); if (typeof value === "string") return interpolate(value, vars); // Fall back to English, then to the key itself, so a missing translation // never renders an empty string. if (lang !== "en") { const fallback = readPath(en, key); if (typeof fallback === "string") return interpolate(fallback, vars); } return key; }; /** * Module-level translator for non-React helpers (`lib/utils.ts`, * `lib/pricing.ts`) that can't call `useT`. The I18nProvider sets this on * mount and whenever the language changes; until then it defaults to English. */ let activeLang: Lang = "en"; export const setTranslator = (lang: Lang) => { activeLang = lang; }; export const tr = (key: string, vars?: Vars, count?: number): string => translate(activeLang, key, vars, count); type I18nContextValue = { lang: Lang; isRTL: boolean; t: (key: string, vars?: Vars, count?: number) => string; }; const I18nContext = createContext(null); export const I18nProvider = ({ children }: { children: ReactNode }) => { const lang = useSettingsStore((state) => state.lang); useEffect(() => { setTranslator(lang); }, [lang]); const value = useMemo( () => ({ lang, isRTL: lang === "ar", t: (key, vars, count) => translate(lang, key, vars, count), }), [lang], ); return ( {children} ); }; export const useT = (): I18nContextValue["t"] => { const context = useContext(I18nContext); if (!context) { throw new Error("useT must be used within I18nProvider."); } return context.t; }; export const useI18n = (): I18nContextValue => { const context = useContext(I18nContext); if (!context) { throw new Error("useI18n must be used within I18nProvider."); } return context; };