""" i18n_rewrite_calls.py - 用 migration-map 自动改所有 $lang 调用 扫描所有 .vue / .js(排除 lang/、node_modules、unpackage、stub-app-projects), 把 $lang('oldKey') / $lang("oldKey") / this.$lang('oldKey') 替换为 $lang('newKey')。 输出: - docs/i18n-call-rewrite.log(每个改写点) - docs/i18n-ambiguous.json(同名 oldKey 映射到多个 newKey 的位置,待人工 review) 注意:本脚本只做"机械替换",不会改 CSS / 模板结构 / 业务文案。 """ import os import re import json from pathlib import Path from collections import defaultdict ROOT = Path(__file__).resolve().parent.parent MAP_FILE = ROOT / 'lang' / '_migration-map.json' LOG_FILE = ROOT / 'docs' / 'i18n-call-rewrite.log' AMBIG_FILE = ROOT / 'docs' / 'i18n-ambiguous.json' EXCLUDE_DIRS = {'node_modules', 'unpackage', 'stub-app-projects', '.git', 'lang'} EXCLUDE_FILES_PARTS = {'_migration-map.json', 'i18n-audit.json', 'i18n-strict-audit.json', 'i18n-shadow-audit.json', 'i18n-call-rewrite.log', 'i18n-ambiguous.json'} # $lang('xxx') / $lang("xxx") / this.$lang('xxx') CALL_RE = re.compile(r"""((?:this\.)?\$lang)\(\s*(['"])([^'"]+)\2""") def collect_old_keys(): """从 migration-map.json 读取 old→new 映射""" if not MAP_FILE.exists(): print(f'[ERROR] {MAP_FILE} 不存在') return {} data = json.loads(MAP_FILE.read_text(encoding='utf-8')) return data.get('old_to_new', {}) def find_target_files(): """扫描所有需要改写的 .vue / .js""" files = [] for p in ROOT.rglob('*'): if not p.is_file(): continue if not (str(p).endswith('.vue') or str(p).endswith('.js')): continue # 排除目录 parts = set(p.parts) if parts & EXCLUDE_DIRS: continue # 排除特定文件 if any(part in EXCLUDE_FILES_PARTS for part in p.name.split('_')): continue if p.name.startswith('i18n-'): continue files.append(p) return files def rewrite_file(path, old_to_new, log_lines, ambiguous): """改写一个文件,记录所有改动""" try: text = path.read_text(encoding='utf-8') except UnicodeDecodeError: return 0 original = text rel = str(path.relative_to(ROOT)) changes = [] def repl(m): prefix = m.group(1) quote = m.group(2) old_key = m.group(3) if old_key in old_to_new: new_key = old_to_new[old_key] changes.append((old_key, new_key)) return f"{prefix}({quote}{new_key}{quote}" return m.group(0) new_text = CALL_RE.sub(repl, text) # 检测 ambiguous: 同一文件里 old_key 被映射到不同 new_key # 这里需要从 migration-map 区分 # 简单办法: 检查 old_key 是否在 old_to_new 里但 new_key 不唯一 # 由于 migration-map 是 1对1 的扁平 dict(一个 old_key 只对应一个 new_key), # 所以 ambiguous 出现在 migration-map 本身(如果未来一个 old key 被映射到多个新 key) if new_text != original: path.write_text(new_text, encoding='utf-8') for old_k, new_k in changes: log_lines.append(f'{rel}: $lang("{old_k}") -> $lang("{new_k}")') return len(changes) return 0 def main(): import argparse parser = argparse.ArgumentParser() parser.add_argument('--apply', action='store_true', help='真的改写文件(默认 dry-run)') args = parser.parse_args() old_to_new = collect_old_keys() print(f'共 {len(old_to_new)} 条迁移规则') rev = defaultdict(list) for old, new in old_to_new.items(): rev[new].append(old) ambiguous_targets = {new: olds for new, olds in rev.items() if len(olds) > 1} if ambiguous_targets: print(f'发现 {len(ambiguous_targets)} 个新 key 被多个旧 key 指向(ambiguous)') files = find_target_files() print(f'扫描到 {len(files)} 个文件') log_lines = [] total_changes = 0 files_changed = 0 for p in files: try: text = p.read_text(encoding='utf-8') except UnicodeDecodeError: continue rel = str(p.relative_to(ROOT)) file_changes = [] def repl(m, _f=file_changes): prefix = m.group(1) quote = m.group(2) old_key = m.group(3) if old_key in old_to_new: new_key = old_to_new[old_key] _f.append((old_key, new_key)) return f"{prefix}({quote}{new_key}{quote}" return m.group(0) new_text = CALL_RE.sub(repl, text) if new_text != text: file_change_count = len(file_changes) total_changes += file_change_count files_changed += 1 for old_k, new_k in file_changes: log_lines.append(f'{rel}: $lang("{old_k}") -> $lang("{new_k}")') # 仅在 --apply 模式下真正写文件 if args.apply: p.write_text(new_text, encoding='utf-8') LOG_FILE.parent.mkdir(exist_ok=True) LOG_FILE.write_text('\n'.join(log_lines), encoding='utf-8') mode = 'APPLY' if args.apply else 'DRY-RUN' print(f'\n[{mode}] 改写日志 -> {LOG_FILE.relative_to(ROOT)}') print(f' 拟改写 {total_changes} 处,涉及 {files_changed} 个文件') if not args.apply: print(f' -- 未实际修改文件。要应用请加 --apply 参数 --') AMBIG_FILE.write_text(json.dumps({ 'ambiguous_new_keys': ambiguous_targets, 'note': '这些 new_key 被多个 old_key 指向,需要人工 review 决定哪个 old_key 应该映射到哪个 new_key', }, ensure_ascii=False, indent=2), encoding='utf-8') print(f'[OK] Ambiguous 清单 -> {AMBIG_FILE.relative_to(ROOT)}') if __name__ == '__main__': main()