You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

118 lines
3.5 KiB

import dayjs from 'dayjs'
import i18n, { Resource } from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import en from './locales/en'
/** Display names only — keeps this module small; locale strings load on demand (except English). */
const LANGUAGE_META = {
ar: 'العربية',
de: 'Deutsch',
en: 'English',
es: 'Español',
fa: 'فارسی',
fr: 'Français',
hi: 'हि',
it: 'Italiano',
ja: '日本語',
ko: '한국어',
pl: 'Polski',
'pt-BR': 'Português (Brasil)',
'pt-PT': 'Português (Portugal)',
ru: 'Русский',
th: 'ไทย',
zh: '简体中文'
} as const
export type TLanguage = keyof typeof LANGUAGE_META
export const LocalizedLanguageNames: { [key in TLanguage]: string } = { ...LANGUAGE_META }
const supportedLanguages = Object.keys(LANGUAGE_META) as TLanguage[]
/** App UI languages (same set used for “Translate to …” in note menus). */
export const SUPPORTED_APP_LANGUAGE_CODES: readonly TLanguage[] = supportedLanguages
const localeModules = import.meta.glob<{ default: Resource }>('./locales/*.ts')
const localePath = (code: TLanguage): string => `./locales/${code}.ts`
/** Normalize a browser / i18next language tag to a supported app locale. */
export function normalizeToSupportedAppLanguage(lng: string): TLanguage {
const exact = supportedLanguages.find((s) => lng === s)
if (exact) return exact
return supportedLanguages.find((s) => lng.startsWith(s)) ?? 'en'
}
async function ensureLocaleLoaded(code: TLanguage): Promise<void> {
if (code === 'en') return
if (i18n.hasResourceBundle(code, 'translation')) return
const load = localeModules[localePath(code)]
if (!load) {
console.warn('[i18n] Missing locale module for', code)
return
}
const mod = await load()
i18n.addResourceBundle(code, 'translation', mod.default.translation, true, true)
}
export async function changeAppLanguage(code: TLanguage): Promise<void> {
await ensureLocaleLoaded(code)
await i18n.changeLanguage(code)
}
let initPromise: Promise<void> | null = null
export function initI18n(): Promise<void> {
if (initPromise) return initPromise
initPromise = (async () => {
await i18n.use(LanguageDetector).use(initReactI18next).init({
fallbackLng: 'en',
supportedLngs: supportedLanguages,
resources: { en },
partialBundledLanguages: true,
interpolation: {
escapeValue: false
},
detection: {
convertDetectedLanguage: (lng) => normalizeToSupportedAppLanguage(lng)
}
})
i18n.services.formatter?.add('date', (timestamp, lng) => {
switch (lng) {
case 'zh':
case 'ja':
return dayjs(timestamp).format('YYYY年MM月DD日')
case 'pl':
case 'de':
case 'ru':
return dayjs(timestamp).format('DD.MM.YYYY')
case 'fa':
return dayjs(timestamp).format('YYYY/MM/DD')
case 'it':
case 'es':
case 'fr':
case 'pt-BR':
case 'pt-PT':
case 'ar':
case 'hi':
case 'th':
return dayjs(timestamp).format('DD/MM/YYYY')
case 'ko':
return dayjs(timestamp).format('YYYY년 MM월 DD일')
default:
return dayjs(timestamp).format('MMM D, YYYY')
}
})
const target = normalizeToSupportedAppLanguage(i18n.language)
if (target !== 'en') {
await ensureLocaleLoaded(target)
await i18n.changeLanguage(target)
}
})()
return initPromise
}
export default i18n