import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from './languages.js'; const LANGUAGE_KEY = 'whowhat_language'; const SUPPORTED_CODES = SUPPORTED_LANGUAGES.map((l) => l.code); // Every locales//.json file is bundled here at build time and merged // into one dictionary per language, keyed by namespace (the filename without extension). const modules = import.meta.glob('./locales/*/*.json', { eager: true }); function buildDictionaries() { const dictionaries = {}; for (const path in modules) { const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json$/); if (!match) continue; const [, lang, namespace] = match; dictionaries[lang] = dictionaries[lang] || {}; dictionaries[lang][namespace] = modules[path].default; } return dictionaries; } const DICTIONARIES = buildDictionaries(); function resolve(dictionary, key) { const [namespace, ...rest] = key.split('.'); let node = dictionary?.[namespace]; for (const part of rest) { if (node == null) return undefined; node = node[part]; } return typeof node === 'string' ? node : undefined; } function interpolate(template, params) { if (!params) return template; return template.replace(/\{\{(\w+)\}\}/g, (match, name) => (name in params ? String(params[name]) : match)); } function detectInitialLanguage() { const stored = localStorage.getItem(LANGUAGE_KEY); if (stored && SUPPORTED_CODES.includes(stored)) return stored; const browserLang = (navigator.language || '').slice(0, 2).toLowerCase(); if (SUPPORTED_CODES.includes(browserLang)) return browserLang; return DEFAULT_LANGUAGE; } const I18nContext = createContext(null); export function I18nProvider({ children }) { const [language, setLanguageState] = useState(detectInitialLanguage); useEffect(() => { document.documentElement.setAttribute('lang', language); }, [language]); const setLanguage = useCallback((lang) => { if (!SUPPORTED_CODES.includes(lang)) return; localStorage.setItem(LANGUAGE_KEY, lang); setLanguageState(lang); }, []); const t = useCallback( (key, params) => { const value = resolve(DICTIONARIES[language], key) ?? resolve(DICTIONARIES[DEFAULT_LANGUAGE], key) ?? resolve(DICTIONARIES.en, key); if (value === undefined) return key; return interpolate(value, params); }, [language] ); // Translates an error code coming from the API (ApiError#message). Falls back to the // raw code (or a generic message) if the backend ever sends something not in errors.json. const tError = useCallback( (err) => { const code = err?.message; if (!code) return t('errors.generic'); return resolve(DICTIONARIES[language], `errors.${code}`) ?? code; }, [language, t] ); const value = useMemo( () => ({ language, setLanguage, languages: SUPPORTED_LANGUAGES, t, tError }), [language, setLanguage, t, tError] ); return {children}; } export function useTranslation() { const ctx = useContext(I18nContext); if (!ctx) throw new Error('useTranslation must be used within I18nProvider'); return ctx; }