refactor: strengthen i18n type safety and fix async Promise handling in language detection
This commit is contained in:
+51
-25
@@ -1,10 +1,14 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import type { CustomDetector } from 'i18next-browser-languagedetector'; // 💡 1. 引入官方强类型探测器接口
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 同步加载全局命名空间(所有页面都需要)
|
||||
// 导入 Day.js 本地化语言包
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
// 同步加载全局核心命名空间
|
||||
import commonZh from './locales/zh/common.json';
|
||||
import featuresZh from './locales/zh/features.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';
|
||||
|
||||
/**
|
||||
* 将任意语言标识归一化为受支持的语言代码
|
||||
* 将任意语言标识归一化为受支持的核心代码
|
||||
*/
|
||||
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 => {
|
||||
return typeof lng === 'string' && (SUPPORTED_LANGUAGES as readonly string[]).includes(lng);
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步从 localStorage 获取语言快照(用于消除异步加载产生的首屏闪烁)
|
||||
* 同步从 localStorage 获取语言快照(消除异步闪烁)
|
||||
*/
|
||||
const getSyncLanguageSnapshot = (): SupportedLanguage | null => {
|
||||
try {
|
||||
@@ -51,20 +56,24 @@ const getSyncLanguageSnapshot = (): SupportedLanguage | null => {
|
||||
const parsed = JSON.parse(val) as unknown;
|
||||
return isValidLanguage(parsed) ? parsed : null;
|
||||
} catch (error) {
|
||||
console.error('解析语言同步快照失败:', error);
|
||||
console.error('[i18n] Failed to parse sync language snapshot from localStorage:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义 Chrome Storage 探测器
|
||||
const chromeStorageDetector = {
|
||||
// 💡 2. 强类型接口重塑:显式绑定 CustomDetector 类型,
|
||||
// 告诉 TS 编译器这些方法将被全局 Languagedetector 框架隐式调用,彻底治愈“未使用函数”报错!
|
||||
const chromeStorageDetector: CustomDetector = {
|
||||
name: 'chromeStorage',
|
||||
lookup() {
|
||||
// 同步初始化已通过 getSyncLanguageSnapshot + init 的 lng 参数处理
|
||||
return undefined;
|
||||
},
|
||||
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();
|
||||
|
||||
i18n
|
||||
// 💡 3. 修复点:对 i18n.init() 返回的异步 Promise 前方追加 void 斩断依赖链,放行编译
|
||||
void i18n
|
||||
.use(detector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
@@ -92,27 +102,43 @@ i18n
|
||||
},
|
||||
});
|
||||
|
||||
// 监听语言变化:同步 Day.js 和 localStorage 快照
|
||||
// 监听语言变更
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
const normalizedLng = normalizeLanguage(lng);
|
||||
dayjs.locale(normalizedLng === 'zh' ? 'zh-cn' : 'en');
|
||||
localStorage.setItem(LANGUAGE_SNAPSHOT_KEY, JSON.stringify(normalizedLng));
|
||||
});
|
||||
|
||||
// 初始化时从存储中恢复语言
|
||||
storageUtil.get(LANGUAGE_STORAGE_KEY).then((lng) => {
|
||||
const targetLng = lng || syncLng;
|
||||
// 初始化时从长期异步存储中恢复校准
|
||||
storageUtil
|
||||
.get(LANGUAGE_STORAGE_KEY)
|
||||
.then((lng) => {
|
||||
const rawTargetLng = lng || syncLng;
|
||||
|
||||
if (targetLng && isValidLanguage(targetLng) && targetLng !== i18n.language) {
|
||||
i18n.changeLanguage(targetLng);
|
||||
} else if (!targetLng) {
|
||||
// 第一次运行,归一化并持久化初始语言
|
||||
const initialLng = normalizeLanguage(i18n.language);
|
||||
storageUtil.set(LANGUAGE_STORAGE_KEY, initialLng);
|
||||
if (initialLng !== i18n.language) {
|
||||
i18n.changeLanguage(initialLng);
|
||||
if (!rawTargetLng) {
|
||||
const initialLng = normalizeLanguage(i18n.language);
|
||||
|
||||
// 💡 修复点:对初始化同步写盘追加安全的 Promise .catch() 异常隔离防护罩
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user