refactor: strengthen i18n type safety and fix async Promise handling in language detection

This commit is contained in:
雨霖铃
2026-05-22 22:58:27 +08:00
parent 88bebfa1b8
commit 7efed8673a
+48 -22
View File
@@ -1,10 +1,14 @@
import i18n from 'i18next'; import i18n from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import type { CustomDetector } from 'i18next-browser-languagedetector'; // 💡 1. 引入官方强类型探测器接口
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
import { storageUtil } from '@/utils/chromeStorage'; import { storageUtil } from '@/utils/chromeStorage';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
// 同步加载全局命名空间(所有页面都需要) // 导入 Day.js 本地化语言包
import 'dayjs/locale/zh-cn';
// 同步加载全局核心命名空间
import commonZh from './locales/zh/common.json'; import commonZh from './locales/zh/common.json';
import featuresZh from './locales/zh/features.json'; import featuresZh from './locales/zh/features.json';
import commonEn from './locales/en/common.json'; import commonEn from './locales/en/common.json';
@@ -28,21 +32,22 @@ const LANGUAGE_STORAGE_KEY = 'app/language';
const LANGUAGE_SNAPSHOT_KEY = 'snapshot/app/language'; const LANGUAGE_SNAPSHOT_KEY = 'snapshot/app/language';
/** /**
* 将任意语言标识归一化为受支持的语言代码 * 将任意语言标识归一化为受支持的核心代码
*/ */
export const normalizeLanguage = (lng: string): SupportedLanguage => { export const normalizeLanguage = (lng: string): SupportedLanguage => {
return lng.startsWith('zh') ? 'zh' : 'en'; if (!lng) return 'en';
return lng.toLowerCase().startsWith('zh') ? 'zh' : 'en';
}; };
/** /**
* 校验语言是否受支持 * 严格校验语言安全边界
*/ */
const isValidLanguage = (lng: unknown): lng is SupportedLanguage => { const isValidLanguage = (lng: unknown): lng is SupportedLanguage => {
return typeof lng === 'string' && (SUPPORTED_LANGUAGES as readonly string[]).includes(lng); return typeof lng === 'string' && (SUPPORTED_LANGUAGES as readonly string[]).includes(lng);
}; };
/** /**
* 同步从 localStorage 获取语言快照(用于消除异步加载产生的首屏闪烁) * 同步从 localStorage 获取语言快照(消除异步闪烁)
*/ */
const getSyncLanguageSnapshot = (): SupportedLanguage | null => { const getSyncLanguageSnapshot = (): SupportedLanguage | null => {
try { try {
@@ -51,20 +56,24 @@ const getSyncLanguageSnapshot = (): SupportedLanguage | null => {
const parsed = JSON.parse(val) as unknown; const parsed = JSON.parse(val) as unknown;
return isValidLanguage(parsed) ? parsed : null; return isValidLanguage(parsed) ? parsed : null;
} catch (error) { } catch (error) {
console.error('解析语言同步快照失败:', error); console.error('[i18n] Failed to parse sync language snapshot from localStorage:', error);
return null; return null;
} }
}; };
// 自定义 Chrome Storage 探测器 // 💡 2. 强类型接口重塑:显式绑定 CustomDetector 类型,
const chromeStorageDetector = { // 告诉 TS 编译器这些方法将被全局 Languagedetector 框架隐式调用,彻底治愈“未使用函数”报错!
const chromeStorageDetector: CustomDetector = {
name: 'chromeStorage', name: 'chromeStorage',
lookup() { lookup() {
// 同步初始化已通过 getSyncLanguageSnapshot + init 的 lng 参数处理
return undefined; return undefined;
}, },
cacheUserLanguage(lng: string) { cacheUserLanguage(lng: string) {
storageUtil.set(LANGUAGE_STORAGE_KEY, lng); const target = normalizeLanguage(lng);
// 💡 修复点:对异步写盘操作追加 void 算子或 catch,吞掉 Promise 被忽略警告
storageUtil.set(LANGUAGE_STORAGE_KEY, target).catch((err) => {
console.error('[i18n Detector Error] Failed to write back language state:', err);
});
}, },
}; };
@@ -73,7 +82,8 @@ detector.addDetector(chromeStorageDetector);
const syncLng = getSyncLanguageSnapshot(); const syncLng = getSyncLanguageSnapshot();
i18n // 💡 3. 修复点:对 i18n.init() 返回的异步 Promise 前方追加 void 斩断依赖链,放行编译
void i18n
.use(detector) .use(detector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
@@ -92,27 +102,43 @@ i18n
}, },
}); });
// 监听语言变化:同步 Day.js 和 localStorage 快照 // 监听语言变
i18n.on('languageChanged', (lng) => { i18n.on('languageChanged', (lng) => {
const normalizedLng = normalizeLanguage(lng); const normalizedLng = normalizeLanguage(lng);
dayjs.locale(normalizedLng === 'zh' ? 'zh-cn' : 'en'); dayjs.locale(normalizedLng === 'zh' ? 'zh-cn' : 'en');
localStorage.setItem(LANGUAGE_SNAPSHOT_KEY, JSON.stringify(normalizedLng)); localStorage.setItem(LANGUAGE_SNAPSHOT_KEY, JSON.stringify(normalizedLng));
}); });
// 初始化时从存储中恢复语言 // 初始化时从长期异步存储中恢复校准
storageUtil.get(LANGUAGE_STORAGE_KEY).then((lng) => { storageUtil
const targetLng = lng || syncLng; .get(LANGUAGE_STORAGE_KEY)
.then((lng) => {
const rawTargetLng = lng || syncLng;
if (targetLng && isValidLanguage(targetLng) && targetLng !== i18n.language) { if (!rawTargetLng) {
i18n.changeLanguage(targetLng);
} else if (!targetLng) {
// 第一次运行,归一化并持久化初始语言
const initialLng = normalizeLanguage(i18n.language); const initialLng = normalizeLanguage(i18n.language);
storageUtil.set(LANGUAGE_STORAGE_KEY, initialLng);
if (initialLng !== i18n.language) { // 💡 修复点:对初始化同步写盘追加安全的 Promise .catch() 异常隔离防护罩
i18n.changeLanguage(initialLng); storageUtil.set(LANGUAGE_STORAGE_KEY, initialLng).catch((err) => {
console.error('[i18n Init Error] Persistent sync collapsed:', err);
});
if (initialLng !== normalizeLanguage(i18n.language)) {
// 💡 修复点:对 changeLanguage 异步微任务进行显式 void 断链安全隔离
void i18n.changeLanguage(initialLng);
} }
return;
} }
const targetLng = normalizeLanguage(String(rawTargetLng));
if (isValidLanguage(targetLng) && targetLng !== normalizeLanguage(i18n.language)) {
// 💡 修复点:对 changeLanguage 异步微任务进行显式 void 断链安全隔离
void i18n.changeLanguage(targetLng);
}
})
.catch((err) => {
console.error('[i18n Context Error] Async local storage lookup collapsed:', err);
}); });
export default i18n; export default i18n;