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?
- Create a practical example on how to apply localization on an SPM project.
- Learn about
Bundle.module, a very specific type of
- Learn how SPM does not support
- Learn how
SwiftUIpreviews do not support localization of the bundle inside a module included via SPM.
- Learn how to inject a specific
Bundleas an explicit dependency for unit testing or for using
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
LanguageTagdeclaration 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
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.
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:
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:
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.
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.
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.
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
It seems however at the time of writing SPM packages don't support
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
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
- Piterwilson on Github, WorldGreetings-final, https://github.com/piterwilson/WorldGreetings-final
- Piterwilson on Github, WorldGreetings-spm, https://github.com/piterwilson/WorldGreetings-spm
- Piterwilson on Github, WorldGreetings-app-sample, https://github.com/piterwilson/WorldGreetings-app-sample
- Apple, Swift packages: Resources and localization, Retrieved from
- SwiftLee, SwiftUI Previews: Validating views in different states, https://www.avanderlee.com/swiftui/previews-different-states/