Software localization
iOS App Localization with Phrase Strings
At scale, iOS localization can be quite a challenge. Translators hardly want to deal with .strings
files, and we, developers, don't want to deal with uploads and downloads that we have to merge into our projects manually.
This is where Phrase Strings, the software localization platform within the Phrase Localization Platform, comes in. A professional localization tool for developers and translators, Phrase Strings can do the heavy lifting of our localization work, giving us more time to focus on the business logic of our app. Let's take a look at how we can localize our iOS apps with Phrase Strings.
Our app
We have a simple demo app that we'll be working on in this article.
Short Circuit, a curated list of electronic music
Our award-winning, two-screen app, Short Circuit, lists electronic music tracks along with their artists and release dates. To display the tracklist, we're using a UITableView
and connecting it to a hard-coded array of tracks via a UIViewController
. Tapping on a track row opens a details screen that shows labeled track info. The app is really quite simple, and we'll use it as a testbed for our localization work with Phrase here.
Note » Grab the starter project for our app in GitHub. The completed project is linked at the bottom of the article.
Setup
To get things started, we'll add a new project to Phrase, install the Phrase CLI on our development machine, and add our first locales.
Note » Before you add a project, you'll need to create a Phrase organization if you don't have one. A trial is free for 14 days, and it only takes a couple of minutes to set up.
Adding the project in Phrase
When we login to Phrase, we arrive at our Projects tab. Here we can add a new project using the Add Project button near the top-right of the page.
Click Add Project to get started
This opens up the Add Project dialog, which allows us to quickly configure our new project's initial settings.
We can give our project any name we want
We need to give our project a name, and we can pick any name we want. We'll use .strings
files to do the majority of our localization, so we pick that option as our project's Main Format. And, of course, we pick iOS as the project's Main Technology. That's enough to get us started. We can click the Save button to create the project.
Note » We can change any of these project settings at any time from the Project Settings dialog.
Installing the Phrase CLI
To be able to sync our translations with Phrase, we'll need the Phrase CLI installed. If we have Homebrew on our Mac, this is pretty straight forward. We just need to run two commands from the command line:
$ brew tap phrase/brewed
tap
ping gives access to the Phrase Homebrew repository. Once that's done, we can run:
$ brew install phraseapp
Et voilà. That should be it. To verify the installation, we can run $ phraseapp
from the command line. If all went well, we should see a list of all available commands the Phrase CLI provides.
Note » If you don't want to use Homebrew, check out the complete CLI installation guide for other installation options.
Adding our locales
With our project created, we can go to the Locales tab to add our app's supported locales. Of course, we can add or delete locales in our project at any time.
Let's add those locales
In the Locales tab, we should see an Add the first locale button. Let's click that puppy. When we do, the Add Locale dialog will open.
Couldn't be simpler
We just give our locale a name and select the actual localization we're targeting. In my case, I'll add English. We can then save the locale and repeat the process for any other locales we want to initially support in our app.
Initializing our client project
Now let's take a look at our client side, i.e. our development machine. To initialize our local Phrase environment for our project, we need to have an API access token. We also need to generate a local .phraseapp.yml
file.
Getting an API access token
To create an API access token, we can log into Phrase and go to our profile page. The link to our profile page will be under our username in the navigation bar.
"Fartknocker2022" is not a reasonable profile name
Once we're on our profile page, we can go to the Access Tokens tab through the link in the top tab bar.
Find the Access Tokens tab bar item
From the Access Tokens tab, we can click the Generate Token button. This opens the Generate Token dialog.
Just a Note and you're good to go
We'll want both read and write access for our project since we will both download and upload translation files. So we can leave that option as it is. We can enter any identifying note for ourselves in the Note field. This will serve as a reminder of the reason we created this token. Clicking Save will generate the token, and we should see the new token along with a button to copy it to our clipboard.
Note » Be sure to copy the token and keep it in a safe place because it will only be revealed to you one time in the Phrase console.
Get your token while it's hot
Creating our client config
Now that we have our access token, we can initialize our project on our development machine. From the root directory of our project, we can run the following command from the command line.
$ phraseapp init
Running this command will get the Phrase parrot photobombing our terminal. We'll be asked for our API access token, and we can paste in the one we generated above.
We'll then be presented with a list of our Phrase projects, and we can select one to link with our local project.
After that, we'll be asked to select our default localization format. We set the format when we created our project in the Phrase web console, so we can simply press Enter to use that same format.
We'll then be prompted to provide the file path to our Localizable.strings
file. We haven't created that file yet, so we can press Enter to stick with the default path template for now. We'll edit this value a bit later.
Our .phraseapp.yml
file will now be generated. Finally, we'll be asked if we want to perform an initial upload of our translation files. We can press n to skip this step for now.
Note » It may be a good idea to commit our .phraseapp.yml
to source control. This will make it easier for other developers on the project to sync translations.
Adding our first localization in XCode
With Phrase's initialization complete, it's time to direct our attention to our iOS app. To make it localizable, we need to add at least one language in XCode. We do this by selecting our project in the navigator, selecting the project itself (not a specific target) in the targets list, and then clicking the ➕ button under Localizations. I'll select Arabic here. You can add any locale you want.
Click ➕ for 🌍
A dialog will open asking us to choose the files and reference language for our new locale's Storyboard translations. We can leave Base selected as the reference language. Let's select Localizable Strings for our file types. This will match the format we've set our project to in Phrase.
Adding source translation files
By default, XCode will use the strings in our Main.storyboard
and LaunchScreen.storyboard
as its source translations. In my case, these are in English. It's good to have these strings in their own Localizable.strings
file, so that we can use them as source translations in Phrase. Let's select our Main.storyboard
file and check the English checkbox in the File inspector under Localizations.
We'll want our source translations in Phrase
Updating our client config
Now that we have our first translation files, we can update our .phraseapp.yml
to add their locations, which will allow us to sync them with Phrase via the CLI. Once we've added our source and target paths, our .phraseapp.yml
will look a little something like the following.
phraseapp: access_token: <your_access_token> project_id: <your_project_id> push: sources: - file: ./Views/en.lproj/Main.strings params: file_format: strings pull: targets: - file: ./Views/ar.lproj/Main.strings params: file_format: strings
This will tell the Phrase Client which files to upload when we push
(our sources), and which ones to download when we pull
(our targets). Pushing is basically uploading, and pulling is downloading.
However, in order to be able to push and pull our translations to and from Phrase, we'll need to add locale IDs to our .phraseapp.yml
file. We can get these from the Phrase web console. When we navigate to the Locales tab on the console, we'll find a gear icon next to each of our locales.
Have no fear, click the gear
Clicking that icon will open the Edit Locale dialog. From there, we can navigate to the API tab to get the locale's ID.
Thar she be, the ID
With our locale IDs in hand, we can update our .phraseapp.yml
file.
phraseapp: access_token: <your_access_token> project_id: <your_project_id> push: sources: - file: ./Views/en.lproj/Main.strings params: locale_id: fd935424a9ef2dfd054d982ccb7a8064 file_format: strings pull: targets: - file: ./Views/ar.lproj/Main.strings params: locale_id: 9c380148d7c1827b6c1db12f0b1a9da8 file_format: strings
Alright, now we can do our first push
. This will upload our source file to Phrase and allow us to create the target translations. Let's run the following command from the same directory housing our .phraseapp.yml
on the command line:
$ phraseapp push
If all went well, we should get an output like the following:
Uploading Views/en.lproj/Main.strings... done!
Check upload ID: fa27e27bd0f4256295d49af64efd8069, filename: Main.strings for information about processing results.
And if we visit our Phrase web console, we should see our Locales list now reflects that we have pending translations.
We got some translating to do
Note » We can upload our files to Phrase using the web console as well, and there are some edge cases to be mindful of when uploading.
OK, awesome! We now have our project synced up with Phrase Let's get to translating.
iOS app localization with Phrase in action
So far we've uploaded our Main.storyboard
labels. They're now ready for our translators to tackle in the Phrase web console. Let's open the console and click through to {Project Name} > Locales > ar to see our pending Arabic (target) translations.
Our translation keys are listed in the left sidebar of the page, each one with its English (source) translation. We can select any of these translations and use the Editor to enter and save its Arabic translation.
So much better than fiddling with .strings
files
Pruning unwanted string keys
First, however, let's prune some of these keys. Our app has a few labels that are populated dynamically by our view controllers, so we don't want to translate those label strings directly. We can add these keys to our blocklist so that Phrase will stop managing them. We first navigate to our main project page. Then, in the tab bar, we select More > Blocked Keys (previously Blacklisted Keys).
Let's prune this beastly bush
Once we're on the Blocked Keys page, we can just click the Add blocked key button near the top-right of the screen to open a dialog where we enter our offending key.
Remember to add the key, not the value
We just add the key, click Save, and repeat for each key we don't want our translators to see.
Note » Read more about blocking, deleting, and excluding keys in our guide, Working with keys.
Translating: The translation editor
Now we can select each of the remaining translations, add their Arabic translation in the translation editor, and click the Save button.
The convenience of the Phrase Translation Editor
It's really that simple. And the cool thing is that the developer working on the app neither has to wait for the translations to be 100% complete, nor for a file share from translators. He or she can download the latest translations at any time using the Phrase Client.
Pulling (downloading) translations
We can pull current translations at any time by running the following command from the command line.
$ phraseapp pull
In our current state, after we run the pull
command, we should see a message like:
Downloaded ar to Views/ar.lproj/Main.strings
We should now have all the current Arabic translations in our project. We can verify this by opening our Views/ar.lproj/Main.strings
.
/* Class = "UILabel"; text = "ARTIST"; ObjectID = "2aL-Sw-YDC"; */ "2aL-Sw-YDC.text" = "الفنان "; /* Class = "UIBarButtonItem"; title = "Back"; ObjectID = "cL2-YV-w5P"; */ "cL2-YV-w5P.title" = "رجوع"; // ...
Phrase has downloaded all our Arabic translations. This is such a time-saver, especially at scale. Imagine teams of developers and translators working on an app with just four or five supported locales. Each locale could have its own translator or translation team, and we would have to manage all of those .strings
files going back and forth. Phrase is already making our localization workflow so much smoother.
If we run our app in Arabic, we can see that all of our Storyboard UI labels have indeed been translated.
Al salam alykum
Translating code: adding a Localizable.strings file
So far we've been focused on translating Main.storyboard
. This is all good and well, but what about the code? We often have strings embedded in our code that we need to localize.
For example, say we want to add a copyright label at the bottom of our app's track details screen.
You gotta fight, for your right
First, we'll add a Localizable.strings
file to our XCode project, and make sure it's localized by selecting the file and clicking Localize in the file inspector. After we do that, we can check the boxes for both Arabic and English under Localizations for the file.
Now we can open the English Localizable.strings
and add the following.
"copyright" = "Copyright © %@ %@. All rights reserved";
This is a formatted string that contains dynamic strings to be interpolated in our Swift code. We'll want to swap the %@ %@
with our current year and the artist's name, since these values change dynamically in our app.
Assuming we've created the copyright label in our storyboard and linked it to an outlet in our controller, we can then update our controller to look like this:
import UIKit class TrackDetailsViewController: UIViewController { // ... @IBOutlet weak var copyrightLabel: UILabel! var track: Track? override func viewDidLoad() { super.viewDidLoad() if let track = track { // ... copyrightLabel.text = getCopyrightText(artistName: track.artistName) } } func setup(with track: Track) { self.track = track } fileprivate func getCopyrightText(artistName: String) -> String { let format = NSLocalizedString("copyright", comment: "") let currentYear = "\(Calendar.current.component(.year, from: Date()))" return String.localizedStringWithFormat(format, currentYear, artistName) } }
Now let's add our Localizable.strings
file to our .phraseapp.yml
so that we can work with it in Phrase.
phraseapp: access_token: <your_access_token> project_id: <your_project_id> push: sources: - file: ./Views/en.lproj/Main.strings params: locale_id: fd935424a9ef2dfd054d982ccb7a8064 file_format: strings - file: ./en.lproj/Localizable.strings params: locale_id: fd935424a9ef2dfd054d982ccb7a8064 file_format: strings pull: targets: - file: ./Views/ar.lproj/Main.strings params: locale_id: 9c380148d7c1827b6c1db12f0b1a9da8 file_format: strings - file: ./ar.lproj/Localizable.strings params: locale_id: 9c380148d7c1827b6c1db12f0b1a9da8 file_format: strings
Just like before, we add the English Localizable.strings
file as a source and its Arabic counterpart as a target.
We can then run $ phraseapp push
to upload the new translation to Phrase. Once that's done, we can open our Phrase web console and navigate to our project's Arabic translations. When we locate our key, we can use the translation editor to translate our formatted string.
That lovely editor again
Notice how Phrase is aware of our %@
placeholders. In fact, we can click on these placeholders (or any word for that matter) in the original English source to have it automatically placed in the editor by our cursor. This is very handy for translators, since they don't have to remember the tricky character sequences that make up our placeholders.
Once we've saved our translation, we can $ phraseapp pull
on our local machine to get it. After the pull
is complete, we can run our app in Arabic to see our update.
Note » When we have multiple targets, Phrase will aggregate all translations into one big list and put that list into all of our targets. This effectively means that our targets will be duplicates of each other. As long as we're not duplicating keys across our .strings
files, this shouldn't really be a problem.
That's our little app totally translated
Note » You can grab the completed, working XCode project from Github.
Note » For even more time-savings, learn how to automate storyboard localization with Phrase by reading Automate iOS Storyboard Localization.
C'est fini
That will do it for our foray into iOS app localization with Phrase Strings. You're hopefully starting to see the time savings the Phrase Strings l10n workflow gives you.—and we've covered only a few of its features. Phrase Strings is designed with developers in mind, so it has a fully-featured CLI and an API that we can wire up to and create custom localization workflows. The best thing about it is that Phrase Strings is just one of the translation products within the Phrase Localization Platform. This allows you to connect with a full-fledged translation management system as your translation needs grow. Sign up for a free 14-day trial and let it do the heavy lifting so you can stay focused on the code you love.