diff --git a/src/hooks/use-i18n.ts b/src/hooks/use-i18n.ts index c82b05ffa..ca04ab170 100644 --- a/src/hooks/use-i18n.ts +++ b/src/hooks/use-i18n.ts @@ -1,7 +1,11 @@ import { useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { changeLanguage, supportedLanguages } from "@/services/i18n"; +import { + changeLanguage, + resolveLanguage, + supportedLanguages, +} from "@/services/i18n"; import { useVerge } from "./use-verge"; @@ -12,21 +16,23 @@ export const useI18n = () => { const switchLanguage = useCallback( async (language: string) => { - if (!supportedLanguages.includes(language)) { + const targetLanguage = resolveLanguage(language); + + if (!supportedLanguages.includes(targetLanguage)) { console.warn(`Unsupported language: ${language}`); return; } - if (i18n.language === language) { + if (i18n.language === targetLanguage) { return; } setIsLoading(true); try { - await changeLanguage(language); + await changeLanguage(targetLanguage); if (patchVerge) { - await patchVerge({ language }); + await patchVerge({ language: targetLanguage }); } } catch (error) { console.error("Failed to change language:", error); diff --git a/src/main.tsx b/src/main.tsx index c9cde899e..1b63d0d04 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,7 +14,14 @@ import { BaseErrorBoundary } from "./components/base"; import { router } from "./pages/_routers"; import { AppDataProvider } from "./providers/app-data-provider"; import { WindowProvider } from "./providers/window"; -import { initializeLanguage } from "./services/i18n"; +import { getVergeConfig } from "./services/cmds"; +import { + FALLBACK_LANGUAGE, + cacheLanguage, + getCachedLanguage, + initializeLanguage, + resolveLanguage, +} from "./services/i18n"; import { LoadingCacheProvider, ThemeModeProvider, @@ -71,20 +78,67 @@ const initializeApp = () => { ); }; -initializeLanguage("zh").catch(console.error); -initializeApp(); +const determineInitialLanguage = async () => { + const cachedLanguage = getCachedLanguage(); + if (cachedLanguage) { + return cachedLanguage; + } -// 错误处理 + try { + const vergeConfig = await getVergeConfig(); + if (vergeConfig?.language) { + const resolved = resolveLanguage(vergeConfig.language); + cacheLanguage(resolved); + return resolved; + } + } catch (error) { + console.warn( + "[main.tsx] Failed to read language from Verge config:", + error, + ); + } + + const browserLanguage = resolveLanguage( + typeof navigator !== "undefined" ? navigator.language : undefined, + ); + cacheLanguage(browserLanguage); + return browserLanguage; +}; + +const bootstrap = async () => { + const initialLanguage = await determineInitialLanguage(); + await initializeLanguage(initialLanguage); + initializeApp(); +}; + +bootstrap().catch((error) => { + console.error( + "[main.tsx] App bootstrap failed, falling back to default language:", + error, + ); + initializeLanguage(FALLBACK_LANGUAGE) + .catch((fallbackError) => { + console.error( + "[main.tsx] Fallback language initialization failed:", + fallbackError, + ); + }) + .finally(() => { + initializeApp(); + }); +}); + +// Error handling window.addEventListener("error", (event) => { - console.error("[main.tsx] 全局错误:", event.error); + console.error("[main.tsx] Global error:", event.error); }); window.addEventListener("unhandledrejection", (event) => { - console.error("[main.tsx] 未处理的Promise拒绝:", event.reason); + console.error("[main.tsx] Unhandled promise rejection:", event.reason); }); -// 页面关闭/刷新事件 +// Page close/refresh events window.addEventListener("beforeunload", () => { - // 同步清理所有 WebSocket 实例, 防止内存泄漏 + // Clean up all WebSocket instances to prevent memory leaks MihomoWebSocket.cleanupAll(); }); diff --git a/src/services/i18n.ts b/src/services/i18n.ts index 710378e79..eb7ae4408 100644 --- a/src/services/i18n.ts +++ b/src/services/i18n.ts @@ -17,7 +17,65 @@ export const supportedLanguages = [ "zhtw", ]; -const FALLBACK_LANGUAGE = "zh"; +export const FALLBACK_LANGUAGE = "zh"; +const LANGUAGE_STORAGE_KEY = "verge-language"; + +const normalizeLanguage = (language?: string) => + language?.toLowerCase().replace(/_/g, "-"); + +export const resolveLanguage = (language?: string) => { + const normalized = normalizeLanguage(language); + if (!normalized) { + return FALLBACK_LANGUAGE; + } + + if (normalized === "zh-tw") return "zhtw"; + if (normalized === "zh-cn") return "zh"; + + if (supportedLanguages.includes(normalized)) { + return normalized; + } + + const baseLanguage = normalized.split("-")[0]; + if (supportedLanguages.includes(baseLanguage)) { + return baseLanguage; + } + + return FALLBACK_LANGUAGE; +}; + +const getLanguageStorage = () => { + if (typeof window === "undefined") return null; + try { + return window.localStorage; + } catch { + return null; + } +}; + +export const cacheLanguage = (language: string) => { + const storage = getLanguageStorage(); + if (!storage) return; + + try { + storage.setItem(LANGUAGE_STORAGE_KEY, resolveLanguage(language)); + } catch (error) { + console.warn("[i18n] Failed to cache language:", error); + } +}; + +export const getCachedLanguage = () => { + const storage = getLanguageStorage(); + if (!storage) return undefined; + + try { + const cached = storage.getItem(LANGUAGE_STORAGE_KEY); + return cached ? resolveLanguage(cached) : undefined; + } catch (error) { + console.warn("[i18n] Failed to read cached language:", error); + return undefined; + } +}; type LocaleModule = { default: Record; @@ -71,22 +129,27 @@ export const loadLanguage = async (language: string) => { i18n.use(initReactI18next).init({ resources: {}, - lng: "zh", - fallbackLng: "zh", + lng: FALLBACK_LANGUAGE, + fallbackLng: FALLBACK_LANGUAGE, interpolation: { escapeValue: false, }, }); export const changeLanguage = async (language: string) => { - if (!i18n.hasResourceBundle(language, "translation")) { - const resources = await loadLanguage(language); - i18n.addResourceBundle(language, "translation", resources); + const targetLanguage = resolveLanguage(language); + + if (!i18n.hasResourceBundle(targetLanguage, "translation")) { + const resources = await loadLanguage(targetLanguage); + i18n.addResourceBundle(targetLanguage, "translation", resources); } - await i18n.changeLanguage(language); + await i18n.changeLanguage(targetLanguage); + cacheLanguage(targetLanguage); }; -export const initializeLanguage = async (initialLanguage: string = "zh") => { +export const initializeLanguage = async ( + initialLanguage: string = FALLBACK_LANGUAGE, +) => { await changeLanguage(initialLanguage); };