diff --git a/src/components/setting/setting-verge-basic.tsx b/src/components/setting/setting-verge-basic.tsx
index 34660f4e..34b7d7c5 100644
--- a/src/components/setting/setting-verge-basic.tsx
+++ b/src/components/setting/setting-verge-basic.tsx
@@ -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) => {
diff --git a/src/hooks/use-i18n.ts b/src/hooks/use-i18n.ts
new file mode 100644
index 00000000..f20baf9a
--- /dev/null
+++ b/src/hooks/use-i18n.ts
@@ -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,
+ };
+};
diff --git a/src/main.tsx b/src/main.tsx
index d1c30806..7bc6ed8c 100644
--- a/src/main.tsx
+++ b/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 = [
- ,
- ,
- ,
-];
+const initializeApp = async () => {
+ try {
+ await initializeLanguage("zh");
-const root = createRoot(container);
-root.render(
-
-
-
-
-
-
-
-
-
-
- ,
-);
+ const contexts = [
+ ,
+ ,
+ ,
+ ];
+
+ const root = createRoot(container);
+ root.render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ } catch (error) {
+ console.error("[main.tsx] 应用初始化失败:", error);
+ const root = createRoot(container);
+ root.render(
+
+ 应用初始化失败: {error instanceof Error ? error.message : String(error)}
+
,
+ );
+ }
+};
+
+initializeApp();
// 错误处理
window.addEventListener("error", (event) => {
diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx
index ac939581..bf761d81 100644
--- a/src/pages/_layout.tsx
+++ b/src/pages/_layout.tsx
@@ -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("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) {
diff --git a/src/services/i18n.ts b/src/services/i18n.ts
index d3947ad4..adc8345a 100644
--- a/src/services/i18n.ts
+++ b/src/services/i18n.ts
@@ -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 = supportedLanguages.reduce(
+ (acc, lang) => {
+ acc[lang] = {};
+ return acc;
+ },
+ {} as Record,
);
+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);
+};