Files
aibis-dream/Tools/validate_yarn_l10n.py
dingyuntian 7d7c8893fc feat(editor): 添加 Yarn 本地化校验工具
- 新增 Editor 窗口:AIBIS → Yarn 本地化校验
- 新增 BatchMode CLI:Tools/validate_yarn_l10n.py
- 新增文档:Docs/本地化校验工具.md
- 校验 Yarn 原文 string table 与各语言 CSV 的 line id 一致性
2026-07-04 23:12:21 +08:00

213 lines
6.9 KiB
Python
Raw Permalink 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.
#!/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())