ENGINEERING NOTE

クラウドMacでString Catalogの完全性ゲートを構築する

クラウドMacでString Catalogの完全性ゲートを構築する

あるリリーススプリントで、開発ブランチに十数件のUIテキストが追加されました。アーカイブ処理は正常に完了したものの、テスターが言語を切り替えると、英語へのフォールバックやラベルが空のボタンが表示されました。原因はコンパイラではなく、ローカライズの完全性が、失敗条件を持つチェック項目としてパイプラインに組み込まれていなかったことです。常時稼働するクラウドMacでは、人手ですべての画面を確認するよりも、ビルドのたびに事前に .xcstrings を直接検査するほうが効果的です。

ゲートで何を阻止するかを先に定義する

String CatalogはJSONファイルですが、「解析できる」ことと「リリースできる」ことは同じではありません。実用的なゲートでは、少なくとも3種類の問題を検査します。プロジェクトで必要な対象言語が存在しない、翻訳状態が translated ではない、最終的な文字列が空である、という問題です。複数形やデバイス別の差分などは variations の下にも格納されるため、最上位の stringUnit だけを読み取ると見落としが発生します。

受け入れ条件はビルドノードの一時的な設定に残さず、リポジトリ内で管理します。プロジェクトのソース言語が英語で、現在は簡体字中国語、日本語、フランス語を必須とする場合、対象言語をスクリプトの引数として渡せます。

チェック項目 失敗条件 処理
対象言語 localizations に該当するキーがない ビルドを停止
翻訳状態 いずれかのリーフノードが translated ではない 該当するキーと言語を出力
文字列の内容 value が空、または空白文字のみ ビルドを停止
翻訳対象外 shouldTranslatefalse 明示的にスキップ

このゲートが判定するのは、Catalog内のローカライズリソースが完全かどうかだけです。UI上の文字切れ、動的パラメータ、意味の正確性に関する人手での受け入れ確認を代替するものではありません。

外部依存のないチェッカーを作成する

スクリプトを 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-Hanszh-Hant は独立した2つの対象言語であり、曖昧なプレフィックスで代用することはできません。

エクスポートした .xcloc はローカライズ担当者による抜き取り確認に適していますが、ビルド成果物をメインブランチへコミットすることは推奨しません。リポジトリにはソースとなる .xcstrings、チェックスクリプト、対象言語の一覧を保存し、エクスポート先のディレクトリは各タスクの成果物として扱います。

よくある誤検知に対処して受け入れ確認を完了する

1つ目の誤検知は、翻訳が不要な技術文字列によるものです。スクリプト内でキーのプレフィックスを基準に安易に除外するのではなく、Catalogで shouldTranslate: false を明示し、ルールをリソース自体に追従させます。2つ目は、複数形の分岐で one だけが翻訳され、other が漏れているケースです。すべての stringUnit を再帰的に読み取れば、該当箇所を正確に特定できます。

3つ目は、開発者が言語を追加した後、CIの引数を更新し忘れるケースです。対象言語の一覧は、プロジェクトスクリプト内の配列やパイプライン変数など、参照元を1つにまとめるのが理想です。複数のタスクでそれぞれ別の一覧を管理することは避けてください。4つ目は、スペース、改行、プレースホルダーが有効な翻訳として誤認されるケースです。現在のスクリプトは空白文字だけの値を拒否します。%@%d などの書式指定子については、ソース文字列と対象文字列のプレースホルダー集合を比較する処理も追加できます。ただし、エスケープや位置指定パラメータに対応してから、ブロッキング条件として有効にしてください。

マージ前に、次の順序で受け入れ確認を行います。

  1. 対象言語を意図的に1つ削除し、タスクが失敗することを確認する。
  2. 翻訳の1つを空白文字に変更し、レポートに具体的なキーが表示されることを確認する。
  3. 未完了の複数形分岐を作り、再帰チェックで検出できることを確認する。
  4. リソースを元に戻して再実行し、古いレポートが結果に影響しないことを確認する。
  5. 最後にRelease構成でビルドを実行し、ゲートと正式なタスクが同じチェックアウト内容を使用していることを確認する。

これにより得られるのは、一度限りの翻訳スキャンではなく、クラウドMac上で安定して再現できるエンジニアリング上の制約です。リソースが不完全なら早い段階で失敗させ、修正後にのみコンパイル、テスト、アーカイブへ進めます。

よくある質問

String Catalogの検証はCIのどこで実行すべきですか?

依存関係の準備後、コンパイルとアーカイブの前に実行します。失敗を早期に確定できるため、不要なアーカイブ処理を避けられます。

translated状態だけを確認すれば十分ですか?

十分ではありません。対象言語の存在、空文字列、複数形などのvariations配下にある全stringUnitも再帰的に確認する必要があります。

専有物理ノード

次回のビルドをクラウドMacで実行しましょう。

3種類のApple Silicon構成を比較し、現在のワークフローに合うリージョンを5つの海外ノードから選べます。

レンタルプランを選ぶ