fix: lazy load i18n sections

This commit is contained in:
Tunglies
2026-06-29 11:11:57 +08:00
parent 2442634b2a
commit 3ce228ff7a
3 changed files with 89 additions and 30 deletions
+5 -1
View File
@@ -48,6 +48,7 @@ import { WindowControls } from '@/components/layout/window-controller'
import { useI18n } from '@/hooks/use-i18n'
import { useVerge } from '@/hooks/use-verge'
import { useWindowDecorations } from '@/hooks/use-window'
import { ensureLanguageSections } from '@/services/i18n'
import { useThemeMode } from '@/services/states'
import getSystem from '@/utils/get-system'
@@ -65,7 +66,10 @@ import 'dayjs/locale/zh-cn'
export const portableFlag = false
const LogsPage = lazy(() => import('./logs'))
const LogsPage = lazy(async () => {
await ensureLanguageSections('logs')
return import('./logs')
})
type NavItem = (typeof navItems)[number]
+16 -5
View File
@@ -17,12 +17,23 @@ import ProxiesSvg from '@/assets/image/itemicon/proxies.svg?react'
import RulesSvg from '@/assets/image/itemicon/rules.svg?react'
import SettingsSvg from '@/assets/image/itemicon/settings.svg?react'
import UnlockSvg from '@/assets/image/itemicon/unlock.svg?react'
import { ensureLanguageSections } from '@/services/i18n'
import Layout from './_layout'
import HomePage from './home'
const createLazyRoute = (load: () => Promise<{ default: ComponentType }>) => {
const Component = lazy(load)
const createLazyRoute = (
load: () => Promise<{ default: ComponentType }>,
sections?: string | readonly string[],
) => {
const Component = lazy(
sections
? async () => {
await ensureLanguageSections(sections)
return load()
}
: load,
)
const LazyRoute = () => (
<Suspense fallback={null}>
<Component />
@@ -49,19 +60,19 @@ export const navItems = [
label: 'layout.components.navigation.tabs.profiles',
path: '/profile',
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
Component: createLazyRoute(() => import('./profiles')),
Component: createLazyRoute(() => import('./profiles'), 'rules'),
},
{
label: 'layout.components.navigation.tabs.connections',
path: '/connections',
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
Component: createLazyRoute(() => import('./connections')),
Component: createLazyRoute(() => import('./connections'), 'connections'),
},
{
label: 'layout.components.navigation.tabs.rules',
path: '/rules',
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
Component: createLazyRoute(() => import('./rules')),
Component: createLazyRoute(() => import('./rules'), 'rules'),
},
{
label: 'layout.components.navigation.tabs.logs',
+68 -24
View File
@@ -81,14 +81,26 @@ type LocaleModule = {
default: Record<string, unknown>
}
const localeModules = import.meta.glob<LocaleModule>('@/locales/*/index.ts')
const STARTUP_LANGUAGE_SECTIONS = [
'layout',
'home',
'shared',
'settings',
'profiles',
'proxies',
'tests',
] as const
const localeModules = import.meta.glob<LocaleModule>('@/locales/*/*.json')
const localeLoaders = Object.entries(localeModules).reduce<
Record<string, () => Promise<LocaleModule>>
Record<string, Record<string, () => Promise<LocaleModule>>>
>((acc, [path, loader]) => {
const match = path.match(/[/\\]locales[/\\]([^/\\]+)[/\\]index\.ts$/)
const match = path.match(/[/\\]locales[/\\]([^/\\]+)[/\\]([^/\\]+)\.json$/)
if (match) {
acc[match[1]] = loader
const [, language, section] = match
acc[language] ??= {}
acc[language][section] = loader
}
return acc
}, {})
@@ -101,33 +113,43 @@ export const languages: Record<string, any> = supportedLanguages.reduce(
{} as Record<string, any>,
)
export const loadLanguage = async (language: string) => {
const loadLanguageSections = async (
language: string,
sections: readonly string[],
) => {
try {
const loader = localeLoaders[language]
if (!loader) {
throw new Error(`Locale loader not found for language "${language}"`)
}
const module = await loader()
return module.default
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}`,
)
const fallbackLoader = localeLoaders[FALLBACK_LANGUAGE]
if (!fallbackLoader) {
throw new Error(
`Fallback language "${FALLBACK_LANGUAGE}" resources are missing.`,
{ cause: error },
)
}
const fallback = await fallbackLoader()
return fallback.default
return loadLanguageSections(FALLBACK_LANGUAGE, sections)
}
throw error
}
}
export const loadLanguage = async (language: string) =>
loadLanguageSections(language, STARTUP_LANGUAGE_SECTIONS)
const getLoadedLanguageSections = (language: string) =>
Object.keys(i18n.getResourceBundle(language, 'translation') ?? {})
i18n.use(initReactI18next).init({
resources: {},
lng: FALLBACK_LANGUAGE,
@@ -137,13 +159,35 @@ i18n.use(initReactI18next).init({
},
})
export 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,
)
if (!i18n.hasResourceBundle(targetLanguage, 'translation')) {
const resources = await loadLanguage(targetLanguage)
i18n.addResourceBundle(targetLanguage, 'translation', resources)
}
await ensureLanguageSections(
loadedSections.length ? loadedSections : STARTUP_LANGUAGE_SECTIONS,
targetLanguage,
)
await i18n.changeLanguage(targetLanguage)
cacheLanguage(targetLanguage)