Pseudo-Localization for Cocoa Apps - codecentric AG Blog

:

Locali… what?

Simply speaking, localizing an application means translating all output it produces on the screen (and printouts etc.) to the language of the people using it. There is more to it, though, than a simple translation of messages. You should also take care of using correctly formatted distances, weights etc. and any other kind of numbers in general.
If you go a step further, you also make sure that e. g. plural forms are created with respect to the locale settings of your application’s users as well as using their calendar system — not everyone in the world uses the Gregorian calendar.

As this is an involved topic, there are quite a few pitfalls one should know about — including how to avoid them. This article tries to provide some background and possible remedies for iOS and OS X applications.

Cocoa & Cocoa Touch

Apple’s two (major) operating systems OS X and iOS both provide the Cocoa (Touch for iOS) application programming interfaces. They both come with a powerful set of methods to allow you to create applications that delight their users with correctly localized output.

The basic concept they use is to extract any messages that will be presented via the application’s user interface out of the source code or interface builder files (Storyboards and XIBs) into dedicated text files with the .strings extension.

At runtime, your code can then look up the concrete values based on the language the OS (or just the application) is configured to use. For development, there are also ways to control this independently. Read on for more details.

Adding localized files in Xcode

For each language you want to support there will be a separate copy of a .strings file. Those are plain (UTF-16) text files with key-value-mappings. The default name for Cocoa apps’ main localization table is simply Localizable.strings. If you do not do anything special, all your texts go into this file. You can also create more of them if you want to split up your texts in a more fine-grained manner. We will not use separate files in this blog post, but if you still want to know how to add and configure them, this is how:

In Xcode create a new file by hitting ⌘N and pick the Strings file entry from the Resources category:

XcodeNewStringsFile

It will appear in the project navigator on the left side. When you select it and reveal the inspector panel at the right side of the Xcode window, you need to make sure you make the new file localizable by clicking the button and then specifying which locales you wish to support:

XcodeLocalizeButton

A dialog will come up, asking you which language any content the file currently might have is written in:

XcodeDoYouWantToLocalizeThisFileDialog

Here you will get a choice of all the languages that you have configured for your project to support. By default this is just English. To add more, change your project settings by adding more localization languages in the Localizations section of the Info tab:

XcodeProjectSettingsLocalization

By selecting a language in the aforementioned dialog, Xcode will move the file to the appropriate folder for you. For each language you add for a file, a copy will be created inside a .lproj folder for that language. You can add support for more languages for any particular localized file by checking the mark in the file inspector on the right side of the Xcode window:

XcodeLocalizationLanguagePicker

Once you have done that, the supported languages are displayed in the project navigator, too:

XcodeStringsFileInProjectExplorer

.strings file syntax

The structure of .strings files is very simple. It looks like this

/* Optional comment for the following entry */
"key" = "value";
 
...

/* Optional comment for the following entry */ "key" = "value";...

The key is some string you refer to in your source code to tell the system which localized string you want to use. The keys are the same for all localized copies of a .strings file. The value on the other hand is the localized text for e. g. a button label, a table column header or an alert message.

Using localized strings in code

Looking up the localized version of say a warning dialog message is done by means of the NSLocalizedString set of macros. They are available on both iOS and OS X and come in a few variants.
The simplest form looks like this:

NSString* localizedMessage = NSLocalizedString(@"Do you really want to log out?",
                                               @"Alert text for confirmation before logout.");

NSString* localizedMessage = NSLocalizedString(@"Do you really want to log out?", @"Alert text for confirmation before logout.");

This macro will expand to the actual code that loads the correct Localizable.strings file for the current locale at runtime and that retrieves the entry for the key “Do you really want to log out?” from that file.
So after executing this line, the localizedMessage string variable will contain the English version of that message if your app runs with an English language setting, but will have something like “Möchten Sie sich wirklich abmelden?” if you prefer a German user interface.

You can use that string like any other string in your application now.

Creating .strings file stub entries automatically

In the snippet above you have seen the second string parameter with a short explanation of what the resolved string is going to be used for. The reason for including this – even though it does not show up anywhere in the finished application – is to give your translators some context on what exactly they are translating.

While developers are usually good at writing code, they might not be the ideal authors of all the user interface labels and texts. Instead, this might be the job of a different department or at least some colleague who is more proficient doing so. Moreover, for any kind of foreign language, you ideally have some native speaker that can do correct idiomatic translations so that your app nicely fits the rest of the system.

Often the context of a sentence or word can have dramatic impact on its translation into different languages. Just consider the word Windows: If it relates to a window being shown on the screen, it is fine to translate it to its German equivalent Fenster. However, when referring to the Microsoft Operating system of the same name, the word Windows would remain untranslated in most, if not all, non-English languages. Without providing some comment, a translator seeing just at the word itself would have no clue which case he was looking at. This has been the source of plenty of strange or outright wrong translations in a multitude of even high-profile applications.

However, translators will, of course, not walk through your source code and try to find all the instances where you called out to NSLocalizedString and check both the key and the corresponding comments. Fortunately, there are tools that can perform this task automatically and create the content of the .strings files automatically. While Apple provides the very basic — and not very pleasant to use — genstrings utility, I much prefer the (commercial) tool Linguan. It makes scanning your source files easy and it also supports translations with wizard like interface. Check out their homepage to see a video of how it is used.

The source code mentioned above will result in the appropriate number of .strings files for all your supported localizations. They will look like this:

...
/* Alert text for confirmation before logout. */
"Do you really want to log out?" = "Do you really want to log out?";
...

... /* Alert text for confirmation before logout. */ "Do you really want to log out?" = "Do you really want to log out?"; ...

As you can see, the comment is extracted from the source code and provided for your translators’ convenience. With the help of this little piece of text the software engineer provides, there is a much better chance of good translations, increasing the quality of the user experience.
This is why I usually am very pedantic about all my colleagues providing sensible comments. Whenever I find the default “No comment provided by engineer” placeholder I will reject that commit…

Missing translations

Because translating and development can progress at different paces, you cannot always prevent mismatches. A typical situation that arises is that developers add new features — and with that new localizable strings — but the files from the translator do not yet contain these new keys.

The default behavior of NSLocalizedString is to return the key in case no value can be looked up. This is good on the one hand, because at least for your base localization your users see a somewhat sensible text on the UI, but bad on the other hand, because for any other localization people will suddenly find a — for example — English message in an otherwise German speaking app.

Linguan

There are different ways of dealing with this. First of all, if you use Linguan, you can use its handy validation feature that will automatically point out missing translations. Before doing any release builds, you should just re-scan your code with it and see that it does not find any new NSLocalizedString invocations and also that for all configured languages all keys have appropriate values.

NSShowNonLocalizedStrings

A slightly less powerful — but free — way to find untranslated strings is to run your app with the -NSShowNonLocalizedStrings YES command line argument. You can set this up in Xcode’s scheme editor for your launch configuration:

XcodeSchemeEditorRunArgs

This will cause the resource loading code that hides behind the NSLocalizedString macros to turn any string it cannot find a localized variant for into ALL UPPERCASE, basically shouting at you to provide a translation.

The primary disadvantage of this is that you need to check each and every piece of your interface for these exclamations, including all kinds of error messages etc. Tedious, but not impossible.

Running apps with a different language

te

Generally iOS and OS X applications inherit the language and regional settings from the operation system. So if you have configured your iPhone or Mac to use English as your primary language, any application that gets run will also go for English output. For testing, however, this can prove to be rather annoying. Switching the language requires digging into the appropriate system preferences panel and waiting for the change to be applied. On OS X this generally requires logging out and back in again, while on iOS it will at least restart a good portion of the phone software.

But there is a way around that. By running any application with the AppleLanguages command line parameter, you can explicitly request it to be run in a different language from the system default. To run an application with German localization on an otherwise English system, add AppleLanguages (de) to your argument list, just as you did with NSShowNonLocalizedStrings.

For existing OS X apps you can use one of several tools that even allow pinning applications to a different language permanently. One example of such an app is the free App Language Chooser from the Mac App Store.

Pseudolocalization

Up to now we have only ever talked about resolving keys to the “correct” language specific strings, the term Pseudolocalization describes a testing approach which resolves the keys to some kind of special string which allows for better testing of your software.

Most often the term is used for the process of using the original language texts which are then modified e. g. by swapping out characters for ones with diacritic marks etc. Even the simple UPPERCASE transformation that can be achieved with NSShowNonLocalizedStrings described above already qualifies as pseudolocalization.

However by investing just a little bit of extra work, we can gain some nice additional features. But first of all, let’s see how we can influence the text resolving globally.

Replacing NSLocalizedString

While NSLocalizedString is the default way to localize application strings, you do not have to use it. As mentioned above, it is just a macro. In our apps, for example, we use a custom version called CDLocalizedString — which is implemented not as macros, but as regular C-functions. They are declared in a file called CDLocalizationFunctions.h with an accompanying implementation (.m) file.

NSString* CDLocalizedString(NSString* key, NSString* comment);
NSString* CDLocalizedStringFromTable(NSString* key, NSString* table, NSString* comment);

NSString* CDLocalizedString(NSString* key, NSString* comment); NSString* CDLocalizedStringFromTable(NSString* key, NSString* table, NSString* comment);

Both Linguan and genstrings can be told to use these instead of the default NSLocalizedString.

To make them available in all source files of a project, just include the .h file in the precompiled headers (.pch) file for your project or target.

While the primary reason we use these custom functions is to get more fine-grained control over the actual process of the message lookup (more on that in a later blog post), it conveniently also allows a different way of highlighting missing localizations.

Finding non-localizable strings

Just remember that in order for the NSShowNonLocalizedStrings argument to work, it modified the way the code behind NSLocalizedString works. What now if developers simply forgot to use NSLocalizedString in the first place? — Right: You would not notice at all while running the application. Creating the .strings files wiht either Linguan or genstrings would never pick up that alert dialog’s text if it was just a simple straight-forward string in your source code:

     [[UIAlertView alloc] initWithTitle:@"Logout"
                                message:@"Really?"
                               delegate:self
                      cancelButtonTitle:@"Cancel"
                      otherButtonTitles:@"Do it", nil];

[[UIAlertView alloc] initWithTitle:@"Logout" message:@"Really?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Do it", nil];

None of these strings would stand out during testing (apart from their particularly bad choice of words in this simple example). Only running with a different language settings would make this English message stick out like a sore thumb. According to Murphy’s law, of course, this would only be noticed until after the app had been released.

To prevent these things from happening, what we do instead is turn the situation around by wrapping correctly resolved strings in a pair of special characters before returning them:

#if PSEUDO_LOCALIZE
NSString* pseudoLocalize(NSString* str)
{
    return [NSString stringWithFormat:@"§§%@§§", str];
}
#endif
 
NSString* CDLocalizedStringFromTable(NSString* key, NSString* table, NSString* comment) {
{
    NSString* s = [[NSBundle mainBundle] localizedStringForKey:key value:nil table:table];
    #if PSEUDO_LOCALIZE
        s = pseudoLocalize(str);
    #endif
    return s;
}

#if PSEUDO_LOCALIZE NSString* pseudoLocalize(NSString* str) { return [NSString stringWithFormat:@"§§%@§§", str]; } #endifNSString* CDLocalizedStringFromTable(NSString* key, NSString* table, NSString* comment) { { NSString* s = [[NSBundle mainBundle] localizedStringForKey:key value:nil table:table]; #if PSEUDO_LOCALIZE s = pseudoLocalize(str); #endif return s; }

If the PSEUDO_LOCALIZE macro is defined (which it is for our debug and pre-release builds), a correctly localized string produced like this:

NSString* localizedMessage = CDLocalizedString(@"Do you really want to log out?",
                                               @"Alert text for confirmation before logout.")

NSString* localizedMessage = CDLocalizedString(@"Do you really want to log out?", @"Alert text for confirmation before logout.")

would actually be resolved to §§Do you really want to log out?§§

Any message in our user interface that does not have these paragraph symbols around it is immediately recognizable as a potentially missing use of CDLocalizedString.

Using technical keys in .strings files

I hold a rather strong belief that most kinds of lookup keys (be it database keys or those in .strings files) should be some kind of artificial value, and not something that is driven by functional requirements. Whenever I saw systems where usernames, email addresses, or any other kind of “meaningful” values were used as lookup keys, sooner or later they got into trouble when those values needed to be changed.

So instead of going the default way and using the base language’s message as a key for the localizable strings, we use artificial keys in our apps. For the example mentioned above our .strings files would not contain these entries

...
/* Alert title for confirmation before logout. */
"Confirm Logout" = "Confirm Logout";
 
 
/* Alert text for confirmation before logout. */
"Do you really want to log out?" = "Do you really want to log out?";
...

... /* Alert title for confirmation before logout. */ "Confirm Logout" = "Confirm Logout";/* Alert text for confirmation before logout. */ "Do you really want to log out?" = "Do you really want to log out?"; ...

but more likely these:

...
/* Alert title for confirmation before logout. */
"logout.alert.title" = "Confirm Logout";
 
 
/* Alert text for confirmation before logout. */
"logout.alert.message" = "Do you really want to log out?";
...

... /* Alert title for confirmation before logout. */ "logout.alert.title" = "Confirm Logout";/* Alert text for confirmation before logout. */ "logout.alert.message" = "Do you really want to log out?"; ...

This allows for easier grouping of messages in the resource files and also encourages each and every text to be reviewed by someone else than the engineer. Sticking to a well-defined pattern also makes finding existing messages easier. In some cases this structure might even help translators with one additional bit of context should the comment not be perfectly clear.

Recap

As you can see, there is quite a few things to consider when trying to get your applications user interface set up for more than one language.
We covered the basics of adding localizable .strings files to an Xcode project, configuring additional supported languages, the use of NSLocalizedString in code and the automatic scanning of your source to create the .strings content automatically.
Moreover we looked into some ways to debug localization issues with missing translations and/or keys and added a basic custom set of pseudolocalization functions.

What is missing is the use of separate localization tables, resolving from other bundles than the main bundle, using parameters, formatting data and more. At least a few of these topics will be part of a future post.

Further reading

There is much more to learn about localizations (and its counterpart internationalization). I can recommend these articles for starters: