Firebase, Google’s serverless backend platform, makes development for small teams and early-phase startups easier and more cost-effective. If you’ve used Firebase before, you know that it gives you a realtime, NoSQL database (Firestore), cloud storage, and push notifications, among several other backend services. Firebase includes SDKs and packages for all the popular runtimes, and iOS is no exception. Building an iOS Firebase app is like being a full-stack developer, except you focus almost entirely on your UI. But what about Firebase i18n? In this two-part series, we’re taking an iOS/Firebase app and internationalizing it so that it can work in multiple languages. We’ve already tackled much of the UI in part one. Here, we’ll round out our app by covering a bit more UI internationalization, look at i18n/l10n in Firebase Firestore, and send push notifications per-language using Firebase Cloud Messaging (FCM).
Table of Contents
Our App: Discounter
Here’s what we’ll build:
When we’re done with this article, we’ll have this beauty
Our demo app, Discounter, targets price-conscious retail consumers and aggregates city’s coupons, flyers, and sale information for these users in one place. Our users can then browse and search for their favourite products to see if they’ve been discounted. In this series, we’ll focus on the Feed screen, which lists recently discounted products in a user’s city.
Note » We’re starting basically where we left off in the last part. So if you’ve been coding along with part 1, you can just keep going. If you’re starting with us here in part 2, feel free to pick up the starter code for this article on Github.
We’ve been internationalizing the app so it can work with both English and Arabic. You can choose any languages to work with, of course.
Our starting point
Photo & Icon Credits
Some photos and icons used in the app were sourced. Here’s a list of these sourced assets, along with the awesome people who provided them for free.
- Feed (Menu) Icon by Jardson Almeida, US on The Noun Project
- Search Icon by Landon LLoyd on The Noun Project
- Alert (Notification) Icon by DewDrops on The Noun Project
- Thumb Icon by Ayub Irawan, ID on The Noun Project
- Nike Air Photo by Fachry Zella Devandra on Unsplash
- PlayStation 4 Photo by JESHOOTS.COM on Unsplash
In the last article, we largely internationalized the UI, adding a language, localizing the storyboard, and flipping image buttons for right-to-left languages like Arabic and Hebrew.
A Quick Tour of the Code
Let’s briefly take a look at the app architecture as it stands.
The bulk of our UI is defined in the usual
Main.storyboard. Here we have a
UITabBarController for our root navigation, and segues to other, simple controllers that make up our app screens.
Our main screen is controlled by a
FeedViewController. It’s a very simple
UIViewController that acts as the data source for our feed’s
Product, our main model class, to register a listener to our product feed in our
viewWillAppear(_:). We do this so we can see changes to our feed in realtime.
Product is just using the Firebase Firestore SDK underneath the hood. We deregister our listener in
viewWillDisappear(_:) to avoid memory leaks and unnecessary Firebase database costs.
We’ll dive deeper into our
Product model a bit later when we look at internationalizing and localizing our Firestore data. For now, let’s take a look at the custom
UITableViewCell that we’re using with our
Again, this is a pretty bread-and-butter iOS code. We take a
Product object in
updateUI(with:), do some light transformation to its fields, and connect the resulting values with our cell’s views. We’re using the popular SDWebImage library for loading our products’ network images.
A Quick Currency Fix
In the last article, we introduced the
Product model but we didn’t go into its code in too much detail. In
Product, we convert our currency numbers to a string via a global helper function,
centsToString(_:). You may have encountered this function if you looked at the code in the last article’s companion Github repo.
centsToString(_:) uses a
NumberFormatter to produce its currency string, and its logic had a bug in it.
Notice the line
formatter.currencyCode = "US$" above. That line wasn’t there in the last article’s Github repo, and without it the
formatter would assume the currency of the current locale. That means that if our app user were in Canada, for example, he or she would see currency strings reading something like
C$ 200. Since our app has all its prices in USD, this would, of course, be problematic. The added line above fixes this by strictly forcing the currency to USD.
Displaying Localized Strings in Swift
Let’s cover a couple of things we missed in the last article. We know how to internationalize and localize our storyboards, but what about strings that we display through our Swift code?
Well, iOS has a built-in macro,
NSLocalizedString(_:comment:), which takes a
key parameter used to lookterser
up a translated string in the current locale’s
Localizable.strings file. This macro works, but it’s a bit inconvenient.
NSLocalizedString("foo", comment: "a foo") is quite a mouthful, and we can have many translated strings in a typical localized app. So we can write a global function that wraps
NSLocalizedString to make our lives easier.
Note » Read more about
NSLocalizedStringin our in-depth article, iOS Localization: The Ultimate Guide to the Right Developer Mindset.
__(_:) function is a little terser and more developer-friendly than
NSLocalizedString. We can now use
__(myKey) whenever we want to fetch a translated string.
Note » If you’re wondering how to create a
Localizable.stringsfile: in the XCode menu bar, go to File > New, select Strings File and click Next. This will create the file and automatically add it to your build settings, ensuring that the file is copied to your app bundle when building the app. Make sure the file is available to your app target and your localizations by clicking on the file and checking the appropriate files in the XCode inspector.
Interpolation in Translated Strings
Sometimes, we have dynamic values in our code that need to be interpolated into a translated string at runtime. Our expiry label string needs a dynamic string within itself, for example.
Our expiry label needs string interpolation
We do this by using the
String(format:arguments:) initializer. Like in other languages’ formatting functions, this initializer takes a
format string and an unlimited list of arguments that replace certain format specifiers.
Our expiry string, for example, can be used as a format:
String("Expires %@", product.expires), where
product.expires is a simple string. Here, the
%@ in the format string is a special sequence that tells the initializer that we’re going to replace the
%@ with a coming string argument: the first argument after the format, which happens to be
Note » Check out the full list of string formats we can use with
String(format:arguments:)in the official documentation.
This is all good and well, but what about using translated strings? Well, we just combine our
__(:) function with
It’s really that simple. Now, in our
Localizable.string files, we can have formatted strings.
You may have noticed that in our
FeedTableViewCell.swift file, we were transforming our strings to uppercase using
String.uppercased(). This method will return an uppercase version of our string, but it won’t always take into account the current locale’s idiosyncrasies. Turkish, for example, capitalizes its i character as İ (note the dot).
uppercased() doesn’t cover these cases, so we’re better off using the
String.localizedUppercase property to make sure our uppercasing is locale-safe.
Note » There is, of course, a
Here’s what our
FeedTableViewCell.swift looks like after our recent changes:
A Closer Look at Our Model
Before we get to internationalizing and localizing our Firestore database, let’s see how we connect to it via our
listenToFeed(onChange:) are static methods, and both will retrieve our product feed from Firestore. The former performs a one-time fetch, while the latter will call its callback closure,
onChange, whenever any write is performed on the product feed Firestore collection.
The rest of our
product methods are helpers that allow us to convert retrieved Firestore objects to
Note » We use
DB.timestampToDate(_:_:)to do some type conversion when reading our models. You can peruse the implementation of these functions in the Github repo.
Product.listenToFeed(onChange:) to keep its
UITableView in realtime sync with the
product-feed Firestore collection.
Note » You may have noticed our call to
humanizeDate(date:)above. This function is what converts a
Dateobject to something liketodayortomorrow. Humanized dates could be the subject of their own post, and if you’d like to see use publish one let us know in the comments below. You can also take a look at the code of
humanizeDate(date:)in the GitHub repo.
Internationalizing our iOS App with Firebase: Localizing the Firestore Collection
This is all well and good, but we do have a problem here.
The Arabic version of our app shows English Firestore content
Our app view is localized, but it’s pulling content from the wrong locale. Let’s fix this by redesigning our database schema in Firestore.
Here’s what our
product-feed collection looks like at the moment:
Our product feed Firestore model, as it is now
Of course, we can internationalize this in several ways, and they will all depend on our app’s needs. For our app, we can use a simple separation across locales for each data collection.
| ├── 0qKcByHYIc7Wi7XZSIzH
| | ├── discount: "تخفيض ٢٠٪"
| | ├── name: "نايك اير"
| | └── ...
| └── 57bEpulnmwUGhI2oRJAV
| └── ...
| ├── discount: "20% off"
| ├── name: "Nike Air"
| └── ...
Instead of our documents nesting directly in the
product-feed collection, our new
product-feed-i18n collection has them broken up per-locale. We have an empty
locales document that allows us to add a collection for each locale under it. Our documents are then placed in each of these collections with their translations.
Updating Our Model
Since our product feed schema has changed, we need to update our app code to access the localized products. Thankfully, this is an easy fix.
We update our
COLLECTION_PATH so that we document our schema change at the top of our file, in its configuration. Then, we do a simple string replacement to get the feed collection corresponding to the user’s current language. Nothing else in our code has to change.
Note » In production, we would have to account for our user’s current language not being supported in our Firestore database, and have an appropriate fallback.
Sending Localized Push Notifications with Firebase
One of the best things about Firebase is its Firebase Cloud Messaging (FCM) service. FCM allows sending messages to one, some, or all users of our app without the hassle of connecting to Apple’s APNS servers ourselves. Doing this through the Firebase Console couldn’t be easier.
Note » To test this, you’ll need to setup push notifications and test on a physical iOS device.
Sending a targeted notification message via the Firebase Console
In the Firebase console, we navigate to Grow > Cloud Messaging. We should arrive at the Cloud Messaging screen defaulted to the Notifications tab. From there, we click the New notification button. Now, we can enter our notification copy in the Notification text field and click the Target label to open that section. In the Target section, we select our target app (iOS in our case), and click the and button to add another target constraint. We select Language from the dropdown menu, leave Is in as the conditional operator, and select a language we want to target. That’s basically it. We can now Review and Publish our message. When we do so, the message will be received only by users who installed an opened the app in the language we just targeted.
That’s All, Folks
That about covers our first journey in internationalizing an iOS and Firebase app. We finished up internationalizing our UI, localized our Firestore database, and learned how to send language-specific push notifications through FCM.
Note » You can get the completed code for this article on Github.
Are you working on an iOS app and internationalizing and localizing it for audiences in multiple locales? Well, if you’re looking for a feature-rich, professional localization solution, Phrase may come in handy. Phrase works with iOS localization XLIFF files natively. It tracks translation versions so that your translators can easily go back to older ones. It’s also built with collaboration in mind, allowing you to add unlimited team members to your project and to integrate with your Slack team. You can even do over-the-air translation updates with Phrase, so your translations can get to your users immediately without waiting for an app update. Check out Phrase’s full feature set, and take it for a spin for free.
I hope you’ve enjoyed this foray into internationalizing a full-stack iOS app. Stay curious, friends 🤓