fix(i18n): prevent zh flash and normalize language caching #5632

This commit is contained in:
Slinetrac 2025-11-28 10:14:33 +08:00
parent 9ce5d27d6e
commit 99dda5496e
No known key found for this signature in database
3 changed files with 144 additions and 21 deletions

View File

@ -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);

View File

@ -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();
});

View File

@ -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<string, unknown>;
@ -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);
};