From 3ce228ff7a988ed5a8bf70e3be873d8bd9cca6a1 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:11:15 +0800 Subject: [PATCH] fix: lazy load i18n sections --- src/pages/_layout.tsx | 6 ++- src/pages/_routers.tsx | 21 +++++++--- src/services/i18n.ts | 92 +++++++++++++++++++++++++++++++----------- 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index c8a94e22c..78d254dea 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -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] diff --git a/src/pages/_routers.tsx b/src/pages/_routers.tsx index b8433d8c5..98d82b806 100644 --- a/src/pages/_routers.tsx +++ b/src/pages/_routers.tsx @@ -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 = () => ( @@ -49,19 +60,19 @@ export const navItems = [ label: 'layout.components.navigation.tabs.profiles', path: '/profile', icon: [, ], - Component: createLazyRoute(() => import('./profiles')), + Component: createLazyRoute(() => import('./profiles'), 'rules'), }, { label: 'layout.components.navigation.tabs.connections', path: '/connections', icon: [, ], - Component: createLazyRoute(() => import('./connections')), + Component: createLazyRoute(() => import('./connections'), 'connections'), }, { label: 'layout.components.navigation.tabs.rules', path: '/rules', icon: [, ], - Component: createLazyRoute(() => import('./rules')), + Component: createLazyRoute(() => import('./rules'), 'rules'), }, { label: 'layout.components.navigation.tabs.logs', diff --git a/src/services/i18n.ts b/src/services/i18n.ts index 9420f3c2b..484c1eafc 100644 --- a/src/services/i18n.ts +++ b/src/services/i18n.ts @@ -81,14 +81,26 @@ type LocaleModule = { default: Record } -const localeModules = import.meta.glob('@/locales/*/index.ts') +const STARTUP_LANGUAGE_SECTIONS = [ + 'layout', + 'home', + 'shared', + 'settings', + 'profiles', + 'proxies', + 'tests', +] as const + +const localeModules = import.meta.glob('@/locales/*/*.json') const localeLoaders = Object.entries(localeModules).reduce< - Record Promise> + Record Promise>> >((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 = supportedLanguages.reduce( {} as Record, ) -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)