Default value on NSLocalizedString

Alberto de Pablo
Turo Engineering
Published in
7 min readMar 30, 2018

It’s in our vision: wherever you are we want you to be able to rent the perfect vehicle from a trusted Turo host. That means that we need to facilitate the use of Turo for everyone in the world. For us engineers, what we want is a solution that scales and that allows us to code fast without giving up a single point of quality. Today, I’m going to talk about something none of us knew at the time but we found to be very useful.

When the Turo app was started, localization was not our main focus. We did our best, but we definitely needed some refactoring when we launched in Canada and started supporting other locales. If you start looking at Apple documentation you will realize that there are a lot of things that do not work as you expect in other languages and regions. It is not just words and sentences, there are quite interesting challenges with plurals, numbers, percentages or currencies. There are also units of measure, date and time representation and some more interesting things like time quantities. Seems like a lot, right? Apple provides good tools for all of those, but let’s focus on them one at a time. Let’s start from the beginning and just take a look at string localization.

There is not only documentation from Apple on the matter, but there are also many good tutorials out there about localization and internationalization of iOS apps. My favorite is this one from objc.io, but a quick “NSLocalizedString” Google search will bring up others such as Mattt Thompson’s NSHipster entry around NSLocalizedString].

If you have read Apple’s documentation, as we normally do, or have completed any of the tutorials, when jumping into building you a new app you will most likely begin to do the following every time you add a new string:

// Swift
NSLocalizedString(“key”, comment: “comment”)

// Objective-c
NSLocalizedString(@”key”, @"comment")

And this will be right, most of the string localization that is needed can be achieved using this function, as long as the key and comment are used correctly.

Key

A common mistake driven by what is shown in Apple’s tutorials is to use the string that will be presented to the user as the key. So, for example, if you are developing a game in which you need to take care of a garden and there is a button to buy more water, you may just create a localized string like this:

// Swift
NSLocalizedString(“Water”, comment: nil)

The problem with this is that sentences, especially single words, may have different translations in different languages depending on the context. For instance, in this context, the Spanish version of your game will display a button with the text “Agua”. However, if within the same app you have an action button to water the plants, you may also want to display “Water” on that button, but the Spanish version of the game should display the word “Regar”. Keys must be unique, which means we will only have one translation per key. In this case, we would need to choose between “Agua” or “Regar”.

To avoid this kind of conflict, we should have some kind of structure for the keys. Something similar to this:

// Swift
NSLocalizedString(“menu.actions.plants.water”, comment: nil)

Comment

Since for a lot of Apple/tutorial examples the comment is nil, it is not unusual to find the comment as nil in several codebases. At Turo, since we were using unique keys from the beginning, we started using this field to show the real string that will be presented to the user. Something along these lines:

// Swift
NSLocalizedString(“menu.actions.plants.water”, comment: “Water”)

Having the string there was helpful in some cases. For example finding a class searching for the string is a little bit faster when you don’t know the class implementing a specific view.

This is not the right approach to follow. It is rather unlikely that the engineers in charge of building the app will also be the ones in charge of translating. Furthermore, the people who will be translating the app may not have a good idea of what your app or business is about. The comment should be used to provide additional context to the translator in order to facilitate their work removing all the ambiguity on the translation.

If the translator doesn’t have the context, they will need to ask, which will delay your translation process by a lot. Not only that, it may make it pretty impossible to track if there is a lot of ambiguity. Or even worse, the translator may assume they know the context, but make a mistake, and have a translation that doesn’t make sense at all in the context of the business, which will be very difficult for you to catch since you will not be developing in that language.

Since we did our refactor, we make sure we always have a useful comment parameter. You never know in what language the context will be completely necessary for the translator. So in our water example, the comment parameter would look more like this:

// Swift
NSLocalizedString(“menu.actions.plants.water”, comment: “Water (action), like in `Water the plants`”)

Localizable.strings

There is just one thing left to make sure we show the appropriate string to the user in any locale. That is, adding the string to show in association with the key. You do this by going to a different file (Localizable.strings), finding the key (or adding the key if it’s not there), and adding the associated base locale version. You can also use genstrings if you want to make sure all the keys are in the file.

Optimizations

As I mentioned before, this was the approach we had when we started coding Turo. However, the process described has a couple of issues we were not fully happy with:

- Error prone: it’s normally fine when you are just adding one string, but when you are working on a commit with several strings, it is not uncommon that you make a mistake by copy/pasting strings in the wrong keys, having duplicates, etc.
- UX problems: because you have to keep track of strings in different files it is relatively easy that you forgot to add a key to Localizable.strings. Or if you use genstrings, it’s easy to forget to add the base localization string for a key. This will mean there is no string for a key in your application, and if you don’t catch that in QA or automated tests, that will show up in the app, delivering a very poor experience.
- Time consuming: we want developers to be focused on how to improve our architecture and user experience, not in copy/pasting strings and keys from one place to another. Having an error-prone system forces engineers to be careful while doing this, making sure everything is right. Going back and forth between your class file and Localizable.strings file to make sure you are adding the right string to the right key shouldn’t be necessary.

I don’t believe these are big issues. I think they can be reduced with practice and time. However, when you have several developers focused on building a great product, you don’t want them to be distracted by a tedious copy/paste process that is subject to errors. That’s why after some investigation, we decided to go with NSLocalizedStringWithDefaultValue. It looks like this:

// Swift
NSLocalizedString(“menu.actions.plants.water”, tableName: nil, bundle: Bundle.main, value: “Water”, comment: “Water (action), like in `Water the plants`”)

//Objective-c
NSLocalizedStringWithDefaultValue(@”menu.actions.plants.water”, nil, [NSBundle mainBundle], @”Water”, @”Water (action), like in `Water the plants`”)

Why using a default value?

The default value of this function can be used as the translation for the base locale. In fact, there is no need to maintain a separate Localizable.strings file, as that file can be generated automatically. You can always generate that file using genstrings (the system will grab the default value as the string) but there is no need for it at all as it will always default to that value if there is no translation, which is by definition what the base locale is.

Using the default value option, we optimize the development process:

1. Error safe: as you are coding, you are adding the string to the line in your code, so you know it’s right. No more copy/paste.
2. Always a value: all the strings will have a base locale translation. Additionally, we also have an automatic job to ensure that all keys are translated in all locales before sending a new build to Apple.
3. Faster development: no more going back and forth between different files or making sure a string doesn’t correspond to a key. With this approach, we just focus on coding. When we get to the line of code we add the key and the string in the same line so there are no concerns of any type and we can focus on the important part of our job.

Are there any cons?

Of course! No solution is perfect. There are a couple of things to be careful of, but luckily, they are very easy to handle in an automatic way.

1. Since all your keys are in different files, you should make sure you don’t have the same key with different default values. At Turo, we have a Build Phase that runs a script which will show a warning if it finds any of these.
2. It shouldn’t happen, but value is technically an optional parameter so make sure it is not `nil` for any function in your code. Again, we have another Build Phase throwing warnings if it finds either a nil value, or a use of NSLocalizedString instead NSLocalizedStringWithDefaultValue.

Final thoughts

With this approach, you can just use the command line tools for Xcode to generate an Xliff file for the translators. The command will read through all the NSLocalizedStringWithDefaultValue functions and apply the default value to the Xliff as the base translation. It will also throw a warning if there is any duplicate key so you can use this to prevent that issue.

xcodebuild -exportLocalizations -localizationPath `localizationPath` -project `ProjectName.xcodeproj`

You can upload this file to your favorite translation platform, it should be supported. Once your Xliff file is translated, you can just import it like this:

xcodebuild -importLocalizations -localizationPath `localizationPath` -project `ProjectName.xcodeproj`

Xcode will update Localizable.strings files in the different locales contained on the Xliff but that is completely transparent for us. We really don’t need to worry about any Localizable.strings files at all.

We began with this process in 2015 and we haven’t found any major issues. Engineers at Turo are happy with the simplicity of it. I hope it works for you too if you want to try!

--

--