Internationalizing Cocoa applications
Much of Apple's market is non-English-speaking. Making your application available in multiple languages is not just community-minded, it makes good business sense. This article illustrates the basics of how to internationalize Cocoa applications, and techniques you can adopt in localizing strings, resources, and user interfaces. It first explains how to internationalize file resources, including nib files, and how to retrieve localized variants using NSBundle methods. It then discusses localizing strings, issues with localized file paths, and how the
nibtool
command-line utility can facilitate localization of the user interface. It does not include in-depth discussion of locales, text encodings, or data formatting. The reader is expected to have some familiarity with Cocoa development.
Internationalization is the process of making an application localizable. Within an application, a range of different resources may need to be localized, but perhaps broadly speaking we can divide them into two categories — words and phrases (strings), and everything else (such as images, sounds, and nib files).
To understand how the application retrieves localized words and phrases, consider how you might behave as a tourist in a foreign country. To make yourself understood, you might use a dictionary or phrase book to look up the translated version of what you want to say (unless you're English, of course, in which case you just speak louder and add "oh" to the end of every other word). You might also have specialized dictionaries for various contexts, such as when you're shopping, or when you're discussing local politics.
Cocoa works in a similar way. NSBundle provides access to "translated" (localized) resources through dictionary-like APIs. In some cases, however, there may be no need for you to write any special code at all, for example when localizing nib files.
You may already have noticed that:
Localized variants of application resources are contained in LocaleName.lproj folders in your project (and folders of the same name in the Resources folder of the built application). You can access them in Project Builder through the disclosure triangle next to the listing.
To localize a resource in a project, select it in the Groups & Files List, and bring up the Info panel ("Show Info" in the Project menu). On the right hand side of the panel, you will see a "Localization & Platforms" popup. Click the popup and select "Make Localized". This moves the resource into your development language lproj folder. Project Builder defines the application's development locale using the
CFBundleDevelopmentRegion
key in the application's Info.plist file.
You make localized variants with the same popup: with a localized resource selected, choose "Add Localized Variant...". This brings up a panel asking for the name of the new locale:
When you enter the name of a locale and click "OK", Project Builder will:
Of course, Project Builder doesn't actually do any translation — you have to update the contents yourself!
These steps apply for any file-based resource that needs to be localized, whether an image (e.g. TIFF, JPEG, ICNS), text (e.g. txt, RTF, HTML), or sound (e.g. AIFF, WAV, SND). There is no need to localize file names — indeed if you do, your application will not be able to find the file. You access the resource using its "native" name. Furthermore, there is no need to localize a resource if it is not localized — a company logo, for example. Non-localized files remain outside of the lproj folders.
Apple recommends that you use the ISO 639-1 two-letter codes for the .lproj name, rather than the English name of the language. ISO 639-1 is an international standard. Almost any language you are likely to localize into will have a two-letter code, including in particular any language that a user can pick from the International preferences pane. For example, for an Azerbaijani lproj you would use az.lproj, or for Peruvian you would use pe.lproj. The form "Azerbaijani.lproj" is treated as an equivalent of "az.lproj", but it is only supported for backwards compatibility. If the language you are interested in does not have an ISO 639-1 two-letter code, the recommendation is to use an ISO 639-2 three-letter code.
For some languages, there are regional variations (e.g. Standard vs. Brazillian Portuguese). Apple recommends that you use "pt.lproj" for non-region-specific Portuguese, and "pt_PT.lproj" and "pt_BR.lproj" for region-specific localizations. Whenever you include a region-specific locale, though, you should also include the non-region-specific locale (for example, if you include either "pt_PT.lproj" or "pt_BR.lproj", you should include "pt.lproj"). If a resource has a value common to both, for example "pt_PT.lproj" and "pt_BR.lproj", then you can put it in "pt.lproj", and both "pt_PT.lproj" and "pt_BR.lproj" users will pick it up from there. You can omit resources from the region-specific .lprojs, but not from the non-region-specific ones.
The final fallback is to the development localization, so it is important that you set the development locale, and that it includes a version of every resource the application uses.
If the language you are interested in does not have a two- or three-letter code, you can use any name you wish, provided that those using it agree on the name, so that the value as listed in the AppleLanguages preference matches the .lproj name. You can add a new locale to the AppleLanguages preference using the
defaults
command in Terminal:
% defaults read NSGlobalDomain AppleLanguages
("en_GB", en, fr, de)
% defaults write NSGlobalDomain AppleLanguages "(Ork, "en_GB", en, fr, de)"
% defaults read NSGlobalDomain AppleLanguages
(Ork, "en_GB", en, fr, de)
You may want to create different user interfaces (and hence nib files) for different languages to cope with issues to do with, for example, text layout — especially if you plan to support differently-aligned scripts. Note, however, that even different Roman languages can present problems. Consider the following example, that might be part of an application used to find timetable information for streetcars:
| English: |
|
| German: |
|
When designing your user interface, plan for different string lengths in different languages.
To localize a nib file, follow the steps as above to create different locale variants. You can then edit each localized variant independently. Note that if you create multiple variants of nib files, all of their connections must be kept synchronized. This can be an arduous process, but can be made easier using the
nibtool
utility, discussed later.
Accessing localized nib files programatically requires no more effort than accessing a non-localized nib file. NSBundle's
-loadNibNamed:owner:
method automatically retrieves the best representation of a nib for the user's current language preference settings. Similarly, when the application is launched, it automatically retrieves the best representation of
Main Menu.nib
.
Good developers give their applications a sanity check before releasing them. In addition, localizers may wish to be able to launch an application in two or more languages, to ensure that the localization looks right in the running app.
You can temporarily switch your language setting simply by selecting a different language in the International pane of System Preferences. It is tedious, however, to keep going back to System Preferences to reset your preferred language. Instead, you can provide a preferred language array from the command-line, e.g. for TextEdit:
% /Applications/TextEdit.app/Contents/MacOS/TextEdit -AppleLanguages "(French)"
Not all users want to use the command-line, so I have created a utility which allows you to launch an application in any language, without resorting to System Preferences or the command-line. It's document-based, so you can also save a localization document with notes. (The disk image also contains a sample project, and another application for manipulating strings files and nibs, of which more anon.) Thanks to Andrew Zamler-Carhart of KavaSoft for field trials and suggestions. Your feedback is also welcome.
During development, you can also supply a launch argument for your executable in Project Builder:
To access localized resources (other than strings) in code, you simply use NSBundle's
-pathForResource:ofType:
method. Just like
-loadNibNamed:owner:
, this automatically returns the path to the best representation of the resource, based on the available variants and the user's language preference list. For example, if the file hello.png were available in English, French and German, and the user's language preference were Spanish, German, English, the following code would return the path to the German version:
NSString *path = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"png"];
Because AppKit uses these APIs internally, any resources included in a nib file will be automatically retrieved in the "best locale" variant.
Access application resources through NSBundle's API. You will automatically retrieve the best available localized variant.
Sometimes you need to display strings that are not part of a nib file per se — in an alert panel, for example. Localized representations of strings are stored in a "strings" file — a dictionary containing keys and corresponding values. Strings files are text files, best saved using Unicode (UTF-16) encoding. The strings format is very simple. Files comprise lines of the form:
"a key" = "a value";
They may also contain C/Java-style comments.
That's it!
NSBundle provides the following method to retrieve a localized string:
-(NSString *)localizedStringForKey:(NSString *)key
value:(NSString *)value
table:(NSString *)tableName;
The arguments are as follows:
Localizable.strings
.
Note that, in contrast to
-loadNibNamed:owner:
, this is not a class method, so you have to send the message to an instance of NSBundle — typically the Main Bundle. To retrieve a localized welcome message from a file
greetings.strings
, therefore, you could use code similar to the following:
NSString *greeting = [[NSBundle mainBundle] localizedStringForKey:@"hello"
value:@"Hello"
table:@"greetings"];
NSBundle also, however, defines the following convenience macros:
#define NSLocalizedString(key, comment) \ [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil] #define NSLocalizedStringFromTable(key, tbl, comment) \ [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \ [bundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \ [bundle localizedStringForKey:(key) value:(val) table:(tbl)]
These macros offer two advantages:
comment
argument is used to provide contextual information, to assist the translator to determine the best translation.
genstrings
utility to automatically generate strings files.Rather than using the code in the previous example, therefore, it may be better to use:
NSString *greeting = NSLocalizedStringFromTable(@"hello",
@"greetings",
@"Greeting used for the MainMenu window");
Anywhere you use a string constant that is displayed to the user — such as messages in an alert panel — you should use
NSLocalizedString()
or one of its variants.
String variables that are used solely internally — notification names, for example — need not be internationalized.
The
genstrings
command (
/usr/bin/genstrings
) parses the files you pass as arguments, looking for occurrences of
NSLocalizedString()
and its variants (note: it does not search for the NSBundle methods), and creates .strings files as appropriate. Any comments in the macro are inserted in the strings file immediately before the corresponding key-value pair. If a source file,
Controller.m
, contains the following line of code:
NSString *greeting = NSLocalizedStringFromTable(@"hello",
@"greetings",
@"Greeting used for the MainMenu window");
then executing
% genstrings Controller.m
will create the following entry in the
greetings.strings
file:
/* Greeting used for the MainMenu window */ "hello" = "hello";
You can then supply suitable values as appropriate:
/* Greeting used for the MainMenu window */ "hello" = "Hello, valued customer";
There may be some merit in using as the key, the default string that will be used:
NSString *greeting = NSLocalizedStringFromTable(@"Hello, valued customer",
@"greetings",
@"Greeting used for the MainMenu window");
since
genstrings
will create a strings file with the same value as the key:
/* Greeting used for the MainMenu window */ "Hello, valued customer" = "Hello, valued customer";
Alternatively, if you use
NSLocalizedStringWithDefaultValue
NSString *greeting = NSLocalizedStringWithDefaultValue(@"greeting",
@"greetings",
[NSBundle mainBundle],
@"Hello, valued customer",
@"Greeting used for the MainMenu window");
genstrings
will create a strings file with the value you provide.
/* Greeting used for the MainMenu window */ "greeting" = "Hello, valued customer";
Localized strings can contain formatting information:
NSString *greeting =
[NSString stringWithFormat: NSLocalizedStringWithDefaultValue(@"fullname",
@"greetings",
[NSBundle mainBundle],
@"%@ %@",
@"first and last name"),
@"mmalc", @"Crawford"];
This example may seem odd, until you consider that some cultures have particular ways of presenting certain information. The French, for example, frequently present a person's name as "Lastname, Firstname". If a string contains multiple variable arguments, you can change the order of the arguments by using the "$" modifier plus the argument number. The French variant of greetings.strings might therefore contain the following:
/* first and last name */ "fullname" = "%2$@, %1$@";
You can also perform additional testing by using the user default NSShowNonLocalizedStrings. This sets
localizedStringForKey:value:table:
to log a message to the console when the method cannot find a localized string.
% /Applications/TextEdit.app/Contents/MacOS/TextEdit -NSShowNonLocalizedStrings YES
Apple are deprecating various NSString methods that take C string parameters because the use of any non-ASCII character in a C string puts you to some extent at the mercy of the compiler. They are not, however, deprecating the Objective-C @"" construct for creating string constants. The @"" construct, though, suffers from the same encodings problem as the C string methods. As a result, you must be careful not to place high-bit characters — like em-dashes — in the string, as they will be displayed incorrectly in Japan and elsewhere.
Em-dashes should be used as a separator in window title bars between the main window title and a subtitle, if any. If you put the em-dash in an @"" construct, it is displayed in Japan as "Ñ". One solution is to use
[NSString stringWithUTF8String:"xxx"]
instead of @"". This requires that you also change the encoding of the code file to UTF-8 in Project Builder so that the "xxx" part is encoded correctly. You can also insert an em-dash in a string like this:
// Assume title and subTitle are NSStrings containing the title and subtitle
NSString *windowTitle =
[NSString stringWithFormat:@"%@ %C %@", title, 0x2014, subTitle];
Note that %C uses an upper-case "C". 0x2014 is the Unicode number for an em-dash,.
When manipulating the file system, it is sometimes necessary to use Carbon functions because Cocoa's NSWorkspace class does not provide all the features you need. For example, the array of dictionaries returned by
-launchedApplications
does not include faceless background applications or the Dock. As a result, you sometimes have to work with Carbon functions that use C strings, such as
FSRefMakePath
. Given an
FSRef
(a data type which identifies a directory or file),
FSRefMakePath
returns the corresponding path as a C string.
You should not use NSString's
+stringWithCString:
or similar methods to convert this C string into an NSString. Although this works if your application is run with a language setting that uses MacRoman encoding for file system strings, it will break if run, for example, on a Japanese system. Even NSString's
+stringWithUTF8String:
method has the potential, theoretically, to cause problems.
To be localization-friendly, you should use NSFileManager's
stringWithFileSystemRepresentation:length:
method, as in the following example:
- (NSArray *)launchedProcessPaths {
NSMutableArray *array = [[[NSMutableArray alloc] init] autorelease];
ProcessSerialNumber psn = {0, kNoProcess};
FSRef ref;
UInt8 cPath[PATH_MAX]; // cPath will be a UTF-8 encoded C string
OSErr err;
do {
err = GetNextProcess(&psn);
if (err == noErr) {
GetProcessBundleLocation(&psn, &ref);
FSRefMakePath(&ref, cPath, PATH_MAX);
[array addObject:[[NSFileManager defaultManager]
stringWithFileSystemRepresentation:cPath length:strlen(cPath)]];
}
} while (err != procNotFound);
return [[array copy] autorelease];
}
The
-stringWithFileSystemRepresentation:length:
method is in the NSFileManager class instead of the NSString class because NSFileManager is the final authority on file system string encodings.
In Mac OS X 10.2 and above, NSFileManager also provides the method
-displayNameAtPath:
which returns a localized representation of the path passed as the argument. For example, with the language preference set to French, the following code
[[NSFileManager defaultManager] displayNameAtPath:@"/Applications/Utilities/"]);
returns
Utilitaires
. If you use localized path names, take care to use them only for display, and not to pass them as arguments to any path manipulation functions.
It can be tedious to manually translate nib files; it can be even more arduous to propagate changes to the user interface and inter-object connections across all nib file variants. Fortunately the
nibtool
command-line utility makes these processes much easier. In addition to a range of other functions, nibtool includes a number of features to support localization. The simplest two arguments to the command (related to localization) are:
-L, --localizable-strings
You can use this to create a strings file which can be given to a translator to edit as text. One significant benefit is that the translator will not have to learn how to use Interface Builder. Once the translations have been supplied, you can generate a translated nib file by combining the original with the translation dictionary, again with
nibtool
, using the
-d
option:
-d, --dictionary strings
You can use:
% nibtool -L English.lproj/MainMenu.nib > French.lproj/MainMenu.strings
to create a dictionary containing localizable strings from the original nib file. You can translate the values in the dictionary (strings file), and use
% nibtool -d French.lproj/MainMenu.strings English.lproj/MainMenu.nib > French.lproj/MainMenu.nib
to substitute the translations into the original nib file and produce a version localized in a different language.
Since not everybody wants to use the command-line, I have created an application which provides a GUI for this operation, available here. It allows you to open a nib file, and view the strings it contains in a table, and edit the translated values. You can also "import" other strings files, for example to add common translations. The translated nib file can then be saved.
It may be useful to create a library of common translations (such as for items in the File and Edit menus) which can then be used to provide a first-pass translation of many application interfaces (although also consider Apple's AppleGlot tool, of which more later). Furthermore, once you have created a strings file for a given nib file, if the layout of the interface is changed, the previous translation can be re-applied (see also other nibtool configuration options).
As illustrated earlier, different languages may need different interfaces. If you need to create different layouts for different languages, however, keeping them in sync can be problematic. To make this easier, you can use
nibtool
with options
-p
and
-I
; these update a nib file using layout and translation information from a previous nib file. From the
nibtool
man page:
-p, --previous nibfile
-I, --incremental nibfile
-p
option, localized attributes for a given element in nibfile are only applied if that element's attribute was not changed between oSource.nib and the new source nib file. If no previous nibfile is specified, all localization changes in nibfile are applied to the new nibfile.An example application is provided here; the starting point is an application which displays text and an image in a single language. In the finished version, the text, image and nib file are all localized.
If your application grows beyond a few nib files, and you want to support more than a handful of languages, you should consider using Apple's AppleGlot tool. From Apple's description: "AppleGlot is a tool for extracting the text strings out of an application so that you can translate them, and then reinsert the strings into the application you are localizing. [In particular,] it supports incremental text changes, so you do not have to start over with each version or build of your application." You might also take a look at Florent Pillet's PowerGlot.
Internationalizing your application in Cocoa is fairly straightforward. The frameworks and tools provide support for including localized resources in a project, and APIs for easily retrieving the best representation for a user's language preferences. Following a few simple rules will make life easy for translators as well. Developer tools from Apple and third parties are available to further streamline the localization process.