Files
lucky_shop/common/js/lang.js
ZF sun 5d3663786c feat: add multi-language page configs and fix i18n issues
1. 新增多套多语言页面翻译文件,覆盖文件列表、案例展示、商户相关、印章相关、分销、小程序发货等页面
2. 修复语言包拼写错误:loginTpis → loginTips
3. 重构语言包合并逻辑,使用深度合并替代浅合并避免子key丢失
4. 优化导航栏标题设置,统一使用$langConfig.title替代硬编码标题
5. 修复底部导航栏语言切换不响应的问题,改为响应式获取当前语言
6. 完善英文通用语言包,补充订单、购物车、会员相关翻译
7. 修复商品详情页默认标题为空的问题,设置为"商品详情"
2026-06-25 14:52:39 +08:00

850 lines
28 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;
}
/**
* 语言包深度合并:将 source 合并到 target 中,返回 target
* - 对象类型递归合并(嵌套 key 不会整体覆盖)
* - 数组/字符串/数字等基本类型直接覆盖
* - 危险 key 过滤:__proto__ / constructor / prototype
*
* 为什么需要深合并:
* 之前用 Object.assign 做顶层浅合并,如果 page 语言包定义了 common: { submit: 'x' },
* 会整体替换 common.js 中的 common 对象,导致 common.currencySymbol 等子 key 丢失。
* 深合并可以保留两边的 key,只在 leaf 节点覆盖。
*/
function deepMergeLang(target, source) {
if (target === null || typeof target !== 'object') return source;
if (source === null || typeof source !== 'object') return source;
if (Array.isArray(target) || Array.isArray(source)) return source;
for (const key in source) {
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
if (FORBIDDEN_KEYS.has(key)) continue;
const srcVal = source[key];
const tgtVal = target[key];
// 两边都是纯对象 → 递归合并
if (
srcVal !== null && typeof srcVal === 'object' && !Array.isArray(srcVal) &&
tgtVal !== null && typeof tgtVal === 'object' && !Array.isArray(tgtVal)
) {
target[key] = deepMergeLang(tgtVal, srcVal);
} else {
// 基本类型 / 数组 / 类型不一致 → 直接覆盖
target[key] = deepCloneLang(srcVal);
}
}
return target;
}
/**
* 翻译参数替换:支持位置参数和命名参数
*
* 位置参数: $lang('key', 'a', 'b') → {0}=a, {1}=b
* 命名参数: $lang('key', { name: 'Tom', count: 3 }) → {name}=Tom, {count}=3
*
* 规则:
* - 第 2 个参数起如果是对象(非 null 非数组),则当作命名参数 map
* - 其他情况当作位置参数
* - 全局替换(同一个占位符出现多次都能替换)
* - 字面量花括号可以用 {{ 和 }} 转义(暂不支持,有需要再加)
*/
function applyLangParams(value, args) {
if (args.length === 0) return value;
let result = String(value);
const firstArg = args[0];
const isNamed = firstArg !== null && typeof firstArg === 'object' && !Array.isArray(firstArg);
if (isNamed) {
// 命名参数:遍历对象的每个 key
const map = firstArg;
for (const key in map) {
if (!Object.prototype.hasOwnProperty.call(map, key)) continue;
if (FORBIDDEN_KEYS.has(key)) continue;
const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
result = result.replace(new RegExp('\\{' + safeKey + '\\}', 'g'), String(map[key]));
}
} else {
// 位置参数: {0}, {1}, {2}...
for (let i = 0; i < args.length; i++) {
result = result.replace(new RegExp('\\{' + i + '\\}', 'g'), String(args[i]));
}
}
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] !== '';
const __DEV__ = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production');
if (cachedFieldResult !== undefined) {
let value = cachedFieldResult;
// 没找到翻译
if (value === field) {
// 有默认值 → 使用默认值
if (hasDefault) {
return arguments[1];
}
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
if (!__DEV__) {
return '';
}
// 开发环境 → 返回 raw key(便于发现问题)
return value;
}
// 带参数的翻译:支持位置参数({0})和命名参数({name})
if (arguments.length > 1) {
// 从第 2 个参数开始提取参数数组
const args = new Array(arguments.length - 1);
for (let i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
value = applyLangParams(value, args);
}
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 = deepMergeLang(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) {
// 有默认值 → 使用默认值
if (hasDefault) {
value = arguments[1];
} else if (!__DEV__) {
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
value = '';
}
// 开发环境无默认值 → 保留 raw key(便于发现问题)
} else if (arguments.length > 1) {
// 带参数的翻译:支持位置参数({0})和命名参数({name})
const args = new Array(arguments.length - 1);
for (let i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
value = applyLangParams(value, args);
}
if (value == undefined) value = '';
// 多语言调试
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,
}