📝

【回想】SPMでPreviewのLocalizeがぶっ壊れたのでなんとかした話

2023/12/09に公開

回想と書いているのは、この記事で紹介する問題がXcode15では既に解決されていて、アプローチが誰かの役にたつものではないからです。読み物としてお楽しみいただければ幸いです。

※この記事は Luup Developers Advent Calendar の9日目の記事です。

こんにちは。はじめまして tarunonです。ご存知の方はお久しぶりです。1年ぶりのアウトプットです。本業ではソフトウェアエンジニアとして、TypeScriptとterraform、時々Pythonを書いています。Luupではテクニカルアドバイザーとして壁打ちや、iOS,Swiftの足回りのお手伝いをさせていただいています。

今回の記事は、SPM、XcodePreview、Xcode14.2の環境で起きた問題、それに対してのアプローチ、辿り着くまでの問題解決についてのまとめになります。一部のスクリーンショットは、当時の状態を再現したものになります。

多言語対応と問題の発生

LUUPでは2023年6月、多言語に対応しました。https://luup.sc/news/2023-06-20-english-application-release/
アプリが対応する言語として従来の日本語に加えて、英語を追加しました。LUUPの開発ではGUIの開発においてXcodePreviewを活用していましたが、多言語対応以降、Previewが英語でしか表示されないという問題が発生するようになりました。 PreviewでLocalizeを行う場合、environmentを用いてLocaleを書き換えるのが有効です。

struct MyView_Previews: PreviewProvider {
  static var previews: some View {
    MyView()
      .environment(\.locale, .init(identifier: "ja-JP"))
  }
}

このようにLocalzeを書き換えると、本来であれば日本語が表示されるはず、でした。 ご覧のように英語が表示されたままになっている。
英語で表示されてしまうPreview
この問題の解決を試みます。

現象の調査

まずはどういうことが起きているのか、仮定、検証します。ここまででわかっていることを整理すると以下のようになります。

  1. 日本語しか対応していない時代は問題なくPreviewが表示されていた
  2. 日本語しか対応していない時代も、stringsファイルによって文言の宣言は行なっていた
  3. 英語のstringsファイルを追加することで、英語しか表示されなくなってしまった

ここから仮定できる現象としては以下のものがあり得るでしょうか。
A. localeがenのままで固定されている
B. Previewに日本語のstringsが含まれていない
C. Previewに英語のstringsが含まれている場合は、英語しか表示できなくなっている

ということで、実際に何が起きているかを検証していきます。XcodePreviewでは、printデバッグを行うことが出来ないので、代わりにTextデバッグを行います。文字通り、PreviewにTextでデバッグ情報を出していきます。 出したい情報は、localeとBundleの情報です。
TextデバッグでPreviewの実行時の情報を得る
Bundleの中身もチェックする
さて、デバッグからいくつかの情報が手に入ったのでさらに整理を進めていきましょう。

α. localeはja-JPになっている
β. main bundleはXCPreviewAgent.appというよく知らないアプリになっている
γ. リソースのバンドルにja.lprojは含まれており、中身も問題はなかった

デバッグ結果のα,γから、仮説のA,Bは棄却されました。おそらく起きていることはCで間違いはないでしょう。 ではなぜCが起きるのか?ということを紐解いていきます。現在手に入っている情報で、未知のもの、したがって最も怪しいのはXCPreviewAgent.appです。

最小再現実験

ところで、Localizeしていると日本語でPreviewができないという問題はかなり大きく、もし一般的にこれが発生するなら、もっと話題になっていて然るべきです。しかし、軽くGoogle検索をしてみても、似たようなものは見かけません。それどころか、LocalizeしていそうなアプリのPreviewで問題なく日本語が写っているキャプチャも散見されます。 現象の再現にはある程度の条件が必要であると予想できます。
まず、普通のSwiftUIアプリを新品の環境で作ってみます。そうすると、問題なく日本語のPreviewが表示されます。ここで、先のTextデバッグを差し込んでみると、main bundleがアプリ本体になっていることがわかります。

アプリ本体は当然日本語をサポートしているので問題がないことがわかります。ではXCPreviewAgent.appは…?これは日本語をサポートしているのでしょうか?
ということで、XCPreviewAgent.appが表出する実験環境にしていきます。XCPreviewAgent.appは、UIを含むFrameworkでPreviewを使うと、main bundleとして差し込まれます。

本当は再現のスクリーンショットなども提供したかったのですが、Xcode14.2は現在のPreviewAgentと互換性がなく、動作させることが出来ませんでした。当時のログから、起きていた事象としては以下になっています。

  • XCPreviewAgent.appからPreviewを表示させていると、日本語が表示できなかった。
  • XCPreviewAgent.appは、Locale.availableIdentifiersにenしか含んでいなかった。
    ※Xcode15でこの挙動は修正され、Locale.availableIdentifiersにあらゆるLocaleが含まれていることを確認しています。

ということで、原因がはっきりわかりました。
XCPreviewAgent.appが英語しかサポートしていないので、Frameworkの言語は英語が最優先で表示されてしまいます。

対策、失敗

原因がわかれば対策ができます。アプリ本体のサポート言語にかかわらず、Frameworkのサポート言語を優先させるInfo.plistに差し込めるオプションとしてCFBundleAllowMixedLocalizationsがあります。これをONにすることで、本体のサポート言語よりも広い範囲のFrameworkの言語サポートを利用できます。
これを無理やりONにする、つまりXCPreviewAgent.appのパスを特定し、InfoPlistを無理やり書き換える必要があります。
結論から言うと、無理やり書き換えることでPreviewは日本語を表示させることが出来ました。が、無理やり書き換えること自体が、安定する方法が存在しませんでした。
したがってこの方法はダメで、別の方法を検討する必要があります。

Previewのビルドプロセスへの介入

さて、もともと、多言語対応を行なっていなかったときは日本語を表示できていました。
つまり「Previewの成果物から英語対応を消してしまえば英語が表示されることはなくなる」ということです。
随分な荒療治ですが、背に腹は変えられません。御免!
Previewを表示させるとき、しっかりビルドが発生しています。このビルドで何が行われているかは、Xcodeのビルドログで確認ができます。
ビルドログに存在するPreview
ビルドログに存在するPreview詳細
Build for Previewsというログが残っています。この中を見ると、何が起きているかを見ることができる。
結構しっかりビルドが走っているんですね。そして、スキーマ対象にビルドが走るということは、post build actionでの介入が可能です。
post build actionを追加すると、環境変数の一覧が手に入る。
これを使って、
デバッグで得たγにあったリソースのバンドルへのパスを組み立て、en.lprojを始末します。
幸い、このリソースはパスからPreviewにしか使われないことが予想できるので、多少壊してしまってもProduction環境のアプリに影響は出ないでしょう。

# Workaround: Xcode14.2ではPreviewかつFrameworkのLocalizeが正常に動作しないため、英語を削除して対応する
# App_Resources.bundleはLUUP固有のものなので、適宜書き換える必要あり
rm -rf $(dirname ${SYMROOT})/Intermediates.noindex/Previews/${SCHEME_NAME}/Products/Debug-iphonesimulator/App_Resources.bundle/en.lproj

結果…
日本語が表示されたPreview
日本語が表示されました!
ただし、今回のアプローチでは英語を表示することが一切出来なくなっています。今回はメンバーと相談の上、業務上差し支えないだろうということで日本語に固定することを決めました。

あとがき

問題が発生した場合にいかにそれを解決するかというのは、エンジニアリングにおいて重要な仕事の1つです。
仮説を立て、再現実験を行い、問題の原因を特定する。問題の原因に対してアプローチを考え、必要があれば差し支えない範囲で妥協する。
今日は必要なくなってしまったアプローチではありますが、全体を通して問題解決のフローとしてよかった(自画自賛)ので、今回はこの内容を紹介いたしました。それではまた次のアウトプットでお会いしましょう。ごきげんよう。

Luupではソフトウェアエンジニアを積極的に募集しています。採用情報もぜひ、ご参照ください。
https://recruit.luup.sc

Luup Developers Blog

Discussion