"use client"; import { createContext, useContext, useState, useCallback, useEffect, useMemo, type ReactNode, } from "react"; import enMessages from "./i18n/messages/en"; export type Locale = | "en" | "fr" | "es" | "de" | "pt" | "it" | "nl" | "ru" | "ja" | "ko" | "zh" | "ar" | "fa"; const VALID_LOCALES: ReadonlyArray = [ "en", "fr", "es", "de", "pt", "it", "nl", "ru", "ja", "ko", "zh", "ar", "fa", ]; const DEFAULT_LOCALE: Locale = "en"; const RTL_LOCALES: ReadonlyArray = ["ar", "fa"]; type TranslationParams = Record; interface I18nContextValue { locale: Locale; dir: "ltr" | "rtl"; isRTL: boolean; setLocale: (locale: Locale) => void; t: (key: string, params?: TranslationParams) => string; isLoading: boolean; } const I18nContext = createContext(null); const messageCache: Partial>> = { en: enMessages, }; function interpolate(template: string, params?: TranslationParams): string { if (!params) return template; return Object.entries(params).reduce( (acc, [k, v]) => acc.replace(new RegExp(`\\{${k}\\}`, "g"), String(v)), template, ); } function detectInitialLocale(): Locale { if (typeof window === "undefined") return DEFAULT_LOCALE; const saved = localStorage.getItem("locale"); if (saved && (VALID_LOCALES as readonly string[]).includes(saved)) { return saved as Locale; } const primary = navigator.language.split("-")[0]; if ((VALID_LOCALES as readonly string[]).includes(primary)) { return primary as Locale; } const allLangs = navigator.languages || []; for (const lang of allLangs) { const code = lang.split("-")[0]; if ((VALID_LOCALES as readonly string[]).includes(code)) { return code as Locale; } } return DEFAULT_LOCALE; } async function loadLocaleMessages(locale: Locale): Promise> { if (messageCache[locale]) return messageCache[locale]; try { const mod = await import(`./i18n/messages/${locale}`); messageCache[locale] = mod.default as Record; return messageCache[locale]; } catch (err) { console.warn(`Failed to load locale messages for ${locale}`, err); messageCache[locale] = enMessages; return enMessages; } } export function formatDate( date: Date, locale: Locale, options?: Intl.DateTimeFormatOptions, ): string { const calendar = locale === "fa" ? "fa-IR-u-ca-persian" : locale === "ar" ? "ar-SA" : locale; const defaults: Intl.DateTimeFormatOptions = { day: "numeric", month: "long", year: "numeric", }; return date.toLocaleDateString(calendar, { ...defaults, ...options }); } export function I18nProvider({ children }: { children: ReactNode }) { const [locale, setLocaleState] = useState(detectInitialLocale); const [messages, setMessages] = useState>(enMessages); const [isLoading, setIsLoading] = useState(false); const isRTL = (RTL_LOCALES as readonly string[]).includes(locale); const dir = isRTL ? ("rtl" as const) : ("ltr" as const); useEffect(() => { document.documentElement.dir = dir; document.documentElement.lang = locale; }, [locale, dir]); useEffect(() => { let cancelled = false; if (locale === "en") { setMessages(enMessages); return; } setIsLoading(true); loadLocaleMessages(locale).then((loaded) => { if (!cancelled) { setMessages(loaded); setIsLoading(false); } }); return () => { cancelled = true; }; }, [locale]); const setLocale = useCallback((newLocale: Locale) => { setLocaleState(newLocale); localStorage.setItem("locale", newLocale); }, []); const t = useCallback( (key: string, params?: TranslationParams): string => { const msg = messages[key] ?? enMessages[key] ?? key; return interpolate(msg, params); }, [messages], ); const value = useMemo( () => ({ locale, dir, isRTL, setLocale, t, isLoading }), [locale, dir, isRTL, setLocale, t, isLoading], ); return ( {children} ); } export function useI18n() { const ctx = useContext(I18nContext); if (!ctx) { throw new Error("useI18n must be used within an I18nProvider"); } return ctx; } export function useTranslation() { const { t } = useI18n(); return { t }; }