When developing apps for iOS, we will sooner or later come across the need for localization. Localization in software development is adding support for different languages. It means that the entire user interface will be adapted for different languages and their requirements. This topic can get very complex and I would suggest watching this entertaining video for some more insight
In this article, I will focus on the most common case that I have encountered and that is to change the strings we display in the user interface, based on the language of the user’s device.
First, we need to add support for different languages to our project.
Add the required languages to the project
Make sure to select the supported languages for you strings file
When you select the supported languages, the strings file will be split into specific versions for every language.
This is an example of the English strings file:
To start using the translated values in our app, we can now use this helper function.
We can now localize our strings like this:
Problems with this approach
I think the biggest issue with this approach is that we have to rely on developers to use the correct string for the given localization key. If the key is misspelled, we will only notice this when we the app is already running and see that the correct string is not showing up, so it’s easy to make a mistake. Keys can change or get removed over time. Using raw strings has the disadvantage that the project will still compile, even if the localization strings are removed or changed. It would be better to have the compiler check for errors.
To improve the localization system we will create a Localization enum, with cases for all possible keys. We also add a computed value string, that localizes the selected key.
Now we can use this enum instead of the raw strings for keys. We will also get autocompletion for keys when we want to use localization because they are all stored in the enum.
If this is enforced throughout the project, there can be much fewer mistakes.
String format specifiers
Sometimes we need to use string format specifiers in localization keys for dynamic data that we pass into the string. This data can be of different types and here we are again relying on the programmer to pass in the correct data type.
We also hope that we will fix all the usages of strings if the required parameter type changes. The app has to be tested manually to check if all occurrences of the strings that use parameters are working because the compiler does not check it for us. If we pass in the wrong type, the app will crash when the erroneous code executes.
We can try to address this issue with enum parameters for our Localization enum. We have to switch up our enum a bit, to allow for parameters. We have to move the keys into the computed variable string and add the correct parameter types (associated values) to the enum cases.
This code will help the programmer make far fewer mistakes and we will be assured that if the code compiles, there won’t be any crashes in runtime.
Can we try to fix the system even more? Now the errors that can happen are more subtle. Enum doesn’t support the name of parameters. If the localized string has more than one parameter, we can make a mistake and send the parameters in the wrong order. The code is also not as readable as we’d like:
We can try to fix this using a struct.
In this example, we are using the enum with raw string values from the first example. Parameters are now handled in the struct. Using a struct will make the usage much more readable. We can also still use autocomplete when we need to pass in the key because we use static functions in the struct.
Autocomplete also gives us more information on the parameters we need to pass in for any specific key.
Testability
If we want to make sure we are using the correct keys, we have to cover localization with our unit tests. If we try to test our UI elements, we will quickly discover, that we can only test if the correct string was set on the UI element. We can’t test if the correct localization key was used, because the key is not saved anywhere. We can fix that by extending our UI element. In this case, let’s extend the UILabel. We will add a new property localizationKey to the UILabel. We will also add a new function set(with key: LocalizationKey). This function will save the key and set the text. We have to create this functionality for all UI elements we want to test.
This could be quite tedious because we have to override all the elements and also specify the correct overridden class whenever we build our user interface. I would still use this option on a larger project because you get a lot of value from the power of having all the UI elements extended.
We can try to use extensions to save the localization key. The main issue here is that extensions don’t allow using stored properties. We can get around this by using associated objects.
This removes the need for extending UI elements and remains fully testable. In some cases, associated objects would be the prefered way to achieve this if we are too lazy to bother dealing with all the extended classes.
I presented a few options on how to handle localization keys in your project. Our tests will look the same for any option we choose. We will simply use XCAssertEqual to check if we are using the correct keys.
If you decide to use enums with raw string values, you don’t have to do any additional setup.
If we want to test with structs, we have to upgrade our struct a bit and save the localization key:
We also have to update our extended UILabel, and save the localization key from the struct:
Testing gets most complicated when using enums with associated values. Here, the equal method for enum has to be implemented. We have to cover all possible cases and also take the associated value into consideration. Here is an article that describes how to achieve this – https://medium.com/@jegnux/on-swift-enums-with-associated-value-equality-e815a768d9b0
Conclusion
We have now created this entire localization system, with the purpose of eliminating human errors. But this system has, in reality, introduced even more possibility for human error. We have now duplicated our keys from the strings file and added them to the enum. We now have to update the keys on even more places, every time we update the keys in the strings file.
For this reason, I also wrote a script that handles the creation and updating of the enum class. Without a script, this system loses a lot of its value. I will go into more detail about the script in the follow-up article.
If you want to use a similar system than the one i described, i would recommend you try using the R.swift library. R.swift supports all of the features we discussed, but you do lose the ability to choose the best way that works for your project.