Localization Unit Test

It happens to the best: you add new features to an app localized in several languages. You are not the lazy type that names the NSLocalizedString key’s the same as the English-language strings. Instead you name the keys semantically, like DOWNLOAD_ALERT_MSG.

Then when it comes to shipping you send the client a note that there are some new strings and assume that he will be able to find out which are new and need to be translated. Which he would if he was using a tool like Linguan. Which he is not, because you might have forgotten to recommend it to him, or if he’s extremely unlucky then he’s a Windows user.

BSA Banner

Long story short, the app ships and then you are beginning to receive LOL-adorned tweets with screen shots attached that show the placeholder string.

Note: this example is totally constructed, it didn’t happen in real life, and certainly not to us. But maybe it happened to you, maybe in a milder form if you have less obvious placeholder keys. In that case you might have gotten reports from German users complaining that certain texts are stubbornly English.

Creating a Localization Completeness Unit Test

Since we never (again) want to come into such an awkward situation we decided to treat app builds which are missing localizations as defective. By our definition if an app is missing tokens it is just as bad as if it where crashing on launch.

This is exactly the kind of thing that prompts you to add a unit test for. The question that the unit test should be asking is: Is there a localization for every string macro? For every language?

In order to be able to ask and answer these questions we need a few ingredients. We need to be able to scan our source code for occurrences of NSLocalizedString and friends. (Or our own custom prefix if we have one) And we need to get the strings files loaded so that we compare these against the list of all tokens.

For this purpose I developed DTLocalizableStringScanner. This is basically my Open Source alternative to genstrings, several orders of magnitude faster and you can link it into your apps or unit tests as a static library. DTLocalizableStringScanner scans source code files for occurrences of the localization macros and aggregates the results into one or more string tables.

One recent addition to this component is a parser for strings files which we developed as the counterpart. By the way: DTLocalizableStringScanner is also part of the above mentioned Linguan app.

Setting Up

For my demonstration we create a simple iOS app. Add a Localizable.string files and set that to be localized in German and English. The demo project is available on our Examples repository.

How-To Reminder: Add new Resource, Localizable.strings. Click on “Localize…” button in inspector. Set the project localization languages in the project settings.

To have something to test against, I added these macros to ViewController.m:

NSLocalizedString(@"ALERT_VIEW_MSG", @"Message in an alert view");
NSLocalizedString(@"ALERT_VIEW_TITLE", @"Title in an alert view");

Let’s assume that the title is a part of a new feature and we only have the ALERT_VIEW_MSG already localized. So we add the key value pair into both Localizable.strings files.

This is the starting point for our experiment. The unit test should fail and show us that ALERT_VIEW_TITLE is missing a localization in both English and German.

The unit test itself is a new LocalizationUnitTest Mac target. I prefer Mac for unit tests that don’t have iOS-specific code because those don’t need to launch the iOS simulator.

Getting the Project Folder Path

Step one for our unit test is that we have to scan the current version of the source code for string macros. The tricky part here is that you need to find out the project root path. One way how to get that I have seen was to add a compiler option that converts the $PROJECT_DIR environment variable into a define which is then available at run time.

My own approach is slightly different. I use the __FILE__ predefined macro to get the source code path of the current file and from there I search for the Xcode project file. This still requires that you set the name of the project file but I don’t think that this changes that often.

At this point we get a list of all the .m files contained in our project. For the actual macro scanning functionality we now need DTLocalizableStringScanner. In my example I added it as git submodule and adjusted the header search path in the LocalizationUnitTest build settings accordingly.

I assume at this point that you know how to add a git submodule and link a target from it into your own project. If not, then please look at this example’s project.

Scanning the source code files for string macros is quite straightforward. For sake of simplicity I’m only working with the single default Localizable.strings table and I’m not using any custom parameters available on this component, like for setting a custom macro prefix.

Parse ALL THE STRINGS

We are going to specify the path to the German and English Localizable.strings files relative to the project folder. Then we are going to parse these files and check off the localization macros we collected earlier. If at the end of the string file parsing we have any macros left then we know that these are missing a translation.

The two actual unit test cases are testGermanStrings and testEnglishStrings. Both call the _testMissingLocalizationsForStringsFileAtPath: passing the path to their respective strings file.

Parsing of the strings file emits a delegate callback and each such found token we know as having a translation. In the end we have another level of filtering, in this example a token needs to contain the underscore character to be identified as a place holder. This still allows for lazy localizations where the English token key is the same as the English translation.

Et voila! Running the unit test gives us the result we are looking for:

Here we are basically done with the core functionality of our unit test. Since we are scanning the source code files whenever the unit test launches we don’t have to worry about adding some dependency.

Of course you may let your creativity run wild and implement things like automatic detection of which localizations are present. Let us hear your ideas and creative approaches in the comments.

Conclusion

Having the proposed Localization Completeness Unit Test run for every development build would probably be way too annoying. So you would probably have it only run before critical stages like a new release or ad hoc to create a list of items that your translators should look at.

One way or the other I think that it a worthy goal to always be 100% certain that you are not shipping incompletely localized apps.

My thanks go to my colleague Stefan Gugarel who implemented this very solution in one of our largest apps.