Localization in Swift like a Pro
How to write safer localization code and save time by getting less distracted from your actual code writing task in Xcode.
The Status Quo in Xcode
As developers we know for a fact that context switches are inefficient. This does not only apply to CPUs though, it’s also true for the coding process itself. Often times we start writing code we need to immerse ourselves into, that’s why developer tools try to support us with any little task that might distract us from writing the actual code.
Xcode is a very good development environment in that it helps us app developers both with basic development tasks (like code completion, syntax highlighting, refactoring) and more complex tasks like defining device-agnostic UIs (Interface Builder) as well as several kinds of debugging and testing tools (View Hierarchy Debugger, UI / Performance Testing).
Missing Feature #1: Keeping Localizations in Sync
But at some tasks which are clearly not part of the coding process, Xcode lacks convenience. One such example is keeping localizations in sync between:
- Code Files (e.g.
- Storyboards / XIBs and their Strings files
- The different locales of a Strings file
Apple actually does provide tools to update Strings files, namely
ibtool and the
genstrings and its successor
extractLocStrings. You can experience them in field within Xcode when you add a language to an existing Strings file: Xcode will automatically use existing keys in a source language to prefill the new languages Strings file. You will also have noticed that when you first localize a Storyboard or XIB file, Xcode automatically adds all localizable Views (like
UILabel) to the corresponding Strings files. But that's where Xcode's localization assistance stops: When you add a new label in a Storyboard for example, Xcode won't automatically add that to the Strings files.
Missing Feature #2: Resource Access
Another example where Xcode lacks convenience is resource access. To be more specific: Image Assets, Color Assets, Storyboards, XIBs and Localization Strings are typically all loaded in code by using a String literal like in the following example:
title = NSLocalizedString("onboarding.page-one.title", comment: "")
This dynamic string reference method is simple to use but lacks two important features:
- There is no compiler checks to ensure a resource is (still) available
- There is no autocompletion, leading to many misspellings & exact name lookups
Google solved resource loading much more gracefully. In Android Studio you can access resources like this with all the above features provided:
title = R.string.onboarding.page_one.title
Xcode disappoints in this perspective and still hasn’t any viable solution.
Enhancing Xcode using Build Scripts
Fortunately, you don’t need to rely on Apple to fix these issues. The Swift developer community found their own ways to enhance Xcode’s features to improve their workflow. One such way is by providing command line tools and running them automatically on every build of your app. This approach needs some initial setup and configuration but turns out to be pretty easy once understood and very powerful in terms of possibilities.
You can find detailed instructions on how to configure a build script here:
How to configure Build Scripts in Xcode
This document provides step-by-step instructions on how to configure a build script to run on each build executing a…
brew install bartycrouch swiftgen
Remember Xcode includes tools for assisting with localization but only uses them in some rare situations? Well, BartyCrouch fixes that. It automatically searches your project for localizations and updates all Strings files incrementally. This works in all directions:
NSLocalizedStringentries are added to
- New localizable views in Storyboards / XIBs are added to their Strings files
- Deleted localizable views in Storyboards / XIBs are removed from their Strings files
- All locales of a Strings file are synced with the keys of a source locale
But this isn’t all BartyCrouch does for you. It also sorts the keys within your Strings files alphabetically to automatically group together keys that have the same prefix and to minimize merge conflicts.
Also, BartyCrouch gives you the option to exclude specific views in Storyboards / XIBs from being localized at all. This is useful if you have labels whose values will be set in code programmatically and thus shouldn’t be translated. Simply add
#bc-ignore! to the
Comment for Localizer field within Interface Builder like described here.
To use BartyCrouch, simply add this build script to your project:
Make sure the build script is run before
Compile Sources by reordering per drag & drop.
But this isn’t all BartyCrouch can do for you …
Eliminate switching to Strings files entirely
The developers classical localization workflow in Xcode is like this:
NSLocalizedStringwith your localization key.
- Switch to the
- Add a new key and provide an initial translation for the dev language.
- Repeat step 3 for all supported locales, keeping translations in other languages empty.
By adding the above build script alone, BartyCrouch already eliminates the annoying steps to add new keys to your Strings files without further configuration. But you still need to switch to the Strings file(s) to provide some initial translations. You can even eliminate this step though with little configuration:
- Add a new Swift file to your project (e.g. named
- Replace its content with the following code (fix the TODO if needed):
From now on, instead of using
NSLocalizedString you should use
Note that within the
translations dictionary you can provide one or multiple initial translations. On the next build, BartyCrouch will not only add them to your Strings files to prevent you from needing to switch to them at all, but it will also automatically replace the
BartyCrouch.translate call with the a call to
NSLocalizedString. So the resulting code will still be original
Foundation macros as you know them. An optional
comment parameter can be provided as well, but isn't required like in
Finding common Strings file issues
If you’re a responsible developer, you’re already using SwiftLint (or another code linter) to enforce code style and prevent some common mistakes in code. Swift provides many warnings and compiler checks itself, so you’re in pretty good hands regarding code. But did you know that there can be issues with translations as well?
The most common issues are:
- Translation Ambiguation:
Multiple entries of the same key within a single Strings file.
- Missing Translations:
Empty values for keys in untested locales of your project.
BartyCrouch has an integrated linter for Strings files which provides line-specific warnings right within Xcode for both of these issues. All you need to do is to run the
lint subcommand right after the
update subcommand in the build script:
Now when building your app you’ll see something like this when issues arise:
All the above features are turned on by default, so you don’t need to configure BartyCrouch at all to use them (just don’t forget the option
-x in the build script to make sure warnings are shown in Xcode).
You can completely customize BartyCrouch using a configuration file — I recommend using one at any rate to optimize BartyCrouch’s performance. You will definitely need one if your development language isn’t English as that’s the default language. The README has a good step-by-step configuration section, so I won’t repeat that here. Go check it out.
Remember how Android Studio deals better with resource access than Xcode by providing compile time checks and autocompletion? Well, SwiftGen fixes that. It automatically searches your project for any specified type of resources and generates a Swift file containing an
enum with all your resources names. This way you both get compile-time checks and autocompletion when accessing them.
For the purpose of this article, we will only generate an enum for localization strings, refer to the README to use more options like statically referencing images, colors and Storyboards.
To configure SwiftGen, we first need to create an empty
Strings.swift file in our project. The contents of this file will be automatically replaced by SwiftGen, so it might be a good idea to place it separately from other code, for example within
Next, create a file named
swiftgen.yml in the root of your project and add the following contents to it:
Replace both entries of
path/to/your with the correct paths of the appropriate files, then add this build script to your project:
Next, make sure the SwiftGen build script runs right after BartyCrouch.
After building your project the first time, you will from now on be able to replace calls to
NSLocalizedString with calls to the generated enum
L10n (which is short for
Localization but without the 10 characters between the beginning
L and end
n). Note that SwiftGen will automatically split your localization keys by
. and use
_ for camelCasing. Thus, using keys like
onboarding.page-one.title will both be usable via a call to
Combine BartyCrouch & SwiftGen
Now you have added both missing features to Xcode, but they don’t work together well. When using the
BartyCrouch.translate call to create new localizations, BartyCrouch transforms that into a
NSLocalizedString call on the next build, and one still needs to replace this with a
L10n call. But BartyCrouch has us covered here, too:
Simply change the configuration option
transformer within the
.bartycrouch.toml file in the section
swiftgenStructured. This will make sure that instead of
Foundation, BartyCrouch will transform the
BartyCrouch.translate to a call to SwiftGen's
Pro Localization Workflow
As a result of all the previous steps, the new localization workflow for developers is like this:
BartyCrouch.translatewith your localization key and one or multiple translations.
- Build your project (you can do multiple step 1’s before doing this).
Since you will do step 2 at some point anyways, it’s practically only a one-step process now.
By configuring BartyCrouch and SwiftGen for your project, you can streamline your localization workflow in Xcode, making it both less distracting for developers and safer in terms of preventing translation issues at the same time. This saves time and nerves.