iOS 15.0+ではNSLocalizedStringよりString(localized:)を使おう(Swift)

## はじめに

本記事は [Swift Advent Calendar 2022](https://qiita.com/advent-calendar/2022/swift) の1日目の記事です。
遅くなりましたが、誰も埋めていなかったので埋めます。

ローカライズの文字列を取得する方法を紹介します。

## 環境

- OS：macOS Ventura 13.0.1
- Xcode：14.1 (14B47b)
- Swift：5.7.1

## 今まで: NSLocalizedStringを使う

ローカライズの文字列を取得するとき、今までは `NSLocalizedString(_:tableName:bundle:value:comment:)` を使っていました。

```swift
func NSLocalizedString(
    _ key: String,
    tableName: String? = nil,
    bundle: Bundle = Bundle.main,
    value: String = "",
    comment: String
) -> String
```

引用: https://developer.apple.com/documentation/foundation/1418095-nslocalizedstring

Swift Packages上にあるSwiftUIのViewから呼び出す場合、以下のように使うことが多いと思います。

```swift:FooView.swift
import SwiftUI

struct FooView: View {
  var body: some View {
    Text("Foo")
      .navigationTitle(NSLocalizedString("Foo", bundle: .module, comment: ""))
  }
}
```

## これから: String(localized:)を使う

たまたま [apple/sample-food-truckのソースコード](https://github.com/apple/sample-food-truck/blob/4b63f0841af4e6656e9ad6a0294fdcab4a5f077b/FoodTruckKit/Sources/City/City.swift#L22) を見ていて、 `String(localized:table:bundle:locale:comment:)` なる `String` のイニシャライザがあることに気づきました。

```swift
init(
    localized keyAndValue: String.LocalizationValue,
    table: String? = nil,
    bundle: Bundle? = nil,
    locale: Locale = .current,
    comment: StaticString? = nil
)
```

引用: https://developer.apple.com/documentation/swift/string/init(localized:table:bundle:locale:comment:)

iOS 15.0+で使えること以外、公式ドキュメントには説明がありません。「No overview available.」すら書いてありません。

先ほどの `NSLocalizedString` を使った例を `String(localized:)` で置き換えてみます。

```diff_swift:FooView.swift
import SwiftUI

struct FooView: View {
  var body: some View {
    Text("Foo")
-     .navigationTitle(NSLocalizedString("Foo", bundle: .module, comment: ""))
+     .navigationTitle(String(localized: "Foo", bundle: .module)
  }
}
```

いろいろ試したところ、iOS 15.0+からはこちらのAPIを使ったほうがいいと判断したので、この記事で紹介することにしました。

## String(localized:)のメリット

`String(localized:table:bundle:locale:comment:)` のメリットを紹介します。

### Stringを返すことがわかりやすい

定義を見るとわかりますが、 `NSLocalizedString()` の実態は `String` を返す関数です。
`String` のイニシャライザを使うことで、 `String` であることがわかりやすくなります。

### commentを省略できる

`NSLocalizedString` では `comment` が `String` 型なのに対し、 `String(localized:)` では ` StaticString?` 型と、オプショナル型になっています。
しかもデフォルトで `nil` が指定されているので、呼び出しを省略できます。

今まで `comment: ""` と書くことが多かったので、省略できるとスッキリして嬉しいです。

### String Interpolationに対応している

「String Interpolation（文字列補間）」とは、文字列リテラル内に埋め込まれたプレースホルダーを、実行時に実際の値へ置き換える処理のことです。

iOSのローカライズでは、以下のようにフォーマット指定子を使って実現できます。

```:Localizable.strings
"Set %lld" = "%lldセット目";
```

`NSLocalizedString` はString Interpolationに対応していないのに対し、 `String(localized:)` は対応しています。

```swift:FooView.swift
import SwiftUI

struct FooView: View {
  var body: some View {
    VStack {
      Text(NSLocalizedString("Set \(Int(3) + 1)", bundle: .module, comment: "")) // "Set 4"
      Text(String(localized: "Set \(Int(3) + 1)", bundle: .module)) // "4セット目"
    }
  }
}
```

これは `String(localized:)` に渡すキーの型が `String` でなく `String.LocalizationValue` になっているためです。

逆に言うと `String` を渡せないので、ローカライズしたい文字列がすでに `String` の場合は、何かしら工夫しないと使えません。

## おまけ: Textはもっとスッキリ書ける

`Text` は `LocalizedStringKey` と `Bundle` の両方を渡せるイニシャライザが用意されているので、上記の記述はもっとスッキリ書けます。
詳細は以下の記事をご参照ください。

https://qiita.com/uhooi/items/d6c4e1fe45a61f7201a7

## おわりに

`String(localized:)` は `NSLocalizedString` の上位互換と言っても過言ではないので、ぜひ使っていきましょう :relaxed:
逆にデメリットがあればコメントなどで教えていただきたいです。

以上 [Swift Advent Calendar 2022](https://qiita.com/advent-calendar/2022/swift) の1日目の記事でした。
明日も私で [LocalizedStringKeyの簡単な説明(Swift)](https://qiita.com/uhooi/items/d6c4e1fe45a61f7201a7) です。

## 参考リンク

- https://twitter.com/the_uhooi/status/1599341068583337984
- https://twitter.com/the_uhooi/status/1599347863540346882
- [Add strings files by uhooi · Pull Request #99 · uhooi/Loki](https://github.com/uhooi/Loki/pull/99)
- [文字列補間 - Wikipedia](https://ja.wikipedia.org/wiki/文字列補間)