iOS Content Localization and Versioning with Firebase

Stan Ostrovskiy
Level Up Coding
Published in
10 min readMay 5, 2021

--

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:

Don’t tell me you still have this in your project

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:

Add localization

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:

Add Localized.strings

lastly, select Localized.strings file in the project navigator, tap Localize in file inspector, and select the language:

Localize strings

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:

Localizable.strings (English)

Now we need to use the localized strings in the Labels:

Key in Localized.strings should match the key in NSLocalizedString. The actual string value will be taken from Localized.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 and Labels.swift. If the key is wrong, or you forgot to provide the value in Localized.strings, the user will see the key itself instead of the actual localized value
  • Localized.strings can be easily malformed. If you have a large Localized.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:
Missing semicolon somewhere
  • 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 use static 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:

Simple Labels example

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:

Localized.strings (English)

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 to Codable protocol so you can encode Labels into JSON. Also, make sure both DefaultLabelsENG and DefaultLabelsRUS have a LabelsParser 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:

  1. Update the Labels struct (add, remove, or rename keys or nested structs, or change the string values)
  2. Xcode will throw the errors in DefaultLabelsENG and DefaultLabelsRUS, so you need to update them accordingly
  3. 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:

Development and Production

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.

Final version of Firebase Database

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:

Full URL string is on the top

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.

--

--