Software localization
Internationalizing a Full-Stack iOS App with Firebase (Part 1): The User Interface
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:
The 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 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.
- 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
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 Product
s 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 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
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 & Filter button 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 UIEdgeInset
s 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.
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 FlippableUIButton
s 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.
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.