LocalizationValue / LocalizedStringResource / LocalizedStringKey 調査隊
環境
macOS Ventura 13.6.6
Xcode 15.2
String(localized: )
String(localized: “abc”)
というのがある。iOS15から使えるものである。従来の
NSLocalizedString("abc", comment: "")
と同じ役割である。
ややこしいのだが、
String(localized:
の後ろに来るものはiOS16の時点で
String.LocalizationValue
LocalizedStringResource
の2つとなっている。
String.LocalizationValue
による String(localized:
定義は
//String
init(
localized keyAndValue: String.LocalizationValue,
table: String? = nil,
bundle: Bundle? = nil,
locale: Locale = .current,
comment: StaticString? = nil
)
一方 LocalizedStringResource
による String(localized:
定義は
//String
init(localized resource: LocalizedStringResource)
ここから String.LocalizationValue
と LocalizedStringResource
についてまとめる。
注意
細かいところに入っていくと
Localize : それぞれの地域/言語にあわせる
Interpolation : \()
を扱う
が混乱しがちなので気を付ける
String.LocalizationValue
String(localized:
に与える、String.LocalizationValue
について調べる。
これはローカライズするときのネタを扱う。イメージでいうと各言語ファイル(Localizable.stringsなど)に投げるネタ。
- 文字
- 変数
という2つの性質のものを持つことができる。
定義
public struct LocalizationValue :
Equatable, ExpressibleByStringInterpolation {
public init(_ value: String)
public init(stringLiteral value: String)
public init(stringInterpolation:
String.LocalizationValue.StringInterpolation)
public struct StringInterpolation : StringInterpolationProtocol {
//展開の定義
}
}
これもまた ExpressibleByStringInterpolation
が付いていて、StringInterpolation
の自家製定義を持ち、\()
展開の定義がされている。
定義されている init
などを使って初期化すると
let s: String = "abc"
let ls1 = String.LocalizationValue(s) //OK
let ls2 = String.LocalizationValue("abc") //OK
let ls3 = String.LocalizationValue("abc\(s)") //OK
let ls4: String.LocalizationValue = s //NG ❌
let ls5: String.LocalizationValue = "abc" //OK
let ls6: String.LocalizationValue = "abc\(s)" //OK
となる。
"abc"
などのストリングリテラルの他に、実行時に決定できるように、\()
方式にも対応している。上の例でいうと、ls3
や ls6
にあたる。
ls6
は先ほどの「ある型が想定されるところにストリングリテラルや string interpolation が投げられて、その型が対応している場合は対応するイニシャライザにつながる」と考えるとスッキリする。
String.LocalizationValue.StringInterpolation
いままで StringInterpolation
は展開するとさんざん言ってきたが、String.LocalizationValue.StringInterpolation
を見ると、
public struct StringInterpolation : StringInterpolationProtocol {
//略
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public mutating func appendInterpolation(placeholder: String.LocalizationValue.Placeholder)
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public mutating func appendInterpolation(placeholder: String.LocalizationValue.Placeholder, specifier: String)
//略
}
となっていて、プレースホルダを扱うものがある(引数の名前がplaceholder)。これに関する説明が 2024年春の時点で見当たらないが、調査で次のコードを走らせる。
let s: String = "abc"
let n: Int = 1
let ph = String.LocalizationValue("abc\(s)\(n)")
print(ph)
すると出力が
LocalizationValue(
arguments: [
(extension in Foundation):
Swift.String.LocalizationValue.FormatArgument(
storage: (extension in Foundation):
Swift.String.LocalizationValue.FormatArgument.Storage.value("abc")),
(extension in Foundation):
Swift.String.LocalizationValue.FormatArgument(
storage: (extension in Foundation):
Swift.String.LocalizationValue.FormatArgument.Storage.value(1))],
key: "abc%@%lld")
となって "abc%@%lld" というキーになるらしい。
資料が見当たらないので推測になるが、String.LocalizationValue
に \()
方式を投げても具体的な値に展開しないで、キーとそれぞれの値をバラバラに保持する模様。
string interpolation 自体は展開処理をしない
let a = 5
let b = "a is \(a)"
と書くと b
は、a is 5
になる。これは "a is \(a)"
自体に \(a)
の展開機能があるように見える(展開してから b
に代入されるように見える)。
しかし
let a = 5
let b = String.LocalizationValue("a is \(a)")
と書くと b
は、"a is " と \(a)
を分解して保持する。
つまり string interpolation の処理方法は string interpolation を受け取った側で決定する
let b = "a is \(a)"
は "a is \(a)"
がStringに投げられて、String内の処理機能によって展開される、と考えるとスッキリする。
再び String(localized:)
で String(localized:)
は String.LocalizationValue
を投げられたらどうするの?というのが次にやることである。
これに関しては推測であるが以前別の記事で書いた。
挙動を観測すると上のような解釈で自然になる。
String.LocalizationValue まとめ
初期化パターン
let s: String = "abc"
let ls1 = String.LocalizationValue(s) //OK
let ls2 = String.LocalizationValue("abc") //OK
let ls3 = String.LocalizationValue("abc\(s)") //OK
let ls4: String.LocalizationValue = s //NG ❌
let ls5: String.LocalizationValue = "abc" //OK
let ls6: String.LocalizationValue = "abc\(s)" //OK
また、string interpolationを渡したときは
- キー(string interpolationの部分は暗号風)でローカライズ
- string interpolationの展開
の順番で行われる。
LocalizedStringResource
String(localized:
に与えるもう一つの方、LocalizedStringResource
について調べる。
iOS16から使える
この型が扱う情報は
- localization key
- table
- bundle
- locale
である。
localeを含むので任意のlocaleに設定できる。後からlocaleを書き換えることが出来る。
定義は
public struct LocalizedStringResource :
Equatable, Codable,
CustomLocalizedStringResourceConvertible,
ExpressibleByStringInterpolation {
public let key: String
public let defaultValue: String.LocalizationValue
public let table: String?
public var locale: Locale
public var bundle: LocalizedStringResource.BundleDescription { get }
public enum BundleDescription : Sendable {
case main
case forClass(AnyClass)
case atURL(URL)
}
public init(
_ keyAndValue: String.LocalizationValue,
table: String? = nil,
locale: Locale = .current,
bundle: LocalizedStringResource.BundleDescription = .main,
comment: StaticString? = nil)
public init(
_ key: StaticString,
defaultValue: String.LocalizationValue,
table: String? = nil,
locale: Locale = .current,
bundle: LocalizedStringResource.BundleDescription = .main,
comment: StaticString? = nil)
public init(
stringLiteral value: String)
public init(
stringInterpolation: String.LocalizationValue.StringInterpolation)
がある。
ExpressibleByStringInterpolation
であるので初期化は
let s = "abc"
var lr1 = LocalizedStringResource(s) //NG
var lr2 = LocalizedStringResource("abc") //OK
var lr3 = LocalizedStringResource("abc\(s)") //OK
var lr4: LocalizedStringResource = s //NG ❌
var lr5: LocalizedStringResource = "abc" //OK
var lr6: LocalizedStringResource = "abc\(s)" //OK
となる。lr5
と lr6
は何度も出てきた「ある型が想定されるところにストリングリテラルや string interpolation が投げられて、その型が対応している場合は対応するイニシャライザにつながる」と考えると辻褄があう。
LocalizedStringResource
の使い方は
var lr1 = LocalizedStringResource("Capital")
var lr2 = LocalizedStringResource("Capital")
lr1.locale = Locale(identifier: "ja-JP")
lr2.locale = Locale(identifier: "en-US")
let lrs1 = String(localized: lr1)
let lrs2 = String(localized: lr2)
print(lrs1)
print(lrs2)
Localizable.stringsには首都が設定してある。
結果
Tokyo
Washington DC
という風に、
- 初期化
- ローカライズ情報設定
が別のタイミングで行える。
バンドルやテーブルの情報も含むことが出来るので、そういう使い方が必要な場合には有効だと思われる。
String(localized: ) どっち問題
String(localized: )
には少し疑問があって、単に
String(localized: "abc")
としたときに何が呼ばれるのか、つまり
public init(localized keyAndValue: String.LocalizationValue, /* 略 */)
か
public init(localized resource: LocalizedStringResource)
か、どっちが呼ばれるのかここまでに出てきた知識では説明することができない。
これは単に私が何かを知らないだけかもしれない。
String.LocalizationValue でハマりがち
英語圏では、lang -> ABC
日本語圏では、lang -> あいう
の変換をしたいとする。ただしキーの値(つまりlang)は実行時に決まるとする。
これは
let key: String = "lang"
let s = String(localized: String.LocalizationValue(key))
と書ける。
これを短く
let key: String = "lang"
let s = String(localized: "\(key)")
とすると、意図するようにはならない。
public init(localized keyAndValue: String.LocalizationValue, /* 略 */)
が走るとすると、String.LocalizationValue
が想定されるところに "(key)" が投げられたので関連するイニシャライズにつながり、キーが %@
の String.LocalizationValue
が作られる。これが String(localized:)
に送られて キーが %@
のローカライズデータを探し、多分何もヒットしないか %@
-> %@
の変換が行われ、その後 %@
が展開されて "lang" になる。
String ローカライズしたい/したくない問題
String、Stringリテラル、String Interpolationのそれぞれをローカライズしたい、ローカライズしたくないで分けて、その実装例を次の記事にまとめた
SwiftUI の Text
SwiftUIのTextを調べる。イニシャライザを見ていく。
@inlinable public init(verbatim content: String)
ローカライズさせないで文字をそのまま出す時に使うのが verbatim:
Text(verbatim: "Direct")
のように使う。しかし 「Stringが想定されるところに "\()"
を流すと解釈するイニシャライズつながる」の法則があるので
let a = "文字a"
Text(verbatim: "\(a)")
なんてのも可能。Textビューは「文字a」と表示する
引数なしで StringProtocol
を受け付けるものもある。
public init<S>(_ content: S) where S : StringProtocol
let a = "abc"
Text(a)
これは ローカライズしない
ただし、Textのイニシャライズに渡す前にローカライズすることは当然できる
Text(String(localized: "abc")) //ローカライズされたStringがTextに渡される
先ほど見た LocalizedStringResource
を使った
public init(_ resource: LocalizedStringResource)
もある。これはローカライズが可能。
次が
Text("abc")
とリテラルで書いたときのやつ。これは後ろで
//Text
public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)
が走っているそうである。Textの別のイニシャライザのコメントにそう書いてある。
コメント
SwiftUI doesn't call the
init(_:)
method when you initialize a text view with a string literal as the input. Instead, a string literal triggers theText/init(_:tableName:bundle:comment:)
method — which treats the input as aLocalizedStringKey
instance — and attempts to perform localization.
この初めの引数が LocalizedStringKey
ですな。ではこれをやります。
LocalizedStringKey
import SwiftUI
が必要
キャッチコピー(説明の一文目)は
The key used to look up an entry in a strings file or strings dictionary file.
ローカライズファイルで使うためのキー
上でやった LocalizationValue
と似ている。キーとその他の関連する情報を持つ。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct LocalizedStringKey : Equatable, ExpressibleByStringInterpolation {
public init(_ value: String)
public init(stringLiteral value: String)
public init(stringInterpolation:
LocalizedStringKey.StringInterpolation)
public struct StringInterpolation : StringInterpolationProtocol {
//日付やらImageやらを展開する機能が書いてある。一部はextensionで追加。
}
}
とあるので、init
ではおなじみの
- Stringインスタンス
- Stringリテラル
- string interpolation
の3点セットに対応している。
let s = "abc"
let n = 1
let lk1 = LocalizedStringKey(s)
let lk2 = LocalizedStringKey("abc")
let lk3 = LocalizedStringKey("string \(s)")
let lk4 = LocalizedStringKey("number \(n)")
print(lk1)
print(lk2)
print(lk3)
print(lk4)
結果は
LocalizedStringKey(
key: "abc",
hasFormatting: false,
arguments: [])
LocalizedStringKey(
key: "abc",
hasFormatting: false,
arguments: [])
LocalizedStringKey(
key: "string %@",
hasFormatting: true,
arguments: [SwiftUI.LocalizedStringKey.FormatArgument(storage: SwiftUI.LocalizedStringKey.FormatArgument.Storage.value("abc", nil))])
LocalizedStringKey(
key: "number %lld",
hasFormatting: true,
arguments: [SwiftUI.LocalizedStringKey.FormatArgument(storage: SwiftUI.LocalizedStringKey.FormatArgument.Storage.value(1, nil))])
要は、いろいろな情報をバラバラに持っている。()方式で渡したときはキーが暗号風( number %lld
)になる。
LocalizedStringKey.StringInterpolation
LocalizedStringKey
の中に StringInterpolation
が新規定義されている(新規で定義ないときはDefaultStringInterpolationが使われる)。この新規定義を見ると、
public struct StringInterpolation : StringInterpolationProtocol {
public mutating func appendLiteral(_ literal: String)
public mutating func appendInterpolation(_ string: String)
public mutating func appendInterpolation(_ resource: LocalizedStringResource)
//略
という感じになっている。
説明によると
The interpolated types can include numeric values, Foundation types, and SwiftUI
Text
andImage
instances.
となっていて、ローカライズ以外の仕組みも大量にある。
例えば
public mutating func appendInterpolation(_ date: Date, style: Text.DateStyle)
というものがあって、引数を2つ持つ \()
表記という、言葉で説明すると何が何やらわからないが
let key = LocalizedStringKey("Date is \(company.foundedDate, style: .offset)")
let text = Text(key) // Text contains "Date is +45 years"
というように、\()
表記の中に引数が2つある書き方が出来る。
Text("") でハマりがち
さて、よくあるシチュエーションで
"money" -> "円"
"money" -> "Dollar"
という変換をしたいが、キーは実行時に決まる場合
let s = "money"
Text("\(s)")
と書いてしまうが、これはローカライズされない。
Text ローカライズしたい/したくない問題
Textを作る時に、素材となるString、Stringリテラル、String Interpolationのそれぞれをローカライズしたい、ローカライズしたくないで分けて、その実装例を次の記事にまとめた
LocalizedStringKey のまとめ
SwiftUIで定義されている LocalizedStringKey
は LocalizationValue
と似たようなものだが、SwiftUI用などの機能がかなり豊富にある。
感想
As a general rule, use a string literal argument when you want localization, and a string variable argument when you don't.
一般的なルールとして、ローカライズしたいならStringリテラルで書き、ローカライズしたくないならString変数を使う。
と言っているが、普通に、
何も指定しないならそのまま、ローカライズするときはその旨を(引数などで)書く
というルールの方がシンプルに感じる。
さいごに
この記事は内容自体は検索したら出てきたものを並べただけというレベルである。
何か指摘がある場合はコメントでお願いしたい。
Discussion