一次版本冲刺中,开发分支新增了十几条界面文案,归档任务正常结束,测试人员却在切换语言后看到英文回退和空按钮。问题不在编译器,而在流水线从未把本地化完整性当成可失败的检查项。对于长期运行的云端 Mac,最有效的做法不是等人工点遍界面,而是在每次构建前直接检查 .xcstrings。
先定义门禁到底拦什么
String Catalog 是 JSON 文件,但“能被解析”不等于“可以交付”。一个实用门禁至少检查三类问题:项目要求的目标语言不存在、翻译状态不是 translated、最终字符串为空。复数、设备差异等内容还会出现在 variations 下,因此只读取第一层 stringUnit 会漏报。
先把验收范围写进仓库,而不是留在构建节点的临时配置中。假设项目源语言为英文,当前要求简体中文、日文和法文,可以把目标语言作为脚本参数传入。
| 检查项 | 失败条件 | 处理方式 |
|---|---|---|
| 目标语言 | localizations 中没有对应键 |
阻止构建 |
| 翻译状态 | 任一叶节点不是 translated |
返回具体键与语言 |
| 字符串内容 | value 为空或只有空白 |
阻止构建 |
| 不参与翻译 | shouldTranslate 为 false |
明确跳过 |
门禁只判断目录中的本地化资源是否完整,不替代界面截断、动态参数和语义准确性的人工验收。
写一个无外部依赖的检查器
把脚本保存为 Scripts/check_xcstrings.py。它只依赖 macOS 自带的 Python 运行环境;如果团队固定了独立 Python 路径,应在构建配置中显式指定,避免交互式 shell 与 CI 的 PATH 不一致。
#!/usr/bin/env python3
import argparse
import json
import pathlib
import sys
def units(node):
found = []
if isinstance(node, dict):
unit = node.get("stringUnit")
if isinstance(unit, dict):
found.append(unit)
for key, value in node.items():
if key != "stringUnit":
found.extend(units(value))
elif isinstance(node, list):
for value in node:
found.extend(units(value))
return found
parser = argparse.ArgumentParser()
parser.add_argument("root")
parser.add_argument("--locale", action="append", required=True)
args = parser.parse_args()
failures = []
files = sorted(pathlib.Path(args.root).rglob("*.xcstrings"))
if not files:
failures.append("no .xcstrings files found")
for path in files:
with path.open(encoding="utf-8") as handle:
catalog = json.load(handle)
for key, entry in catalog.get("strings", {}).items():
if entry.get("shouldTranslate") is False:
continue
localizations = entry.get("localizations", {})
for locale in args.locale:
localized = localizations.get(locale)
if localized is None:
failures.append(f"{path}:{key}:{locale}:missing locale")
continue
leaves = units(localized)
if not leaves:
failures.append(f"{path}:{key}:{locale}:missing stringUnit")
continue
for index, unit in enumerate(leaves):
state = unit.get("state")
value = unit.get("value", "")
if state != "translated":
failures.append(
f"{path}:{key}:{locale}:{index}:state={state}"
)
if not value.strip():
failures.append(
f"{path}:{key}:{locale}:{index}:empty value"
)
for failure in failures:
print(failure, file=sys.stderr)
sys.exit(1 if failures else 0)
在仓库根目录执行:
python3 Scripts/check_xcstrings.py . \
--locale zh-Hans \
--locale ja \
--locale fr
脚本以非零状态退出后,任何常见任务编排器都能终止后续步骤。输出包含文件、字符串键、语言和叶节点序号,修复者无需先翻完整构建日志。
在编译前接入云端 Mac 流程
建议把检查放在依赖解析之后、xcodebuild build 或 archive 之前。依赖准备可以确认仓库内容已经完整落盘,而提前检查又能避免为明显不合格的提交执行编译、测试和归档。
set -euo pipefail
mkdir -p build/reports
python3 Scripts/check_xcstrings.py Sources \
--locale zh-Hans \
--locale ja \
--locale fr \
2> build/reports/localization-errors.txt
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
build
set -o pipefail 很重要。后续若把输出接到 tee,没有它时流水线可能只看到最后一个命令成功,从而吞掉检查器的失败状态。报告目录也应在任务开始时重建,避免云端 Mac 上一次执行留下的文件被误认成本次结果。
用导出结果补一层人工复核
直接解析 .xcstrings 适合机器门禁,但发布前还应导出本地化包,核对开发语言、注释和上下文是否齐全。可以针对目标语言执行:
rm -rf build/xcloc
mkdir -p build/xcloc
xcodebuild -exportLocalizations \
-project App.xcodeproj \
-localizationPath build/xcloc \
-exportLanguage zh-Hans
xcodebuild -exportLocalizations \
-project App.xcodeproj \
-localizationPath build/xcloc \
-exportLanguage ja
导出失败时不要直接重试覆盖现场。先保留完整标准错误,确认 scheme、工程路径和目标语言标识。语言代码必须与 Catalog 中的键一致,例如 zh-Hans 与 zh-Hant 是两个独立目标,不能用模糊前缀代替。
导出的 .xcloc 适合交给本地化负责人抽查,但不建议提交构建产物到主分支。仓库应保存源 .xcstrings、检查脚本和目标语言清单,导出目录则作为单次任务制品。
处理常见误报并完成验收
第一类误报来自“不需要翻译”的技术字符串。不要在脚本里按键名前缀随意忽略,应在 Catalog 中明确设置 shouldTranslate: false,让规则跟随资源本身。第二类来自复数分支只翻译了 one,遗漏 other;递归读取每个 stringUnit 可以把它准确定位出来。
第三类是开发者新增语言后忘记更新 CI 参数。目标语言清单最好只有一个来源,例如项目脚本中的数组或流水线变量,避免多个任务各维护一份。第四类是空格、换行或占位符被误当成有效翻译。当前脚本会拒绝纯空白;对于 %@、%d 等格式参数,还可以增加源字符串与目标字符串的占位符集合对比,但应先覆盖转义和位置参数,再启用阻断。
合并前按以下顺序验收:
- 人为删除一个目标语言,确认任务失败。
- 把一条翻译改成空格,确认报告指出具体键。
- 制造一个未完成的复数分支,确认递归检查能发现。
- 恢复资源后重新运行,确认旧报告不会影响结果。
- 最后执行 Release 配置构建,验证门禁与正式任务使用同一份检出内容。
这样得到的不是一次性的翻译扫描,而是一条可在云端 Mac 上稳定复现的工程约束:资源不完整时尽早失败,资源修复后才进入编译、测试与归档。
常见问题
String Catalog 检查应该放在构建流程的哪个阶段?
应放在依赖准备完成之后、编译与归档之前。检查失败时立即停止,可以避免把计算时间浪费在注定不能发布的归档任务上。
只检查 state 是否为 translated 就够了吗?
不够。还要检查目标语言是否存在、翻译值是否为空,以及复数等 variations 分支中的每个 stringUnit,必要时再导出 xcloc 做人工抽查。
把下一次构建放到云端 Mac 上运行。
比较三档 Apple Silicon 配置,在五个海外节点中选择适合当前工作流的区域。