mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-02 12:52:13 +08:00
193 lines
4.5 KiB
TypeScript
193 lines
4.5 KiB
TypeScript
import i18n from 'i18next'
|
|
import { initReactI18next } from 'react-i18next'
|
|
|
|
export const supportedLanguages = [
|
|
'en',
|
|
'ru',
|
|
'zh',
|
|
'fa',
|
|
'tt',
|
|
'id',
|
|
'ar',
|
|
'ko',
|
|
'tr',
|
|
'de',
|
|
'es',
|
|
'jp',
|
|
'zhtw',
|
|
]
|
|
|
|
export const FALLBACK_LANGUAGE = 'zh'
|
|
const LANGUAGE_STORAGE_KEY = 'verge-language'
|
|
|
|
const normalizeLanguage = (language?: string) =>
|
|
language?.toLowerCase().replace(/_/g, '-')
|
|
|
|
export const resolveLanguage = (language?: string) => {
|
|
const normalized = normalizeLanguage(language)
|
|
if (!normalized) {
|
|
return FALLBACK_LANGUAGE
|
|
}
|
|
|
|
if (normalized === 'zh-tw') return 'zhtw'
|
|
if (normalized === 'zh-cn') return 'zh'
|
|
|
|
if (supportedLanguages.includes(normalized)) {
|
|
return normalized
|
|
}
|
|
|
|
const baseLanguage = normalized.split('-')[0]
|
|
if (supportedLanguages.includes(baseLanguage)) {
|
|
return baseLanguage
|
|
}
|
|
|
|
return FALLBACK_LANGUAGE
|
|
}
|
|
|
|
const getLanguageStorage = () => {
|
|
if (typeof window === 'undefined') return null
|
|
try {
|
|
return window.localStorage
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const cacheLanguage = (language: string) => {
|
|
const storage = getLanguageStorage()
|
|
if (!storage) return
|
|
|
|
try {
|
|
storage.setItem(LANGUAGE_STORAGE_KEY, resolveLanguage(language))
|
|
} catch (error) {
|
|
console.warn('[i18n] Failed to cache language:', error)
|
|
}
|
|
}
|
|
|
|
export const getCachedLanguage = () => {
|
|
const storage = getLanguageStorage()
|
|
if (!storage) return undefined
|
|
|
|
try {
|
|
const cached = storage.getItem(LANGUAGE_STORAGE_KEY)
|
|
return cached ? resolveLanguage(cached) : undefined
|
|
} catch (error) {
|
|
console.warn('[i18n] Failed to read cached language:', error)
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
type LocaleModule = {
|
|
default: Record<string, unknown>
|
|
}
|
|
|
|
const STARTUP_LANGUAGE_SECTIONS = [
|
|
'layout',
|
|
'home',
|
|
'shared',
|
|
'settings',
|
|
'profiles',
|
|
'proxies',
|
|
'tests',
|
|
'rules',
|
|
'connections',
|
|
'logs',
|
|
] as const
|
|
|
|
const localeModules = import.meta.glob<LocaleModule>('@/locales/*/*.json')
|
|
|
|
const localeLoaders = Object.entries(localeModules).reduce<
|
|
Record<string, Record<string, () => Promise<LocaleModule>>>
|
|
>((acc, [path, loader]) => {
|
|
const match = path.match(/[/\\]locales[/\\]([^/\\]+)[/\\]([^/\\]+)\.json$/)
|
|
if (match) {
|
|
const [, language, section] = match
|
|
acc[language] ??= {}
|
|
acc[language][section] = loader
|
|
}
|
|
return acc
|
|
}, {})
|
|
|
|
const loadLanguageSections = async (
|
|
language: string,
|
|
sections: readonly string[],
|
|
) => {
|
|
try {
|
|
const entries = await Promise.all(
|
|
sections.map(async (section) => {
|
|
const loader = localeLoaders[language]?.[section]
|
|
if (!loader) {
|
|
throw new Error(
|
|
`Locale loader not found for language "${language}" section "${section}"`,
|
|
)
|
|
}
|
|
|
|
const module = await loader()
|
|
return [section, module.default] as const
|
|
}),
|
|
)
|
|
|
|
return Object.fromEntries(entries)
|
|
} catch (error) {
|
|
if (language !== FALLBACK_LANGUAGE) {
|
|
console.warn(
|
|
`Failed to load language ${language}, fallback to ${FALLBACK_LANGUAGE}, ${error}`,
|
|
)
|
|
return loadLanguageSections(FALLBACK_LANGUAGE, sections)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const getLoadedLanguageSections = (language: string) =>
|
|
Object.keys(i18n.getResourceBundle(language, 'translation') ?? {})
|
|
|
|
i18n.use(initReactI18next).init({
|
|
resources: {},
|
|
lng: FALLBACK_LANGUAGE,
|
|
fallbackLng: FALLBACK_LANGUAGE,
|
|
interpolation: {
|
|
escapeValue: false,
|
|
},
|
|
})
|
|
|
|
const ensureLanguageSections = async (
|
|
sections: string | readonly string[],
|
|
language: string = i18n.language || FALLBACK_LANGUAGE,
|
|
) => {
|
|
const targetLanguage = resolveLanguage(language)
|
|
const sectionList = Array.isArray(sections) ? sections : [sections]
|
|
const loadedSections = new Set(getLoadedLanguageSections(targetLanguage))
|
|
const missingSections = sectionList.filter(
|
|
(section) => !loadedSections.has(section),
|
|
)
|
|
|
|
if (!missingSections.length) {
|
|
return
|
|
}
|
|
|
|
const resources = await loadLanguageSections(targetLanguage, missingSections)
|
|
i18n.addResourceBundle(targetLanguage, 'translation', resources, true, true)
|
|
}
|
|
|
|
export const changeLanguage = async (language: string) => {
|
|
const targetLanguage = resolveLanguage(language)
|
|
const loadedSections = getLoadedLanguageSections(
|
|
i18n.language || FALLBACK_LANGUAGE,
|
|
)
|
|
|
|
await ensureLanguageSections(
|
|
[...new Set([...loadedSections, ...STARTUP_LANGUAGE_SECTIONS])],
|
|
targetLanguage,
|
|
)
|
|
|
|
await i18n.changeLanguage(targetLanguage)
|
|
cacheLanguage(targetLanguage)
|
|
}
|
|
|
|
export const initializeLanguage = async (
|
|
initialLanguage: string = FALLBACK_LANGUAGE,
|
|
) => {
|
|
await changeLanguage(initialLanguage)
|
|
}
|