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 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);
|
const initialLng = normalizeLanguage(i18n.language);
|
||||||
} else if (!targetLng) {
|
|
||||||
// 第一次运行,归一化并持久化初始语言
|
// 💡 修复点:对初始化同步写盘追加安全的 Promise .catch() 异常隔离防护罩
|
||||||
const initialLng = normalizeLanguage(i18n.language);
|
storageUtil.set(LANGUAGE_STORAGE_KEY, initialLng).catch((err) => {
|
||||||
storageUtil.set(LANGUAGE_STORAGE_KEY, initialLng);
|
console.error('[i18n Init Error] Persistent sync collapsed:', err);
|
||||||
if (initialLng !== i18n.language) {
|
});
|
||||||
i18n.changeLanguage(initialLng);
|
|
||||||
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user