Well, it’s quite a lot since my first approach to Objective C, Cocoa and iPhone development and now I’m starting to move my first concrete steps into this beautiful world. Today I would like to face an important aspect of iPhone development (and application development generally), localization! Fortunately the excellent Cocoa framework and Objective C ecosystem make internalization process easy, fast and clean, thus localize an application into several languages can be accomplished with a minimum effort by developers.
Fundamentally there are three main steps to get a multi language app in action. The first is to create a special folder with the extension .lproj under application’s root for each language we want/can support. So, if we suppose to support english, italian and spanish languages, we will create those folders: en.lproj, it.lproj, es.lproj. “en”, “it” and “es” represent the ISO 639-1 language designation (the same used in domain suffixes), it’s also possible to adopt the ISO 639-2 convention and the folders would be renamed as “eng”, “ita” and “spa”, anyway first approach is the preferred and widely adopted. If you want to know all ISO 639-1 and ISO 639-2 designators, I suggest you this page: http://www.loc.gov/standards/iso639-2/php/code_list.php.
Once we create all the necessary .lproj folders (using Finder or Terminal) we can switch to our beloved Xcode and create all the necessary resources that will keep the different localized strings. We must now select the logical “Resources” folder and add a new file by choosing “String file” under Mac OS X “Resource“, then click next and naming this file “Localizable.strings“, finally save it under previous created folders (we must repeat this operation for each folder).
Those .strings file are just simple text file, where we can specify keys and values as the following example:
“hello” = “Hello”;
“hello” = “Ciao”;
“hello” = “Hola”;
The basic approach is to use the english word as the key and assign it different values based on the localization file, but we can be more cryptic and use our custom conventions, for example by assigning incremental keys like: “k1”, “k2”, “k3” and so on. As last step, we can automatically read the right localized string version at runtime (based on user’s language setting) by using macro (NSLocalizedString, NSLocalizedStringFromTable, NSLocalizedStringFromTableInBundle, NSLocalizedStringWithDefaultValue) or class methods available in Apple’s framework. The simplest way to do that is to use the function NSLocalizedString(), which accepts two arguments: an NSString representing the key we are looking for and a second optional argument which has the mere purpose of serving as note for developers by trying to describe string’s context (*), for this reason this argument will be “nil” the most of times:
// set myLabel's text using NSLocalizedString()
myLabel.text = NSLocalizedString(@"hello", nil);
Although this approach is very simple, it’s not recommendable in my opinion, because it doesn’t provide a way to handle missing keys and if a key can’t be found, it will print that key literally (and in case of a not human readable key like “k-15”, it would be very annoying for the user). Thus, a far better option is to use the NSLocalizedStringWithDefaultValue(), which accepts 5 arguments: an NSString for the key, an NSString for the table, an NSBundle reference, an NSString for a fallback string’s value, and an optional NSString for a comment (like NSLocalizedString()). The table argument refers to the name of .strings file (without extension), in fact we are not limited to “Localizable.strings” but we can create as many .strings files we need (“Menu.strings”, “InfoPanel.strings”…). “Localizable.strings” is only a conventional name used to specify a default file for localization that can be loaded without specify its name (NSLocalizedString() can read only that file). The bundle is a reference to the NSBundle where the .strings file is located (usually the main bundle).
// set myLabel's text using NSLocalizedStringWithDefaultValue()
myLabel.text = NSLocalizedStringWithDefaultValue(@"MyKey", @"MyStringFile", [NSBundle mainBundle], @"Missing key", nil);
It’s also possible to use bundle’s method localizedStringForKey:value:table, which accepts an NSString as key, a second NSString as fallback value, and a third NSString for the table (or nil if you are lazy and want to use “Localizable.strings” without specifying it) :
NSBundle *bundle = [NSBundle mainBundle];
myLabel.text = [bundle localizedStringForKey:@"MyKey" value:@"Missing key :(" table:nil];
An important point to understand is how localization process works, that is which steps are involved at runtime on the device. As far I know (by doing several experiment using iPhone simulator), the process is the following:
1. code is executed and a request for a localized string is found
2. the system try to find the .strings file of the current locale (according to user language settings). The NSString represenitng current locale can be found using:
[[NSLocale currentLocale] localeIdentifier];
3. If the file is found, it will search for the requested key and it will return it’s value if found or a nil otherwise. If .strings file can’t be found, the system will try to find others .strings file sequentially, using preferredLanguages‘s NSArray:
NSArray *preferredLanguages = [NSLocale preferredLanguages];
and it will stops as soon a file is found, by returning key’s value if found or nil otherwise.
So, considering the simple “hello” localization example into english, italian and spanish, our @”hello” key will be successfully translated if we use one of the three locales (en, it, sp) and it will be printed in english for extra locales (like german, french and so on).
Finally it’s also possible to generate strings file automatically from the source code by using Terminal’s command genstrings, see here for details: http://developer.apple.com/mac/library/documentation/Darwin/Reference/ManPages/man1/genstrings.1.html
* the comment parameter is also used to automatically generate a comment (/* comment */) in .strings files using genstrings