Localized SwiftUI Views in a Swift Package

Localized SwiftUI Views in a Swift Package

A strings file contains the translations of localized user-facing strings for one language with optional comments. The syntax for each string in a strings file is a key-value pair in which key is the identifier for looking up the value that contains the translation.

/* A friendly greeting. */
"Hello, World!" = "Hallo, Welt!";

Initializers for several SwiftUI types – such as Text, Toggle, Picker and others – implicitly lookup a localized string when you provide a string literal.

Text("Hello, World!") // you might expect that it will show "Hallo, Welt!"

This implicit lookup won't work if you use those SwiftUI types in a Swift package !

If you initialize a SwiftUI Text view with a string literal, the view uses the init(_:tableName:bundle:comment:) initializer, which interprets the string as a localization key and searches for the key in the table you specify or in the default table if you don’t specify one. More importantly it will use Bundle.main if you don't specify one.

Text("Hello, World!") // Shows "Hello, World!" as it searches the default table in the main bundle.

When building your Swift package, Xcode treats each target as a Swift module. If a target includes resources, Xcode creates a resource bundle and an internal static extension on Bundle to access each module. You have to use this extension Bundle.module to locate package resources.

Text("Hello, World!", bundle: .module) // Shows "Hallo, Welt!" :)

Not all SwiftUI types have such flexible initializers. Button view has an initializer expecting a LocalizedStringKeyand will lookup the text in the Localizable.strings file through Bundle.main without the option to specify a different bundle.

Then you have to use other initializers (if possible)

Button(action: { print("Label shows 'Hallo, Welt!'") }, label: {
  Text("Hello, World!", bundle: .module)
})

or you lookup the localized text directly

Bundle.module.localizedString(forKey: "Hello, World!", value: nil, table: nil) // returns "Hallo, Welt!"

The statement takes a lot of space which can be changed by introducing an extension on Bundle

extension Bundle {
  func localizedString(forKey key: String) -> String {
    self.localizedString(forKey: key, value: nil, table: nil)
  }
}

and an extension on String

extension String {
    var localizedString: String {
        Bundle.module.localizedString(forKey: self)
    }
}

to get a localized string concisely.

Button("Hello, World!".localizedString) { print("Label shows 'Hallo, Welt!'") }

An extra tip for people who toy with the idea to generate a binary framework (xcframework) from a Swift Package: It is necessary to abstract the bundle access as Xcode won't create the internal static extension Bundle.module for XCFrameworks. You can do this with the following code snippet

import Foundation

class BundleLocator {}

extension Bundle {
    static var myModule: Bundle {
        #if SWIFT_PACKAGE
            return Bundle.module
        #else
            return Bundle(for: BundleLocator.self)
        #endif
    }

    func localizedString(forKey key: String) -> String {
      self.localizedString(forKey: key, value: nil, table: nil)
  }
}

extension String {
    var localizedString: String {
        Bundle.myModule.localizedString(forKey: self)
    }
}

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!