Ospina-Gonzalez

30 Nov, 2020

A closer look at localization with Swift Package Manager

Swift build tools 5.3, released earlier this adds support for Swift Package Manager to bundle resource files along with code. Bundled with this update (no pun intended) it's the possibility to properly support localization in your SPM project.

Although the feature seems simple enough, it is not without its Gotchas. How does it work exactly? how can localization setup this way be used and/or overridden when necessary? How can I preview the changes using SwiftUI? How do I unit test this setup?

TLDR;

  • Create a practical example on how to apply localization on an SPM project.
  • Learn about Bundle.module, a very specific type of Bundle.
  • Learn how SPM does not support xctestplan.
  • Learn how SwiftUI previews do not support localization of the bundle inside a module included via SPM.
  • Learn how to inject a specific Bundle as an explicit dependency for unit testing or for using SwiftUI previews.

The Basics

The manifest

I will start by setting up an SPM module called "WorldGreetings" for the purposes of this article. It will contain different localizations of the world "Hello".

Most of this setup should be familiar but there are 2 key parts here relevant to localization support:

  • // swift-tools-version:5.3 - Declaring the minimum Swift version required to build this package, which to support localization must be 5.3 (or above, if you are coming from the future)
  • defaultLocalization: "en" - This is a LanguageTag declaration stating the default language supported by this package. The value is a RFC5646 compliant value. Broadly defined this standard is composed of smaller ISO 639 language code identifiers such as en (ISO 639-1 for English) separated by dashes to potentially create more specific identifiers such as en-US (American English).

Localizable.strings

With the manifest out of the way, it is time to create our .strings file. The format and location of this resource is the same as you would expect on a regular Xcode project.

The Localizable.strings files must be placed under the folder Sources/<Target name>/<language tag>.lproj

In my case, I will add localized text for English and Spanish, so I will need two Localizable.strings files at the locations:

  • Sources/WorldGreetings/en.lproj/Localizable.strings
  • Sources/WorldGreetings/es.lproj/Localizable.strings

Usage

With all the setup steps complete, we can go ahead and use our localized strings:

The interesting part here is the usage of Bundle.module. This specific property is created by SPM to refer to the resource bundle for our specific module, that is, the bundle in which the specific code resides separate from other SPM modules and the host application.

This property is interesting in that Bundle.module will only be generated by SPM if the resources are placed in the right location in your package.

A common mistake here is to place it under the `Sources` folder directly. If you do this, SPM won't know that your project contains localized resources.

Using the localized package in an App

Using this SPM package in an App, seems easy enough:

File/Swift Packages/Add Package Dependency
Choose a version of the package to import
Make sure your App project supports the Localizations you want to use!

And with all of that we should be ready to go.

We have included the WorldGreetings SPM module and if we run in the simulator modifying the scheme to set "Spanish" as the App Language, our app correctly displays localized text.

SwiftUI Previews

One of the nicest perks of using SwiftUI is being able to preview our work side by side our editor. This should also be a straight forward process:

Running this preview however does not yield the expected results.

The previews on the canvas show the same default localization

What is happening here? We know the localization is working from running the app in the simulator.

From my testing it seems that SwiftUI Previews are not smart enough to pass down the locale information to code included using SPM. This only seems to work when the localization come from Bundle.main, that is, the App's Bundle.

Could there be a way to get around this? I will show how to get that done, but first, let's try unit testing our original SPM package to reveal more weaknesses exposed by SPM now that we can localize our modules.

Unit testing

For verification purposes and to give yourself a safety net in future updates to your code, it's a good idea to unit test your implementation while we are at it.

Unit testing that your strings are correctly pulling their values from the localizable resource file should be straight forward, or is it?

Consider the following test suite for our module:

Normally you would be able to write a simple test for each language and then use xctestplan to run the suite including or skipping tests for the relevant language(s).

For an example of this, check out WorldGreetings-app-sample on Github

Using xctestplan to run different language variations of a unit test

It seems however at the time of writing SPM packages don't support xctestplan.

This is why a little workaround is necessary if we want to be able to run these kinds of tests.

Overriding the Bundle

To pull this off, we need to be able to have control over which Bundle is being used to fetch our localized resources. In other words, we want to inject Bundle as a explicit dependency.

What this extra init(languageCode: String) provides is a way to specify a language code that we will use to load a specific Bundle that matches that code. This Bundle instance is then used as the bundle parameter in NSLocalizedString(_, bundle: Bundle, comment: String) inside our WorldGreetings struct.

Because a Bundle belonging a specific language code might not exist, this initializer is fallible.

And with that in place, our unit tests can be changed to test multiple localizations in the same XCTestCase by force loading a specific resource bundle matching a language code:

Back to the SwiftUI previews

Now that we have a way to inject the Bundle as a dependency, we can modify our code to show previews with specific languages:

Sources and Further reading

About

My name is Juan Carlos Ospina Gonzalez and I am an experienced iOS Developer with a strong background in Art and Design.

My education in Graphic Design and New Media combined with over 15 years of experience in Software Development gives me a good overview and insight in the creation of Digital products.

phone

small

medium

large