#!/usr/bin/env python3 """ Merge i18n translations from all pipeline modules into wwwroot/i18n. 模块自动发现(不再手工维护清单——2026-09-12 i18n 治理:手工清单漏了 17 个模块, 词条永远进不了字典): 1. 主应用自身 i18n/ 2. pipeline_core / pipeline_ops(本地或 pkgs/) 3. build.sh clone 清单里的全部业务模块(pkgs/ 或同级仓库目录) 4. pkgs/ 下任何带 i18n/ 目录的其它模块(兜底扫描) 每模块读 i18n//msg.txt 与 i18n//i18n.json,先到先得不覆盖。 Usage: python scripts/merge_i18n.py """ import os, json, re, sys from collections import OrderedDict SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(SCRIPT_DIR) WWWROOT = os.path.join(ROOT_DIR, 'wwwroot') I18N_OUT = os.path.join(WWWROOT, 'i18n') LANGS = ['zh', 'en', 'ko', 'jp'] def discover_modules(): """返回 [(name, i18n_dir), ...],顺序即合并优先级(主应用最先)。""" mods = [] def add(name, i18n_dir): if os.path.isdir(i18n_dir) and not any(n == name for n, _ in mods): mods.append((name, i18n_dir)) # 1. 主应用 add('app', os.path.join(ROOT_DIR, 'i18n')) # 2/3. build.sh clone 清单 + 本地业务模块 names = ['pipeline_core', 'pipeline_ops'] build_sh = os.path.join(ROOT_DIR, 'build.sh') if os.path.exists(build_sh): content = open(build_sh, encoding='utf-8').read() for m in re.finditer(r'^for mod in ([^;]+); do\s*$', content, re.M): for n in m.group(1).split(): if n not in names: names.append(n) repos_parent = os.path.dirname(ROOT_DIR) for n in names: for base in (os.path.join(ROOT_DIR, 'pkgs'), ROOT_DIR, repos_parent, os.path.join(ROOT_DIR, n)): cand = os.path.join(base, n, 'i18n') if not base.endswith(n) else os.path.join(base, 'i18n') if os.path.isdir(cand): add(n, cand) break # 4. 兜底:pkgs/ 下所有带 i18n/ 的目录 pkgs = os.path.join(ROOT_DIR, 'pkgs') if os.path.isdir(pkgs): for n in sorted(os.listdir(pkgs)): add(n, os.path.join(pkgs, n, 'i18n')) return mods def parse_msg_txt(filepath): """Parse msg.txt: 'key=value' or 'key: value' format.""" result = OrderedDict() if not os.path.exists(filepath): return result with open(filepath, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue if '=' in line: key, val = line.split('=', 1) result[key.strip()] = val.strip() elif ':' in line: key, val = line.split(':', 1) result[key.strip()] = val.strip() return result def merge(): modules = discover_modules() print(f'Discovered {len(modules)} i18n sources: {[n for n, _ in modules]}') os.makedirs(I18N_OUT, exist_ok=True) for lang in LANGS: merged = OrderedDict() out_dir = os.path.join(I18N_OUT, lang) os.makedirs(out_dir, exist_ok=True) # Load existing merged translations i18n_file = os.path.join(out_dir, 'i18n.json') if os.path.exists(i18n_file): with open(i18n_file, 'r', encoding='utf-8') as f: merged = json.load(f, object_pairs_hook=OrderedDict) for mod_name, mod_i18n_dir in modules: lang_dir = os.path.join(mod_i18n_dir, lang) if not os.path.isdir(lang_dir): continue # msg.txt msgs = parse_msg_txt(os.path.join(lang_dir, 'msg.txt')) for k, v in msgs.items(): if k not in merged: merged[k] = v # i18n.json mod_i18n = os.path.join(lang_dir, 'i18n.json') if os.path.exists(mod_i18n): try: with open(mod_i18n, 'r', encoding='utf-8') as f: extra = json.load(f, object_pairs_hook=OrderedDict) except Exception as e: print(f' !! {mod_name}/{lang}/i18n.json parse error: {e}', file=sys.stderr) continue for k, v in extra.items(): if k not in merged: merged[k] = v with open(i18n_file, 'w', encoding='utf-8') as f: json.dump(merged, f, ensure_ascii=False, indent=2) print(f' {lang}: {len(merged)} keys') print(f'\nMerged i18n for {len(modules)} modules -> {I18N_OUT}') if __name__ == '__main__': merge()