fix(remote lang tree): 修复命名空间被基本类型值覆盖的bug

修复remote-only模式下语言包归一化的历史bug:当页面leaf顶层存在与命名空间同名的字符串键时,会覆盖整个namespace导致子key失效。通过对齐normalizePageLangPack的两层合并逻辑来修复:
1. 先将页面语言包按路径段转为命名空间结构再合并
2. 额外平铺leaf的基本类型键到根节点以兼容短key调用
This commit is contained in:
2026-07-04 09:25:01 +08:00
parent 783c10fc62
commit 2d7d7f731c
7 changed files with 12220 additions and 16 deletions

View File

@@ -170,11 +170,35 @@ function normalizePageLangPack(path, langData) {
}
let namespaced = {};
let cursor = namespaced;
{
let cursor = namespaced;
for (let i = 0; i < segments.length; i++) {
const camelSegment = toCamelCaseName(segments[i]);
cursor[camelSegment] = i === segments.length - 1 ? deepCloneLang(currentPageLeaf) : {};
cursor = cursor[camelSegment];
}
}
// 同步建立 snake_case 镜像:让 camelCase 与 snake_case 路径都能解析到同一份 leaf
// (与 bundle 服务器侧 emit 形式对齐 —— bundle 用 snake_case 段名,如 point.order_list.statusAll)
// 实现:在每个 namespace 节点同时挂一个 snake 别名键,与 camel 节点共享 leaf 引用(0 拷贝)
const nested = [];
{
let cursor = namespaced;
nested.push(cursor);
for (let i = 0; i < segments.length - 1; i++) {
cursor = cursor[toCamelCaseName(segments[i])];
nested.push(cursor);
}
}
for (let i = 0; i < segments.length; i++) {
const segment = toCamelCaseName(segments[i]);
cursor[segment] = i === segments.length - 1 ? deepCloneLang(currentPageLeaf) : {};
cursor = cursor[segment];
const raw = segments[i];
const camel = toCamelCaseName(raw);
const snake = toSnakeCaseName(raw);
if (snake !== camel) {
const isLeaf = i === segments.length - 1;
nested[i][snake] = isLeaf ? deepCloneLang(currentPageLeaf) : {};
}
}
return deepMergeLang(result, namespaced);
@@ -811,6 +835,19 @@ function emitUpdated(lang) {
/**
* remote-only 模式下,为当前页面做 tree 归一化
* 把页面路径对应的 subtree 合并到顶层,兼容 $lang('title') 等短 key 调用
*
* 历史 bug: 之前直接 `deepMergeLang(tree, currentPageLeaf)`,
* 如果 currentPageLeaf 顶层有一个与 `tree.login` 命名空间同名(key)且值为字符串的键
* (例如 `login.login.js` 里的 `login: '登录/注册'` 会出现在 leaf 顶层),
* 会触发 `merge` 的"基本类型覆盖对象"分支,把 `tree.login` 整个 namespace 替换为字符串,
* 导致 `tree.login.login.welcomeText` 等所有子页 key 全部失效。
*
* 修复方案(对齐 `normalizePageLangPack` 的两层合并):
* 1) namespaced merge: 把 currentPageLeaf 按 langPath 段逐层包成 `{seg1: {seg2: leaf}}`,
* 再 merge 进 tree —— 同样的 key 仍能递归合并,而 namespace 不会被字符串覆盖。
* 2) 在 result 上额外"轻量 overlay":把 leaf 里的 SCALAR key 平铺到 root,
* 但跳过 target 已经是 namespace 对象的 key,避免再次触发 clobber。
* 这一步让 `$lang('title')` / `$lang('welcomeText')` 这类短 key 仍能在 root 命中页面副本。
*/
function normalizeRemoteTreeForPage(tree, langPath) {
if (!tree || typeof tree !== 'object') return tree || {};
@@ -839,8 +876,37 @@ function normalizeRemoteTreeForPage(tree, langPath) {
if (!currentPageLeaf) return tree;
// 页面 leaf 合并到顶层,同时也保留完整路径
return deepMergeLang(deepCloneLang(tree), deepCloneLang(currentPageLeaf));
// (1) namespaced merge
const namespaced = {};
let cursor = namespaced;
for (let i = 0; i < segments.length; i++) {
const segment = toCamelCaseName(segments[i]);
cursor[segment] = i === segments.length - 1 ? deepCloneLang(currentPageLeaf) : {};
cursor = cursor[segment];
}
const merged = deepMergeLang(deepCloneLang(tree), namespaced);
// (2) 短 key 兼容: 把 leaf 里的 SCALAR 平铺到 root,但跳过 target 已有 namespace 的 key
try {
for (const k of Object.prototype.hasOwnProperty.call(currentPageLeaf, '__proto__') ? [] : Object.keys(currentPageLeaf)) {
if (FORBIDDEN_KEYS.has(k)) continue;
const srcVal = currentPageLeaf[k];
const tgtVal = merged[k];
// 源非对象(基本类型)才平铺;源是对象则让上一步 namespaced merge 处理
if (srcVal === null || typeof srcVal !== 'object' || Array.isArray(srcVal)) {
// 若 target 已是 namespace 对象,跳过,避免 string-vs-object clobber
if (tgtVal && typeof tgtVal === 'object' && !Array.isArray(tgtVal)) continue;
merged[k] = srcVal;
} else {
// 源是对象 —— 递归合并到 merged[k](若存在)或跳过
if (tgtVal && typeof tgtVal === 'object' && !Array.isArray(tgtVal)) {
merged[k] = deepMergeLang(deepCloneLang(tgtVal), deepCloneLang(srcVal));
}
}
}
} catch (e) { /* 静默 */ }
return merged;
}
// 导出主体

View File

@@ -0,0 +1,24 @@
// Node script: load all lang files via require and dump as JSON for Python to consume.
const fs = require('fs');
const path = require('path');
const langDir = path.resolve(process.cwd(), 'lang', process.argv[2]);
function walk(dir) {
const out = {};
if (!fs.existsSync(dir)) return out;
for (const f of fs.readdirSync(dir)) {
const full = path.join(dir, f);
const stat = fs.statSync(full);
if (stat.isDirectory()) {
Object.assign(out, walk(full));
} else if (f.endsWith('.js')) {
try {
const m = require(full);
if (m && m.lang && typeof m.lang === 'object') {
out[full] = m.lang;
}
} catch (e) {}
}
}
return out;
}
console.log(JSON.stringify(walk(langDir), null, 2));

View File

@@ -0,0 +1,231 @@
"""Full $lang vs bundle audit.
Steps:
1. Load /api/lang/bundle (lang-bundle.json) tree
2. Extract every $lang('xxx'[, ...]) first-arg string from .vue/.js files
3. Locate each key in bundle tree (and a small set of structural fallbacks)
4. Classify: FOUND / MISSING / SYNTAX_ERROR
5. For MISSING: also check local lang files (zh-cn, en-us) to determine
whether the key exists locally but bundle missed it
"""
import json, os, re, subprocess
from collections import defaultdict, Counter
ROOT = r'D:\projects\shop-projects\frontend'
BUNDLE_PATH = os.path.join(ROOT, 'lang-bundle.json')
NODE_SCRIPT = os.path.join(ROOT, 'scripts', 'audit_lang_bundle.js')
# ---------- 1. Load bundle ----------
with open(BUNDLE_PATH, 'r', encoding='utf-8') as f:
bundle = json.load(f)
tree = bundle['data']['tree']
print(f'[bundle] version={bundle["data"].get("version")} lang={bundle["data"].get("lang")} source={bundle["data"].get("source")}')
# Flatten bundle keys (leaf string values)
bundle_leaves = set()
bundle_nodes = set() # intermediate nodes too, for fallback lookups
def walk(obj, prefix=''):
if isinstance(obj, dict):
if prefix:
bundle_nodes.add(prefix)
for k, v in obj.items():
nk = f'{prefix}.{k}' if prefix else k
if isinstance(v, dict):
walk(v, nk)
else:
bundle_leaves.add(nk)
elif prefix:
bundle_leaves.add(prefix)
walk(tree)
print(f'[bundle] {len(bundle_leaves)} leaf keys, {len(bundle_nodes)} intermediate nodes')
# ---------- 2. Extract $lang() calls ----------
SKIP_DIRS = {'node_modules', '.unpackage', 'unpackage', '.git', 'dist', 'stub-app-projects'}
all_files = []
for dirpath, dirnames, filenames in os.walk(ROOT):
if any(s in dirpath for s in SKIP_DIRS):
continue
for fn in filenames:
if fn.endswith(('.vue', '.js')):
all_files.append(os.path.join(dirpath, fn))
# Match $lang('key'[, ...]) - first string arg, single OR double quotes
# $lang() can span many lines but first arg is generally on same line
call_re = re.compile(r"\$lang\(\s*(['\"])([^'\"]+)\1")
all_calls = []
for fp in all_files:
rel = os.path.relpath(fp, ROOT)
with open(fp, 'r', encoding='utf-8') as fh:
try:
content = fh.read()
except UnicodeDecodeError:
continue
for m in call_re.finditer(content):
all_calls.append({'file': rel, 'key': m.group(2), 'pos': m.start()})
print(f'[code] {len(all_calls)} $lang calls across {len(set(c["file"] for c in all_calls))} files')
unique_keys = sorted(set(c['key'] for c in all_calls))
print(f'[code] {len(unique_keys)} unique keys called')
# ---------- 3. Classify ----------
found, missing = [], []
for k in unique_keys:
if k in bundle_leaves:
found.append(k)
else:
missing.append(k)
print(f'[match] FOUND={len(found)} MISSING={len(missing)}')
# ---------- 4. For MISSING keys: check local lang files too ----------
print()
print('Loading local lang files via Node...')
def load_locale(locale):
res = subprocess.run(['node', NODE_SCRIPT, locale], cwd=ROOT,
capture_output=True, text=True, encoding='utf-8')
if res.returncode != 0:
print(f' node {locale} stderr: {res.stderr[:200]}')
return {}
parsed = json.loads(res.stdout)
cleaned = {}
for full, langobj in parsed.items():
rel = os.path.relpath(full, os.path.join(ROOT, 'lang', locale))
rel_no_ext = rel[:-3] if rel.endswith('.js') else rel
cleaned[rel_no_ext] = langobj
return cleaned
zh_cn = load_locale('zh-cn')
en_us = load_locale('en-us')
print(f' zh-cn: {len(zh_cn)} files; en-us: {len(en_us)} files')
def lookup_full(key, data):
"""Walk dotted segments in nested map."""
cur = data
for seg in key.split('.'):
if not isinstance(cur, dict) or seg not in cur:
return None
cur = cur[seg]
return cur if isinstance(cur, str) else None
def local_lookup(key, locale_map):
arr = key.split('.')
last = arr[-1]
fname_segments = '/'.join(arr[:-1])
matches = []
# 1) Short key in flat page file: e.g. fenxiao/bill.js exports {billNoPrefix}
for fname, data in locale_map.items():
if not isinstance(data, dict):
continue
if (fname.endswith('/' + fname_segments) or fname == fname_segments) and \
last in data and isinstance(data[last], str):
matches.append(('short:' + last, fname, data[last]))
# 2) Full dotted in any nested file
for fname, data in locale_map.items():
if not isinstance(data, dict):
continue
v = lookup_full(key, data)
if isinstance(v, str):
matches.append(('nested:' + key, fname, v))
break
return matches
def classify_missing(key):
"""Returns (category, evidence). category in {LOCALLY_DEFINED, SYNTAX_BUG, TRULY_MISSING}"""
# Syntax error heuristics
if key.endswith('.') or key in ('key', 'title', 'xxx'):
return 'SYNTAX_BUG', 'trailing dot or generic identifier'
if 'xxx' in key.lower() and not (key.startswith('xxx') and key.count('.') == 0):
# 'components.foo.title' / 'components-diy.xxx' style placeholders in mixin
return 'SYNTAX_BUG', "placeholder/example string (e.g. 'components.foo.title')"
# Check local files
zh = local_lookup(key, zh_cn)
en = local_lookup(key, en_us)
if zh or en:
# Capture the actual found path in local file for evidence
ev = ''
if zh:
ev += f'zh-cn: {zh[0][1]} {zh[0][0]}={zh[0][2]!r}'
if en:
ev += (' | ' if ev else '') + f'en-us: {en[0][1]} {en[0][0]}'
return 'LOCALLY_DEFINED', ev
return 'TRULY_MISSING', 'not in zh-cn / en-us / bundle'
cat_counts = Counter()
locally_defined = []
syntax_bugs = []
truly_missing = []
for k in missing:
cat, ev = classify_missing(k)
cat_counts[cat] += 1
usage = [c for c in all_calls if c['key'] == k]
entry = {
'key': k, 'category': cat, 'evidence': ev,
'usage_count': len(usage), 'files': sorted(set(c['file'] for c in usage))[:6],
}
if cat == 'LOCALLY_DEFINED':
locally_defined.append(entry)
elif cat == 'SYNTAX_BUG':
syntax_bugs.append(entry)
else:
truly_missing.append(entry)
print()
print('=== Classification of missing keys ===')
for cat, n in cat_counts.most_common():
print(f' {cat}: {n}')
# ---------- 5. Inspect a few interesting key paths in bundle ----------
# Find for each "locally defined" key, the actual structure in bundle (closest match)
def closest_in_bundle(key):
"""Return bundle path segment that contains parts of key."""
parts = key.split('.')
cur = tree
best = tree
path = []
for p in parts:
if isinstance(cur, dict):
cand = cur.get(p)
if cand is None:
# Try camelCase
cc = re.sub(r'[-_]([a-z])', lambda m: m.group(1).upper(), p)
cand = cur.get(cc)
if cand is not None:
path.append(cc)
cur = cand
continue
if cand is not None:
path.append(p)
cur = cand
else:
return '.'.join(path), type(cur).__name__
else:
return '.'.join(path), type(cur).__name__
return '.'.join(path), type(cur).__name__
print()
print('=== For LOCALLY_DEFINED keys: real bundle structure check ===')
for e in locally_defined[:10]:
actual = closest_in_bundle(e['key'])
print(f' $lang({e["key"]})')
print(f' call: {e["files"][0]}')
print(f' bundle partial walk stopped at: {actual[0]} ({actual[1]})')
# ---------- 6. Save report ----------
report = {
'bundle_version': bundle['data'].get('version'),
'bundle_lang': bundle['data'].get('lang'),
'bundle_source': bundle['data'].get('source'),
'bundle_leaves': len(bundle_leaves),
'code_calls_total': len(all_calls),
'code_unique_keys': len(unique_keys),
'found_in_bundle': len(found),
'missing_summary': dict(cat_counts),
'missing_locally_defined': locally_defined,
'missing_syntax_bugs': syntax_bugs,
'missing_truly_missing': truly_missing,
}
out_path = os.path.join(ROOT, 'scripts', 'lang_audit_report.json')
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f'\nReport saved to {out_path}')

View File

@@ -0,0 +1,91 @@
{
"bundle_version": "1782901586",
"bundle_lang": "zh-cn",
"bundle_source": "merged",
"bundle_leaves": 2649,
"code_calls_total": 2692,
"code_unique_keys": 2092,
"found_in_bundle": 2084,
"missing_summary": {
"SYNTAX_BUG": 7,
"LOCALLY_DEFINED": 1
},
"missing_locally_defined": [
{
"key": "welcomeText",
"category": "LOCALLY_DEFINED",
"evidence": "zh-cn: login\\login nested:welcomeText='您好,<br />欢迎使用' | en-us: login\\login nested:welcomeText",
"usage_count": 1,
"files": [
"common\\js\\lang.js"
]
}
],
"missing_syntax_bugs": [
{
"key": "components-diy.xxx",
"category": "SYNTAX_BUG",
"evidence": "placeholder/example string (e.g. 'components.foo.title')",
"usage_count": 1,
"files": [
"common\\js\\lang.js"
]
},
{
"key": "components.privacyPopup.xxx",
"category": "SYNTAX_BUG",
"evidence": "placeholder/example string (e.g. 'components.foo.title')",
"usage_count": 1,
"files": [
"common\\js\\lang.js"
]
},
{
"key": "components.xxx",
"category": "SYNTAX_BUG",
"evidence": "placeholder/example string (e.g. 'components.foo.title')",
"usage_count": 1,
"files": [
"common\\js\\lang.js"
]
},
{
"key": "key",
"category": "SYNTAX_BUG",
"evidence": "trailing dot or generic identifier",
"usage_count": 2,
"files": [
"common\\js\\lang.js"
]
},
{
"key": "order.list.status.",
"category": "SYNTAX_BUG",
"evidence": "trailing dot or generic identifier",
"usage_count": 1,
"files": [
"pages_order\\list.vue"
]
},
{
"key": "title",
"category": "SYNTAX_BUG",
"evidence": "trailing dot or generic identifier",
"usage_count": 3,
"files": [
"common\\js\\lang.js"
]
},
{
"key": "xxx",
"category": "SYNTAX_BUG",
"evidence": "trailing dot or generic identifier",
"usage_count": 3,
"files": [
"common\\js\\lang.js",
"main.js"
]
}
],
"missing_truly_missing": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,987 @@
{
"summary": {
"total_missing": 117,
"truly_missing_count": 105,
"defined_locally_only_count": 1,
"syntax_error_count": 11
},
"defined_locally_only": [
{
"key": "title",
"usage_count": 2,
"files": [
"common\\js\\lang.js",
"common\\js\\lang.js"
],
"local_match": [
[
"zh-cn",
"agreement\\contenr",
"nested:title",
"协议"
],
[
"en-us",
"agreement\\contenr",
"nested:title",
"Agreement"
]
]
}
],
"syntax_error": [
{
"key": "components-diy.xxx",
"usage_count": 1,
"files": [
"common\\js\\lang.js"
],
"local_match": []
},
{
"key": "components.foo.default",
"usage_count": 1,
"files": [
"common\\mixin\\i18n-lazy.js"
],
"local_match": []
},
{
"key": "components.foo.title",
"usage_count": 1,
"files": [
"common\\mixin\\i18n-lazy.js"
],
"local_match": []
},
{
"key": "components.payment.",
"usage_count": 1,
"files": [
"components\\payment\\payment.vue"
],
"local_match": []
},
{
"key": "components.privacyPopup.xxx",
"usage_count": 1,
"files": [
"common\\js\\lang.js"
],
"local_match": []
},
{
"key": "components.xxx",
"usage_count": 1,
"files": [
"common\\js\\lang.js"
],
"local_match": []
},
{
"key": "key",
"usage_count": 2,
"files": [
"common\\js\\lang.js",
"common\\js\\lang.js"
],
"local_match": []
},
{
"key": "pay.cashier.",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "point.order_list.",
"usage_count": 1,
"files": [
"pages_promotion\\point\\order_list.vue"
],
"local_match": []
},
{
"key": "status.",
"usage_count": 1,
"files": [
"pages_order\\list.vue"
],
"local_match": []
},
{
"key": "xxx",
"usage_count": 3,
"files": [
"main.js",
"common\\js\\lang.js",
"common\\js\\lang.js"
],
"local_match": []
}
],
"truly_missing": [
{
"key": "agreement.contenr.noContent",
"usage_count": 1,
"files": [
"pages_tool\\agreement\\contenr.vue"
],
"local_match": []
},
{
"key": "agreement.contenr.noContentDesc",
"usage_count": 1,
"files": [
"pages_tool\\agreement\\contenr.vue"
],
"local_match": []
},
{
"key": "agreement.contenr.reload",
"usage_count": 1,
"files": [
"pages_tool\\agreement\\contenr.vue"
],
"local_match": []
},
{
"key": "fenxiao.bill.addonClosed",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\bill.vue"
],
"local_match": []
},
{
"key": "fenxiao.bill.billNoPrefix",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\bill.vue"
],
"local_match": []
},
{
"key": "fenxiao.bill.noBill",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\bill.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.applyTime",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.auditTime",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.bankName",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.currentStatus",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.receivingAccount",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.refuseReason",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.serviceFee",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.transactionNo",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.transferTime",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "fenxiao.withdrawal_detail.transferTypeName",
"usage_count": 1,
"files": [
"pages_promotion\\fenxiao\\withdrawal_detail.vue"
],
"local_match": []
},
{
"key": "file-preview.file-preview.fileUrlNotExist",
"usage_count": 2,
"files": [
"pages_tool\\file-preview\\file-preview.vue",
"pages_tool\\file-preview\\file-preview.vue"
],
"local_match": []
},
{
"key": "file-preview.file-preview.unsupportedFileType",
"usage_count": 1,
"files": [
"pages_tool\\file-preview\\file-preview.vue"
],
"local_match": []
},
{
"key": "file-preview.file-preview.videoUrlNotExist",
"usage_count": 1,
"files": [
"pages_tool\\file-preview\\file-preview.vue"
],
"local_match": []
},
{
"key": "form.form.noFormInfo",
"usage_count": 1,
"files": [
"pages_tool\\form\\form.vue"
],
"local_match": []
},
{
"key": "form.form.submitSuccess",
"usage_count": 2,
"files": [
"pages_tool\\form\\form.vue",
"pages_tool\\form\\form.vue"
],
"local_match": []
},
{
"key": "form.form.title",
"usage_count": 1,
"files": [
"pages_tool\\form\\form.vue"
],
"local_match": []
},
{
"key": "form.formdata.formTipPlaceholder",
"usage_count": 1,
"files": [
"pages_tool\\form\\formdata.vue"
],
"local_match": []
},
{
"key": "form.formdata.noFormInfo",
"usage_count": 1,
"files": [
"pages_tool\\form\\formdata.vue"
],
"local_match": []
},
{
"key": "form.formdata.paramError",
"usage_count": 1,
"files": [
"pages_tool\\form\\formdata.vue"
],
"local_match": []
},
{
"key": "form.formdata.submitSuccess",
"usage_count": 2,
"files": [
"pages_tool\\form\\formdata.vue",
"pages_tool\\form\\formdata.vue"
],
"local_match": []
},
{
"key": "form.formdata.tip",
"usage_count": 2,
"files": [
"pages_tool\\form\\formdata.vue",
"pages_tool\\form\\formdata.vue"
],
"local_match": []
},
{
"key": "goods.brand.noMoreBrand",
"usage_count": 1,
"files": [
"pages_tool\\goods\\brand.vue"
],
"local_match": []
},
{
"key": "login.find.complexityLowercase",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.complexityNumber",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.complexitySymbol",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.complexityUppercase",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.mobileInvalid",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.mobileNotRegistered",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.passwordComplexityPrefix",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.passwordMismatch",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.passwordRequired",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.passwordTooShort",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.resendCode",
"usage_count": 2,
"files": [
"pages_tool\\login\\find.vue",
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.resendCountdown",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.step0Tip",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.step0Title",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.step1Tip",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.step1Title",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.step2Tip",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "login.find.step2Title",
"usage_count": 1,
"files": [
"pages_tool\\login\\find.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.alikePhone",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.animateCodeInput",
"usage_count": 2,
"files": [
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.bindBenefitTip",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.bindSuccess",
"usage_count": 2,
"files": [
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.codeSent",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.confirmCode",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.confirmCodeInput",
"usage_count": 3,
"files": [
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.doNotRepeatedlyClick",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.findanimateCode",
"usage_count": 3,
"files": [
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.manualBind",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.oneClickAuthBind",
"usage_count": 2,
"files": [
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.phoneNumber",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.pleInputMobile",
"usage_count": 2,
"files": [
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.save",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.smsCode",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.surePhoneNumber",
"usage_count": 2,
"files": [
"pages_tool\\member\\bind_mobile.vue",
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.bind_mobile.tipTitle",
"usage_count": 1,
"files": [
"pages_tool\\member\\bind_mobile.vue"
],
"local_match": []
},
{
"key": "member.index.iKnow",
"usage_count": 1,
"files": [
"pages_tool\\member\\index.vue"
],
"local_match": []
},
{
"key": "member.info.avatar",
"usage_count": 1,
"files": [
"pages_tool\\member\\info.vue"
],
"local_match": []
},
{
"key": "member.info.gender",
"usage_count": 1,
"files": [
"pages_tool\\member\\info.vue"
],
"local_match": []
},
{
"key": "member.public.cancelAccountConfirm",
"usage_count": 1,
"files": [
"pages_tool\\member\\public\\js\\member-public.js"
],
"local_match": []
},
{
"key": "member.public.riskWarningTitle",
"usage_count": 1,
"files": [
"pages_tool\\member\\public\\js\\member-public.js"
],
"local_match": []
},
{
"key": "pay.cashier.confirmPay",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.cashier.noPayInfo",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.cashier.noPayMethodConfigured",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.cashier.orderAlreadyPaid",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.cashier.paying",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.cashier.paymentCancelled",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.cashier.paymentFailed",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.cashier.pleaseSelectPayMethod",
"usage_count": 1,
"files": [
"pages_tool\\pay\\cashier.vue"
],
"local_match": []
},
{
"key": "pay.result.congratulations",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.couponsCount",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.goHome",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.growthValue",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.memberCard",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.noPayInfo",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.paymentFail",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.paymentSuccess",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.points",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.rechargeRecords",
"usage_count": 1,
"files": [
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "pay.result.viewOrder",
"usage_count": 4,
"files": [
"pages_tool\\pay\\result.vue",
"pages_tool\\pay\\result.vue",
"pages_tool\\pay\\result.vue",
"pages_tool\\pay\\result.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.downloadFailed",
"usage_count": 2,
"files": [
"pages_tool\\seal\\medium\\detail.vue",
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.exportImage",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.exportPdf",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.fileNotExist",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.loading",
"usage_count": 2,
"files": [
"pages_tool\\seal\\medium\\detail.vue",
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.saveFailed",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.saveSuccess",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.detail.title",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\detail.vue"
],
"local_match": []
},
{
"key": "seal.medium.search.confirmDeleteAllHistory",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\search.vue"
],
"local_match": []
},
{
"key": "seal.medium.search.confirmDeleteHistory",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\search.vue"
],
"local_match": []
},
{
"key": "seal.medium.search.noData",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\search.vue"
],
"local_match": []
},
{
"key": "seal.medium.search.placeholder",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\search.vue"
],
"local_match": []
},
{
"key": "seal.medium.search.searchResult",
"usage_count": 1,
"files": [
"pages_tool\\seal\\medium\\search.vue"
],
"local_match": []
},
{
"key": "seal.medium.search.tip",
"usage_count": 2,
"files": [
"pages_tool\\seal\\medium\\search.vue",
"pages_tool\\seal\\medium\\search.vue"
],
"local_match": []
},
{
"key": "seal.structure.currentChoicePrefix",
"usage_count": 1,
"files": [
"pages_tool\\seal\\structure.vue"
],
"local_match": []
},
{
"key": "seal.structure.pleaseSelect",
"usage_count": 1,
"files": [
"pages_tool\\seal\\structure.vue"
],
"local_match": []
},
{
"key": "seal.structure.resultFile",
"usage_count": 1,
"files": [
"pages_tool\\seal\\structure.vue"
],
"local_match": []
},
{
"key": "seal.structure.resultPrefix",
"usage_count": 1,
"files": [
"pages_tool\\seal\\structure.vue"
],
"local_match": []
}
]
}

View File

@@ -150,11 +150,34 @@ function normalizePageLangPack(packPath, langData) {
}
let namespaced = {};
let cursor = namespaced;
{
let cursor = namespaced;
for (let i = 0; i < segments.length; i++) {
const segment = toCamelCaseName(segments[i]);
cursor[segment] = i === segments.length - 1 ? deepCloneLang(currentPageLeaf) : {};
cursor = cursor[segment];
}
}
// 同步建立 snake_case 镜像:让 camelCase 与 snake_case 路径都能解析到同一份 leaf
// 与 bundle 服务器侧 emit 形式对齐(bundle 用 snake_case 段名,如 point.order_list.statusAll)
const nested = [];
{
let cursor = namespaced;
nested.push(cursor);
for (let i = 0; i < segments.length - 1; i++) {
cursor = cursor[toCamelCaseName(segments[i])];
nested.push(cursor);
}
}
for (let i = 0; i < segments.length; i++) {
const segment = toCamelCaseName(segments[i]);
cursor[segment] = i === segments.length - 1 ? deepCloneLang(currentPageLeaf) : {};
cursor = cursor[segment];
const raw = segments[i];
const camel = toCamelCaseName(raw);
const snake = raw.replace(/([A-Z])/g, '_$1').replace(/-/g, '_').toLowerCase();
if (snake !== camel) {
const isLeaf = i === segments.length - 1;
nested[i][snake] = isLeaf ? deepCloneLang(currentPageLeaf) : {};
}
}
return deepMergeLang(result, namespaced);
@@ -190,25 +213,25 @@ const P2_CASES = [
file: 'components/payment/payment.vue',
locale: 'zh-cn',
shared: ['components/payment'],
keys: ['components.payment.alipay', 'components.payment.wechatpay']
keys: ['components.payment.alipay', 'components.payment.wechatpay', 'components.payment.balancePay']
},
{
file: 'components/payment/payment.vue',
locale: 'en-us',
shared: ['components/payment'],
keys: ['components.payment.alipay', 'components.payment.wechatpay']
keys: ['components.payment.alipay', 'components.payment.wechatpay', 'components.payment.balancePay']
},
{
file: 'pages_order/list.vue',
locale: 'zh-cn',
page: 'order/list',
keys: ['status.all', 'status.waitpay', 'status.waitsend', 'status.waitconfirm', 'status.waitrate']
keys: ['order.list.status.all', 'order.list.status.waitpay', 'order.list.status.waitsend', 'order.list.status.waitconfirm', 'order.list.status.waitrate']
},
{
file: 'pages_order/list.vue',
locale: 'en-us',
page: 'order/list',
keys: ['status.all', 'status.waitpay', 'status.waitsend', 'status.waitconfirm', 'status.waitrate']
keys: ['order.list.status.all', 'order.list.status.waitpay', 'order.list.status.waitsend', 'order.list.status.waitconfirm', 'order.list.status.waitrate']
},
{
file: 'pages_promotion/fenxiao/order.vue',
@@ -238,13 +261,13 @@ const P2_CASES = [
file: 'pages_promotion/point/order_list.vue',
locale: 'zh-cn',
page: 'point/order_list',
keys: ['point.orderList.statusAll', 'point.orderList.statusPendingPayment', 'point.orderList.statusCompleted']
keys: ['point.order_list.statusAll', 'point.order_list.statusPendingPayment', 'point.order_list.statusCompleted']
},
{
file: 'pages_promotion/point/order_list.vue',
locale: 'en-us',
page: 'point/order_list',
keys: ['point.orderList.statusAll', 'point.orderList.statusPendingPayment', 'point.orderList.statusCompleted']
keys: ['point.order_list.statusAll', 'point.order_list.statusPendingPayment', 'point.order_list.statusCompleted']
},
{
file: 'pages_tool/goods/evaluate.vue',