146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
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<string> = 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<string, unknown>;
|
|
|
|
const localeLoaders: Record<
|
|
Exclude<LanguageCode, 'en'>,
|
|
() => 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<LanguageCode, 'en'>
|
|
): Promise<TranslationResource> {
|
|
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<void> {
|
|
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<void> {
|
|
await loadLanguage(code);
|
|
await i18n.changeLanguage(code);
|
|
setDocumentLanguage(code);
|
|
}
|
|
|
|
export const i18nReady = (async () => {
|
|
const resources: Record<string, { translation: TranslationResource }> = {
|
|
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;
|