feat: 新增多语言国际化页面配置,修复部分页面硬编码文案

1. 新增了zh-cn和en-us两套语言下的大量页面国际化配置文件,覆盖商品、订单、会员、营销活动等多个业务模块
2. 修复了分销页面、VR页面、支付页面、商品不存在页面的硬编码提示文案,替换为国际化调用
3. 删除了原有的i18n检查脚本文件
This commit is contained in:
2026-06-25 11:00:50 +08:00
parent 5f41febd70
commit 8e218bb663
509 changed files with 49155 additions and 55313 deletions

View File

@@ -1,160 +0,0 @@
"""
i18n audit script - scans lang/ for duplicate keys + cross-references with call sites.
Run: python scripts/i18n_audit.py
"""
import os
import re
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# 自 2026-06-24 起:lang 包按业务域拆分到 lang/<locale>/<module>.js
# 运行时同时加载旧单文件 (lang/<locale>.js) 和新多文件 (lang/<locale>/index.js)
# 所以审计也要同时扫两边,否则会漏掉 60%+ 的 key
LANG_FILES = [ROOT / 'lang' / 'en-us.js', ROOT / 'lang' / 'zh-cn.js']
LANG_DIRS = [ROOT / 'lang' / 'en-us', ROOT / 'lang' / 'zh-cn']
# 跳过 lang/ 目录下的子目录扫描(已经被 LANG_DIRS 覆盖)
SKIP_LANG_SUBPATHS = True
SRC_GLOBS = ['**/*.vue', '**/*.js']
KEY_RE = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*[:=]')
CALL_RE = re.compile(r'\$lang\(\s*[\'"]([^\'"]+)[\'"]')
LANG_CALL_RE = re.compile(r'\$lang\(|\$t\(')
def walk_files(files):
"""扫所有 lang 源:旧单文件 + 新多文件聚合目录(lang/<locale>/<module>.js)
跳过 index.js 本身(它是聚合入口,导入的内容已被各模块文件覆盖)"""
out = []
for f in files:
if f.exists():
out.append(f)
for d in LANG_DIRS:
if not d.exists():
continue
for f in sorted(d.glob('*.js')):
# 跳过聚合入口,避免重复扫描(聚合内容 = 各模块 spread)
if f.name == 'index.js':
continue
out.append(f)
return out
def defined_keys(files):
"""Returns (per_file_keys, all_keys_set, in_file_dup, cross_file_dup).
Properly tracks nesting via indentation so 'common.activist' vs 'login.activist' are distinct keys."""
per_file = {}
in_file_dup = defaultdict(list)
key_to_files = defaultdict(set)
for f in files:
seen = set()
text = f.read_text(encoding='utf-8', errors='ignore')
indent_stack = [(-1, '')]
for ln in text.splitlines():
if ln.strip().startswith('//'):
continue
if not ln.strip():
continue
indent = len(ln) - len(ln.lstrip())
stripped = ln.lstrip()
m = re.match(r"""^['\"]?([A-Za-z_][A-Za-z0-9_]*)['\"]?\s*[:=]\s*(.*)$""", stripped)
if not m:
continue
key, rest = m.group(1), m.group(2).strip().rstrip(',')
while len(indent_stack) > 1 and indent_stack[-1][0] >= indent:
indent_stack.pop()
parent = indent_stack[-1][1]
full_key = f'{parent}.{key}' if parent else key
if rest.endswith('{'):
indent_stack.append((indent, full_key))
if rest.endswith('{'):
continue # 不把 object 容器当 key
if full_key in seen:
in_file_dup[str(f)].append(full_key)
seen.add(full_key)
key_to_files[full_key].add(str(f.relative_to(ROOT)))
per_file[str(f.relative_to(ROOT))] = seen
cross = {k: sorted(v) for k, v in key_to_files.items() if len(v) > 1}
return per_file, set(key_to_files.keys()), in_file_dup, cross
def called_keys():
used = set()
for f in ROOT.rglob('*'):
if not f.is_file():
continue
if not (str(f).endswith('.vue') or str(f).endswith('.js')):
continue
# 跳过整个 lang 目录(lang 文件不是调用方;它们的 key 在别处被消费)
if 'lang' in f.parts:
continue
if 'node_modules' in f.parts or 'unpackage' in f.parts:
continue
try:
text = f.read_text(encoding='utf-8')
except UnicodeDecodeError:
continue
if not LANG_CALL_RE.search(text):
continue
for m in CALL_RE.finditer(text):
used.add(m.group(1))
return used
def main():
files = walk_files(LANG_FILES)
print(f'Lang files scanned: {len(files)} (旧单文件 + 新多文件模块目录)')
print(f' - 旧单文件: lang/en-us.js + lang/zh-cn.js')
print(f' - 新多文件: lang/<locale>/<module>.js × {len(LANG_DIRS)} 个目录')
per_file, all_keys, in_file_dup, cross_dup = defined_keys(files)
used = called_keys()
print(f'\n=== Aggregate ===')
print(f' Defined keys (unique paths): {len(all_keys)}')
print(f' Called keys (unique paths): {len(used)}')
orphans_called = used - all_keys
orphans_defined = all_keys - used
print(f' Called but not defined (orphan calls): {len(orphans_called)}')
print(f' Defined but not called (dead keys): {len(orphans_defined)}')
print(f'\n=== In-file duplicates (top 30) ===')
flat = []
for f, ks in in_file_dup.items():
for k in ks:
flat.append((f, k))
for f, k in flat[:30]:
print(f' {f}: {k}')
print(f'\n=== Cross-file duplicates (defined in N files, top 40) ===')
cross_sorted = sorted(cross_dup.items(), key=lambda x: -len(x[1]))
for k, fs in cross_sorted[:40]:
print(f' {k} <- {len(fs)} files: {", ".join(fs[:3])}{" ..." if len(fs)>3 else ""}')
print(f'\n TOTAL cross-file duplicate keys: {len(cross_dup)}')
print('\n=== Orphan call keys (top 30 of %d) ===' % len(orphans_called))
for k in sorted(orphans_called)[:30]:
print(f' {k}')
print('\n=== Dead defined keys (top 30 of %d) ===' % len(orphans_defined))
for k in sorted(orphans_defined)[:30]:
print(f' {k}')
# Write machine-readable output
out = ROOT / 'docs' / 'i18n-audit.json'
out.parent.mkdir(exist_ok=True)
import json
out.write_text(json.dumps({
'defined_count': len(all_keys),
'called_count': len(used),
'orphan_calls': sorted(orphans_called),
'dead_keys': sorted(orphans_defined),
'in_file_dup': {f: ks for f, ks in in_file_dup.items()},
'cross_file_dup': cross_dup,
}, ensure_ascii=False, indent=2), encoding='utf-8')
print(f'\nFull report -> {out.relative_to(ROOT)}')
if __name__ == '__main__':
main()