Swift Digest
SE-0278 | Swift Evolution

Package Manager Localized Resources

Proposal
SE-0278
Authors
David Hart
Review Manager
Boris Buegling
Status
Implemented (Swift 5.3)

01 何が問題だったのか

SE-0271 により、SwiftPM のマニフェストで画像やデータファイルなどのリソースを宣言し、ランタイムに Foundation の Bundle API から利用できるようになりました。しかし、その時点ではリソースのローカライズされたバリアント(言語・地域ごとの差し替え)を宣言する方法が用意されていませんでした。

Foundation の Bundle は、実行環境に応じて適切なローカライズバージョンを選ぶ仕組みを備えているにもかかわらず、SwiftPM 側で次のような課題があったため、パッケージ作者はこの機能を素直に活用できませんでした。

  • ローカライズを実現する手段としては、Bundle API が期待するディレクトリ構造を自前で用意し、.copy ルールでそのまま取り込む回避策しかありませんでした。
  • .copy はファイルをそのまま配置するだけなので、.process で得られるプラットフォーム固有の処理(ストーリーボード・XIB・strings・stringsdict などのコンパイル)が効きません。
  • ローカライズリソースの構成ミスを SwiftPM がコンパイル時に診断する余地もなく、問題は実行時まで気付けませんでした。

結果として、アプリやライブラリを地域・言語に合わせて切り替えるための Foundation API を、Swift パッケージからは十分に活かせない状態になっていました。

02 どのように解決されるのか

SwiftPM のマニフェストにローカライズリソースを宣言する仕組みを追加します。リソースはビルド時に適切な場所へ配置され、ランタイムでは既存の Foundation API からそのまま参照できます。

デフォルトローカライゼーションの宣言

Package イニシャライザに、新しいオプションの defaultLocalization パラメータを追加します。これは実行環境にマッチするローカライズが存在しないときのフォールバックとして使われるロケールで、パッケージがローカライズリソースを含む場合は必須になります(未設定だとエラー)。値の型は IETF Language Tag をラップした LocalizationTag で、文字列リテラルからそのまま渡せます。

let package = Package(
    name: "BestPackage",
    defaultLocalization: "en",
    targets: [
        .target(name: "BestTarget", resources: [
            .process("Resources/Icon.png"),
        ])
    ]
)

ロケールの表記は、enfr-CH のように 2 文字の ISO 639-1(または 3 文字の ISO 639-2)言語コードに、必要ならハイフン区切りで地域・方言コードを続ける IETF Language Tag 形式が推奨されます。

.lproj ディレクトリによるローカライズリソースの配置

ローカライズリソースの検出は、.process ルールの対象パス配下にある .lproj ディレクトリに基づいて行われます。ディレクトリ名の .lproj より前の部分がロケールを表し、たとえば en.lproj/ には英語版、fr-CH.lproj/ にはスイス向けフランス語版のリソースを置きます。Apple プラットフォームの Base Internationalization 用に、特別な Base.lproj/ も同様に扱われます。

.lproj 配下のファイルは、親ディレクトリに同じ名前で存在する「仮想的なリソース」のバリアントと見なされます。マニフェストからはその仮想パスを指定します。たとえば次のようなレイアウトでは、Resources/Icon.png を指定するだけで英仏両方のバリアントがひとまとまりのリソースとして扱われます。

Sources/BestTarget/Resources/
├── en.lproj/
│   └── Icon.png
└── fr.lproj/
    └── Icon.png
.target(name: "BestTarget", resources: [
    .process("Resources/Icon.png"),
])

明示的なローカライゼーション宣言

.lproj ディレクトリの外にあるファイルをローカライズ済みとして扱いたい場合のために、Resource.processlocalization パラメータが追加されます。値は列挙型 LocalizationType.default(デフォルトローカライゼーションのバリアント)または .base(Base Internationalization のバリアント)です。ローカライズされていないリソースと同じ場所にデフォルト版を置きつつ、別途のポストプロセスで他言語版を足すような運用に向いています。

.target(name: "BestTarget", resources: [
    .process("Resources", localization: .base),
])

なお、明示的な localization 宣言と .lproj ディレクトリ配置はいずれか一方のみを使うことが求められ、両方を併用するとエラーになります。

SwiftPM による診断

SwiftPM はローカライズリソースの構成ミスをビルド時に診断します。主なものは次のとおりです。

  • ローカライズリソースがあるのに defaultLocalization が設定されていない(エラー)。
  • .lproj ディレクトリの中にさらにサブディレクトリがある(エラー)。
  • デフォルトロケール向けのバリアントが欠けている。このとき Foundation はフォールバックできずリソースを見つけられない可能性があるため、警告が出ます。
  • 同じリソースに「ローカライズされていないバリアント」と「ローカライズされたバリアント」が同居している。Foundation の検索順序の都合で、この場合ローカライズ版が決して選ばれないため警告が出ます。

ランタイムでの利用

ビルド時には、各ローカライズリソースが Foundation の Bundle 検索パターンに合致する位置へ配置され、併せて CFBundleDevelopmentRegiondefaultLocalization を設定した Info.plist が生成されます。これによりランタイム側のコードは、通常のリソース読み込みと同じ API を使うだけで、適切にローカライズされた結果を得られます。

// 実行環境に応じたローカライズ版が自動的に選ばれる
let path = Bundle.module.path(forResource: "TOC", ofType: "md")
let image = UIImage(named: "Sign", in: .module, with: nil)

// strings ファイルからの取得も通常どおり
let localizedGreeting = NSLocalizedString("greeting", bundle: .module)