1. 扩展支持多语种并对齐后端白名单配置 2. 实现租户/应用/语言三维缓存隔离 3. 加固语言包合并的原型链安全防护 4. 优化语言列表加载逻辑与调试日志 5. 调整uniacid切换与语言更新的事件处理
597 lines
19 KiB
JavaScript
597 lines
19 KiB
JavaScript
import { langConfig } from './config-external.js';
|
||
import Config from './config.js';
|
||
import http from './http.js';
|
||
import { Platform } from './platform.js';
|
||
|
||
// 缓存已加载的语言包
|
||
const loadedLangPacks = {};
|
||
|
||
// 远端覆写:内存缓存
|
||
// 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() {
|
||
return uni.getStorageSync('lang') || langConfig.defaultLang;
|
||
}
|
||
|
||
/**
|
||
* 获得当前租户 id(用于多租户文案隔离)
|
||
* 复用 Config.uniacid 作为兜底,避免硬编码 0
|
||
*/
|
||
function getCurrentUniacid() {
|
||
const u = uni.getStorageSync('uniacid');
|
||
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;
|
||
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;
|
||
for (let i = 0; i < parts.length - 1; i++) {
|
||
const p = parts[i];
|
||
if (typeof cur[p] !== 'object' || cur[p] === null || Array.isArray(cur[p])) {
|
||
cur[p] = {};
|
||
}
|
||
cur = cur[p];
|
||
}
|
||
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) {
|
||
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;
|
||
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) {
|
||
let _page = getCurrentPages()[getCurrentPages().length - 1];
|
||
if (!_page) return field;
|
||
|
||
const locale = getCurrentLocale(); // 获得当前本地语言
|
||
|
||
let value = field; // 存放解析后的语言值
|
||
let langPath = ''; // 存放当前页面语言包路径
|
||
try {
|
||
//公共语言包(同步加载)
|
||
var lang = loadLangPackSync(locale, 'common');
|
||
|
||
//当前页面语言包(同步加载)
|
||
let route = _page.route;
|
||
langPath = processRoutePath(route);
|
||
|
||
// 加载当前页面语言包
|
||
let currentPageLang = loadLangPackSync(locale, langPath);
|
||
|
||
// 合并语言包 + 远端 KV 覆写
|
||
// 兜底语义:
|
||
// - local 永远在底层(merge 进 mergedLang)
|
||
// - 远端覆写由 overrideCache[mk].map 提供;失败时 map 是 {} 或上次成功的值
|
||
// - 无论 ok/fail,本次渲染都不会因为"远端拉不到"而崩 — 走的就是本地语言包
|
||
const mergedLang = Object.assign({}, lang, currentPageLang);
|
||
const ovr = overrideCache[getOverrideMemKey(locale)];
|
||
if (ovr && ovr.map) applyOverrides(mergedLang, ovr.map);
|
||
|
||
// 解析字段
|
||
var arr = field.split(".");
|
||
if (arr.length > 1) {
|
||
// 处理嵌套属性,如 common.currencySymbol
|
||
let temp = mergedLang;
|
||
let found = true;
|
||
for (let key of arr) {
|
||
if (temp[key] !== undefined) {
|
||
temp = temp[key];
|
||
} else {
|
||
found = false;
|
||
break;
|
||
}
|
||
}
|
||
value = found ? temp : field;
|
||
} else {
|
||
value = mergedLang[field] !== undefined ? mergedLang[field] : field;
|
||
}
|
||
} catch (e) {
|
||
console.error('解析语言包失败:', e, { langPath, field, locale });
|
||
value = field;
|
||
}
|
||
|
||
if (arguments.length > 1) {
|
||
//有参数,需要替换
|
||
for (var i = 1; i < arguments.length; i++) {
|
||
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
|
||
}
|
||
}
|
||
if (value == undefined || (value == 'title' && field == 'title')) value = ''; // field
|
||
|
||
// 多语言调试,只在开发模式输出
|
||
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;
|
||
},
|
||
|
||
/**
|
||
* * 切换语言
|
||
* @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);
|
||
const locale = getCurrentLocale();
|
||
|
||
// 清空已加载的"非新语种"语言包缓存
|
||
// 用 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) {
|
||
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")
|
||
// });
|
||
},
|
||
|
||
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,
|
||
}
|