March 15 2009
ibtool: Localization Made Easy
Supporting multiple languages in a Mac app, known as internationalization or localization, is very easy on Mac OS X. Before we dig into the exact process, I’ll briefly cover what makes up a Mac app. Although they appear to be atomic items in Finder, Mac applications are actually folders containing various resources. (Right-click on an application and select ‘Show Package Contents’ to explore one.) In the Contents/Resources folder, there are folders with .lproj extensions. For example, Calculator.app has English.lproj, French.lproj, zh_CN.lproj, and fifteen others. Each of these .lproj folders contains UI files (NIBs, compiled down from XIBs) and strings (InfoPlist.strings and Localizable.strings that NSLocalizedString leverages) translated into that specific language. By simply changing the language in System Preferences->International, I can switch an app from English to French. Very neat.
When I first write a Mac application, the Resources folder only has an English.lproj folder in it. And the only reason it does is Xcode 3.1.2 defaults to localizing MainMenu.xib and InfoPlist.strings. When I create a new XIB, it’s not localized; I can localize it by right-clicking on it, selecting ‘Get Info’, and clicking ‘Make File Localizable’.
And I can click ‘Add Localization’ to add a French localization for that XIB. (Interface Builder suggests ‘French’, ‘German’, etc, but Apple recommends using the two-letter ISO 639-1 spec.)
It duplicates the XIB into French.lproj, creating the folder if it doesn’t exist. This process sounds convenient, and it’s absolutely necessary as a way to store localization knowledge in the project file. But it’s only meant as a first step. If I have ten languages, I don’t want to maintain ten copies of every XIB; I want to maintain one copy of every XIB and ten files with localized strings. Luckily, Apple provides this slick workflow through the command-line program ibtool.
Go to Terminal, find or create a MainMenu.xib, and type
ibtool will look through MainMenu.xib for every user-visible string and insert that string with an associated ObjectID into MainMenu.strings, essentially a dictionary of strings keyed by ObjectID. Even better, the tool sorts them by ObjectID, so versioned .strings files are nicely diff’able. I can easily see what strings are added, removed, or just changed. Let me repeat this because it’s so incredibly handy: .strings files diff well! Of course, these .strings files are unicode, so they are not grep’able. Below is an example of the output for a string:
That’s the obvious part. It seems easy to extract strings with IDs from a XIB. I can send these .strings files to a localizer for translation. Here’s the awesome part: The same tool lets me create a new XIB with the original XIB and a modified .strings file. Go through the MainMenu.strings file we just generated, and change all the strings to something different. Go back to Terminal, create French.lproj if it doesn’t exist, and type
A new XIB is created in French.lproj with same UI elements as English.lproj but with the new strings. I can use these same commands in scripts to automate extracting the .strings files and re-generating the localized XIBs. Below are my two Ruby scripts for these tasks, both MIT license. Let me know if you have any issues with them.
generate_xib_strings.rb
localize_xibs.rb
An interesting datapoint is SproutCore, the web app technology MobileMe is built on, also uses .lproj folders for localization. Isolating UI and localization in this way is a fantastic design pattern, so I’m happy to see it extended to the web world. Frankly, I have no idea how other websites and web apps handle localization. Perhaps they have a similar method.
UPDATE: I just discovered (from a great iPhone SDK article) an excellent companion tool to ibtool: genstrings. As ibtool works with XIBs, genstrings works with .m files. The command line tool allows me to extract all the strings declared in NSLocalizedString into Localizable.strings, with their comments. Here is an example output from genstrings -o English.lproj/ Classes/*.m: