import { computed, reactive } from "vue"; import { SUCCESS_CODE } from "@/constants/codes.constants"; import { DEFAULT_LANG, I18N_LANG_LIST_STORAGE_KEY, I18N_LANG_MAP_STORAGE_KEY, I18N_LANG_STORAGE_KEY, } from "@/constants/i18n.constants"; import { getLocalFallback, resolveLiteralKey, toBundleKey, } from "@/constants/i18n.local.constants"; import { apiI18nLangList, apiI18nQuery, apiI18nSetLang } from "@/servers/i18n"; import type { I18nLangDto, I18nState } from "@/types/interfaces/i18n"; import { syncThirdPartyLocales } from "@/utils/i18n-third-party"; const parseJSON = (value: string, fallback: T): T => { if (!value) return fallback; try { return JSON.parse(value) as T; } catch (error) { return fallback; } }; const isH5Platform = (): boolean => { // #ifdef H5 || WEB return true; // #endif return false; }; const isMpWeixinPlatform = (): boolean => { // #ifdef MP-WEIXIN return true; // #endif return false; }; const getBrowserLang = (): string => { // #ifdef H5 || WEB const navLang = navigator?.language || DEFAULT_LANG; return navLang; // #endif // #ifdef MP-WEIXIN try { const sysLang = uni.getSystemInfoSync().language; if (sysLang) return sysLang; } catch (e) {} // #endif return DEFAULT_LANG; }; export const normalizeLang = (lang: string): string => { const source = (lang || "").trim(); if (!source) return DEFAULT_LANG; const bundleKey = toBundleKey(source); // 将 bundle key 转换为 PascalCase 显示格式 if (bundleKey === "zh-cn") return "zh-CN"; if (bundleKey === "zh-tw") return "zh-TW"; if (bundleKey === "zh-hk") return "zh-HK"; if (bundleKey === "en-us") return "en-US"; // 未知语言保持原始归一化形式 return source.replaceAll("_", "-"); }; const getStoredLang = (): string => { const cachedLang = uni.getStorageSync(I18N_LANG_STORAGE_KEY); if (cachedLang) return normalizeLang(cachedLang); return ""; }; const setStoredLang = (lang: string) => { uni.setStorageSync(I18N_LANG_STORAGE_KEY, normalizeLang(lang)); }; const setStoredLangMap = (langMap: Record) => { uni.setStorageSync(I18N_LANG_MAP_STORAGE_KEY, JSON.stringify(langMap || {})); }; const setStoredLangList = (langList: I18nLangDto[]) => { uni.setStorageSync( I18N_LANG_LIST_STORAGE_KEY, JSON.stringify(langList || []), ); }; const getCachedLangMap = (): Record => { const text = uni.getStorageSync(I18N_LANG_MAP_STORAGE_KEY) || ""; return parseJSON>(text, {}); }; const getCachedLangList = (): I18nLangDto[] => { const text = uni.getStorageSync(I18N_LANG_LIST_STORAGE_KEY) || ""; return parseJSON(text, []); }; const sortLangList = (list: I18nLangDto[]): I18nLangDto[] => { const filtered = (list || []).filter((item) => item?.status === 1); return filtered.sort((a, b) => { if (a.sort !== b.sort) return a.sort - b.sort; return a.id - b.id; }); }; const getDefaultLangFromList = (list: I18nLangDto[]): string => { const defaultItem = list.find((item) => item?.isDefault === 1); return normalizeLang(defaultItem?.lang || DEFAULT_LANG); }; export const i18nState = reactive({ currentLang: DEFAULT_LANG, langMap: {}, langList: [], loaded: false, loading: false, }); let loadI18nTask: Promise | null = null; let loadLangListTask: Promise | null = null; const TAB_BAR_I18N_ITEMS: Array<{ index: number; key: string }> = [ { index: 0, key: "Mobile.Nav.home" }, { index: 1, key: "Mobile.Nav.unionRecord" }, { index: 2, key: "Mobile.Nav.agent" }, { index: 3, key: "Mobile.Nav.app" }, ]; const fillTemplate = ( source: string, params: Record = {}, ): string => { let output = source || ""; Object.keys(params || {}).forEach((key) => { const val = String(params[key] ?? ""); output = output.replace(new RegExp(`\\{${key}\\}`, "g"), val); }); return output; }; const pickLangForInit = (): string => { const cacheLang = getStoredLang(); const browserLang = normalizeLang(getBrowserLang()); return normalizeLang(cacheLang || browserLang || DEFAULT_LANG); }; const fetchLangList = async (): Promise => { // 允许在 H5 和微信小程序环境调用接口 if (isH5Platform() || isMpWeixinPlatform()) { try { const res = await apiI18nLangList(); if (res?.code === SUCCESS_CODE && Array.isArray(res?.data)) { const sortedList = sortLangList(res.data); setStoredLangList(sortedList); return sortedList; } } catch (error) { console.error("fetchLangList failed:", error); } } return sortLangList(getCachedLangList()); }; const fetchLangMap = async (lang: string = "", active: boolean = false): Promise> => { // 允许在 H5 和微信小程序环境调用接口 if (!isH5Platform() && !isMpWeixinPlatform()) { return {}; } // 仅在主动切换 (active=true) 且有有效 lang 时才透传给后端, // 否则传空字符串,后端将返回默认翻译而不会修改用户配置。 const targetLang = (active && lang) ? normalizeLang(lang) : ""; try { // lang 为空时由后端按用户已保存语言/命中逻辑返回对应词典 const res = await apiI18nQuery(targetLang); // 只有成功且有数据才返回数据,其余(失败、异常、data=null)统统返回空对象以触发兜底逻辑 if (res?.code === SUCCESS_CODE && res?.data) { return res.data; } } catch (error) { console.error("fetchLangMap failed:", error); } return {}; }; export const bootstrapI18nCache = () => { const lang = getStoredLang(); i18nState.currentLang = normalizeLang(lang || pickLangForInit()); i18nState.langMap = getCachedLangMap(); i18nState.langList = sortLangList(getCachedLangList()); i18nState.loaded = Object.keys(i18nState.langMap || {}).length > 0; syncThirdPartyLocales(i18nState.currentLang); applyTabBarI18n(); }; const resolveMessage = ( lang: string, keyOrText: string, fallback: string = "", ): string => { const key = resolveLiteralKey(keyOrText); // 1. 优先尝试从服务端下发的语言包匹配 const langMap = i18nState.langMap; if (langMap != null) { const fromServer = langMap[key]; // 如果 key 存在(即便 value 是空字符串) if (fromServer !== undefined) { // 如果 value 为空字符串,则返回 key 本身;否则返回翻译值(保持同 PC 端一致) return fromServer === "" ? key : fromServer; } } // 2. 映射未命中或原生端通过本地包兜底 const currentLangKey = toBundleKey(i18nState.currentLang || DEFAULT_LANG); // 如果当前是中文类语言,优先尝试本地中文包兜底(实现原生端默认中文需求) if (currentLangKey.startsWith("zh")) { const zhFallbackValue = getLocalFallback(currentLangKey, key); if (zhFallbackValue) return zhFallbackValue; } // 3. 全局英语兜底(仅当非中文且远程失败,或中文本地包仍缺失时) const enFallbackValue = getLocalFallback("en-us", key); if (enFallbackValue) return enFallbackValue; // 3. 最后兜底返回 return fallback || keyOrText; }; export const t = ( key: string, params: Record = {}, fallback: string = "", ): string => { const lang = normalizeLang(i18nState.currentLang || DEFAULT_LANG); const value = resolveMessage(lang, key, fallback); return fillTemplate(value, params); }; export const translateText = ( text: string, params: Record = {}, ): string => { if (!text) return ""; return t(text, params, text); }; export const applyTabBarI18n = () => { // #ifndef MP-WEIXIN || H5 || WEB return; // #endif TAB_BAR_I18N_ITEMS.forEach((item) => { try { uni.setTabBarItem({ index: item.index, text: t(item.key), fail: (err) => { // Ignore tabBar sync failures on non-tab pages. }, }); } catch (error) { // Ignore tabBar sync failures on non-tab pages. } }); }; export const loadI18n = async (active: boolean = false) => { if (loadI18nTask) { await loadI18nTask; return; } loadI18nTask = (async () => { i18nState.loading = true; try { const initLang = pickLangForInit(); let targetLang = initLang; // 如果翻译包为空(接口失败或返回 null),统一强制回退到本地英语态作为安全兜底 i18nState.currentLang = normalizeLang(targetLang); const langMap = await fetchLangMap(targetLang, active); // 如果翻译包为空(接口失败或返回 null),且非中文环境,统一回退到本地英语态作为安全兜底 if (Object.keys(langMap).length === 0 && !targetLang.toLowerCase().startsWith("zh")) { i18nState.currentLang = normalizeLang("en-us"); i18nState.langMap = {}; } else { i18nState.currentLang = normalizeLang(targetLang); i18nState.langMap = langMap; } i18nState.loaded = true; setStoredLang(i18nState.currentLang); setStoredLangMap(i18nState.langMap); syncThirdPartyLocales(i18nState.currentLang); applyTabBarI18n(); } finally { i18nState.loading = false; } })(); try { await loadI18nTask; } finally { loadI18nTask = null; } }; /** * 确保语言列表已加载(按需加载) */ export const ensureLangList = async () => { if (loadLangListTask) { await loadLangListTask; return; } loadLangListTask = (async () => { try { const list = await fetchLangList(); i18nState.langList = list; } catch (error) { console.error("ensureLangList failed:", error); } })(); try { await loadLangListTask; } finally { loadLangListTask = null; } }; export const setLanguage = async (lang: string): Promise => { const targetLang = normalizeLang(lang); if (!targetLang) return false; const previousLang = i18nState.currentLang; const previousLangMap = i18nState.langMap; try { const queryResult = await apiI18nQuery(targetLang); // 接口请求失败或业务码错误,执行统一英语兜底 if (!queryResult || queryResult.code !== SUCCESS_CODE || !queryResult.data) { i18nState.currentLang = normalizeLang("en-us"); i18nState.langMap = {}; } else { i18nState.currentLang = targetLang; i18nState.langMap = queryResult.data; } setStoredLang(i18nState.currentLang); setStoredLangMap(i18nState.langMap); syncThirdPartyLocales(i18nState.currentLang); applyTabBarI18n(); return true; } catch (error) { console.error("setLanguage failed, perform English fallback:", error); // 接口异常,执行统一英语兜底 i18nState.currentLang = normalizeLang("en-us"); i18nState.langMap = {}; setStoredLang(i18nState.currentLang); setStoredLangMap(i18nState.langMap); syncThirdPartyLocales(i18nState.currentLang); applyTabBarI18n(); return true; // 即使异常也视为“处理成功(切换到了兜底态)”以避免业务阻塞 } }; export const syncLanguageFromUser = async ( lang: string = "", ): Promise => { const userLang = normalizeLang(lang); if (!userLang) return; if (userLang === normalizeLang(i18nState.currentLang)) { setStoredLang(userLang); if (Object.keys(i18nState.langMap || {}).length === 0) { const latestLangMap = await fetchLangMap(userLang, false); // 如果翻译包为空且本地已为空,强制开启英语兜底状态 if (Object.keys(latestLangMap).length === 0) { i18nState.currentLang = normalizeLang("en-us"); i18nState.langMap = {}; } else { i18nState.langMap = latestLangMap; } setStoredLangMap(i18nState.langMap); setStoredLang(i18nState.currentLang); syncThirdPartyLocales(i18nState.currentLang); applyTabBarI18n(); } return; } i18nState.currentLang = userLang; setStoredLang(userLang); const latestLangMap = await fetchLangMap(userLang, false); // 如果同步中翻译包为空(接口异常等),统一回退到英语 if (Object.keys(latestLangMap).length === 0) { i18nState.currentLang = normalizeLang("en-us"); i18nState.langMap = {}; } else { i18nState.langMap = latestLangMap; } setStoredLangMap(i18nState.langMap); setStoredLang(i18nState.currentLang); syncThirdPartyLocales(i18nState.currentLang); applyTabBarI18n(); }; export const useI18n = () => { return { i18nState, t, translateText, currentLang: computed(() => i18nState.currentLang), langList: computed(() => sortLangList(i18nState.langList || [])), loadI18n, ensureLangList, setLanguage, syncLanguageFromUser, }; };