工程筆記

在雲端 Mac 上建立 String Catalog 完整性把關

在雲端 Mac 上建立 String Catalog 完整性把關

在一次版本衝刺中,開發分支新增了十多條介面文案。封存作業順利完成,但測試人員切換語言後,卻看到內容回退成英文,部分按鈕甚至顯示空白。問題不在編譯器,而是流水線從未將在地化完整性列為可能失敗的檢查項目。對於長期運行的雲端 Mac,最有效的做法不是等人逐一操作所有介面,而是在每次建置前直接檢查 .xcstrings

先定義把關機制要攔截哪些問題

String Catalog 是 JSON 檔案,但「能夠解析」不代表「可以交付」。實用的把關機制至少應檢查三類問題:專案要求的目標語言不存在、翻譯狀態不是 translated,以及最終字串為空。複數形式、裝置差異等內容還會出現在 variations 之下,因此只讀取第一層 stringUnit 會造成漏報。

應先將驗收範圍寫入儲存庫,而不是留在建置節點的暫時設定中。假設專案的來源語言是英文,目前要求支援簡體中文、日文與法文,可以將目標語言當作指令碼參數傳入。

檢查項目 失敗條件 處理方式
目標語言 localizations 中沒有對應鍵 阻止建置
翻譯狀態 任一葉節點不是 translated 回傳具體鍵與語言
字串內容 value 為空或只有空白 阻止建置
不參與翻譯 shouldTranslatefalse 明確略過

把關機制只判斷目錄中的在地化資源是否完整,不能取代針對介面截斷、動態參數與語意準確性的人工驗收。

撰寫不依賴外部套件的檢查器

將指令碼儲存為 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 buildarchive 之前。相依項目準備階段可以確認儲存庫內容已完整寫入磁碟,而提前檢查也能避免對明顯不合格的提交執行編譯、測試與封存。

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-Hanszh-Hant 是兩個獨立目標,不能以模糊的前綴取代。

匯出的 .xcloc 適合交由在地化負責人抽查,但不建議將建置產物提交到主分支。儲存庫應保存來源 .xcstrings、檢查指令碼與目標語言清單;匯出目錄則應作為單次工作的產物。

排除常見誤報並完成驗收

第一類誤報來自「不需要翻譯」的技術字串。不要在指令碼中任意依照鍵名前綴忽略,而應在 Catalog 中明確設定 shouldTranslate: false,讓規則跟隨資源本身。第二類來自複數分支只翻譯了 one,卻遺漏 other;遞迴讀取每個 stringUnit,即可精確找出這類問題。

第三類情況是開發人員新增語言後,忘記更新 CI 參數。目標語言清單最好只有單一來源,例如專案指令碼中的陣列或流水線變數,避免多個工作各自維護一份。第四類則是空格、換行或預留位置被誤認為有效翻譯。目前的指令碼會拒絕純空白內容;針對 %@%d 等格式參數,也可以增加來源字串與目標字串的預留位置集合比對,但應先涵蓋跳脫字元與位置參數,再啟用阻斷。

合併前依照以下順序驗收:

  1. 刻意刪除一個目標語言,確認工作失敗。
  2. 將一條翻譯改成空格,確認報告指出具體鍵。
  3. 建立一個未完成的複數分支,確認遞迴檢查能夠發現。
  4. 還原資源後重新執行,確認舊報告不會影響結果。
  5. 最後執行 Release 設定的建置,驗證把關機制與正式工作使用同一份簽出內容。

最後建立的不是一次性的翻譯掃描,而是一項能在雲端 Mac 上穩定重現的工程約束:資源不完整時及早失敗,只有修復資源後才進入編譯、測試與封存。

常見問題

String Catalog 檢查應放在建置流程的哪個階段?

應放在相依項目準備完成之後、編譯與封存之前。發現問題便立即停止,避免把時間消耗在無法交付的封存工作。

只檢查 state 是否為 translated 就足夠嗎?

不夠。還要確認目標語言存在、翻譯內容不是空值,並遞迴檢查複數等 variations 分支中的每一個 stringUnit。

獨享實體節點

讓下一次建置在雲端 Mac 上執行。

比較三種 Apple Silicon 設定,從五個海外節點中選擇適合目前工作流程的區域。

選擇租用方案