feat: implement i18n lazy loading optimization
🚀 Performance improvements: - Replace static language imports with dynamic imports - Load only current language on startup instead of all 13 languages - Implement on-demand loading when switching languages 📦 Bundle optimization: - Reduce initial bundle size by avoiding preloading all language files - Add resource caching to prevent reloading same language - Support all 13 languages: en, ru, zh, fa, tt, id, ar, ko, tr, de, es, jp, zhtw 🔧 Technical changes: - Convert i18n.ts to use dynamic import() for language resources - Add async initializeLanguage() for app startup - Create useI18n hook for language management with loading states - Update main.tsx for async language initialization - Fix language display labels in settings dropdown - Maintain backward compatibility with existing language system ✅ Fixed issues: - Resolve infinite loop in React components - Fix missing language labels in settings UI - Prevent circular dependencies in language loading - Add proper error handling and fallback mechanisms
This commit is contained in:
@@ -19,7 +19,7 @@ import getSystem from "@/utils/get-system";
|
||||
import { routers } from "@/pages/_routers";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
import { ContentCopyRounded } from "@mui/icons-material";
|
||||
import { languages } from "@/services/i18n";
|
||||
import { supportedLanguages } from "@/services/i18n";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
interface Props {
|
||||
@@ -28,7 +28,7 @@ interface Props {
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
const languageOptions = Object.entries(languages).map(([code, _]) => {
|
||||
const languageOptions = supportedLanguages.map((code) => {
|
||||
const labels: { [key: string]: string } = {
|
||||
en: "English",
|
||||
ru: "Русский",
|
||||
@@ -39,8 +39,13 @@ const languageOptions = Object.entries(languages).map(([code, _]) => {
|
||||
ar: "العربية",
|
||||
ko: "한국어",
|
||||
tr: "Türkçe",
|
||||
de: "Deutsch",
|
||||
es: "Español",
|
||||
jp: "日本語",
|
||||
zhtw: "繁體中文",
|
||||
};
|
||||
return { code, label: labels[code] };
|
||||
const label = labels[code] || code;
|
||||
return { code, label };
|
||||
});
|
||||
|
||||
const SettingVergeBasic = ({ onError }: Props) => {
|
||||
|
||||
45
src/hooks/use-i18n.ts
Normal file
45
src/hooks/use-i18n.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { changeLanguage, supportedLanguages } from "@/services/i18n";
|
||||
import { useVerge } from "./use-verge";
|
||||
|
||||
export const useI18n = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
const { patchVerge } = useVerge();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const switchLanguage = useCallback(
|
||||
async (language: string) => {
|
||||
if (!supportedLanguages.includes(language)) {
|
||||
console.warn(`Unsupported language: ${language}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (i18n.language === language) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await changeLanguage(language);
|
||||
|
||||
if (patchVerge) {
|
||||
await patchVerge({ language });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to change language:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[i18n.language, patchVerge],
|
||||
);
|
||||
|
||||
return {
|
||||
currentLanguage: i18n.language,
|
||||
supportedLanguages,
|
||||
switchLanguage,
|
||||
isLoading,
|
||||
t,
|
||||
};
|
||||
};
|
||||
60
src/main.tsx
60
src/main.tsx
@@ -13,7 +13,7 @@ import { ComposeContextProvider } from "foxact/compose-context-provider";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { BaseErrorBoundary } from "./components/base";
|
||||
import Layout from "./pages/_layout";
|
||||
import "./services/i18n";
|
||||
import { initializeLanguage } from "./services/i18n";
|
||||
import {
|
||||
LoadingCacheProvider,
|
||||
ThemeModeProvider,
|
||||
@@ -39,29 +39,47 @@ document.addEventListener("keydown", (event) => {
|
||||
["F", "G", "H", "J", "P", "Q", "R", "U"].includes(
|
||||
event.key.toUpperCase(),
|
||||
));
|
||||
disabledShortcuts && event.preventDefault();
|
||||
if (disabledShortcuts) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const contexts = [
|
||||
<ThemeModeProvider />,
|
||||
<LoadingCacheProvider />,
|
||||
<UpdateStateProvider />,
|
||||
];
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
await initializeLanguage("zh");
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ComposeContextProvider contexts={contexts}>
|
||||
<BaseErrorBoundary>
|
||||
<AppDataProvider>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</AppDataProvider>
|
||||
</BaseErrorBoundary>
|
||||
</ComposeContextProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
const contexts = [
|
||||
<ThemeModeProvider key="theme" />,
|
||||
<LoadingCacheProvider key="loading" />,
|
||||
<UpdateStateProvider key="update" />,
|
||||
];
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ComposeContextProvider contexts={contexts}>
|
||||
<BaseErrorBoundary>
|
||||
<AppDataProvider>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</AppDataProvider>
|
||||
</BaseErrorBoundary>
|
||||
</ComposeContextProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[main.tsx] 应用初始化失败:", error);
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<div style={{ padding: "20px", color: "red" }}>
|
||||
应用初始化失败: {error instanceof Error ? error.message : String(error)}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
initializeApp();
|
||||
|
||||
// 错误处理
|
||||
window.addEventListener("error", (event) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import dayjs from "dayjs";
|
||||
import i18next from "i18next";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { SWRConfig, mutate } from "swr";
|
||||
import { useEffect, useCallback, useState, useRef } from "react";
|
||||
@@ -11,6 +10,7 @@ import { routers } from "./_routers";
|
||||
import { getAxios } from "@/services/api";
|
||||
import { forceRefreshClashConfig } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import LogoSvg from "@/assets/image/logo.svg?react";
|
||||
import iconLight from "@/assets/image/icon_light.svg?react";
|
||||
import iconDark from "@/assets/image/icon_dark.svg?react";
|
||||
@@ -158,6 +158,7 @@ const Layout = () => {
|
||||
const [enableLog] = useEnableLog();
|
||||
const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
|
||||
const { language, start_page } = verge ?? {};
|
||||
const { switchLanguage } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const routersEles = useRoutes(routers);
|
||||
@@ -439,9 +440,9 @@ const Layout = () => {
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
dayjs.locale(language === "zh" ? "zh-cn" : language);
|
||||
i18next.changeLanguage(language);
|
||||
switchLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
}, [language, switchLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (start_page) {
|
||||
|
||||
@@ -1,29 +1,59 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import en from "@/locales/en.json";
|
||||
import ru from "@/locales/ru.json";
|
||||
import zh from "@/locales/zh.json";
|
||||
import fa from "@/locales/fa.json";
|
||||
import tt from "@/locales/tt.json";
|
||||
import id from "@/locales/id.json";
|
||||
import ar from "@/locales/ar.json";
|
||||
import ko from "@/locales/ko.json";
|
||||
import tr from "@/locales/tr.json";
|
||||
|
||||
export const languages = { en, ru, zh, fa, tt, id, ar, ko, tr };
|
||||
export const supportedLanguages = [
|
||||
"en",
|
||||
"ru",
|
||||
"zh",
|
||||
"fa",
|
||||
"tt",
|
||||
"id",
|
||||
"ar",
|
||||
"ko",
|
||||
"tr",
|
||||
"de",
|
||||
"es",
|
||||
"jp",
|
||||
"zhtw",
|
||||
];
|
||||
|
||||
const resources = Object.fromEntries(
|
||||
Object.entries(languages).map(([key, value]) => [
|
||||
key,
|
||||
{ translation: value },
|
||||
]),
|
||||
export const languages: Record<string, any> = supportedLanguages.reduce(
|
||||
(acc, lang) => {
|
||||
acc[lang] = {};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
export const loadLanguage = async (language: string) => {
|
||||
try {
|
||||
const module = await import(`@/locales/${language}.json`);
|
||||
return module.default;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load language ${language}, fallback to zh`);
|
||||
const fallback = await import("@/locales/zh.json");
|
||||
return fallback.default;
|
||||
}
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
resources: {},
|
||||
lng: "zh",
|
||||
fallbackLng: "zh",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const changeLanguage = async (language: string) => {
|
||||
if (!i18n.hasResourceBundle(language, "translation")) {
|
||||
const resources = await loadLanguage(language);
|
||||
i18n.addResourceBundle(language, "translation", resources);
|
||||
}
|
||||
|
||||
await i18n.changeLanguage(language);
|
||||
};
|
||||
|
||||
export const initializeLanguage = async (initialLanguage: string = "zh") => {
|
||||
await changeLanguage(initialLanguage);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user