Archive

Archive for December, 2009

Automatically localize your nibs when building

December 22nd, 2009 21 comments

This post applies to all Cocoa apps, whether on the desktop or on the iPhone.

When you want to localize your application, you can take several routes.

No nibs, only .strings files

The first one is to not use nibs (or xibs, in the new parlance). If you build everything programmatically, +alloc and -init-ing your controls and your views, you can store all the strings in .strings files. These files are simple key-value pairs of Unicode-encoded strings. Call the files “Localizable.strings”, drop them in the appropriate language folders (English.lproj, French.lproj, etc…).

	English.lproj/
		Localizable.strings
			[file contents]
			"Dawn" = "Dawn";
			"Sunrise" = "Sunrise";
			"Sunset" = "Sunset";
			"Dusk" = "Dusk";
	French.lproj/
		Localizable.strings
			[file contents]
			"Dawn" = "Aube";
			"Sunrise" = "Lever";
			"Sunset" = "Coucher";
			"Dusk" = "Crépuscule";

To use, simply call call:

	NSString *str = NSLocalizedString(@"Dawn", nil);

This technique is important even if you have nib files, because most of your strings are probably in .strings files already. Don’t use hard-coded strings for anything that is user-visible. Only use hard-coded keys.

Use nibs

This looks easy: duplicate your English nib, move it to the appropriate folder, open it in Interface Builder and translate all the strings directly in IB.

This is very bad. Don’t do it.

  • It’s unmaintainable: as soon as you change one thing in the original nib, you have to do it in one or more other nibs.
  • It’s error-prone: you can easily disconnect a binding without realizing it, and Undo support in IB is spotty at best (I don’t rely on it). By the time you realize your mistake, it may be too late and you have to revert everything.
  • It’s not versionable: although xib files are XML files, they are not meant to be user-editable. Think of them as binary files. You wouldn’t merge binary files now, would you?
  • It’s hard to do: there are many nooks and crannies where localizable strings are found in IB (tooltips?), you’re bound to forget some of them in some language.
  • It needs a developer: you would not hand off a nib file to a translator and expect him to know all of the above: there is some training involved. And even if you have the Fear of God when editing nib files (because you’ve been burnt before by a disconnected IBOutlet), chances are your translator has no such qualms.

Translators want strings files, not nib files

You could use Wil Shipley’s approach of translating everything at run-time. That approach has one great advantage: everything is driven by strings files, which only contain strings (duh!). Your translator(s) will do a great job with them, and you can easily diff the results in your own version control system when the translations come back.

There are, however, drawbacks to this approach. Since everything happens at run-time:

  • You have to ship and execute more code than necessary (this is a minor point, but still valid for the iPhone since it is very resource-limited).
  • You can only see the results by launching your app in the appropriate language, switching using the International Settings panel, which is tedious.

A compile-time approach

My approach expands on Brian Dunagan’s use of ibtools and genstrings.

Xcode has excellent build scriptability due to Run Script build phases. In this example, we will use a script to generate a localized French xib from an English xib, using a strings file as a template for translation.

First, create a French version of your xib file by adding a French localization to your English xib file.
Assuming it is called “MainWindow.xib”, select the xib file, choose File > Get Info and in the “General” pane click “Add Localization”. Type “French”.

This will create the “French.lproj” folder, which will contain a copy of the English MainWindow.xib.

Next, add a Run Script phase to your application target:

Run Script build phase in Xcode

Finally, enter this as your script:

# Extract English strings (use this to check if you added new strings to your nibs)
ibtool --generate-strings-file Resources/English.lproj/MainWindow.strings Resources/English.lproj/MainWindow.xib
# Generate French interface
ibtool --strings-file Resources/French.lproj/MainWindow.strings --write Resources/French.lproj/MainWindow.xib Resources/English.lproj/MainWindow.xib

Repeat each pair of lines for each xib you need to localize, and adjust the names accordingly.

The first command extracts all the localizable strings from your English xib, and stores them in English.lproj/MainWindow.strings. This is your reference file: add it to version control, but you do not need to add it to your application. You can add it to your project, but make sure it is not included in your app bundle (it is useless at runtime).

The second command takes a French-localized version of the same strings file (French.lproj/MainWindow.strings) and, using the English xib as a template, generates the French xib.

Wait a moment…

If you followed so far, build your app. The script should fail, because French.lproj/MainWindow.strings does not exist yet. Just make a copy of English.lproj/MainWindow.strings and put it in the French folder. Just like the English MainWindow.strings, you want to add this file to version control and your project, but not to your app’s resources.

If you build again, everything should go fine and your French MainWindow.xib should be created… in English.

Translation

Of course, you have to translate the French MainWindow.strings. Mine looks a bit like this:

/* Class = "IBUIBarButtonItem"; title = "Done"; ObjectID = "138"; */
"138.title" = "Terminé";
/* Class = "IBUIBarButtonItem"; title = "Today"; ObjectID = "140"; */
"140.title" = "Aujourd'hui";
/* etc... */

You can send this file to your translator, and instruct her to only translate the words in quotes on the right-side of the equal (“=”) sign. Don’t touch anything else.

But… the Fear of God?

Everything else in this file is necessary for ibtool to do its job properly, and should not be touched. There are two safeguards against you (or your translator) accidentally touching this information:

  1. You have the original English file. When the file comes back from your translator, you can diff it against the original (which is in English, remember?) and see that she has followed your instructions (or not). It should be pretty easy to spot the differences with FileMerge.
  2. Every build, the script re-creates the English MainWindow.strings file. Your version control system should flag any differences immediately. For instance, if you added a label, you would see it in your new English file and you could apply the exact same change to your French file, and make a note to send it for translation again.

I found that these two safeguards more than compensated for the fact that the generated strings file are really source code, since they contain essential information for ibtool to translate properly.

Summary

Since everything happens at compile-time, my solution has none of the drawbacks of Wil’s solution:

  • No extra code needed.
  • You can look at a generated xib file in Interface Builder and immediately see any layout issues you might have. In this case, change the English xib, rebuild and check again.

Remember, these files should be added to version control, but not to your app bundle:

  • English.lproj/MainWindow.strings
  • English.lproj/MainWindow.xib
  • French.lproj/MainWindow.strings

And this file should be added to your app bundle, but not to version control (since it is generated each time):

  • French.lproj/MainWindow.xib

Now, every time you build, you will generate your French interface and it will be added to your app. Simple and efficient.

Categories: Development, MacOSX Tags: