Software localization

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

In this series, we'll internationalize both the front-end and the back-end of a full-stack iOS app with Firebase, starting with the iOS user interface.
Software localization blog category featured image | Phrase

You may well have heard of Firebase, Google's serverless back-end platform. With Firebase, iOS (and other platforms) 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 installment.

Our App: Discounter

Here's what we'll build:

Our demo app | PhraseThe app we'll have by the end of this article

Our demo app, Discounter, targets price-conscious retail consumers and aggregates the city’s coupons, flyers, and sale information for these users in one place. Our users can then browse and search for their favorite 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 demo app with any internationalization | PhraseOur 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 and 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.

The architecture of our demo app | PhraseOur 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 UITableViewCell that 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 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.

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 Firebase Firestore database structure | Phrase

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 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 | Phrase

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.

Changing the Application Language to Arabic | Phrase

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.

Demo app with header translated into Arabic | Phrase

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 & Filter button has its icon on the wrong side of the text.

Demo app header with broken Arabic button | Phrase

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.

Setting the Stack View to Horizontal | Phrase

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 | Phrase

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.

Stack view architecture | Phrase

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.

Broken button with left-to-right orientation | Phrase

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 | Phrase

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 Locale class.

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.

Buttons and UI-text with spacing | Phrase

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.

FlippableUI button turned On | Phrase

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.

Button in demo app with too much text | Phrase

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.

Button title in Button menu | Phrase

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.

Translated and internationalized demo app | Phrase

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.

If you're working on an iOS app for audiences in multiple locales and looking for a robust 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 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 product set, and see for yourself how it can make your life as a developer easier.

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.