#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Yarn 本地化校验 CLI — 通过 Unity BatchMode 调用 YarnL10nValidatorCli。 用法: python Tools/validate_yarn_l10n.py Assets/Yarn/FP/FP_Day1_mid python Tools/validate_yarn_l10n.py Assets/Yarn --scan-all python Tools/validate_yarn_l10n.py Assets/Yarn/FP/FP_Day1_mid --export report.csv python Tools/validate_yarn_l10n.py Assets/Yarn --scan-all --fail-on-issues """ from __future__ import annotations import argparse import json import os import subprocess import sys from pathlib import Path JSON_BEGIN = "YARN_L10N_JSON_BEGIN" JSON_END = "YARN_L10N_JSON_END" DEFAULT_UNITY_EDITOR = r"C:\Program Files\Unity\Hub\Editor\2022.3.7f1c1\Editor\Unity.exe" def find_project_root() -> Path: current = Path(__file__).resolve() for parent in current.parents: if (parent / "Assets").is_dir() and (parent / "ProjectSettings").is_dir(): return parent raise RuntimeError("无法定位 Unity 项目根目录。") def resolve_unity_editor() -> Path: env_path = os.environ.get("UNITY_EDITOR_PATH") if env_path: candidate = Path(env_path) if candidate.exists(): return candidate if Path(DEFAULT_UNITY_EDITOR).exists(): return Path(DEFAULT_UNITY_EDITOR) raise RuntimeError( "未找到 Unity Editor。请设置环境变量 UNITY_EDITOR_PATH," f"或安装默认路径下的 Unity: {DEFAULT_UNITY_EDITOR}" ) def extract_json_from_output(text: str) -> dict: if JSON_BEGIN in text and JSON_END in text: payload = text.split(JSON_BEGIN, 1)[1].split(JSON_END, 1)[0].strip() return json.loads(payload) raise RuntimeError("Unity 输出中未找到 YARN_L10N_JSON 标记。") def load_result_json(project_root: Path, log_file: Path) -> dict: candidates = [ project_root / "Build" / "yarn_l10n_result.json", project_root / "Temp" / "yarn_l10n_result.json", ] for candidate in candidates: if candidate.exists(): return json.loads(candidate.read_text(encoding="utf-8")) if log_file.exists(): return extract_json_from_output(log_file.read_text(encoding="utf-8", errors="replace")) raise RuntimeError("未找到校验结果 JSON,且无法从 Unity 日志解析。") def print_summary(result: dict) -> None: print(f"Issue 总数: {result.get('TotalIssueCount', 0)}") chapters = result.get("Chapters") or [] print(f"章节数: {len(chapters)}") for chapter in chapters: folder = chapter.get("ChapterFolder", "") source_count = chapter.get("SourceLineCount", 0) locales = chapter.get("Locales") or {} locale_parts = [] for language, summary in locales.items(): locale_parts.append( f"{language}: 缺 {summary.get('MissingCount', 0)} / " f"多余 {summary.get('OrphanCount', 0)} / " f"变更 {summary.get('SourceChangedCount', 0)}" ) locale_text = " | ".join(locale_parts) if locale_parts else "无翻译语言" print(f"- {folder} — 原文 {source_count} 行 | {locale_text}") issues = result.get("Issues") or [] if not issues: print("未发现 issue。") return print("\nIssues:") for issue in issues[:50]: issue_type = issue.get("IssueTypeLabel") or issue.get("Type", "") language = issue.get("Language", "") line_id = issue.get("LineId", "") chapter = issue.get("ChapterFolder", "") print(f" [{issue_type}] {language} {line_id} @ {chapter}") if len(issues) > 50: print(f" ... 另有 {len(issues) - 50} 条 issue 未显示") def build_unity_command( project_root: Path, unity_editor: Path, yarn_path: str, scan_all: bool, export_path: str | None, output_json_path: Path, log_file: Path, ) -> list[str]: command = [ str(unity_editor), "-batchmode", "-nographics", "-projectPath", str(project_root), "-executeMethod", "AibisDream.YarnLocalizationValidation.Editor.YarnL10nValidatorCli.Run", "-yarnL10nPath", yarn_path.replace("\\", "/"), f"-yarnL10nOutput={output_json_path.as_posix()}", "-logFile", str(log_file), ] if scan_all: command.append("-yarnL10nScanAll") if export_path: command.append(f"-yarnL10nExport={export_path.replace(chr(92), '/')}") return command def main() -> int: parser = argparse.ArgumentParser(description="Yarn 本地化校验(Unity BatchMode)") parser.add_argument("path", help="章节文件夹或扫描根目录(相对 Assets/ 或绝对路径)") parser.add_argument("--scan-all", action="store_true", help="递归扫描子目录下所有 .yarnproject") parser.add_argument("--export", dest="export_path", help="导出 CSV 报告路径") parser.add_argument( "--fail-on-issues", action="store_true", help="存在 blocking issue 时返回非零退出码", ) parser.add_argument( "--unity", dest="unity_editor", help="Unity Editor 可执行文件路径(默认读取 UNITY_EDITOR_PATH)", ) args = parser.parse_args() project_root = find_project_root() unity_editor = Path(args.unity_editor) if args.unity_editor else resolve_unity_editor() temp_dir = project_root / "Build" temp_dir.mkdir(parents=True, exist_ok=True) output_json_path = temp_dir / "yarn_l10n_result.json" log_file = project_root / "Build" / "yarn_l10n_validate.log" log_file.parent.mkdir(parents=True, exist_ok=True) if output_json_path.exists(): output_json_path.unlink() command = build_unity_command( project_root=project_root, unity_editor=unity_editor, yarn_path=args.path, scan_all=args.scan_all, export_path=args.export_path, output_json_path=output_json_path, log_file=log_file, ) print("Running Unity BatchMode validation...") completed = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace") combined_output = "\n".join(part for part in [completed.stdout, completed.stderr] if part) result = load_result_json(project_root, log_file) print_summary(result) if args.export_path and not Path(args.export_path).is_absolute(): export_absolute = project_root / args.export_path if export_absolute.exists(): print(f"CSV 已导出: {export_absolute}") exit_code = completed.returncode if args.fail_on_issues and result.get("HasBlockingIssues"): exit_code = max(exit_code, 1) if completed.returncode != 0 and exit_code == 0: exit_code = completed.returncode if exit_code != 0: print(f"Unity 退出码: {completed.returncode}") print(f"日志: {log_file}") return exit_code if __name__ == "__main__": sys.exit(main())