Files
lucky_shop/scripts/i18n_clean_dead.py
ZF sun 5f41febd70 chore(i18n): 清理死代码 + 遗留 lang 文件 + 旧工具
## 删除的死代码脚本
- 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
2026-06-25 10:48:15 +08:00

165 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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())