Internationalizing a Full-Stack iOS App with Firebase (Part 1): The User Interface

Phrase
Software Localization Tutorials
12 min readApr 18, 2019
We can become blazing fast full-stack developers with iOS and Firebase. In this series, we’ll internationalize both the front-end and the back-end of a full-stack app, starting with the iOS user interface.

You may well have heard of Firebase, Google’s serverless back-end platform. With Firebase, iOS (and other platform) developers can cut down on early development costs by not worrying about building server architecture for their apps. Firebase has a real-time database (Firestore), cloud storage, notifications, analytics, and other services that many of our apps need. And without the need for DevOps and dedicated server developers, we can start our iOS app with smaller teams and move faster. And of course, we’ll want to internationalize our iOS/Firebase apps as we build them so we can reach as many people as possible and maximize our revenue.

In this two-part series, we’ll take an iOS app with Firebase and internationalize it so that it can work in multiple languages. We’ll start with the user interface in this article, and move on to the Firebase back-end in the next instalment.

Our App: Discounter

Here’s what we’ll build:

The app we’ll have by the end of this article

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 » The design of our demo app was established in our article, Designing Apps in Sketch for iOS Internationalization, so be sure to check that article out if you’re more into the design side of things.

We’ll internationalize the app so it can work with both English and Arabic. You can choose any languages to work with, of course. But let’s start at the start. We’ll assume we’ve built out one of the screens of our app without internationalization. Something like this:

Our starting point

Note » If you want to code along with us you can grab the starter project on Github. We’re using the starter project as our launching point here.

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.

The colour palette used in the app was largely derived from the Smashing Magazine Wallpaper, Let’s Get Outside, by Lívia Lénárt.

A Quick Tour of the Code

Let’s briefly take a look at the app architecture as it stands.

Our Main.storyboard

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 focus will be on the FeedViewController. It’s a very simple UIViewController that acts as the data source for our feed’s UITableview.

import UIKit

class FeedViewController:
UIViewController,
UITableViewDataSource
{

@IBOutlet weak var feedTableView: UITableView!

fileprivate var productListener: Product.Listener?

fileprivate var products: [Product] = []
{
didSet
{
feedTableView.reloadData()
}
}

override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)

productListener = Product.listenToFeed
{
[unowned self] in self.products = $0
}
}

override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated)

productListener?.remove()
}

func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int)
-> Int
{
return products.count
}

func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL") as!
FeedTableViewCell

cell.updateUI(with: products[indexPath.row])

return cell
}
}

Note » I’ve been writing a lot of C# lately, so I’ve gotten used to placing my { on their own lines. Forgive me if my less than idiomatic Swift looks odd to you.

We use Product, our main model, to register a listener to our product feed in our viewWillAppear(_:). We do this so we can see changes to our feed in real-time. 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 be getting a bit deeper into the Product model in our next article. Suffice it to say for the time being that our Products have simple String fields like productName and priceAfterDiscount, which serve to populate our UI. Now let’s take a look at the custom UITableViewCellthat we’re using with our FeedViewController.

Note » You can check out the code for the Product model, as well as the entire starter XCode project on Github.

import UIKit
import SDWebImage

class FeedTableViewCell: UITableViewCell
{

@IBOutlet weak var productNameLabel: UILabel!

@IBOutlet weak var storeNameLabel: UILabel!

@IBOutlet weak var discountLabel: UILabel!

@IBOutlet weak var expiryLabel: UILabel!

@IBOutlet weak var priceAfterDiscountLabel: UILabel!

@IBOutlet weak var priceBeforeDiscountLabel: UILabel!

@IBOutlet weak var productImageView: UIImageView!

func updateUI(with product: Product)
{
productNameLabel.text = product.name

storeNameLabel.text = product.store.uppercased()

discountLabel.text = product.discount.uppercased()

expiryLabel.text = "Expires \(product.expires)".uppercased()

priceAfterDiscountLabel.text = product.priceAfterDiscount

priceBeforeDiscountLabel.attributedText =
strikeThrough(product.priceBeforeDiscount)

productImageView.sd_setImage(with: URL(string: product.imageUrl))
}
}

Again, this is a pretty bread-and-butter iOS code. We take a Productobject 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.

Note » We’re using a helper function, strikeThrough(_:), to convert a string into an attributed string with a line through it. You can see its code on Github.

That’s generally it for our feed’s MVC. We’ll get to more of the code that pertains to our UI as we delve deeper into our internationalization work. Speaking of which, let’s get started on internationalizing this puppy.

The Firestore Model

If you’re working along and you have a Firebase project setup and connected to your iOS app, here’s a quick look at the Firebase Firestore database structure in case you want to replicate it.

Our Firestore product feed model

Beginning Our iOS/Firebase Internationalization: Adding a Language

To begin internationalizing our UI we just need to add a language to our project in XCode. We select our project in our navigator, select our project again (not a specific target) in our project window, and click the +button under Localizations.

We start our i18n by adding a language

We then select the language we want to add and select Localizable Strings when asked how to internationalize our current files. In my case, I’ve selected Arabic.

Note » We go into language settings and storyboard localization in much more detail in our article, iOS i18n: Internationalizing Storyboards in XCode.

Localizing Storyboard Text

Since we opted for localizable strings when we added our language, translating our storyboard strings is pretty straightforward. We just need to unfold Main.storyboard in the project navigator and select the Main.strings (Arabic) file. With the .strings file open, we can add our Arabic translations.

Here’s an excerpt of the translated Main.strings:

/* Class = "UIButton"; normalTitle = "Sort & Filter"; ObjectID = "2Sk-5H-KF9"; */
"2Sk-5H-KF9.normalTitle" = "فرز وتصفية";

/* Class = "UILabel"; text = "Search"; ObjectID = "448-81-bRb"; */
"448-81-bRb.text" = "بحث";

Testing Our Localized App

Now that we have our storyboard strings translated, we can preview our app in Arabic by opening Main.storyboard. We then open the Assistant Editor, and select Preview > Main.storyboard (Preview)

Previewing our FeedController in Arabic using the Assistant Editor

This will show us the translated strings, but won’t reflow our layout to match Arabic’s right-to-left direction. To see how our app will look to real users reading in Arabic, we can edit our Run scheme. We go to Product > Scheme > Edit Scheme in XCode’s menu, and change the Application Language to Arabic.

We can select a runtime language for our Run Scheme

After we make this change and run the app in our simulator, we can see our Arabic text and the app’s right-to-left layout.

We’re getting there: our Arabic strings are showing up in the app

Fixing Layout using Stack Views

Our app looks a bit odd in Arabic: buttons with images look broken. Let’s take a closer look at each button one by one. Our Sort & Filterbutton has its icon on the wrong side of the text.

Well that looks broken

It’s currently laid out using good old Auto Layout constraints, with trailing and leading edges pinned to the button’s superview and adjacent title label, respectively. This should automatically take care of the button when our layout changes to right-to-left, but for some reason — and for the life of me I couldn’t figure out what it is — it does not. However, we can fix this by embedding our button and the app’s title label in a Stack View.

We take the label and the button out of the View they’re in and embed them in a Stack View. We then set the Stack View’s properties so that we get its two children horizontally aligned.

We make sure that our Stack View is horizontally aligned

This gets us part of the way there. We also need to make sure our button’s subview alignment is to the button’s trailing edge in the Attributes Inspector.

Aligning our button’s content to the trailing edge

Now we need to add some spacing between the edges of the screen and our content. One way to do this is to embed the Stack View itself in another View. This is because our Stack View itself is inside a parent Stack View, and Stack View’s children generally don’t respect explicit Auto Layout constraints. After all, the Stack View’s job is to lay out its children automatically. We can get around this problem by wrapping the child Stack View in a plain old View and setting our spacing constraints on the inner Stack View.

We can now add spacing by constraining the Stack View to its parent View

After we make these changes and run our app, we see that the Sort & Filter button has its image on the correct side of the text.

The button’s image is beginning to behave itself

Flipping Edge Insets

The image in our Sort & Filter button is now rendering on the correct side of the button. However, the image is flush with the button’s title, which isn’t what we want. We’d like a bit of spacing between the button’s image and its title. This is currently set up through the button’s UIEdgeInsets settings in the Size Inspector.

Our button’s edge inset settings

We want these to flip for right-to-left layouts, and we might as well generalize this flip for a button’s three inset types: content, title, and image. A UIButton subclass can do the trick for us here.

import UIKit

class FlippableUIButton: UIButton
{
override func awakeFromNib()
{
if (Locale.current.isRightToLeft)
{
flipAllEdgeInsets()
}
}

fileprivate func flipAllEdgeInsets() -> Void
{
flip(edgeInsets: &contentEdgeInsets)

flip(edgeInsets: &titleEdgeInsets)

flip(edgeInsets: &imageEdgeInsets)
}

fileprivate func flip(edgeInsets: inout UIEdgeInsets) -> Void
{
let leftEdgeInset = edgeInsets.left

edgeInsets.left = edgeInsets.right
edgeInsets.right = leftEdgeInset
}
}

We simply swap all of our button’s right and left UIEdgeInsets on awakeFromNib() when our current Locale‘s language direction is right-to-left. To facilitate this, we have a simple extension to Swift’s Localeclass.

import Foundation

extension Locale
{
var isRightToLeft: Bool
{
get
{
if let languageCode = self.languageCode
{
return Locale.characterDirection(forLanguage: languageCode) ==
.rightToLeft
}

return false
}
}
}

isRightToLeft is a simple computed Bool that allows us to avoid the verbose characterDirection(forLanguage:) call every time we want to know if the current locale’s language has a right-to-left direction.

With this code in place, we can change our button’s class to FlippableUIButton in its Identity Inspector in Main.storyboard. In fact, we can do so for our sorting indicator button as well, which you may have noticed was also broken in right-to-left orientation. Once we do that and re-run our app, we can see that our two buttons have the correct spacing in Arabic.

Two birds with one stone: our FlippableUIButton can be applied to multiple buttons to handle right-to-left layouts automatically

Flipping Images

OK, our buttons’ edge insets are largely taken care of. However, our Sort & Filter button’s icon looks weird in Arabic. It should be horizontally flipped. We can add this behaviour to our FlippableUIButton class. However, we don’t want to flip all button images. Sometimes it makes sense to have the same image orientation for both left-to-right and right-to-left layouts. So we can make our FlippableUIButton editable in our inspectors and add a flag that indicates whether to flip a button’s image or not.

import UIKit

@IBDesignable
class FlippableUIButton: UIButton
{
var _flipImageForRTL: Bool = false

@IBInspectable
var flipImageForRightToLeftLanguages: Bool
{
get { return _flipImageForRTL }

set { _flipImageForRTL = newValue }
}

override func awakeFromNib() {
if (Locale.current.isRightToLeft)
{
flipAllEdgeInsets()

if (flipImageForRightToLeftLanguages)
{
flipImage()
}
}
}

// ...

fileprivate func flipImage() -> Void
{
if let image = imageView?.image
{
let flippedImage = UIImage(
cgImage: image.cgImage!,
scale: image.scale,
orientation: .upMirrored)

setImage(flippedImage, for: .normal)
}
}
}

We opt not to flip a button’s image by default and provide an @IBInspectable flag that can force the flip in a storyboard’s Attribute Inspector. If the flag is set to true, we flip the button’s image by making a copy of it and setting the copy’s orientation to UIImage.Orientation.upMirrored (which flips it horizontally). We then set the flipped copy as the button’s image via UIButton‘s setImage(_:for:).

With this in place, we can pick which of our FlippableUIButtons should have flipped images in our Main.storyboard.

Our setting to flip a button’s image is available in the Attribute Inspector

Avoid Adding Text to Image-Only Buttons

Notice that in right-to-left orientation our “favourite” (thumbs up) button has too much space to its left.

That’s way too much space

This seems to be happening because even though this is an image-only button, we have default text set in its title.

The button’s default title text is breaking our layout

While this didn’t cause problems in English, probably because the text was clipping to the right of the image, it seems to be breaking our layout in Arabic. The quick fix is just to remove the title text, setting it to an empty string. Once we do that, all is right with the world again.

Our app in Arabic with all the UI fixes

Adios for Now

That takes care of most of the UI internationalization work for our FeedViewController. Next time, we’ll round out our internationalization and localization work for the app, looking at how to work with multiple locales in the Firebase Firestore and sending push notifications per language.

Note » You can get the complete 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 has got you covered. 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. We started with the front-end here, and in the next part of this series, we’ll turn our attention to the Firebase back-end. Stay tuned 📻

Originally published on The Phrase Blog.

--

--