## 删除的死代码脚本
- scripts/i18n_class_c.py (脚本自承'新架构下永远输出 0',纯防御性)
- scripts/full_migrate_i18n.py (其产出违反 CLAUDE.md 1.2 中文 key 铁律,已标 DEPRECATED)
- scripts/__pycache__/ (Python 缓存)
## 删除的旧 per-page lang 文件
- lang/zh-cn/<page>/<file>.js (140 个)
- lang/en-us/<page>/<file>.js (139 个)
- 共 279 个文件,运行时不再加载,0 个 key 被 .vue 调用(经 diagnose_keys.py 验证)
## 删除的备份和迁移表
- lang/_backup/ (git 已跟踪原文件)
- lang/_migration_map.json / lang/_migration-map.json (运行时不用)
## 删除的中间产物 JSON
- docs/i18n-rename-*.json (smart_rename 中间产物,已用完)
- docs/i18n-ambiguous.json / i18n-audit.json / i18n-call-rewrite.log /
i18n-clean-dead.log / i18n-file-texts.json / i18n-page-titles.json /
i18n-vue-extract.json (前一会话残留,无引用方)
- docs/i18n-{naming,shadow,strict}-audit.json (脚本会自动重新生成)
## 删除的 modular lang 文件
- lang/zh-cn/*.js / lang/en-us/*.js (20 个模块文件,内容是上一会话产生的死代码)
- lang/zh-cn/index.js / lang/en-us/index.js (聚合入口)
- lang/DEPRECATED.md (已不再相关)
## 删除的不成熟脚本
- scripts/check_perpage.py (诊断用,已完成)
- scripts/diagnose_keys.py (诊断用,已完成)
- scripts/smart_rename_i18n.py (v1-v3 都 bug,未修完)
## 现状
- lang/ 现在只有 zh-cn.js + en-us.js (单文件,运行时实际加载)
- 这两个文件已被 smart_rename 部分修复: camelCase → snake_case (8 个 key)
- 命名违规从 8236 降到 3505 (剩余都在单文件,可独立 commit 修复)
- 1362 个 orphan () 调用仍存在,详见 docs/i18n-handoff.md
165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
"""
|
||
i18n_clean_dead.py - 从 lang/en-us.js + lang/zh-cn.js 删除未被任何代码调用的死 key
|
||
|
||
设计:
|
||
- 解析 lang/<locale>.js 的全部 keys(带嵌套追踪)
|
||
- 扫描所有 .vue / .js(非 lang/、非 node_modules)的 $lang('xxx') / $lang("xxx") / this.$lang('xxx')
|
||
- 计算 dead keys = defined - used
|
||
- 对每个 dead key:
|
||
- 如果是嵌套对象内的叶子值(字符串),从父对象里删
|
||
- 如果是嵌套对象的最后一个子项,整个对象也删(如果是空对象了)
|
||
- 同步删除 en-us 和 zh-cn 两侧
|
||
|
||
退出码:
|
||
0 = 成功
|
||
非 0 = 有 dead key 列出但未删(dry-run 默认)
|
||
--apply 才会真删
|
||
"""
|
||
import re
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from collections import defaultdict
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
EN_FILE = ROOT / 'lang' / 'en-us.js'
|
||
ZH_FILE = ROOT / 'lang' / 'zh-cn.js'
|
||
EXCLUDE_DIRS = {'node_modules', 'unpackage', 'stub-app-projects', '.git', 'lang'}
|
||
|
||
|
||
def parse_keys_with_lines(path):
|
||
"""Parse lang file with nesting + line tracking.
|
||
Returns: dict of full_key -> (value, line_no, indent, parent_path)
|
||
"""
|
||
text = path.read_text(encoding='utf-8')
|
||
out = {}
|
||
indent_stack = [(-1, '', -1)] # (indent, parent_path, parent_line)
|
||
lines = text.split('\n')
|
||
for i, ln in enumerate(lines, 1):
|
||
stripped = ln.lstrip()
|
||
if stripped.startswith('//') or not stripped.strip():
|
||
continue
|
||
indent = len(ln) - len(stripped)
|
||
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_path = indent_stack[-1][1]
|
||
full_key = f'{parent_path}.{key}' if parent_path else key
|
||
if rest.endswith('{'):
|
||
indent_stack.append((indent, full_key, i))
|
||
out[full_key] = ('<object>', i, indent, parent_path)
|
||
else:
|
||
v = rest
|
||
if (v.startswith("'") and v.endswith("'")) or (v.startswith('"') and v.endswith('"')):
|
||
if len(v) >= 2:
|
||
v = v[1:-1]
|
||
out[full_key] = (v, i, indent, parent_path)
|
||
return out
|
||
|
||
|
||
def collect_used_keys():
|
||
"""Scan all .vue / .js files for $lang('xxx') / this.$lang('xxx') calls"""
|
||
used = set()
|
||
call_re = re.compile(r"\$lang\(\s*['\"]([^'\"]+)['\"]")
|
||
this_re = re.compile(r"this\.\$lang\(\s*['\"]([^'\"]+)['\"]")
|
||
for p in list(ROOT.rglob('*.vue')) + list(ROOT.rglob('*.js')):
|
||
if any(d in p.parts for d in EXCLUDE_DIRS):
|
||
continue
|
||
if p.name.startswith('i18n_') or p.name.startswith('_verify') or p.name.startswith('_find'):
|
||
continue
|
||
try:
|
||
text = p.read_text(encoding='utf-8')
|
||
except Exception:
|
||
continue
|
||
if '$lang' not in text:
|
||
continue
|
||
# strip comments to avoid false positives
|
||
text_no_comments = re.sub(r'//.*?$|/\*.*?\*/', '', text, flags=re.DOTALL | re.MULTILINE)
|
||
for m in call_re.finditer(text_no_comments):
|
||
used.add(m.group(1))
|
||
for m in this_re.finditer(text_no_comments):
|
||
used.add(m.group(1))
|
||
return used
|
||
|
||
|
||
def remove_dead_keys_from_file(path, dead_keys):
|
||
"""从 lang 文件删除 dead_keys。返回 (new_text, removed_count)"""
|
||
text = path.read_text(encoding='utf-8')
|
||
lines = text.split('\n')
|
||
|
||
# Build line removal set
|
||
lines_to_remove = set()
|
||
removed = 0
|
||
|
||
# Parse again with full info
|
||
keys_info = parse_keys_with_lines(path)
|
||
for dead_key in dead_keys:
|
||
if dead_key in keys_info:
|
||
val, line_no, indent, parent = keys_info[dead_key]
|
||
lines_to_remove.add(line_no - 1) # 0-indexed
|
||
removed += 1
|
||
|
||
new_lines = [ln for i, ln in enumerate(lines) if i not in lines_to_remove]
|
||
return '\n'.join(new_lines), removed
|
||
|
||
|
||
def main():
|
||
apply = '--apply' in sys.argv
|
||
|
||
en_keys = parse_keys_with_lines(EN_FILE)
|
||
zh_keys = parse_keys_with_lines(ZH_FILE)
|
||
used = collect_used_keys()
|
||
|
||
en_defined = {k for k, v in en_keys.items() if v[0] != '<object>'}
|
||
zh_defined = {k for k, v in zh_keys.items() if v[0] != '<object>'}
|
||
all_defined = en_defined | zh_defined
|
||
|
||
dead = sorted(all_defined - used)
|
||
|
||
print(f'en-us defined: {len(en_defined)}')
|
||
print(f'zh-cn defined: {len(zh_defined)}')
|
||
print(f'unique used: {len(used)}')
|
||
print(f'dead keys: {len(dead)}')
|
||
print()
|
||
|
||
if not dead:
|
||
print('[OK] No dead keys to remove')
|
||
return 0
|
||
|
||
print('Dead keys to remove:')
|
||
for k in dead[:50]:
|
||
print(f' - {k}')
|
||
if len(dead) > 50:
|
||
print(f' ... and {len(dead) - 50} more')
|
||
|
||
if not apply:
|
||
print('\n[DRY-RUN] 未实际删除文件。加 --apply 参数真正删除。')
|
||
return 1
|
||
|
||
# 真删
|
||
new_en, removed_en = remove_dead_keys_from_file(EN_FILE, dead)
|
||
new_zh, removed_zh = remove_dead_keys_from_file(ZH_FILE, dead)
|
||
|
||
EN_FILE.write_text(new_en, encoding='utf-8')
|
||
ZH_FILE.write_text(new_zh, encoding='utf-8')
|
||
|
||
print(f'\n[OK] en-us: removed {removed_en} keys')
|
||
print(f'[OK] zh-cn: removed {removed_zh} keys')
|
||
|
||
# 写报告
|
||
out = ROOT / 'docs' / 'i18n-clean-dead.log'
|
||
out.write_text(
|
||
f'en-us removed: {removed_en}\n'
|
||
f'zh-cn removed: {removed_zh}\n\n'
|
||
'Removed keys:\n' + '\n'.join(dead),
|
||
encoding='utf-8'
|
||
)
|
||
print(f'[OK] Log -> {out.relative_to(ROOT)}')
|
||
return 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main()) |