import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import en from './locales/en'; export const SUPPORTED_LANGUAGES = [ { code: 'en', label: 'English', flag: '\uD83C\uDDEC\uD83C\uDDE7' }, { code: 'fr', label: 'Fran\u00E7ais', flag: '\uD83C\uDDEB\uD83C\uDDF7' }, { code: 'de', label: 'Deutsch', flag: '\uD83C\uDDE9\uD83C\uDDEA' }, { code: 'zh', label: '\u4E2D\u6587', flag: '\uD83C\uDDE8\uD83C\uDDF3' }, { code: 'hi', label: '\u0939\u093F\u0928\u094D\u0926\u0940', flag: '\uD83C\uDDEE\uD83C\uDDF3' }, { code: 'hu', label: 'Magyar', flag: '\uD83C\uDDED\uD83C\uDDFA' }, ] as const; export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code']; const supportedCodes: Set = new Set(SUPPORTED_LANGUAGES.map((l) => l.code)); function toSupportedLanguage(value: string): LanguageCode | null { const lower = value.toLowerCase(); if (supportedCodes.has(lower)) return lower as LanguageCode; const prefix = lower.split('-')[0]; if (supportedCodes.has(prefix)) return prefix as LanguageCode; return null; } function getStoredLanguage(): LanguageCode | null { try { const stored = localStorage.getItem('language'); return stored ? toSupportedLanguage(stored) : null; } catch { return null; } } function getUrlLanguage(): LanguageCode | null { try { if (typeof window === 'undefined') return null; const value = new URLSearchParams(window.location.search).get('lang'); return value ? toSupportedLanguage(value) : null; } catch { return null; } } function getBrowserLanguages(): readonly string[] { if (typeof navigator === 'undefined') return []; return navigator.languages?.length ? navigator.languages : [navigator.language]; } function detectLanguage(): LanguageCode { // 1. Explicit URL language, used by generated screenshot/OG image URLs. const urlLanguage = getUrlLanguage(); if (urlLanguage) return urlLanguage; // 2. Explicit user choice (persisted from the language dropdown) const stored = getStoredLanguage(); if (stored) return stored; // 3. Browser preference (navigator.languages falls back to navigator.language) for (const tag of getBrowserLanguages()) { const language = toSupportedLanguage(tag); if (language) return language; } return 'en'; } export const INITIAL_LANGUAGE = detectLanguage(); type TranslationResource = Record; const localeLoaders: Record< Exclude, () => Promise<{ default: TranslationResource }> > = { fr: () => import('./locales/fr'), de: () => import('./locales/de'), hu: () => import('./locales/hu'), zh: () => import('./locales/zh'), hi: () => import('./locales/hi'), }; async function getLanguageResource( code: Exclude ): Promise { const module = await localeLoaders[code](); return module.default; } function setDocumentLanguage(code: LanguageCode) { if (typeof document !== 'undefined') { document.documentElement.lang = code; } } export async function loadLanguage(code: LanguageCode): Promise { if (code === 'en' || i18n.hasResourceBundle(code, 'translation')) return; const resource = await getLanguageResource(code); i18n.addResourceBundle(code, 'translation', resource, true, true); } export async function changeLanguage(code: LanguageCode): Promise { await loadLanguage(code); await i18n.changeLanguage(code); setDocumentLanguage(code); } export const i18nReady = (async () => { const resources: Record = { en: { translation: en }, }; let language = INITIAL_LANGUAGE; if (language !== 'en') { try { resources[language] = { translation: await getLanguageResource(language) }; } catch (error) { console.error(`Failed to load ${language} translations`, error); language = 'en'; } } await i18n.use(initReactI18next).init({ resources, lng: language, fallbackLng: 'en', interpolation: { escapeValue: false, // React already escapes }, }); setDocumentLanguage(language); })(); /** * Translate a key that is computed at runtime (not a literal). * Bypasses the strict type checking on t() for dynamic key construction. */ export function tDynamic(key: string): string { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (i18n.t as any)(key); } export default i18n;