クラウド Mac で iOS 疑似ローカライズ回帰ゲートを作る

クラウド Mac で iOS 疑似ローカライズ回帰ゲートを作る

開発環境では 6 文字しかない英語のボタンでも、ドイツ語にすると 1 行いっぱいに広がることがあります。右から左へ記述するインターフェースに切り替えても、左側の戻るボタンが誤った方向を指したままになるかもしれません。翻訳がすべて完了してからこうした問題が見つかると、修正はレイアウト、テスト、リリース日程のすべてに影響します。より確実なのは、クラウド Mac 上のマージチェックごとに文字列を意図的に拡張し、レイアウト方向を反転させ、表示できない、または操作できないコントロールがあれば直ちに失敗と判定する方法です。

ゲートで検出する問題を先に定義する

疑似ローカライズの目的は、インターフェースを読みにくい文字へ置き換えることではありません。制御可能な変換によって、レイアウト上のリスクを顕在化させることです。まずは次の 2 つのモードを用意します。

ゲートでは、主に 4 種類の問題を確認します。テキストコントロールの表示領域が明らかに不足している、主要ボタンが存在しない、タップできない、またはウィンドウ外にある、ローカライズ用の入口を経由していないハードコード文字列が表示される、ナビゲーション、水平リスト、方向を示すアイコンが RTL に合わせて変化しない、といった問題です。

疑似ローカライズは構造上の問題を顕在化させるためのものであり、翻訳が正確かどうかを判断するものではありません。実際の言語による受け入れ確認は引き続き必要ですが、固定幅のような基本的な不具合をそこで発見するコストまで負わせるべきではありません。

制御可能な文字列変換レイヤーを構築する

テスト内でビューを走査し、ラベルを書き換えてはいけません。この方法では、アプリが実際に文字列を取得する経路を迂回してしまい、ハードコードされた文字列も検出できません。より確実なのは、プロジェクトで既に使用しているローカライズの入口に、デバッグビルドでのみ有効になる変換処理を追加する方法です。

enum PseudoMode: String {
    case expanded
    case rtl
}

#if DEBUG
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
    guard let mode else { return value }

    switch mode {
    case .expanded:
        let padding = String(repeating: "·", count: max(2, value.count * 2 / 5))
        return "[\(value)\(padding)]"
    case .rtl:
        return "⁧\(value)⁩"
    }
}
#else
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
    value
}
#endif

アプリの起動時に PSEUDO_LOCALE を読み取り、明示的に定義した列挙値だけを受け付けます。ユーザー向けの文字列はすべて、L10n.text("checkout.confirm") のような共通の入口を経由させてから変換処理へ渡します。あるタイトルに境界マーカーが表示されなければ、その文字列がローカライズレイヤーを迂回していると判断できます。

RTL モードでは、アプリのルートビューにもセマンティック方向を設定する必要があります。UIKit ではテスト用ウィンドウに semanticContentAttribute = .forceRightToLeft を設定できます。SwiftUI ではルートビューに layoutDirection を注入できます。画像の内容を反転したり、数値、パス、コード断片を無理に逆順にしたりしてはいけません。

UI テストで表示状態と文字切れを確認する

テストで全ピクセルを比較する必要はありません。まずはログイン後のホーム画面、一覧、詳細、フォーム、エラー状態などの主要フローを対象にし、主要コントロールへ安定した accessibility identifier を設定します。テストプロセスは、環境変数で対象モードを指定して起動します。

func launchApp(mode: String) -> XCUIApplication {
    let app = XCUIApplication()
    app.launchEnvironment["PSEUDO_LOCALE"] = mode
    app.launchArguments += ["-UI_TESTING", "1"]
    app.launch()
    return app
}

func testExpandedCheckoutLayout() {
    let app = launchApp(mode: "expanded")
    let confirm = app.buttons["checkout.confirm"]

    XCTAssertTrue(confirm.waitForExistence(timeout: 8))
    XCTAssertTrue(confirm.isHittable)
    XCTAssertFalse(confirm.frame.isEmpty)
    XCTAssertTrue(app.windows.firstMatch.frame.contains(confirm.frame))
}

isHittable は、単なる exists よりも有用です。ボタンがアクセシビリティツリー内に存在していても、モーダルに覆われていたり、画面外に配置されていたりする可能性があるためです。ラベル系のコントロールでは、identifier を使って frame を取得し、所属するコンテナからはみ出していないことも確認できます。テキストが切れているかどうかを OCR で推測してはいけません。重要なコンポーネントでは、デバッグビルド時に intrinsicContentSize と実際の frame の差分を公開する方法を優先します。

確認範囲をチェックリストにする

画面・状態expanded の確認項目RTL の確認項目失敗時の証拠
ナビゲーションバータイトルと操作が重ならない戻る方向と並び順が正しい画面階層、コントロールの frame
フォームラベルが改行でき、エラーメッセージが完全に表示されるラベルと入力欄の軸が一致するフィールドの identifier、現在値
一覧長いタイトルが主要操作を押し出さない付属要素と矢印の位置が正しい失敗した行のインデックス、スクリーンショット
モーダルボタンが完全に表示され、タップできる操作順がセマンティクスに合っているモーダルの frame、要素ツリー

クラウド Mac 上で実行環境を構成する

ジョブごとに独立したシミュレータデバイスセットを作成し、並列ジョブ間で起動状態が共有されないようにします。デバイスセットのディレクトリはジョブの一時ディレクトリ内に配置し、成功・失敗にかかわらず終了時に必ず削除します。テスト前に Xcode のパス、シミュレータのモデル、OS バージョンを固定し、それらの値をレポートへ記録します。

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
export PSEUDO_LOCALE="${PSEUDO_LOCALE:-expanded}"
RESULT_DIR="$PWD/TestResults/$PSEUDO_LOCALE"
mkdir -p "$RESULT_DIR"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "platform=iOS Simulator,name=iPhone 16,OS=latest" \
  -resultBundlePath "$RESULT_DIR/LayoutTests.xcresult" \
  PSEUDO_LOCALE="$PSEUDO_LOCALE"

expanded と rtl は、同じシミュレータセッション内で切り替えるのではなく、2 つの独立したジョブとして実行します。これにより失敗したモードを明確に特定でき、再実行時に直前の方向モードを引き継ぐこともありません。クラウド Mac で複数のジョブを同時実行する場合は、テスト成果物が互いに上書きされないよう、ジョブごとに独立した DerivedData パスも使用します。

よくある誤検知と実際の不具合を切り分ける

よくある誤検知の原因は、アニメーションがまだ終了していない、システムのモーダルが閉じていない、テストデータの長さが一定でない、といったものです。対策は一律の待機時間を増やすことではなく、明確な状態を待つことです。たとえば、読み込みインジケータが消える、対象要素が表示される、ウィンドウ数が安定する、といった条件を使います。テストデータも固定する必要があり、特にユーザー名、日付、サーバーから返されるエラーテキストは一定にします。

実際の不具合は、固定の高さ、単一行のラベル、手動計算された幅、視覚的な位置だけで順序を表現しているコンポーネントに集中する傾向があります。修正時は、特定の言語向けに例外を追加するのではなく、まず不要な制約を取り除きます。ボタンタイトルの改行を許可した場合は、タップ領域がテキスト全体を覆っているかどうかも改めて確認します。

失敗を検出したら、少なくとも次の情報を保存します。

  1. 現在の疑似言語モード、Xcode とシミュレータのバージョン。
  2. 失敗したコントロールの identifier、frame、label、タップ可能かどうか。
  3. 現在の画面のアクセシビリティ階層。
  4. 対応する xcresult と失敗時のスクリーンショット。
  5. その画面を再現するために必要な固定テストデータ。

マージ基準を設定し、疑似ローカライズの混入を防ぐ

ゲートでは、タスクの完了を妨げる問題をブロック対象にします。主要操作が表示されない、またはタップできない、テキストが切れて意味を失う、RTL で順序が誤っている、インタラクティブなコントロールにアクセシブルな名前がない、といった問題です。わずかな間隔の変化は要確認項目として記録できますが、すべてのピクセル差分でマージを止める必要はありません。

最後に Release ビルドの受け入れ確認を追加します。ビルド設定とアプリのリソースを走査し、テスト専用の疑似言語ファイルが含まれていないことを確認するとともに、正式ビルドでは PSEUDO_LOCALE が無視されることを検証します。通常モードでも一連のスモークテストを実行し、変換レイヤー自体が文字列を変更していないことを確認します。

この仕組みの価値は、奇妙に見える画面を生成することではありません。リリース前の手作業による多言語レイアウト確認を、コード変更のたびに繰り返し実行できるエンジニアリング上の制約へ変えることにあります。まず主要フローを対象にし、その後でエラー状態や利用頻度の低いモーダルを段階的に追加すれば、ゲートの安定性と保守性を維持できます。

よくある質問

疑似ローカライズは実言語での確認を置き換えられますか?

置き換えられません。固定幅、未翻訳文字列、双方向レイアウトの不具合を早く見つける手段であり、最後は長文になりやすい言語と RTL 言語で確認します。

疑似ローカライズ用コードを製品版へ入れない方法は?

変換処理を DEBUG 条件内に限定し、Release ビルドで PSEUDO_LOCALE 環境変数やテスト専用リソースを検出したら失敗させます。

どの不具合をマージ阻止の対象にすべきですか?

主要操作の非表示や操作不能、意味を失う文字切れ、RTL 順序の誤り、アクセシビリティラベルの欠落はマージを阻止すべきです。

専用物理ノード

次のビルドをクラウド Mac mini で実行

構成、請求期間、ノードのリージョンを選び、専用物理マシンで開発・ビルド・自動化タスクを実行できます。

Mac miniを今すぐレンタル