LoginSignup
2
2

More than 1 year has passed since last update.

[iOS]SwiftでLocalizable.stringsを動的に差し替える

Posted at

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 をつける

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 は神
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2