| import i18next from 'i18next'; |
| import LanguageDetector from 'i18next-browser-languagedetector'; |
| import en from './locales/en.json'; |
| import { resolveEffectiveWelcomeContentLanguage } from './welcome-language'; |
|
|
| type TranslationDictionary = Record<string, unknown>; |
|
|
| const SUPPORTED_LANGUAGES = ['en', 'bg', 'cs', 'fr', 'de', 'el', 'es', 'hr', 'hu', 'it', 'pl', 'pt', 'nl', 'sv', 'ru', 'ar', 'fa', 'zh', 'ja', 'ko', 'ro', 'tr', 'th', 'vi', 'hi'] as const; |
| type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number]; |
| const SUPPORTED_SET = new Set<SupportedLanguage>(SUPPORTED_LANGUAGES); |
| const loadedLanguages = new Set<SupportedLanguage>(['en']); |
|
|
| const RTL_LANGUAGES = new Set(['ar', 'fa']); |
|
|
| const localeModules = import.meta.glob<TranslationDictionary>( |
| ['./locales/*.json', '!./locales/en.json'], |
| { import: 'default' }, |
| ); |
|
|
| function normalize(lng: string): SupportedLanguage { |
| const base = (lng || 'en').split('-')[0]?.toLowerCase() || 'en'; |
| return SUPPORTED_SET.has(base as SupportedLanguage) ? base as SupportedLanguage : 'en'; |
| } |
|
|
| async function ensureLoaded(lng: string): Promise<SupportedLanguage> { |
| const n = normalize(lng); |
| if (loadedLanguages.has(n)) return n; |
| const loader = localeModules[`./locales/${n}.json`]; |
| const translation = loader ? await loader() : en as TranslationDictionary; |
| i18next.addResourceBundle(n, 'translation', translation, true, true); |
| loadedLanguages.add(n); |
| return n; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| const OG_LOCALE: Record<string, string> = { |
| en: 'en_US', bg: 'bg_BG', cs: 'cs_CZ', fr: 'fr_FR', de: 'de_DE', el: 'el_GR', |
| es: 'es_ES', hr: 'hr_HR', hu: 'hu_HU', it: 'it_IT', pl: 'pl_PL', pt: 'pt_BR', |
| nl: 'nl_NL', sv: 'sv_SE', ru: 'ru_RU', ar: 'ar_SA', fa: 'fa_IR', zh: 'zh_CN', |
| ja: 'ja_JP', ko: 'ko_KR', ro: 'ro_RO', tr: 'tr_TR', th: 'th_TH', vi: 'vi_VN', |
| hi: 'hi_IN', |
| }; |
|
|
| function applyMetaTags(prefix = 'meta', contentLanguage = currentLanguageBase()): void { |
| const title = i18next.t(`${prefix}.title`); |
| const desc = i18next.t(`${prefix}.description`); |
| const ogTitle = i18next.t(`${prefix}.ogTitle`); |
| const ogDesc = i18next.t(`${prefix}.ogDescription`); |
|
|
| document.title = title; |
| const set = (sel: string, val: string) => { |
| const el = document.querySelector(sel); |
| if (el) el.setAttribute('content', val); |
| }; |
| set('meta[name="description"]', desc); |
| set('meta[property="og:title"]', ogTitle); |
| set('meta[property="og:description"]', ogDesc); |
| set( |
| 'meta[property="og:locale"]', |
| OG_LOCALE[contentLanguage] || `${contentLanguage}_${contentLanguage.toUpperCase()}`, |
| ); |
| set('meta[name="twitter:title"]', ogTitle); |
| set('meta[name="twitter:description"]', ogDesc); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function initI18n(options?: { metaPrefix?: string }): Promise<void> { |
| const metaPrefix = options?.metaPrefix ?? 'meta'; |
| if (i18next.isInitialized) return; |
| |
| |
| |
| try { localStorage.removeItem('i18nextLng'); } catch { } |
| await i18next.use(LanguageDetector).init({ |
| resources: { en: { translation: en as TranslationDictionary } }, |
| supportedLngs: [...SUPPORTED_LANGUAGES], |
| nonExplicitSupportedLngs: true, |
| fallbackLng: 'en', |
| interpolation: { escapeValue: false }, |
| detection: { order: ['querystring', 'navigator'], lookupQuerystring: 'lang', caches: [] }, |
| }); |
| const detected = await ensureLoaded(i18next.language || 'en'); |
| if (detected !== 'en') await i18next.changeLanguage(detected); |
| const base = (i18next.language || detected).split('-')[0] || 'en'; |
| const contentLanguage = metaPrefix === 'welcome.meta' |
| ? effectiveWelcomeContentLanguage() |
| : base; |
| if (metaPrefix === 'welcome.meta' && contentLanguage !== base) { |
| |
| |
| |
| await i18next.changeLanguage(contentLanguage); |
| } |
| document.documentElement.setAttribute( |
| 'lang', |
| contentLanguage === 'zh' ? 'zh-CN' : contentLanguage, |
| ); |
| if (RTL_LANGUAGES.has(contentLanguage)) { |
| document.documentElement.setAttribute('dir', 'rtl'); |
| } else { |
| document.documentElement.removeAttribute('dir'); |
| } |
| applyMetaTags(metaPrefix, contentLanguage); |
| } |
|
|
| export async function initStaticI18n(): Promise<void> { |
| if (i18next.isInitialized) { |
| if (currentLanguageBase() !== 'en') { |
| await i18next.changeLanguage('en'); |
| } |
| return; |
| } |
| await i18next.init({ |
| resources: { en: { translation: en as TranslationDictionary } }, |
| lng: 'en', |
| fallbackLng: 'en', |
| interpolation: { escapeValue: false }, |
| }); |
| } |
|
|
| export function currentLanguageBase(): string { |
| return (i18next.language || 'en').split('-')[0] || 'en'; |
| } |
|
|
| |
| |
| |
| |
| |
| export function effectiveWelcomeContentLanguage(): string { |
| const currentLanguage = currentLanguageBase(); |
| const resources = i18next.getResourceBundle( |
| currentLanguage, |
| 'translation', |
| ) as TranslationDictionary | undefined; |
| return resolveEffectiveWelcomeContentLanguage( |
| currentLanguage, |
| (en as TranslationDictionary).welcome, |
| resources, |
| ); |
| } |
|
|
| export function t(key: string, options?: Record<string, unknown>): string { |
| return i18next.t(key, options); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function tArray(key: string): string[] | null { |
| const value: unknown = i18next.t(key, { returnObjects: true, defaultValue: null }); |
| return Array.isArray(value) && value.every((v): v is string => typeof v === 'string') ? value : null; |
| } |
|
|