import { createI18n } from 'vue-i18n' import type { I18n, I18nOptions } from 'vue-i18n' import en from '../../../lang/en.json' /** * Locale-to-filename mapping for language files whose filename does * not match the locale code exactly. */ const LOCALE_FILE_MAP: Record = { zh_CN: 'zh-cn', pt_BR: 'pt-br', } /** Tracks which languages have already been loaded. */ const loadedLanguages = new Set(['en']) /** In-memory cache of loaded message objects keyed by locale. */ const languageCache = new Map>() /** Messages registered by compiled modules. Kept separate from the lazy locale * cache so a later locale import cannot overwrite module translations. */ const additionalMessages = new Map>() let activeI18n: AppI18n | null = null function isMessageObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } /** Recursively merge locale trees so one module's `settings.*` keys never * replace the host's complete `settings` namespace. */ export function mergeMessageObjects( base: Record, incoming: Record, ): Record { const merged = { ...base } for (const [key, value] of Object.entries(incoming)) { merged[key] = isMessageObject(value) && isMessageObject(merged[key]) ? mergeMessageObjects(merged[key], value) : value } return merged } export function registerAdditionalMessages( messages: Record>, ): void { for (const [locale, bundle] of Object.entries(messages)) { additionalMessages.set( locale, mergeMessageObjects(additionalMessages.get(locale) ?? {}, bundle), ) if (activeI18n) { activeI18n.global.mergeLocaleMessage(locale, bundle) } } } /** * Dynamically import a language JSON file for a given locale. */ async function loadLanguageMessages( locale: string ): Promise> { if (languageCache.has(locale)) { return languageCache.get(locale)! } // English is already statically imported — no dynamic import needed if (locale === 'en') { const messages = en as unknown as Record languageCache.set('en', messages) return messages } const fileName = LOCALE_FILE_MAP[locale] ?? locale try { const mod: { default: Record } = await import( `../../../lang/${fileName}.json` ) const messages = mergeMessageObjects( mod.default ?? mod, additionalMessages.get(locale) ?? {}, ) languageCache.set(locale, messages) loadedLanguages.add(locale) return messages } catch (error: unknown) { console.warn(`Failed to load language: ${locale}`, error) // Fall back to English if (locale !== 'en' && !languageCache.has('en')) { try { const fallback: { default: Record } = await import( '../../../lang/en.json' ) const fallbackMessages = fallback.default ?? fallback languageCache.set('en', fallbackMessages) return fallbackMessages } catch (fallbackError: unknown) { console.error('Failed to load fallback language (en)', fallbackError) return {} } } return languageCache.get('en') ?? {} } } /** * Load a language and activate it on the given i18n instance. */ export async function setI18nLanguage( i18n: I18n, Record, Record, string, false>, locale: string ): Promise { if (!loadedLanguages.has(locale)) { const messages = await loadLanguageMessages(locale) i18n.global.setLocaleMessage(locale, messages) } i18n.global.locale.value = locale } /** * Check whether a language has already been loaded. */ export function isLanguageLoaded(locale: string): boolean { return loadedLanguages.has(locale) } /** Type alias for the i18n instance created by this module. */ export type AppI18n = I18n< Record, Record, Record, string, false > /** * Create and return the vue-i18n plugin instance. * * Only the English bundle is included synchronously; all other * languages are loaded on demand via `setI18nLanguage`. */ export function createAppI18n( extraMessages?: Record> ): AppI18n { const messages: Record> = {} for (const [locale, bundle] of additionalMessages) { messages[locale] = mergeMessageObjects(messages[locale] ?? {}, bundle) } for (const [locale, bundle] of Object.entries(extraMessages ?? {})) { messages[locale] = mergeMessageObjects(messages[locale] ?? {}, bundle) } messages.en = mergeMessageObjects( en as unknown as Record, messages.en ?? {}, ) const options: I18nOptions = { legacy: false, locale: 'en', fallbackLocale: 'en', globalInjection: true, messages: messages as I18nOptions['messages'], } activeI18n = createI18n(options) as AppI18n return activeI18n }