Our app team doesn’t need to rely on us developers (and take up our precious time) when deploying new translations to our app. We can also sidestep App Store approval when updating the localized copy of our app. Let’s see how we can realize these benefits in our iOS app with PhraseApp’s Over the Air feature. We’ll add PhraseApp Over the Air to a working app, wiring everything up on the PhraseApp console and our XCode project.
We’re picking up where we left off in our previous article, iOS App Localization with PhraseApp. In that tutorial, we took an app and connected it to PhraseApp to streamline our localization workflow. We’re assuming that you’ve read that article, and/or that you have a PhraseApp account connected to your app already. If not, we highly recommend checking out that article before you proceed.
Short Circuit, our demo app, a curated list of electronic music
Our little demo app lists electronic music tracks along with their artists and release dates. It’s a simple, two-screen app with a
UITableViewController that lists all our tracks, and has a details screen for displaying a single track’s info.
🔗 Resource » If you want to work along, you can grab the starter project from Github. The completed project is linked near the bottom of the article.
Alright, let’s start getting our app ready for over the air translations. In our iOS app, we’ll move our localized text from storyboards to a good old
Localizable.strings. We’ll then add over the air translation support to our PhraseApp project. To round out our prep, we’ll install the PhraseApp iOS SDK, which syncs our app’s translations with our PhraseApp project over the air.
Moving Our App’s Translations from Storyboards to Localizable.strings
PhraseApp Over the Air translations will work just fine with storyboard
.strings files. However, just to keep things simple here, let’s put all our translatable strings in a single
Localizable.strings file. It’s a simple refactor from
Localizable.strings, so let’s get to it.
📖 Go Deeper » If you want to learn more about iOS storyboard localization, check out our guide, iOS i18n: Internationalizing Storyboards in XCode
Let’s start with our screen’s navigation item titles. We can simply override the titles set in our storyboards by setting the
title field in our view controllers. Our home screen, which lists our artists, is connected to
TrackListViewController. Let’s update that controller now.
We use the usual
NSLocalizedString to grab our translation from
Localizable.strings. Let’s make sure that the translation string exists in the English version of
With that in place, our app will look exactly the same when run in English. We’ll add our non-English locales a bit later. Let’s finish moving our translations out of our storyboards first.
For our labels, we have to ensure that we have outlets from our storyboards to their respective controllers.
All of our storyboard labels get IBOutlets
After we connect our labels, we just repeat the same process of calling
NSLocalizedString to provide translated strings to them. And, of course, we add our new strings to our English
TrackDetailsViewController, which helps display a single track’s info, would look like this after we move our storyboard translations:
getLocalizedHeaderText function is just a little helper that we use to provide localized uppercase formatting to our translated label text.
After we add our translation strings to our
Localizable.strings, our app looks exactly the same as it did before.
Looks like a successful refactor
We can now completely remove our storyboard translation files from our XCode project. We do so by selecting the storyboard file in the Project navigator and unchecking each locale under the Localizations header in the File inspector.
Bye, bye clutter
📖 Go Deeper » If you’re working with iOS storyboards a lot you may want to check out our article, Automate iOS Storyboard Localization.
Updating Our Translations with PhraseApp
We now need to update our the
Localizable.strings translations for our non-source (non-English in my case). We do this so that our non-English users can see our update labels in their language. We can, of course, use our usual PhraseApp workflow to translate our labels.
First, we’ll upload our new source by doing a
$ phraseapp push from the command line.
✋🏽 Head’s Up » Don’t forget to remove your
Main.stringsfile from your
targets, or you’ll get an error when you
phraseapp push. Remember those files no longer exist since we removed them from our XCode project and file system earlier.
Our new translation keys should now show up on the PhraseApp web console. Once our translators have translated the keys into all the locales our app supports, we can
$ phraseapp pull from the command line to get our updated
Localizable.strings files. Now when we run our app in a non-source locale (Arabic in my case), we see that our views are translated as expected.
All well in all locales
📖 Go Deeper » You can learn more about the core PhraseApp translation workflow with iOS in iOS App Localization with PhraseApp.
Now that we’ve moved our storyboard translation strings to
Localizable.strings files, we can move on to setting up over the air translations in our PhraseApp project.
Adding an Over the Air Distribution in PhraseApp
Let’s make our PhraseApp project ready for over the air translations. In our PhraseApp web console, let’s open our account dropdown in the top navigation bar and select Integrations.
OTA is under Integrations
This will open up our PhraseApp Integrations page. Here we can find the Over the Air (OTA) row and click its Configure button.
The growing number of PhraseApp integrations
We should now see the Over the Air page inviting us to create a distribution. A distribution is a project OTA configuration that targets one or more platforms and has fallback options.
We can create an OTA distribution specific to our iOS app
Let’s click the Create distribution button to open the Add distribution dialog.
The Add distribution options are pretty self-explanatory
For our current project, we can give the distribution a name of our choosing, select the PhraseApp project associated with our iOS app, and check the ios platform checkbox. We can leave the default fallback options as they are since they’re fine for our purposes. Let’s click the Save button to create the distribution.
💡FYI » You can change your distribution settings at any time by returning to the Over the Air page.
Adding Our First OTA Release
In order to work with the iOS SDK without seeing a failure result when we attempt to update our translations in the app, we need to have an OTA release. A release is basically a snapshot of our PhraseApp translations that we can test and deploy to our iOS app over the air. We’ll get into releases later. For now, let’s create a first release to get working with the SDK.
We’ll start by clicking the Create release button at the bottom of our distribution page.
This opens the Add release dialog. We can add a description to help us identify the release later, leave everything else as it is, and click Save.
A release is a translation snapshot that we can deploy
We should now see an entry under Releases near the bottom of our distribution page.
Now that we have an OTA distribution and a first release we can set up OTA in our iOS app.
Installing the PhraseApp iOS SDK
We’ll need the PhraseApp SDK installed to use OTA. We can do so through CocoaPods, Carthage, or manually. We’ll go over the CocoaPods installation here, but you can see the learn ways to install the SDK by reading our guide, iOS SDK Installation Instructions.
To install the SDK via CocoaPods, we need to to have CocoaPods installed on our Mac. You can see the installation instructions on the CocoaPods website. Assuming that we have CocoaPods installed, we can navigate to our root project directory (the one that contains our
.xcodeproj file) and run the following command from the terminal.
$ pod init
This will create a
Podfile in our root directory. Let’s open this file in our favorite code editor and add a line to it so that it looks like so:
With that edit saved, we can run the following from our terminal:
$ pod install
If all went well, we should have received output that indicates that PhraseApp has been installed. After that, we can close any XCode windows we have open and open the
.xcworkspace file that CocoaPods added to our project root after installing PhraseApp.
Configuring PhraseApp in Our iOS App
With the SDK installed, let’s get it wired up to our app. First, let’s take a quick look at two main methods that the PhraseApp SDK exposes.
PhraseApp.shared.setup(distributionID:environmentToken:timeout:) initializes the shared PhraseApp singleton object, taking our credentials and an optional timeout parameter.
PhraseApp.shared.updateTranslations(translationResult:) manually updates the translations in the app, pulling in fresh ones from the PhraseApp servers if a new release exists.
updateTranslations takes an optional escaping closure,
translationResult, that we can provide if we want to take action when the update request completes.
📖 Go Deeper » You can find more details about the PhraseApp iOS SDK
in our guide, iOS SDK Installation Instructions.
Ok, let’s add an
OTATranslations class that provides a light wrapper around
Phraseapp.shared.debugModeis a flag that makes the PhraseApp SDK more verbose, outputting more details to the console, when turned on. We turn it on when we’re running a debug build of our app.
💡FYI » The
printIfDebug(_:)function is a custom helper that simply outputs its given string to the console if the
#DEBUGflag is on. If you want you can view the function’s code on this project’s Github repo.
We provide a simple
OTATranslations.shared singleton object to be used in our app. Our object is initialized by pulling PhraseApp config values from a
PhraseApp.plist and providing them to
PhraseApp.setup. To make this work we need to create the
PhraseApp.plist class in our XCode project. Let’s do so now. It should look a little like the following.
Make sure that your keys match the ones here
Here’s a text version if you want to
✋🏽 Head’s Up » You might want to add your
PhraseApp.plistfile to your
.gitignoreto guard your secret keys.
💡FYI » The
PListclass we’re using to load our
.plistfile values is a custom helper class. You can check out its code on this project’s Github repo.
The values for our
prodToken can be found on the OTA distribution page in the PhraseApp web console. We can navigate to our project’s dashboard Overview tab, and click the View distributions button under the Over the Air section. This opens the Over the Air page, and from there we can find and click our project’s OTA distribution in the Distributions list. Once on the specific distribution’s page, we can find the Distribution ID, Development secret, and Production secret values to copy and paste into our
.plist file. Of course, we’re using “token” instead of “secret” in our
.plist, but you probably figured that out already.
Copy & paste your ID and secret tokens
Ok, now that we have the PhraseApp SDK wired up to our
OTATranslations class, let’s make use of it in our
application(_:didFinishLaunchingWithOptions:) method, which is run when our app is first started, we manually update our translations over the air. This pulls in any new translations in the background. Our current setup will not have the UI update immediately, however. The translation updates that are pulled during this app launch will appear to the user during her or his next app launch. This is often the behaviour we want: having new translation strings pop into view as the user is in the middle of doing something in the app could be a weird and potentially unsettling user experience.
Manually Refreshing the UI After Pulling in New Translations
However, depending on our requirements, we may need to refresh the app UI immediately after pulling in a translation update. We can do that using the callback closure parameter we provided in
Here we provide the
onUpdateComplete argument to
updateTranslations as an escaping closure. This closure will be called when there are new translations that have been pulled in successfully. When this happens, we call our custom
AppDelegate.reloadViewController() method. This will effectively reload our app and show the user our most recent translations.
✋🏽 Head’s Up » Be careful when reloading the app’s root view controller. If the user has already started to do something in your app, he or she can have that flow interrupted—and their unsaved data lost—when the reload happens. To reduce the chances of this happening, you may want to use a relatively low
timeoutconfiguration when setting up the
PhraseAppSDK. That way, if the translation pull request takes a while, it will just cancel out and not trigger the reload. Alternatively, and more robustly, you could have a blocking loading indicator appear during the request. This would disallow the user from interacting with your app altogether during the translation load, preventing the aforementioned data loss.
The Freedom of iOS Over the Air Translations with PhraseApp
We’re basically done with integrating OTA translations into our app. After we publish this version of our app to the App Store, we’re free to send translations to our users without going through App Store approval again. All we have to do is head to our PhraseApp web console, and
- Update our locale translations, then
- Create a new release for our OTA distribution (just as we did before), and
- Publish the release.
Don’t forget to publish your release after reviewing
It’s really that simple. Once we publish our release in the PhraseApp web console, our users will start getting the updated translations. All users that have the version of the app wired up to the
PhraseApp SDK will pull in the new translations over the air. No need to publish a new app version, no need to wait for App Store approval. Sweet liberty.
🔗 Resource » You can view and download the completed code for the iOS project we built here from our Github repo.
If you want to dive deeper into iOS internationalization and localization, check out some of our other articles on the subject.
And That’s a Wrap
Wiring up our app to PhraseApp over the air translations is the work of a day or two. This can save us dozens upon dozens of hours as we publish new translations directly to our app users. With OTA we also have the added benefit of skipping App Store approval (just for a text update). And PhraseApp offers you much more than OTA: PhraseApp can really streamline the localization process for your app. Take a look at all of PhraseApp’s features and sign up for a free 14-day trial.
I hope you’re starting to see the potential of over the air translations, and that you enjoyed this little guide into making OTA work in your iOS app. We’ll be adding more OTA content in the days to come, so stay tuned, and happy coding 🙂