import { createInstance } from 'i18next' import { initReactI18next } from 'react-i18next' import type { i18n as I18n, TOptions } from 'i18next' import type { LocaleCode } from '@/lib/locales' // Keep Japanese placeholders editable without reporting them as completed translations. const translatedResource = (resource: Record): Record => { const untranslated = new Set ( Array.isArray (resource._untranslated) ? resource._untranslated : []) if (untranslated.has ('*')) return { } const select = (entries: Record, prefix = ''): Record => Object.fromEntries (Object.entries (entries).flatMap (([key, value]) => { const path = prefix ? `${ prefix }.${ key }` : key if (key === '_untranslated' || untranslated.has (path)) return [] const translated = (value != null && typeof value === 'object' && !(Array.isArray (value))) ? select (value as Record, path) : value return [[key, translated]] })) return select (resource) } const dictionaries = import.meta.glob> ('../locales/*/*.json', { eager: true, import: 'default' }) const resources: Record>> = { } for (const [path, dictionary] of Object.entries (dictionaries)) { const [, , locale, filename] = path.split ('/') resources[locale] ??= { } resources[locale][filename.slice (0, -5)] = locale === 'ain-Kana' ? translatedResource (dictionary) : dictionary } const namespaces = Object.keys (resources.ja ?? { }) export const createUII18n = (locale: string): I18n => { const instance = createInstance () void instance.init ({ lng: locale, fallbackLng: 'ja', supportedLngs: ['ja', 'ain-Kana'], load: 'currentOnly', initAsync: false, defaultNS: 'navigation', ns: namespaces, resources, interpolation: { escapeValue: false }, returnNull: false, returnEmptyString: false, saveMissing: import.meta.env.DEV, missingKeyHandler: (languages, namespace, key) => { console.warn ('[i18n] missing key', { languages, namespace, key }) }, react: { useSuspense: false } }) return instance } // This instance is used only by legacy helpers with an explicit lng option. export const i18n = createUII18n ('ja') initReactI18next.init (i18n) export const translateUIMessage = ( key: string, namespace: string, locale: string, values: Record = { }, instance: I18n = i18n, ): { text: string; language: string } => { const languages = new Set () const visiting = new Set () const collectLanguages = (reference: string, options: TOptions): string => { const details = instance.t (reference, { ...options, returnDetails: true, returnObjects: false, nest: false, skipInterpolation: true }) const identity = `${details.usedLng}:${details.usedNS}:${details.exactUsedKey}` if (visiting.has (identity) || typeof details.res !== 'string') return details.usedLng visiting.add (identity) const nestingOptions = { ...instance.options.interpolation, ...options, ns: details.usedNS, lng: details.usedLng } const remainder = instance.services.interpolator.nest (details.res, (nestedKey: string, nestedOptions: TOptions) => { collectLanguages (nestedKey, nestedOptions) return ' ' }, nestingOptions) // References, punctuation and supplied values do not define the wrapper's language. if (/\p{L}/u.test (remainder.replace (/\{\{[^}]*\}\}/g, ''))) languages.add (details.usedLng) visiting.delete (identity) return details.usedLng } const options = { ...values, ns: namespace, lng: locale } const resolvedLanguage = collectLanguages (key, options) // A single lang attribute cannot describe mixed fallback text: use the full Japanese message. const mixed = languages.size > 1 const language = mixed ? 'ja' : languages.values ().next ().value ?? resolvedLanguage const text = instance.t (key, { ...options, lng: mixed ? 'ja' : resolvedLanguage, returnDetails: false, returnObjects: false }) if (typeof text !== 'string') { console.warn ('[i18n] UI message must be a string', { key, namespace, locale }) return { text: key, language } } return { text, language } } export const languageUIMessage = ( code: string, locale: string, instance: I18n = i18n, ): { text: string; language: string } => { const key = `languageNames.${code}` return instance.exists (key, { ns: 'common', lng: locale }) ? translateUIMessage (key, 'common', locale, { }, instance) : { text: code, language: 'und' } } export const untranslatedKeys = ( locale: LocaleCode, namespace = 'navigation', ): string[] => { const collect = (resource: Record, prefix = ''): string[] => Object.entries (resource).flatMap (([key, value]) => { const path = prefix ? `${ prefix }.${ key }` : key return (value != null && typeof value === 'object') ? collect (value as Record, path) : [path] }) const baseline = i18n.getResourceBundle ('ja', namespace) ?? { } return collect (baseline).filter (key => { const value = i18n.getResource (locale, namespace, key) return typeof value !== 'string' || value.length === 0 }) } if (import.meta.env.DEV) { for (const namespace of namespaces) { const missing = untranslatedKeys ('ain-Kana', namespace) if (missing.length > 0) console.warn (`[i18n] ain-Kana: untranslated ${ namespace } keys`, missing) } }