iOS App Localization with Phrase

Software localization

At scale, iOS app localization can be quite cumbersome. Translators don’t 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 comes in. A professional localization tool for developers and translators, Phrase can do the […]
Software localization blog category featured image | Phrase

At scale, iOS app localization can be quite cumbersome. Translators don't 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 comes in. A professional localization tool for developers and translators, Phrase 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.

Note » I'm assuming you're familiar with the basics of iOS internationalization and localization here. If not, check out one of our guides:

Our app

We have a simple demo app that we'll be working on in this article.

Demo app | PhraseShort 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 which 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 » If you want to work along, you can grab the starter project from Github. The completed project is linked at the bottom of the article.


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.

Adding a new project in Phrase | PhraseClick Add Project to get started

This opens up the Add Project dialog, which allows us to quickly configure our new project's initial settings.

Add project menu | PhraseWe 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

tapping 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.

Locales tab in Phrase | PhraseLet'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.

Add Locale dialog | Phrase

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.

Going to our profile page | Phrase

"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.

Access Tokens tab bar item | PhraseFind the Access Tokens tab bar item

From the Access Tokens tab, we can click the Generate Token button. This opens the Generate Token dialog.

Generate Token dialog | PhraseJust 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.

Secret token for demo app project | Phrase

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.

Phrase API client setup | Phrase

It's an exuberant little parrot

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.

Adding a new language to our project | Phrase

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.

Our source translations | Phrase

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.


  access_token: <your_access_token>

  project_id: <your_project_id>



    - file: ./Views/en.lproj/Main.strings


        file_format: strings



    - file: ./Views/ar.lproj/Main.strings


        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.

Locale tab gear icons | Phrase

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.

Locale ID in API menu | Phrase

Thar she be, the ID

With our locale IDs in hand, we can update our .phraseapp.yml file.


  access_token: <your_access_token>

  project_id: <your_project_id>



    - file: ./Views/en.lproj/Main.strings


        locale_id: fd935424a9ef2dfd054d982ccb7a8064

        file_format: strings



    - file: ./Views/ar.lproj/Main.strings


        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.

Translation status in Phrase | Phrase

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. Check out the guide, Uploading localization files, for a more in-depth look at 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.

Phrase translation menu | Phrase

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).

Blacklisted Keys menu point in Phrase | Phrase

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.

Adding blacklisted key | PhraseRemember 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.

Saving our translation | PhraseThe 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.

Demo app translated to Arabic | PhraseAl 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.

Copyright bar in demo app | PhraseYou 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() {


        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.


  access_token: <your_access_token>

  project_id: <your_project_id>



    - file: ./Views/en.lproj/Main.strings


        locale_id: fd935424a9ef2dfd054d982ccb7a8064

        file_format: strings

    - file: ./en.lproj/Localizable.strings


        locale_id: fd935424a9ef2dfd054d982ccb7a8064

        file_format: strings



    - file: ./Views/ar.lproj/Main.strings


        locale_id: 9c380148d7c1827b6c1db12f0b1a9da8

        file_format: strings

    - file: ./ar.lproj/Localizable.strings


        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.

Translating our copyright bar to Arabic | PhraseThat 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.

Completely translated demo app | PhraseThat'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. You're hopefully starting to see the time-savings the Phrase l10n workflow gives you. And we've only covered only a few of the features Phrase helps us with. The service was made with developers in mind, so it has a fully-featured CLI and an API that we can wire up to and create custom workflows. Phrase also has professional features like over-the-air translations, translation versioning, and more. Sign up for a free 14-day trial. And happy coding, amigos and amigas :)