Files
lucky_shop/common/js/lang.js
ZF sun 8e218bb663 feat: 新增多语言国际化页面配置,修复部分页面硬编码文案
1. 新增了zh-cn和en-us两套语言下的大量页面国际化配置文件,覆盖商品、订单、会员、营销活动等多个业务模块
2. 修复了分销页面、VR页面、支付页面、商品不存在页面的硬编码提示文案,替换为国际化调用
3. 删除了原有的i18n检查脚本文件
2026-06-25 11:00:50 +08:00

754 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { langConfig } from './config-external.js';
import Config from './config.js';
import http from './http.js';
import { Platform } from './platform.js';
// 缓存已加载的语言包
const loadedLangPacks = {};
// 缓存合并后的语言包(common + page + 远端覆写)
// key: `${locale}__${langPath}__${overrideVersion}, value: 合并后的对象
const _mergedLangCache = {};
let _overrideVersion = 0;
// 缓存字段解析结果
// key: `${locale}__${langPath}__${overrideVersion}__${field}, value: 翻译后的字符串
const _fieldResultCache = {};
// 缓存当前页面路由(页面生命周期内不变)
// key: 页面实例标识, value: langPath
let _cachedLangPath = '';
let _cachedLangPathPage = null;
// 清除所有语言缓存(切语言/切租户/远端覆写更新时调用)
function invalidateLangCaches() {
for (const k in _mergedLangCache) { if (Object.prototype.hasOwnProperty.call(_mergedLangCache, k)) delete _mergedLangCache[k]; }
for (const k in _fieldResultCache) { if (Object.prototype.hasOwnProperty.call(_fieldResultCache, k)) delete _fieldResultCache[k]; }
_cachedLangPath = '';
_cachedLangPathPage = null;
_overrideVersion++;
}
// 递归调用保护:防止 $lang() 被间接递归(如响应式 getter 回调中再次调用 $lang)
let _langRecursionDepth = 0;
const MAX_LANG_RECURSION_DEPTH = 8;
// refresh 重入保护:防止 refresh() 内部调用 lang() 触发事件后再次调用 refresh()
let _refreshInProgress = false;
// storage 结果内存缓存:防止反复调用 uni.getStorageSync 触发 Vue 事件导致循环
// key: storage key, value: { value, ts }
const _storageResultCache = {};
const STORAGE_CACHE_TTL_MS = 200; // 200ms 内复用缓存值,足以阻断单次渲染中的事件循环
// 从缓存读取 storage 值,未命中则调用 uni.getStorageSync 并存入缓存
function cachedStorageGet(key, defaultValue) {
const now = Date.now();
const cached = _storageResultCache[key];
if (cached && (now - cached.ts) < STORAGE_CACHE_TTL_MS) {
return cached.value;
}
let value;
try {
value = uni.getStorageSync(key);
} catch (e) {
value = defaultValue;
}
_storageResultCache[key] = { value, ts: now };
return value;
}
// 手动清除 storage 缓存(切语言/切租户时调用)
function invalidateStorageCache(key) {
if (key) {
delete _storageResultCache[key];
} else {
Object.keys(_storageResultCache).forEach(k => delete _storageResultCache[k]);
}
}
// 语言包安全深复制:JSON 安全的对象即可,不处理函数/symbol/循环引用
function deepCloneLang(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
const result = new Array(obj.length);
for (let i = 0; i < obj.length; i++) {
result[i] = deepCloneLang(obj[i]);
}
return result;
}
const result = {};
for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
result[k] = deepCloneLang(obj[k]);
}
}
return result;
}
// 远端覆写:内存缓存
// shape: { [app_lang]: { version, map, ts, global_version } }
// key 格式 `${appType}_${lang}`,天然按平台 + 语种二维隔离,避免多端 cache 串味
const overrideCache = {};
// 远端覆写:并发去重
const overrideInflight = {};
// 远端可用性三态机:'unknown'(启动后未拉过) | 'ok'(最近一次成功) | 'fail'(最近一次失败)
// 用 epoch ms 记录"最近一次状态变更时间",便于 UI 显示"离线 X 秒"
let remoteStatus = 'unknown';
let remoteStatusAt = 0;
const remoteStatusListeners = [];
/**
* 校验 source 配置:只接受 local / remote / hybrid,其它一律视为 remote(安全默认)
*/
function normalizeSource(s) {
return ['local', 'remote', 'hybrid'].includes(s) ? s : 'remote';
}
const SOURCE = normalizeSource(langConfig.source);
/**
* 远端失败原因归一(对外稳定枚举,内部细节不外泄)
* - timeout : 网络/服务端超时
* - network : 网络层失败(断网/DNS/...)
* - http : 业务码非 0
* - parse : 响应解析失败
* - exception : JS 异常
*/
function normalizeReason(s) {
const allow = ['timeout', 'network', 'http', 'parse', 'exception', 'unknown'];
return allow.includes(s) ? s : 'unknown';
}
/**
* 设置远端状态 + 触发监听器 + 发 uni.$emit
* 注意:ok 和 fail 都是"终态",允许相互覆盖
* detail 字段只对外透出 enum(无原始 message / error),避免内部栈外泄
*/
function setRemoteStatus(s, detail) {
if (remoteStatus === s) return;
const prev = remoteStatus;
remoteStatus = s;
remoteStatusAt = Date.now();
// 归一化 detail:reason,detail 内部不向外透 error/message
const safeDetail = {
reason: normalizeReason(detail && detail.reason),
lang: detail && detail.lang,
};
remoteStatusListeners.forEach(fn => {
try { fn(s, safeDetail); } catch (e) { /* 忽略监听器异常 */ }
});
try { uni.$emit && uni.$emit('lang:remote-status', { from: prev, to: s, ...safeDetail }); } catch (e) {}
// 状态变成 fail 时,额外发一个"已切到本地兜底"事件,方便 UI 弹 toast
if (s === 'fail') {
try { uni.$emit && uni.$emit('lang:fallback', { reason: safeDetail.reason }); } catch (e) {}
}
}
// 处理页面目录映射
function processRoutePath(route) {
let routeParts = route.split("/");
// ---- 处理页面目录映射 <begin> 分包造成的,需要根据实际目录结构进行映射----
// 先处理特殊的分包路径
if (routeParts[0] === 'pages_tool') {
// pages_tool 分包下的页面,直接使用子目录作为语言包路径
routeParts = [routeParts[1], ...routeParts.slice(2)];
} else if (routeParts[0] === 'pages_goods') {
// pages_goods 分包映射到 goods 目录
routeParts[0] = 'goods';
} else if (routeParts[0] === 'pages_member') {
// pages_member 分包映射到 member 目录
routeParts[0] = 'member';
} else if (routeParts[0] === 'pages_order') {
// pages_order 分包映射到 order 目录
routeParts[0] = 'order';
} else if (routeParts[0] === 'pages_promotion') {
// pages_promotion 分包特殊处理
const promotionModules = ['point', 'fenxiao', 'merch'];
if (routeParts[1] && promotionModules.includes(routeParts[1])) {
routeParts = [routeParts[1], ...routeParts.slice(2)];
}
}
// ---- 处理页面目录目录映射 <end>----
// 去掉pages目录,只保留子目录
if (routeParts[0] === 'pages') {
routeParts = routeParts.slice(1);
}
return routeParts.join("/");
}
// 加载语言包(同步方式)
function loadLangPackSync(lang, path) {
try {
if (loadedLangPacks[`${lang}_${path}`]) {
return loadedLangPacks[`${lang}_${path}`];
}
const langData = require(`@/lang/${lang}/${path}.js`).lang;
loadedLangPacks[`${lang}_${path}`] = langData;
return langData;
} catch (error) {
// console.error(`加载语言包 ${lang}/${path} 失败:`, error);
return {};
}
}
/**
* 获得当前本地语言
* @returns
*/
function getCurrentLocale() {
// 关键:使用 cachedStorageGet,防止 uni.getStorageSync 触发 Vue 事件导致 lang() 递归
const val = cachedStorageGet('lang', null);
return val || langConfig.defaultLang;
}
/**
* 获得当前租户 id(用于多租户文案隔离)
* 复用 Config.uniacid 作为兜底,避免硬编码 0
*/
function getCurrentUniacid() {
// 关键:使用 cachedStorageGet,防止 uni.getStorageSync 触发 Vue 事件导致 lang() 递归
const u = cachedStorageGet('uniacid', null);
return u ? parseInt(u, 10) || Config.uniacid : Config.uniacid;
}
/**
* 当前平台类型(多端翻译隔离)
* 复用 Platform.getAppType(),与服务端 /api/BaseApi 的 $app_type 对齐
*/
function getCurrentAppType() {
try { return Platform.getAppType() || '*'; } catch (e) { return '*'; }
}
/**
* 持久化 key: <prefix><uniacid>_<appType>_<lang>
* 三维隔离:多租户 × 多端 × 多语种,任何一维切换都拿不到旧值
*/
function getOverrideStorageKey(lang) {
const u = getCurrentUniacid();
const a = getCurrentAppType();
return `${langConfig.remote.storagePrefix}${u}_${a}_${lang}`;
}
/**
* 内存 cache key: <uniacid>_<appType>_<lang>
* 必须与 storage key 三维一致 — 切租户时内存不会"巧合"命中旧值
*/
function getOverrideMemKey(lang) {
return `${getCurrentUniacid()}_${getCurrentAppType()}_${lang}`;
}
/**
* 把"远端扁平覆写 map"按 key 路径合并到 nested 对象上
* null/undefined/非 string 视为"还原",不写入(本地值保留)
* 空串 "" 是合法覆写,会写入
*
* 危险 key 过滤:拦截 __proto__/constructor/prototype,防止恶意覆写污染对象原型链
*/
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function applyOverrides(merged, flatMap) {
if (!flatMap) return;
// 遍历路径 visited 集合:防止循环引用(如 cur[p] 恰好指向 cur 自身或父级)
const visited = new Set();
for (const key in flatMap) {
if (!Object.prototype.hasOwnProperty.call(flatMap, key)) continue;
const val = flatMap[key];
// 类型白名单:只接 string,其它都视为非法值,跳过(不污染 merged)
if (typeof val !== 'string') continue;
// 危险 key 过滤
const parts = String(key).split('.').filter(p => p && !FORBIDDEN_KEYS.has(p));
if (parts.length === 0) continue;
let cur = merged;
visited.clear();
visited.add(cur);
for (let i = 0; i < parts.length - 1; i++) {
const p = parts[i];
// 条件:非对象 / null / 数组 → 必须创建新对象
// 或:已在 visited 中(循环引用) → 创建新对象覆盖
const needsNew = typeof cur[p] !== 'object' || cur[p] === null || Array.isArray(cur[p]) || visited.has(cur[p]);
if (needsNew) {
cur[p] = {};
}
cur = cur[p];
visited.add(cur);
}
cur[parts[parts.length - 1]] = val;
}
}
/**
* 拉取并刷新内存中的远端覆写
* 设计:
* 1) source === 'local' 时整个函数直接返回 null(永远走本地)
* 2) 否则:先从 storage 取上一份缓存作为首屏兜底
* 3) 再异步拉远端,根据结果设置三态机:
* - code=0 + 有 data -> ok
* - 任何失败/超时/异常 -> fail(reason 分类: timeout/network/parse/empty)
* 4) 拉取期间不抛错;不更新 overrideCache 时,既有的 local fallback 自然生效
*
* 缓存按 (appType, lang) 二维隔离,避免 H5/MP/APP 端 cache 串味
*
* @returns {Promise<null | { source:'cache'|'remote'|'local', map, version, ok:boolean, reason?:string }>}
*/
async function loadOverrides(lang) {
if (SOURCE === 'local') return null;
const mk = getOverrideMemKey(lang);
if (overrideInflight[mk]) return overrideInflight[mk];
const task = (async () => {
const sk = getOverrideStorageKey(lang);
// 1) 复用本地缓存作为首屏兜底
try {
const cached = uni.getStorageSync(sk);
if (cached && cached.map && !overrideCache[mk]) {
overrideCache[mk] = {
version: cached.version || 0,
map: cached.map || {},
ts: cached.ts || 0,
global_version: cached.global_version || 0,
};
}
} catch (e) { /* 忽略 */ }
if (!overrideCache[mk]) overrideCache[mk] = { version: 0, map: {}, ts: 0, global_version: 0 };
// 2) 拉远端(async:false 让 sendRequest 返回 Promise)
// app_type 不用传,http.js 内部已自动注入
try {
const res = await Promise.race([
http.sendRequest({
url: langConfig.remote.overridesUrl,
data: {
lang,
version: overrideCache[mk].version,
global_version: overrideCache[mk].global_version || 0, // 穿透全局缓存
},
async: false,
}),
new Promise((r) => setTimeout(() => r({ code: -1, message: 'timeout' }), langConfig.remote.requestTimeout)),
]);
// 远端返回不合法(code!=0 / 没 data) -> fail
if (!res || res.code !== 0 || !res.data) {
const reason = !res ? 'network' : (res.code === -1 ? 'timeout' : (res.message || 'bad-response'));
setRemoteStatus('fail', { reason, lang });
return {
source: 'local', // 此时实际渲染:本地 + 空覆写 = 纯本地兜底
map: overrideCache[mk].map,
version: overrideCache[mk].version,
ok: false,
reason,
};
}
const d = res.data;
const remoteMap = (d.overrides && typeof d.overrides === 'object') ? d.overrides : {};
// 合并:远端为准(本身就是已合并"全局+租户 × 平台+跨端"的结果)
const merged = Object.assign({}, overrideCache[mk].map, remoteMap);
overrideCache[mk] = {
version: d.version || overrideCache[mk].version,
map: merged,
ts: Date.now(),
global_version: d.global_version || 0,
};
// 持久化
try { uni.setStorageSync(sk, overrideCache[mk]); } catch (e) {}
// 标记远端可用
setRemoteStatus('ok', { lang });
// 通知前端刷新
emitUpdated(lang);
return {
source: 'remote',
map: overrideCache[mk].map,
version: overrideCache[mk].version,
ok: true,
};
} catch (e) {
// 任何异常(网络/解析/...) -> 静默,落到本地
setRemoteStatus('fail', { reason: 'exception', error: String(e && e.message || e), lang });
return {
source: 'local',
map: overrideCache[mk].map,
version: overrideCache[mk].version,
ok: false,
reason: 'exception',
};
} finally {
delete overrideInflight[mk];
}
})();
overrideInflight[mk] = task;
return task;
}
/**
* 触发"翻译已更新"事件
* 复用现有 componentRefresh 模式:commit 后, golbalConfig.js 里注册的全局 computed
* `componentRefresh` 会感知到引用变化,所有使用它的组件自动重新渲染,
* 模板里的 {{$lang('xxx')}} 就会重新执行,读到最新 overrideCache
*/
function emitUpdated(lang) {
invalidateLangCaches();
try {
const store = require('@/store/index.js').default;
store.commit('setComponentRefresh');
} catch (e) {}
try { uni.$emit && uni.$emit('lang:updated', { lang }); } catch (e) {}
}
// 导出主体
export default {
langList: langConfig.langList,
/**
* 获得当前本地语言
* @returns
*/
getCurrentLocale,
/**
* 当前租户 id
*/
getCurrentUniacid,
/**
* * 阻塞式预热:App.onLaunch 调用
* 等待远端覆写到达(多租户强依赖),但有上限
* 同时检测 uniacid 是否在 App 启动时已变更,自动触发刷新
*
* @returns {Promise<{ok:boolean, source:'local'|'remote'|'cache'|'timeout', status:'unknown'|'ok'|'fail', reason?:string, lang:string}>}
*/
async boot() {
const lang = getCurrentLocale();
// source === 'local' 时不需要预热远端,本地全量已可用
if (SOURCE === 'local') {
return { ok: true, source: 'local', status: remoteStatus, lang };
}
const remote = langConfig.remote;
const t = new Promise((r) => setTimeout(() => r({ __timeout: true, code: -1, message: 'timeout' }), remote.bootTimeout));
// 检测 uniacid 是否变了(防止 App.onLaunch 改了 uniacid 但没触发 onUniacidChange)
const currentUid = getCurrentUniacid();
try {
const lastUid = uni.getStorageSync('lang_last_uniacid');
if (lastUid && String(lastUid) !== String(currentUid)) {
this.onUniacidChange();
}
uni.setStorageSync('lang_last_uniacid', currentUid);
} catch (e) {}
// 跑一次远端拉取(被超时切走时,fail 状态会由 loadOverrides 内部设置)
const result = await Promise.race([loadOverrides(lang), t]);
const finalStatus = remoteStatus;
// 远端成功/失败与否,本地兜底都已可用
if (result && result.__timeout) {
// 超时分支:loadOverrides 内部 setRemoteStatus('fail') 已触发
return { ok: false, source: 'timeout', status: finalStatus, reason: 'timeout', lang };
}
return {
ok: !!(result && result.ok),
source: (result && result.source) || 'local',
status: finalStatus,
reason: result && result.reason,
lang,
};
},
/**
* * 切租户/uniacid 后调用:清旧缓存,强制重拉
* 受 langConfig.remote.refreshOnUniacidChange 配置控制
*/
onUniacidChange() {
if (SOURCE === 'local') return;
// 尊重配置项:关闭时不强制刷新
if (langConfig.remote.refreshOnUniacidChange === false) return;
// 切租户后清除 storage 缓存,确保下次读时拿到新 uniacid
invalidateStorageCache('uniacid');
const lang = getCurrentLocale();
const sk = getOverrideStorageKey(lang);
try { uni.removeStorageSync(sk); } catch (e) {}
delete overrideCache[getOverrideMemKey(lang)];
loadOverrides(lang);
},
/**
* * 订阅远端状态变化(status: 'unknown' | 'ok' | 'fail', detail.reason 有 fail 分类)
* @param {(status:string, detail:object) => void} fn
* @returns {() => void} unsubscribe
*/
onRemoteStatusChange(fn) {
if (typeof fn !== 'function') return () => {};
remoteStatusListeners.push(fn);
// 立刻 push 当前状态(订阅时拿到初始值)
try { fn(remoteStatus, { at: remoteStatusAt, source: SOURCE }); } catch (e) {}
return () => {
const i = remoteStatusListeners.indexOf(fn);
if (i >= 0) remoteStatusListeners.splice(i, 1);
};
},
/**
* 取得当前远端状态('unknown' | 'ok' | 'fail')
*/
getRemoteStatus() { return remoteStatus; },
/**
* 当前 source 配置(规范化后):'local' | 'remote' | 'hybrid'
*/
getSource() { return SOURCE; },
/**
* * 解析多语言
* @param {Object} field
*/
lang(field) {
// 递归保护:防止响应式 getter 回调中间接调用 $lang 导致栈溢出
if (_langRecursionDepth >= MAX_LANG_RECURSION_DEPTH) {
return typeof field === 'string' ? field : String(field || '');
}
_langRecursionDepth++;
try {
const locale = getCurrentLocale();
// ------- 优化1: 页面路由缓存(页面生命周期内不变) -------
// 每次都检查当前页面实例是否变化(页面切换后需重新计算)
let _page = getCurrentPages()[getCurrentPages().length - 1];
let langPath;
if (_page && _page === _cachedLangPathPage && _cachedLangPath) {
// 同一页面实例,直接复用缓存
langPath = _cachedLangPath;
} else if (_page) {
// 新页面或首次计算,重新计算并缓存
const route = _page.route || (_page.$page && _page.$page.path) || '';
langPath = route ? processRoutePath(route) : '';
_cachedLangPath = langPath;
_cachedLangPathPage = _page;
} else {
// _page 为空时,只使用 common 语言包
langPath = '';
_cachedLangPath = '';
_cachedLangPathPage = null;
}
// ------- 优化2: 字段解析结果缓存(命中直接返回) -------
const fieldCacheKey = `${locale}__${langPath}__${_overrideVersion}__${field}`;
const cachedFieldResult = _fieldResultCache[fieldCacheKey];
const hasDefault = arguments.length > 1 && typeof arguments[1] === 'string' && arguments[1] !== '';
if (cachedFieldResult !== undefined) {
let value = cachedFieldResult;
// 没找到翻译且提供了默认值 → 使用默认值
if (value === field && hasDefault) {
return arguments[1];
}
// 带参数的翻译(如 hello {0})需要在缓存后仍做参数替换
if (arguments.length > 1) {
for (let i = 1; i < arguments.length; i++) {
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
}
}
return value;
}
// ------- 优化3: 合并语言包缓存(避免每次 deepClone + merge + 覆写应用) -------
const mergeCacheKey = `${locale}__${langPath}__${_overrideVersion}`;
let mergedLang = _mergedLangCache[mergeCacheKey];
if (!mergedLang) {
try {
const lang = loadLangPackSync(locale, 'common');
const currentPageLang = langPath ? loadLangPackSync(locale, langPath) : {};
mergedLang = Object.assign(deepCloneLang(lang), deepCloneLang(currentPageLang));
const ovr = overrideCache[getOverrideMemKey(locale)];
if (ovr && ovr.map) applyOverrides(mergedLang, ovr.map);
} catch (e) {
console.error('解析语言包失败:', e, { langPath, field, locale });
mergedLang = {};
}
_mergedLangCache[mergeCacheKey] = mergedLang;
}
// ------- 字段解析 -------
let value = field;
var arr = field.split(".");
if (arr.length > 1) {
let temp = mergedLang;
let tempVisited = new Set([temp]);
let found = true;
for (let key of arr) {
if (temp[key] !== undefined) {
temp = temp[key];
if (tempVisited.has(temp)) {
found = false;
break;
}
tempVisited.add(temp);
} else {
found = false;
break;
}
}
value = found ? (typeof temp === 'string' ? temp : field) : field;
} else {
const raw = mergedLang[field];
value = (raw !== undefined && typeof raw === 'string') ? raw : field;
}
// 将结果存入字段缓存(不含参数替换部分:参数每次可能不同)
_fieldResultCache[fieldCacheKey] = value;
// 没找到翻译且提供了默认值 → 使用默认值
if (value === field && hasDefault) {
value = arguments[1];
} else if (arguments.length > 1) {
// 带参数的翻译
for (let i = 1; i < arguments.length; i++) {
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
}
}
if (value == undefined || (value == 'title' && field == 'title')) value = '';
// 多语言调试
const __DEV__ = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production');
if (__DEV__ && field == value) {
console.warn(`[i18n] missing key "${field}" in ${langPath || '<root>'} (${locale})`);
}
return value;
} finally {
_langRecursionDepth--;
if (_langRecursionDepth < 0) _langRecursionDepth = 0;
}
},
/**
* * 切换语言
* @param {String} value 语言值
* @param {String} url 切换后跳转的页面url
*/
async change(value, url = '/pages_tool/member/index') {
let _page = getCurrentPages()[getCurrentPages().length - 1];
if (!_page) return;
const prevLocale = getCurrentLocale();
uni.setStorageSync("lang", value);
// 切语言后清除 storage 缓存,确保下次读时拿到新值
invalidateStorageCache('lang');
const locale = getCurrentLocale();
// 切语言后清除语言包合并缓存 + 字段解析缓存
invalidateLangCaches();
// 清空已加载的"非新语种"语言包缓存
// 用 Object.keys 防止被原型链污染(for...in 会遍历继承属性)
Object.keys(loadedLangPacks).forEach(key => {
if (!key.startsWith(locale + '_')) delete loadedLangPacks[key];
});
// 清空内存里的"非新语种"远端覆写 — 防止反复切语种导致内存泄漏
Object.keys(overrideCache).forEach(mk => {
if (!mk.endsWith('_' + locale)) delete overrideCache[mk];
});
// 切换语种后,先 await 远端覆写(有 storage 缓存的同语种秒开,无缓存的会触发一次网络)
// 远端失败/超时也返回 — await 拿到的就是 { ok:false, source:'local' },不会阻塞后续 refresh/reLaunch
// 注意:loadOverrides 内部已 emitUpdated(若成功),refresh() 内部不再重复 emit
if (SOURCE !== 'local') {
try { await loadOverrides(value); } catch (e) { /* 静默 */ }
}
this.refresh(false); // false = 不重复 emitUpdated
if (url) {
uni.reLaunch({ url: url });
}
},
//刷新标题、tabbar
// @param {boolean} [emit=true] 是否触发 setComponentRefresh
// change() 流程里 loadOverrides 已经 emit 过,这里要传 false 防重复
refresh(emit = true) {
// 重入保护:防止 refresh() 内部调用 lang() → 触发 storage 事件 → App.vue 再次调用 refresh() 形成死循环
if (_refreshInProgress) return;
_refreshInProgress = true;
try {
let _page = getCurrentPages()[getCurrentPages().length - 1];
if (_page) this.title(this.lang("title"));
if (emit) {
// 远端 i18n 翻译更新后,推送重渲染信号;
// 复用现有 componentRefresh 机制, golbalConfig.js 已把 componentRefresh
// 注册为全局 computed,commit 后所有引用它的组件自动重渲染
emitUpdated(getCurrentLocale());
}
//设置tabbar的文字语言
// uni.setTabBarItem({
// index: 0,
// text: this.lang("tabBar.home")
// });
// uni.setTabBarItem({
// index: 1,
// text: this.lang("tabBar.category")
// });
// uni.setTabBarItem({
// index: 2,
// text: this.lang("tabBar.cart")
// });
// uni.setTabBarItem({
// index: 3,
// text: this.lang("tabBar.member")
// });
} finally {
_refreshInProgress = false;
}
},
title(str) {
if (str) {
uni.setNavigationBarTitle({
title: str,
success: function (res) {
},
fail: function (err) {
}
});
}
},
// 获取语言包列表
// 优化:只读取 common.name 字段,不要全量 require 进 loadedLangPacks
// 否则后续切到不常用的语种会"恰好命中缓存",但首次访问要全量加载
list() {
var list = [];
// 用一次性临时缓存避免污染 loadedLangPacks
const tmp = {};
const getCommonName = (langType) => {
try {
if (tmp[langType]) return tmp[langType];
const data = require(`@/lang/${langType}/common.js`).lang;
tmp[langType] = (data && data.common) ? data.common.name : langType;
return tmp[langType];
} catch (e) {
return langType;
}
};
for (var i = 0; i < langConfig.langList.length; i++) {
let langType = langConfig.langList[i];
list.push({ name: getCommonName(langType), value: langType });
}
return list;
},
// 内部测试/调试用
__debug_overrideCache() { return overrideCache; },
__debug_loadOverrides: loadOverrides,
}