iOS Content Localization and Versioning with Firebase
Firebase has become the first tool any iOS developer adds to his projects. And while Analysts
, Crashlytics
, and Performance
usually work out-of-the-box, there is one tool that rises more questions. It’s Firebase Database
.
How does it handle app localization?
How to configure it in production?
How to set proper environment and versioning?
And how to make this integration smooth, maintainable, and testable?
We will talk about this in the context of the app strings
and briefly review different ways to handle strings
. From the simplest setup with no localization to remote localized database environment with versioning.
Part 1. Do you have a Labels file?
Working on any iOS project, sooner or later, you will have to decide how to organize and store the string values for all your labels, buttons, and text fields. These days no one (hopefully) hardcodes the strings directly:
Most likely you have a file Labels.swift
somewhere in your project, that keeps your stings structured and easily accessible:
It can evolve to something even more organized:
Did you notice how we changed Struct to Enum here? It makes more sense because we use static properties, and we don’t want to mislead ourselves or other engineers by providing a way to create a useless instance of
Labels
.
Now you can assign your string value as easy as
This setup will do the job, and there is a good chance you won’t need anything else.
However, one day you may decide to support another language. So let’s talk localization.
Part 2. Localization
Adding localization to the project is pretty straightforward. Go to Project -> Info -> Localization -> Plus button and select desired laguage:
Now you need to add a Localized.strings
file, that will hold the strings for all supported languages: go to File -> New -> File , choose Strings File, and name it Localizable.strings
:
lastly, select Localized.strings
file in the project navigator, tap Localize
in file inspector, and select the language:
Now you should have this a file for each language you support:
The question is how does it work with our Labels? Well, here is the point when it may turn into a mess.
First, you need to add all your strings to Localizable.strings
for all supported languages:
Now we need to use the localized strings in the Labels
:
Key in
Localized.strings
should match the key inNSLocalizedString
. The actual string value will be taken fromLocalized.strings
depending on the current language.
If you add a NSLocalizedString
to a String extension, I can become more readable:
Again, depending on the complexity of your project and your current goals this configuration may be all you need to handle copy and localization in your app.
But let’s s review this solution and see what may go wrong here.
- We need to carefully match the keys between
Localized.strings
andLabels.swift
. If the key is wrong, or you forgot to provide the value inLocalized.strings
, the user will see the key itself instead of the actual localized value Localized.strings
can be easily malformed. If you have a largeLocalized.strings
file, and one of the lines is missing a semicolon at the end — good luck finding the right place to fix it. Xcode doesn’t give any clues for this:
Localized.strings
is a plain text file, and there is no way to make it structured. Once it reaches a couple of hundred lines, it becomes almost unmaintainable. Yes, you can use external tools to generate it automatically, but key matching is still a tedious and error-prone job.- More important, one day you may want to update some strings. Let’s say, you want to change “Welcome to my cool App” to “Good morning!”. With the current approach, you need to update the values in all
Localized.strings
files and release a new build of your app. This doesn’t sound right for a simple string update, does it?
If you have any of the above-listed problems, it’s time to use remote labels!
Part 3. Remote labels
We will use Firebase Database as a backend to provide the labels for your app. This will remove most of the problems above: you no longer have to support the multiple languages on the client, nor maintain the key matching in Localized.strings
. And best of all, you can simply update the labels with a few clicks without releasing the new app version. However, it still comes with trade-offs:
- Localization didn’t go anywhere, it’s just moved to Firebase. So we need to proper maintain it here
- Configure the App to read the proper labels for each locale
- Maintain the database versioning on Firebase
Let’s dive in and see how to make it work.
What is Firebase Database and how it works
We will not review how to add Firebase dependency to your project and make a basic setup. Firebase provides good step-by-step instructions for this. Let’s jump straight to Firebase Database and start implementing it for our project.
Firebase Database is a cloud-hosted noSQL database. It holds the data in the plain JSON format, and Firebase provides an API to fetch the data as JSON:
Don’t forget to import
FirebaseDatabase
.
You can also use DatabaseReference.observe
method if you want to subscribe for real-time updates, but we don’t need this for our purpose. We want to download the labels once the app starts, and observeSingleEvent
will do the job.
You should be already familiar with JSON format if you use any web services in your app. Swift provides a simple way to parse the JSON to your Swift models with a Decodable
protocol.
Before we can use Firebase to fetch the Labels and decode them from the JSON, we need to make some updates:
Instead of holding the actual values, Labels will be a struct that conforms to the
Decodable
protocol. Note, it’s not an enum anymore because we have to use an instance of Labels to access its content. We no longer usestatic let
for the same reason.
Putting it together, we have a way to fetch and decode labels from FirebaseDatabase:
Let’s open FirebaseDatabase and create the Labels JSON we can use:
This is a Firebase visual representation of the following JSON:
If you run this code, you will successfully load the Labels. Congratulations, you have simple remotely-stored labels!
But wait, how about localization, versioning, production and development databases, and all the perks we discussed before?
Firebase database path
FirebaseDatabase provides a concept of a child that allows fetching only a certain chunk of the database.
In the example above we can request only Labels.Login
by using the DatabaseReference
with a child login
:
Note, the “login” path component should match the key in your database. Firebase will only return the data for the key you provide as a child.
With this in mind, we can come up with a simple way to add multiple localizations. We will also use a Labels
as a top-level key for our labels:
We only need to point to the right path of the Firebase database. How can we tell the app either to append the child ENG
or RUS
to the path? We already have this mechanism in place: Localized.strings
. Instead of having the user-facing values, we need a single value:
Now we use this localized string to create a path to a proper database section:
Now your app will be able to fetch the strings for your app depending on the current localization.
But how do we add all the labels for all localizations to Firebase Database?
Create JSON for Firebase
We create a local default Labels
instance in your app for each locale:
In this case, the compiler will ensure you don’t miss any key, as it can happen when you create a JSON manually. Instead, we will generate JSON from Swift instances of our Label class, and upload it to Firebase through the firebase console.
JSON Labels generator is a service tool, so it makes sense to create a separate target within your app. Let’s call it a LabelsParser
. Now we can move DefaultLabelsENG
and DefaultLabelsRUS
to this target, and add a code snippet that generates JSON files and saves them to your computer:
Make sure your
Labels
and all nested structs conform toCodable
protocol so you can encodeLabels
into JSON. Also, make sure bothDefaultLabelsENG
andDefaultLabelsRUS
have aLabelsParser
in target membership.
In the Firebase Database console, navigate path ENG/labels
by clicking on the appropriate label’s key, select Import JSON from the menu on the right, locate the created LabelsENG.json
, and import the file. Firebase will update the labels if the import is successful. Repeat this for RUS/labels
path and LabelsRUS.json
file.
Updating the labels will be a 3-step process:
- Update the
Labels
struct (add, remove, or rename keys or nested structs, or change the string values) - Xcode will throw the errors in
DefaultLabelsENG
andDefaultLabelsRUS
, so you need to update them accordingly - Finally, re-generate the JSON files with
LabelsParser
and upload them to Firebase
If you want to only update the existing string value, you can do it directly in Firebase, and all the users will fetch the updated labels on the next app launch. Or not?
How can we update the live database, if all the users are expecting the existing JSON format? If we change or remove some keys in Firebase Database, the Labels
decoding will fail.
This question means you are ready to configure the database environments and versioning.
Database environment
Before you configure the database environment, there is one more thing we may want to do to ensure the labels parsing always succeed. Provide the default labels. We already have a DefaultLabelsENG
, and we simply need to return this instead of throwing the error if either Firebase connection or JSON parsing failed for any reason:
You can also add a localization check in the snipped above to return the correct default labels depending on the current app locale.
Let’s get back to our environment and versioning problem. Any remote database should have at least two environments: Development
and Production
(you can add Staging
, Beta
, and as many as you need for your project). All the real users of your app should be connected to Production
environment, while the developers and testers should use Development
one.
To achieve this with Firebase, we will take the same approach as we took for Localization: database paths. In the root of the database we create two keys, Development
and Production
, and each of them will have a copy of the current database:
To point the app to the right Database path, we you use a #if DEBUG
check when you crate a Firebase database reference:
Instead of using
#if DEBUG
flag, you can have.xcconfig
files to choose between the development and production environments in Xcode. This approach provides more flexibility and clarity. For this example, we will stick to using#if DEBUG
flag
Having development and production databases are great, but how about supporting more than one app version? For example, you have version 1.0.0
that has a database in production. Later you release a new 1.1.0
version that has some changes in Labels
. If you update your Production
database to match the new release, all the users with the previous app version won’t be able to download the labels. Now we are talking about the database versioning.
By database versioning we mean that the database has to support all the versions on your app. So we need to have a separate database path for version
1.0.0
,1.1.0
, etc.
To handle this, we will create an Environment
enum:
As you can notice, for the production environment we provide an app version as a String. Later we will match this to the current iOS app version.
stringValue
creates a database path for the given environment.
Wrapping everything we have on your app database config, let’s create a new Endpoint
class that will be responsible for building the proper path for all your environments, app versions, and locales:
Having the Endpoint
, we can create out WebService
that we will use to fetch the labels
:
Finally, we initialize the WebService
:
You can also use
CFBundleShortVersionString
to get the current app version instead of hardcoding it. Just make sure the Firebase database has an appropriate key for this app version, and the version formatting is the same.
With this setup, you can use Firebase Database to support any number of localizations, app environments, and versions.
Part 4. One more thing
It will be great to add tests URL paths for your database. In this example, you can test if Endpoint.labelsReference
matches the one use see in Firebase for given Endpoint
:
Let me know what you think about this database setup, I am always open to your feedback.
I just created the same WebService
in my latest Bike Tracker app. If you like biking as much as coding, feel free to check it out. And ping me on LinkedIn if you want a promo code for a free app subscription.