feat(i18n): add remote translation override and hot update support
1. add remote translation override feature with multi-tenant isolation 2. implement hot reload for i18n texts via componentRefresh mechanism 3. add boot timeout fallback and local cache support 4. add events for translation update and fallback state
This commit is contained in:
22
App.vue
22
App.vue
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import auth from 'common/js/auth.js';
|
||||
import configExternal from 'common/js/config-external.js'
|
||||
import Lang from 'common/js/lang.js';
|
||||
|
||||
import {
|
||||
Weixin
|
||||
@@ -13,6 +14,27 @@
|
||||
if(options.query.uniacid){
|
||||
uni.setStorageSync('uniacid', options.query.uniacid);
|
||||
}
|
||||
|
||||
// 首屏强依赖远端翻译覆写(多租户文案强隔离)
|
||||
// boot() 内部已有 bootTimeout 上限 + 失败静默,这里包 try 防止阻塞首屏
|
||||
try { await Lang.boot(); } catch (e) { console.warn('[i18n] boot fail', e); }
|
||||
|
||||
// 监听远端翻译更新事件(管理后台改了词条后由后端推送 / 主动 reload)
|
||||
uni.$on && uni.$on('lang:updated', (info) => {
|
||||
// 复用现有 componentRefresh 机制,触发所有引用它的组件 $forceUpdate
|
||||
// 注意:lang.js 内 emitUpdated 已经 commit 了 setComponentRefresh,
|
||||
// 这里再调用一次 Lang.refresh() 是为了保证 setNavigationBarTitle
|
||||
// 这类副作用也被一并触发
|
||||
if (info && info.lang) {
|
||||
Lang.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// 远端不可用 -> 自动落到本地兜底;这里给个静默提示,业务可自行改 UI
|
||||
uni.$on && uni.$on('lang:fallback', (info) => {
|
||||
// console.warn('[i18n] remote unavailable, fallback to local', info);
|
||||
// 不阻塞首屏,不抛错,仅记录
|
||||
});
|
||||
uni.hideTabBar();
|
||||
|
||||
// #ifdef MP
|
||||
|
||||
@@ -8,7 +8,25 @@ export const langConfig = {
|
||||
// 默认语言
|
||||
defaultLang: 'zh-cn',
|
||||
// 语言包加载方式:'lazy'(按需加载)或 'preload'(预加载)
|
||||
loadMode: 'lazy'
|
||||
loadMode: 'lazy',
|
||||
// 数据源开关
|
||||
// local : 纯本地包,忽略 remote
|
||||
// remote : 本地兜底 + 后端 KV 覆写(首屏强依赖,见 bootTimeout)
|
||||
// hybrid : 同 remote,后端不可用时用本地
|
||||
source: 'remote',
|
||||
// 远端覆写相关
|
||||
remote: {
|
||||
overridesUrl: '/api/lang/overrides',
|
||||
// 首屏强依赖远端翻译(多租户文案强隔离),阻塞启动
|
||||
// 但必须有上限避免白屏
|
||||
bootTimeout: 2000,
|
||||
// 单次请求超时(ms)
|
||||
requestTimeout: 4000,
|
||||
// 本地缓存 key 前缀: <prefix><uniacid>_<lang>
|
||||
storagePrefix: 'LANG_OVR_',
|
||||
// 切租户/uniacid 时强制重拉
|
||||
refreshOnUniacidChange: true,
|
||||
}
|
||||
};
|
||||
|
||||
// 主题配置
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
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);
|
||||
|
||||
/**
|
||||
* 设置远端状态 + 触发监听器 + 发 uni.$emit
|
||||
* 注意:ok 和 fail 都是"终态",允许相互覆盖
|
||||
*/
|
||||
function setRemoteStatus(s, detail) {
|
||||
if (remoteStatus === s) return;
|
||||
const prev = remoteStatus;
|
||||
remoteStatus = s;
|
||||
remoteStatusAt = Date.now();
|
||||
remoteStatusListeners.forEach(fn => {
|
||||
try { fn(s, detail || {}); } catch (e) { /* 忽略监听器异常 */ }
|
||||
});
|
||||
try { uni.$emit && uni.$emit('lang:remote-status', { from: prev, to: s, ...(detail || {}) }); } catch (e) {}
|
||||
// 状态变成 fail 时,额外发一个"已切到本地兜底"事件,方便 UI 弹 toast
|
||||
if (s === 'fail') {
|
||||
try { uni.$emit && uni.$emit('lang:fallback', { reason: (detail && detail.reason) || 'remote-unavailable' }); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理页面目录映射
|
||||
function processRoutePath(route) {
|
||||
let routeParts = route.split("/");
|
||||
@@ -29,7 +72,7 @@ function processRoutePath(route) {
|
||||
routeParts = [routeParts[1], ...routeParts.slice(2)];
|
||||
}
|
||||
}
|
||||
// ---- 处理页面目录映射 <end>----
|
||||
// ---- 处理页面目录目录映射 <end>----
|
||||
|
||||
// 去掉pages目录,只保留子目录
|
||||
if (routeParts[0] === 'pages') {
|
||||
@@ -59,9 +102,190 @@ function loadLangPackSync(lang, path) {
|
||||
* @returns
|
||||
*/
|
||||
function getCurrentLocale() {
|
||||
return uni.getStorageSync('lang') || "zh-cn";
|
||||
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>
|
||||
* 加入 appType 是为了:H5 端缓存不会污染 MP/APP 端
|
||||
*/
|
||||
function getOverrideStorageKey(lang) {
|
||||
const u = getCurrentUniacid();
|
||||
const a = getCurrentAppType();
|
||||
return `${langConfig.remote.storagePrefix}${u}_${a}_${lang}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存 cache key: <appType>_<lang>
|
||||
* 与 storage key 一样按平台+语种二维区分
|
||||
*/
|
||||
function getOverrideMemKey(lang) {
|
||||
return `${getCurrentAppType()}_${lang}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把"远端扁平覆写 map"按 key 路径合并到 nested 对象上
|
||||
* null/undefined/非 string 视为"还原",不写入(本地值保留)
|
||||
* 空串 "" 是合法覆写,会写入
|
||||
*/
|
||||
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;
|
||||
const parts = String(key).split('.');
|
||||
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,
|
||||
|
||||
@@ -71,19 +295,110 @@ export default {
|
||||
*/
|
||||
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;
|
||||
if (!_page) return field;
|
||||
|
||||
const locale = getCurrentLocale(); // 获得当前本地语言
|
||||
|
||||
let value = ''; // 存放解析后的语言值
|
||||
let value = field; // 存放解析后的语言值
|
||||
let langPath = ''; // 存放当前页面语言包路径
|
||||
|
||||
try {
|
||||
//公共语言包(同步加载)
|
||||
var lang = loadLangPackSync(locale, 'common');
|
||||
@@ -95,13 +410,19 @@ export default {
|
||||
// 加载当前页面语言包
|
||||
let currentPageLang = loadLangPackSync(locale, langPath);
|
||||
|
||||
// 合并语言包
|
||||
let mergedLang = { ...lang, ...currentPageLang };
|
||||
// 合并语言包 + 远端 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
|
||||
// 处理嵌套属性,如 common.currencySymbol
|
||||
let temp = mergedLang;
|
||||
let found = true;
|
||||
for (let key of arr) {
|
||||
@@ -116,7 +437,6 @@ export default {
|
||||
} else {
|
||||
value = mergedLang[field] !== undefined ? mergedLang[field] : field;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('解析语言包失败:', e, { langPath, field, locale });
|
||||
value = field;
|
||||
@@ -125,24 +445,25 @@ export default {
|
||||
if (arguments.length > 1) {
|
||||
//有参数,需要替换
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
value = value.replace("{" + (i - 1) + "}", arguments[i]);
|
||||
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
|
||||
}
|
||||
}
|
||||
if (value == undefined || (value == 'title' && field == 'title')) value = ''; // field
|
||||
|
||||
// 多语言调试,注释后可以关闭控制台输出
|
||||
// 多语言调试,注释后可以关闭控制台输出
|
||||
if (field == value) {
|
||||
console.warn(`警告: 字段 ${field} 在语言包 ${langPath} 中未找到对应值,使用默认值 ${field} 当前语言: ${locale}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
/**
|
||||
* * 切换语言
|
||||
* @param {String} value 语言值
|
||||
* @param {String} url 切换后跳转的页面url
|
||||
*/
|
||||
change(value, url = '/pages_tool/member/index') {
|
||||
async change(value, url = '/pages_tool/member/index') {
|
||||
let _page = getCurrentPages()[getCurrentPages().length - 1];
|
||||
if (!_page) return;
|
||||
|
||||
@@ -156,20 +477,28 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换语种后,先 await 远端覆写(有 storage 缓存的同语种秒开,无缓存的会触发一次网络)
|
||||
// 远端失败/超时也返回 — await 拿到的就是 { ok:false, source:'local' },不会阻塞后续 refresh/reLaunch
|
||||
if (SOURCE !== 'local') {
|
||||
try { await loadOverrides(value); } catch (e) { /* 静默 */ }
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
|
||||
if (url) {
|
||||
uni.reLaunch({ url: url });
|
||||
}
|
||||
},
|
||||
|
||||
//刷新标题、tabbar
|
||||
refresh() {
|
||||
let _page = getCurrentPages()[getCurrentPages().length - 1];
|
||||
if (!_page) return;
|
||||
if (_page) this.title(this.lang("title"));
|
||||
|
||||
const locale = getCurrentLocale();
|
||||
|
||||
this.title(this.lang("title"));
|
||||
// 远端 i18n 翻译更新后,推送重渲染信号;
|
||||
// 复用现有 componentRefresh 机制, golbalConfig.js 已把 componentRefresh
|
||||
// 注册为全局 computed,commit 后所有引用它的组件自动重渲染
|
||||
emitUpdated(getCurrentLocale());
|
||||
|
||||
//设置tabbar的文字语言
|
||||
// uni.setTabBarItem({
|
||||
@@ -189,6 +518,7 @@ export default {
|
||||
// text: this.lang("tabBar.member")
|
||||
// });
|
||||
},
|
||||
|
||||
title(str) {
|
||||
if (str) {
|
||||
uni.setNavigationBarTitle({
|
||||
@@ -200,6 +530,7 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取语言包列表
|
||||
list() {
|
||||
var list = [];
|
||||
@@ -217,5 +548,9 @@ export default {
|
||||
console.error('获取语言包列表失败:', e);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
},
|
||||
|
||||
// 内部测试/调试用
|
||||
__debug_overrideCache() { return overrideCache; },
|
||||
__debug_loadOverrides: loadOverrides,
|
||||
}
|
||||
|
||||
12
main.js
12
main.js
@@ -35,6 +35,18 @@ Vue.prototype.$eventBus = EventBus;
|
||||
|
||||
Vue.mixin(globalConfig);
|
||||
|
||||
// 远端 i18n 翻译热更新:复用项目原有"componentRefresh"机制(同 diy.js 下拉刷新)
|
||||
// lang.js 在翻译覆写更新时 commit('setComponentRefresh'),
|
||||
// golbalConfig.js 已把 componentRefresh 注册为全局 computed,
|
||||
// 在此 watch 它并 $forceUpdate,触发所有页面内 {{$lang('xxx')}} 重新求值
|
||||
Vue.mixin({
|
||||
watch: {
|
||||
componentRefresh() {
|
||||
this.$forceUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
App.mpType = 'app';
|
||||
|
||||
// 重写存储,增加前缀
|
||||
|
||||
Reference in New Issue
Block a user