feat(i18n): add fallback toast and cached bundle fallback logic

1. add global i18n fallback toast on lang:fallback event, only show once per process
2. add getCachedBundleAny to retrieve expired cache for boot fallback
3. add cache fallback logic when remote i18n bundle load fails
This commit is contained in:
2026-07-02 16:33:22 +08:00
parent c86e688e47
commit e1ce2ae268
3 changed files with 102 additions and 8 deletions

View File

@@ -2,7 +2,7 @@ import { langConfig } from './config-external.js';
import Config from './config.js';
import http from './http.js';
import { Platform } from './platform.js';
import { loadBundleTree, getCachedBundle, clearBundleCache } from './lang-loader.js';
import { loadBundleTree, getCachedBundle, getCachedBundleAny, clearBundleCache } from './lang-loader.js';
// 缓存已加载的语言包
const loadedLangPacks = {};
@@ -969,14 +969,47 @@ export default {
emitUpdated(lang);
return { ok: true, source, status: 'ok', version, lang };
} catch (e) {
// 完整性校验失败 / force 重试失败 / 任何异常
// 关键修复: 不再回退到 bundleCache
// 旧逻辑: catch → getCachedBundle → 用旧 cache(可能也是不完整 bundle)
// 新逻辑: catch → 不写 remoteTreeCache → $lang() 返回空 → privacy-popup 文案可能为空
// 但**永远不会展示"残缺文案"**, 也不会"用本地 lang 掩盖远端"
const _errMsg = (e && e.message) || String(e) || 'unknown';
// 归一化 reason:
// - 'timeout' : fetchBundle 因超时抛出
// - 'http' : 远端返回 code != 0
// - 'parse' : 签名校验失败等
// - 'exception' : 其他 JS 异常
const _reason = _errMsg === 'timeout' ? 'timeout'
: _errMsg === 'bundle sign invalid' ? 'parse'
: 'exception';
console.warn('[lang.boot] remote-only bundle load failed:', _errMsg);
setRemoteStatus('fail', { reason: 'exception', lang });
// 强制重试失败 / 远端拉取异常时,用任意缓存兜底(不检查 TTL)
// - loadBundleTree 内部 force=true 时直接抛错(不回退),由这里兜底
// - 比"bootCompleted=true + remoteTreeCache 为空 → $lang() 全返回空串"体验更好
// - 至少能展示过期文案,UI 不至于全空
// - 仅当内存+storage 都没缓存时,才走真正的失败路径
const cached = getCachedBundleAny({ lang, appType, uniacid });
if (cached && cached.tree) {
console.warn('[lang.boot] falling back to cached bundle:', cached.source);
remoteTreeCache[_preloadMk] = {
version: cached.version || '',
tree: cached.tree,
ts: Date.now(),
source: 'cache-fallback',
};
setRemoteStatus('fail', { reason: _reason, lang });
bootCompleted = true;
emitUpdated(lang);
return {
ok: true,
source: 'cache',
status: 'fail',
reason: _errMsg || 'fallback to cache',
lang,
cacheAge: cached.ts ? Date.now() - cached.ts : 0,
};
}
// 没有任何缓存(首次安装 + 远端挂) → 真正的失败
// $lang() 在 bootCompleted=true + remoteTreeCache 为空时返回空串(UI 降级显示空文案)
setRemoteStatus('fail', { reason: _reason, lang });
bootCompleted = true;
emitUpdated(lang);
return { ok: false, source: 'remote-failed', status: 'fail', reason: _errMsg || 'bundle failed', lang };