Swift で Localizable.strings を動的に差し替える
- 特に多言語対応しているiOSアプリでは、
Localizable.strings
を使ってアプリ内の文字列を管理している場合が多いと思います - 私が作っているアプリでは、SwiftPM で作ったリソース用のモジュールがあり、
Bundle.module.localizedString
を使ってバンドルした文字列を取り出しています - この方法では、翻訳データを更新するためには、アプリを再度ビルドする必要があります
- そこで、動的に
Localizable.strings
を差し替える仕組みを作ってみました
うまくいったやり方
- 実際に作成したコードはこちらです
- 文字列データは、
DynamicLocalize.localizedString
を使って取り出します
public class DynamicLocalize {
internal static func localizedString(_ key: String, _ table: String) -> String {
return bundle.localizedString(forKey: key, value: nil, table: table + ".nocache")
}
private static let bundleName = "DynamicLocalize.bundle"
private static let bundleURL: URL = {
let doc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return doc.first!
.appendingPathComponent(bundleName, isDirectory: true)
}()
private static let bundle: Bundle = {
let dir = bundleURL.appendingPathComponent("en.lproj")
if !FileManager.default.fileExists(atPath: dir.path) {
try! FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)
}
let url = dir.appendingPathComponent("Localizable.nocache.strings")
try! "\"Key001\" = \"First Value\";".write(to: url, atomically: true, encoding: .utf8)
return Bundle(url: bundleURL)!
}()
public static func update() {
let url = bundleURL
.appendingPathComponent("en.lproj")
.appendingPathComponent("Localizable.nocache.strings")
try! "\"Key001\" = \"\(UUID().uuidString)\";".write(to: url, atomically: true, encoding: .utf8)
}
}
実装上のポイント
- ドキュメントディレクトリ下に bundle のディレクトリを作成する
- Bundle.module は編集する事ができません
- そこで、ドキュメントディレクトリ下に作る必要があります
- 文字列のファイル名に、
.nocache
をつける- これで、キャッシュされなくなります
-
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html
- Table 2-1 あたり
- 文字列を差し替えた時に、
.nocache
を付けておくことで、最新のデータが取得されます
SwiftGen で DynamicLocalize を利用する
- 文字列を使う場所で毎回、localizedString すると大変なので、SwiftGen を利用して文字列データを管理したいです
- 設定ファイル(swiftgen.yml) はこちらです
strings:
inputs:
- Modules/Sources/Assets/ResourceFiles/Locales/en.lproj
filter: .+\.strings$
outputs:
- templateName: structured-swift5
output: Modules/Sources/Assets/Generated/Strings+Generated.swift
params:
publicAccess: true
enumName: AppLocales
lookupFunction: DynamicLocalize.localizedString
- 実際に生成されるコードがこちらです
public enum AppLocales {
/// Value001
public static var key001: String { return AppLocales.tr("Localizable", "Key001") }
}
extension AppLocales {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let format = DynamicLocalize.localizedString(key, table)
return String(format: format, locale: Locale.current, arguments: args)
}
}
- lookupFunction を指定したので、
tr
内で、DynamicLocalize.localizedString が呼ばれています - また、AppLocales.key001 が Stored property から Computed property に変わります
- おかげで stencil をいじらなくて済みました
- これで、AppLocales.key001 が呼ばれる度に、DynamicLocalize に問い合わせられるようになります
動的に差し替えられる文字列データを利用する
- AppLocales.key001 を利用できます
- DynamicLocalize.update() してから objectWillChange などで画面を再描画させると、文字列が更新されていることを確認できます
import SwiftUI
import Assets
public struct ContentView: View {
@ObservedObject var viewModel = ContentViewModel()
public init() { }
public var body: some View {
VStack {
Text(AppLocales.key001)
Button("Update") {
DynamicLocalize.update()
viewModel.update()
}
}
}
}
class ContentViewModel: ObservableObject {
func update() {
self.objectWillChange.send()
}
}
まとめ
- 最終的にかなりシンプルな仕組みになりましたが、
.nocache
など辿り着くまで大変でした - あと、SwiftGen は神