Managing strings translations with Lokalise.co
Since 2018 at Back Market, the engineering team decided to manage translation strings with a new solution. The team compared a lot of tools, like POEditor or Transifex. After some weeks of benchmarking and workshops, a competitor takes the point: Lokalise.co.
For the mobile software team, on iOS and Android, Lokalise.co is a great solution, particularly with the Over The Air feature. With the iOS or Android SDKs, translations can be updated without submitting a new binary. In a world of release deployment, it makes much sense!
You can also directly download Localizable.strings & Localizable.stringsdict files from the platform. There are two ways to do it:
- The Build & Download feature: directly from the website, which give you all keys and strings files available for a dedicated platform;
- The API, via /files: it gives you the opportunity to download specific files for a dedicated platform.
Both are the same but not the methods.
Let’s begin with what we use and how we structure our project for Localization.
Use SwiftGen for safety translation code
It’s been many years I am using SwiftGen across iOS projects, and one of the great features I like is generating the L10n interface(s).
The Swift code generator for your assets, storyboards, Localizable.strings, ... - Get rid of all String-based APIs! …
SwiftGen gives you the opportunity to manage resources like storyboards, assets, strings, colors, fonts and many others.
The purpose is simple: using constants and safety functions to get resources across your project. If a resource disappears, your code doesn’t compile anymore (if you were using the key). Pretty efficient to detect regressions.
For instance, if we delete a common_ok string key in the code below:
Configuring and using SwiftGen is pretty easy, but it’s not the final purpose here. To go further, check the documentation.
Create a dedicated Localization.framework for your app
Now we can safely manage localizable keys on our iOS project, the final purpose for your iOS app architecture is to isolate translation management from a dedicated framework or module. That’s why on our iOS & Android app we create a dedicated Localization framework.
It’s mighty, because of:
- Single responsibility principle: it only manages strings translations.
- Third party substitution: tomorrow, if we switch from Lokalise.co SDK to another, only an update on the Localization.framework will be necessary.
To use SwiftGen with Lokalise.co, we have to update the default strings template for the SDK call. Here we use the flat-swift3.stencil template of SwiftGen:
That’s all. Now our Localization module:
- Manages local strings;
- Can fetch OTA updates from Lokalise.co (keep in mind that Lokalise.co keys take the overriding priority over local ones if key available on both);
- Safely expose Localizable keys with SwiftGen across the iOS project.
However, two things are missing:
- Automatically update Localizable.strings file at any time to be aligned with the last Lokalise.co version;
- Manage plurals: SwiftGen does not manage plurals .stringsdict files by default, and Lokalise.co use them.
Update our Localizable.strings file: time for Swift scripting!
The issue and fallback to think about
To make Lokalise.co working correctly with OTA updates, you have to:
- Generate a bundle version of your available keys;
- Make the bundle version available on preprod/production. Note that, with this feature, it will be useful to rollback or A/B test translations from a version to another:
After the setup, when your app starts and you call the dedicated SDK methods for OTA updates, your app receive the last version of your Localizable.strings for the next start. Keep this in mind!
Moreover, you are encouraged to freeze previous bundles for previous app versions. Therefore, some updated values (not keys) will produce crashes with String(format:). Like an expected format value ”Version %s” instead of ”Version %@”) or any translator error.
Bundle featurr sounds great but:
- What about the first start, or network issues?
- What if, for any reason, Lokalise.co does not respond anymore? You got the point. We need to keep the local Localizable.strings file up-to-date too.
- Moreover, we need safety generated SwiftGen keys to use them across the project, because nobody got time for this type of line:
[[Lokalise sharedObject] localizedStringForKey:@"key" value:@"default value" table:@"table name"];
We’ve already done it before by updated the stencil template.
To download the last Localizable files via the Lokalise.co API, we can make a bash script, or a Ruby script, or Python script.
Create the Lokalise.co Swift script downloader 🚀
Make shell commands call possible 🤓
To interface with the shell from Swift code, we have to write two utility methods. They are the key for script scripting with bash commands.
Create the API download calls with URLSession & Codable ⬇️
We use Foundation, so we have access to the URLSession API and it is sufficient for our use cases.
- URLSession configuration:
- Lokalise.co download requests:
- Writing file logic:
Multi-languages support 🇫🇷
Your app supports multi-languages and a base language. To copy all your supported Localizable variants, we create two properties: one for the base language, another for the languages listing:
Create the zip unarchiving and copy files code 👯♀️
Now we’ve downloaded the .zip file (AWS URL) from the Lokalise response, we can unzip it:
The unarchived folder contains multiple _language_.lproj folders with Localizables.strings and .stringsdict files inside. We have to copy them in the destination of our Localization framework.
Copy files method:
Use the command line parameters 🚀
If we use the command line tool, why not using parameters 🤓? Let’s write it to get APIToken and ProjectID directly from the command line:
Clean & make it beautiful 🤩
Seems good but some beautiful details are missing:
- Cleaning and deleting unzipped files
- Create console logs to follow script work in your Terminal
- Use emojis to make it «alive» and visible
- Use Semaphore for asynchronous logic
Let’s write the entire script now 🚀:
Run it! 👨💻
Now, each time a developer or a translator update your translations on Lokalise.co, just run your Swift script to put them automatically in your project 🚀.
Much better: create an alias for it ;)
To be informed on updated keys by translators on your project, don’t hesitate to use integrations. We use the Slack integration, and it’s very useful to follow changes every day. You can directly access to a specific updated or added key via a link.
Create a Swift script Build Phase to generate plurals
Wait, SwiftGen doesn’t manage plurals?
Yes, SwiftGen does not manage plurals (yet):
Parse Localizable.stringsdict file to support plurals · Issue #184 · SwiftGen/SwiftGen
I checked the whole repository to find some documentation about how to handle plurals with Swiftgen, but I didn't find…
You can create a dedicated template or an evolution for it, but since our first Lokalise.co Swift script, we can’t wait writing another one, particularly my colleague Arnaud 🍻.
Let’s Swift it!
The L10n extension for plurals
What we want in terms of Swift code is this type of line:
To do so, we have to create an extension on our previously generated file, L10n.swift:
Create the script
What do we want?
- Specify access control: public, private, etc.
- Execute it via a Build Phase: each time we re-build the Localization.framework, generate the related plurals if needed. To do so, you have to prefix your script with
#!/usr/bin/env xcrun — sdk macosx swift
- Generate the L10n+Plurals.swift file extension
- Manage errors and console logging
Let’s go straight to the point with the entire script 🤓.
Now, just update the SwiftGen Build Script Phase in your project:
… and build it!
With those two scripts, our productivity getting better by managing Localizable strings files on our iOS project. Our code is safer, fully automated and generated in Swift!
Moreover, any member of your team can improve it and make it owns.
Thanks for reading, and thanks to my colleague Arnaud for the L10nPlurals Swift script 👨💻.
Want to go further?
We hope you’ve liked this post, don’t hesitate to comment 💬 or clap 👏 if you like it!
If you want to join our Bureau of Technology or any other Back Market department, take a look here, we’re hiring! 🦄