Kacper Cz
Kacper Cz

Reputation: 586

Changing language at runtime using SwiftGen

My app is supposed to support language change at runtime. I'm using SwiftGen 5.0. ViewControllers subscribe to language change notification and I've checked that the localisation function fires correctly. My overriden tr function looks like this:

fileprivate static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
    guard let bundle = LanguageManager.shared.bundle else {
        fatalError("Cannot find bundle!")
    }
    let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "")
    let locale = Locale(identifier: LanguageManager.shared.currentLanguageKey!)
    return String(format: format, locale: locale, arguments: args)
}

The bundle is set like so:

if let path = Bundle.main.path(forResource: currentLanguageKey, ofType: "lproj") {
    bundle = Bundle(path: path)
} 

However, the tr function returns mostly previous language strings. Only one out of all labels currently in memory refreshes. Setting a breakpoint inside the function and printing bundle returns

NSBundle </var/containers/Bundle/Application/ED5A6C7D-1807-4319-8817-45E693BC45E2/MyApp.app/en_US.lproj> (not yet loaded)

which is the correct new language. After app restarts the language is set correctly. Am I doing something wrong?

Upvotes: 9

Views: 3348

Answers (3)

user8675
user8675

Reputation: 686

I experienced the same issue caused by runtime localization/language changes in my Swift app. To address this, I followed Kacper's solution and extended the internal extension as outlined below:

Swift L10n:

extension L10n {
    private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
        // Custom Setup
        let storedLanguages = ["de", "en"]
        if let languages = UserDefaults.standard.array(forKey: "AppleLanguages") as? [String],
           let language = languages.first,
           storedLanguages.contains(language),
           let path = Bundle.main.path(forResource: language, ofType: "lproj"),
           let bundle = Bundle(path: path)
        {
            return bundle.localizedString(forKey: key, value: nil, table: nil)
        }

        // Default Setup
        let format = Bundle.main.localizedString(forKey: key, value: nil, table: table)
        return String(format: format, locale: Locale.current, arguments: args)
    }
}

Origin Stencil:

// MARK: - Implementation Details

extension {{enumName}} {
    private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
        // Default Setup
        {% if param.lookupFunction %}
        let format = {{ param.lookupFunction }}(key, table)
        {% else %}
        let format = {{param.bundle|default:"Bundle.main"}}.localizedString(forKey: key, value: nil, table: table)
        {% endif %}
        return String(format: format, locale: Locale.current, arguments: args)
    }
}

Modified Stencil:

// MARK: - Implementation Details

extension {{enumName}} {
    private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
    // Custom Setup
    let storedLanguages = ["de", "en"]
    if let languages = UserDefaults.standard.array(forKey: "AppleLanguages") as? [String],
       let language = languages.first,
       storedLanguages.contains(language),
       {% if param.lookupFunction %}
           let path = {{ param.lookupFunction }}.path(forResource: language, ofType: "lproj"),
       {% else %}
           let path = {{param.bundle|default:"Bundle.main"}}.path(forResource: language, ofType: "lproj"),
       {% endif %}
       let bundle = Bundle(path: path)
       {
           return bundle.localizedString(forKey: key, value: nil, table: nil)
       }
    
    // Default Setup
    {% if param.lookupFunction %}
    let format = {{ param.lookupFunction }}(key, table)
    {% else %}
    let format = {{param.bundle|default:"Bundle.main"}}.localizedString(forKey: key, value: nil, table: table)
    {% endif %}
    return String(format: format, locale: Locale.current, arguments: args)
    }
}

The ideation was to enable the app to immediately utilize the newly addressed localized table without requiring any restart or reopening. I also used Kacper's solution with:

static var label: String { 
    return L10n.tr("Localizable", "registration_verify.pin_code.label")
}

Origin Stencil:

{{accessModifier}}static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")

Modified Stencil:

{{accessModifier}}static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String {  {{enumName}}.tr("{{table}}", "{{string.key}}") }

Upvotes: 0

Chongzi Liming
Chongzi Liming

Reputation: 86

Now you can config lookupFunction params in swiftgen.yml file

strings:
  inputs:
    - en.lproj
  outputs:
    - templateName: structured-swift5
      params:
        lookupFunction: Localize_Swift_bridge(forKey:table:fallbackValue:)
      output: L10n-Constants.swift

in your project you just need implement lookupFunction, your can use use Localize_Swift library

import Localize_Swift;
func Localize_Swift_bridge(forKey:String,table:String,fallbackValue:String)->String {
    return forKey.localized(using: table);
}

generated code may like this:

internal enum Localizable {
       internal static var baseConfig: String { return 
         L10n.tr("Localizable", "base config", fallback: #"Base Config"#) }}


extension L10n {
  private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String {
    let format = Localize_Swift_bridge(forKey:table:fallbackValue:)(key, table, value)
    return String(format: format, locale: Locale.current, arguments: args)
  }
}

https://github.com/SwiftGen/SwiftGen/blob/stable/Documentation/templates/strings/structured-swift5.md

https://github.com/marmelroy/Localize-Swift

Upvotes: 7

Kacper Cz
Kacper Cz

Reputation: 586

Okay, I found the problem. The stencil was generating static variables:

static let label = L10n.tr("Localizable", "registration_verify.pin_code.label")

Changing stencil to generate computed properties fixed the behaviour:

static var label: String { 
    return L10n.tr("Localizable", "registration_verify.pin_code.label")
}

Upvotes: 11

Related Questions