mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 11:02:08 +08:00
fix: lazy load i18n sections
This commit is contained in:
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user