Sharing Localizations In A Reusable Framework

Posted 2/6/2018.

Localization is a great way to expand the potential customer base of your apps. Xcode makes it easy to add localization to an app, but what if you want to provide reusable localizations in a framework? You may be using the same strings in multiple apps or on multiple platforms, or you may be writing an open-source framework that includes localizations. In this tutorial we'll see how easy it is to do this.

Starter Project

You can download the starter project for this tutorial here. It includes two targets, one for the framework called "ExampleLocalizationFramework" and one for a single view app called "ExampleApp". The example app has one view controller, a simple login screen with a title and two text fields for entering an email address and password.

The Xcode project uses Base Internationalization and has two localizations, Chinese (Simplified) and English (the development language). There is a Localizable.strings file with three localizations, which will be used for the login screen's title and the placeholder text for each text field.

We won't actually be including any localized strings in "ExampleApp". Our goal here is to centralize them in the framework, which is reusable. However, Xcode will need at least one localized file in the app target in order to enable localizations for a language, so in this case the LaunchScreen.storyboard file is also localized.

Localization Framework

Open Localization.swift in the framework target. The first thing we'll add here is an extension on String with a convenience function to make getting localizations easier.

extension String {

    func localized(withComment comment: String = "") -> String {
        return NSLocalizedString(self, comment: comment)
    }

}

All this does so far is wrap the NSLocalizedString macro, allowing us to call .localized() on any String value to look up a localization. Let's use it to define the localizations the framework will provide:

public enum Localization {

    public static let login = "login".localized()
    public static let emailAddress = "email address".localized()
    public static let password = "password".localized()

}

Here we have a simple enum which defines public constants for the localizations we want to provide. Now let's try using this in the example app. In ViewController.swift, find setupLocalizedText and replace it with the following:

func setupLocalizedText() {
    title = Localization.login
    emailTextField.placeholder = Localization.emailAddress
    passwordTextField.placeholder = Localization.password
}

Edit the example app's scheme to use Chinese (Simplified) as the Application Language:

Now build and run. The login screen is still in English. What's happening?

The problem is that by default the NSLocalizedString macro that our String extension wraps is using the app's main bundle, while Localizable.strings is contained in the framework's bundle. Let's fix that now by adding an extension on Bundle that gets the framework's bundle. In Localization.swift, add the following:

extension Bundle {

    private static let bundleID = "com.toddkramer.ExampleLocalizationFramework"

    static var module: Bundle {
        return Bundle(identifier: bundleID) ?? .main
    }

}

Here we use the framework's bundle identifier to get the correct Bundle. If that fails, we fall back to the main bundle.

If you are following along separately, you will need to replace the bundleID with whatever is defined in the framework's settings.

Now let's replace our String extension with the following:

extension String {

    func localized(withComment comment: String = "") -> String {
        return NSLocalizedString(self, bundle: Bundle.module, comment: comment)
    }

}

Now that we're passing in the framework's bundle, the framework will find the Localizable.strings file and the localization lookup will succeed. Build and run the app again. You should now see the Chinese localizations.

Cocoapods

If you are planning to distribute your framework via Cocoapods, you will need to make a few adjustments. Cocoapods creates a bundle for you with resources like strings files, images, and xibs when you define a resource bundle in your framework's podspec. This bundle will be added inside the framework's bundle when the pod is generated.

First you would add a line like the following to your podspec:

Pod::Spec.new do |s|

    ...
    s.resource_bundle = { "ExampleLocalizationFramework" => ["ExampleLocalizationFramework/*.lproj/*.strings"] }

end

Here we're defining a resource bundle that includes all .strings files in our framework's lproj directories (which are created for each localization).

Now we need to return this bundle in our bundle extension instead of the framework bundle. Replace the bundle extension in Localization.swift with the following:

extension Bundle {

    private static let bundleID = "com.toddkramer.ExampleLocalizationFramework"

    static var module: Bundle {
        guard let path = Bundle(identifier: bundleID)?.resourcePath else { return .main }
        return Bundle(path: path.appending("/ExampleLocalizationFramework.bundle")) ?? .main
    }

}

Moving localizations into a framework and wrapping them in an enum makes it easy to safely access and reuse them. If you're writing an open-source framework that provides text that will be displayed in the UI, localization can be a great way to stand out from similar frameworks. Developers who have localized their apps will want to use a framework that already provides the same localizations. And if you're working on a development team supporting multiple apps and platforms for the same company, centralizing localizations will streamline the localization process and ensure that copy is consistent.

Next Steps