Adventures in Localizing a Cappuccino Application

One of my favorite beta testers for MemoryMiner 2.x is a Czech language speaker named Marek. Shortly after the release of 2.1—with its greatly improved web viewer—he whispered in my ear (loudly!) about the need to create a localized version of the web viewer. As he put it in an email:

“Just to explain: My MemoryMiner projects are focused on archival photographs and looking at a lot of mainly older people who can not even English …” [sic]

Prior to the Cappuccino version of the web viewer, he customized the template by modifying an HTML file. The index.html file in the Cappuccino version is merely a shell: all the magic is performed in Obj-J, and a good chunk of the user interface is archived away in .cib files. In other words, there was no chance for him to do the work himself.

Last fall, while working on the MemoryMiner Web Application, I noticed that in all the code samples I’d seen, people were using hard-coded strings, which is a huge no-no. At the time, I was too busy exploring to care too much. At some point, I innocently asked on the Cappuccino IRC channel to see if there was anything like NSLocalizedString() which is what one uses in Cocoa to work with localized strings in code.

Ask and you shall receive: I was pointed to this github gist created by Nicholas Small for CPLocalizedString():

http://gist.github.com/152167

I tried it out, and it works as advertised, with only one problem: it only supports one localization at a time. It also doesn’t address the need for localized .cib files.

Not wanting to leave poor Marek in the lurch for too much longer, this week I decided to revisit the issue. The first task was to come up with a way of supporting multiple localizations while providing a mechanism to determine the best match for a given user. Since web browsers can tell you which language they’re using, I used that as a starting point.

Here’s how I’m getting the browser’s language:

Using the language code (e.g “fr”), you can then enumerate a list of available localizations, each one identified by a key. To store the list of available localizations I added this key/value pair to the application’s Info.plist:

    <key>CPBundleLocalizedResourceKeys</key>
    <array>
        <string>en</string>
        <string>cs</string>
        <string>de</string>
    </array>

But, what if the user wants to use a language that’s different from the language they’ve set in their browser? This is particularly important since, for example, a French user might understand German better than English. To handle this case, I allow for a user language preference to be passed to the method that determines the locale to be used for the application:

Here’s the method I created:

For now, I’m getting the user’s preferred language from a URL argument, (e.g. “…&language=de”), but in the future it would be nice to create a graphical language picker and maybe store the preference in a cookie.

So, now that strings can be localized, here’s how you can (dare I say should) use them in code:

[sharingWindow setTitle:CPLocalizedString("sharingOverlayWindowTitle",
                                      "Share this MemoryMiner Story")];

If you read through the original gist, you’ll see that CPLocalizedString takes two arguments, the key, and the comment. On OS X, you can use a command line tool called genstrings which automatically generates strings files from your code. These files are given to a translator, a single entry for which looks like this:

/* Formatter for creating labels like "Item x of y" in Person/Photo Picker */
"itemCountFormat" = "Item %i of %i";

Again, noting the original gist, a final processing is used to convert the strings file into an XML property list. You could just as well hand the XML based property list to your translator (or better yet create an app for editing them!).

The end result? The images below show two different localizations, in German and Czech. You can click on either image to view the actual localized web viewer:

What about localized .cib files?

I’ve mentioned several times in previous posts that the Cappuccino framework is improving at very fast clip. Anyone who’s started their project anytime within the last year or so surely has a bunch of code for programmatically creating UI elements and placing them within views. The 280 Atlas application can radically reduce the amount of tedious coding to create your app’s UI since you work can work graphically with actual “freeze-dried” objects (e.g. windows, buttons, sliders, image views, etc.).

When localizing into certain languages (I’m looking at you German!) the length of a string translated from English can oftentimes be quite a bit longer. For this reason, the placement of text labels, or the size of your buttons may need to be adjusted. If you chose to create multiple cib files with different localizations, then it sure would be nice to have a standardized way to load them.

Here’s what I did:

* Create a simple convenience for getting the full path to a localized resource that would live in ~Resources/x.lproj where “x” is your language code such as “en”:

* Create a method for loading a localized cib file.

Here’s the code:

Now, in order to support localized .cib files, you simply create them in your language-specific *.lproj folders (e.g. ~Resources/fr.lproj/MainMenu.cib). Since the Atlas editor is pretty mature, I’m hard-pressed to return to creating my user interfaces in code, if I can at all avoid doing so.

A shortcoming of the localized .cib loading method at this point is that, unlike with Cocoa, it doesn’t gracefully recover if a localized .cib file is not found (i.e. it doesn’t load the default language .cib file). This could be done, but it’s a bit trickier. On the desktop, it’s fast, easy and cheap to determine if a resource is available on your local hard drive. To determine if a resource on the web is available is a bit more involved, but certainly doable in a future refinement.

Parenthetically, the topic of how best to localize .xib files in Cocoa applications has been much discussed. One very interesting approach is to programmatically localize at run-time, a practice used by the wildly popular Delicious Library software. The technique is discussed at length in this post by Wil Shipley:

http://wilshipley.com/blog/2009/10/pimp-my-code-part-17-lost-in.html

I’d love to see a similar approach in Cappuccino. In many cases, it’s possible to configure your user interface to handle even the longest strings used for labels, etc. In some cases, though, it can look rather awkward. As a developer who cares about having localized applications, and knowing full well what a lot of work it is to produce them, I’m happy to have as many options as possible to make the job as painless possible. Most importantly, I’d like to see proper official” localization be baked right into Atlas and the Cappuccino source code.

I’m sure this will happen, but in the meantime, I’m happy to offer a complete gist with all the code and updated ReadMe, with the hope that it will be useful to others. Feedback and improvements are greatly appreciated:

http://gist.github.com/466301

8 Responses to “Adventures in Localizing a Cappuccino Application”

  1. Marek Says:

    Excellent work! Thank You John!!!!!!!!!!!!!!!!!!

  2. Lokalisierung von Cappuccino Apps « Cappuccino Tutorials & News Says:

    [...] Cappuccino App in eine andere Sprache übersetzten wollt, dann ist dieser Artikel Pflichtlektüre. http://www.memoryminer.com/blog/?p=134 Kategorien [...]

  3. amorican Says:

    Thanks a lot for sharing on your blog! I’m writing a Cappuccino application that must be both in English and French, this is exactly what I needed!

  4. Johannes Fahrenkrug Says:

    Hi John,

    great work! Thank you for sharing this and I apologize the lengthiness of the German language ;-)

    - Johannes

  5. Calvin Says:

    That is some stylish code!

  6. ChrisCruft Says:

    Accept-Language (the HTTP header that is behind your browserLanguage method) is already designed to support multiple languages in preference order specifically for the case you identify: finding the best match between the user’s preference and the site’s capabilities.

    You might still need some overrides, but perhaps the browserLanguage method should return an array in cases where the HTTP Accept-Language header looks like this:

    en-us,en;q=0.8,fr;q=0.7,fr-fr;q=0.5,fr-be;q=0.3,fr-ca;q=0.2

    That’s mine, as built by Firefox.

  7. MemoryMiner - Digital Storytelling Software - Blog » Blog Archive » Academic & Institutional Uses for MemoryMiner Says:

    [...] Blog « Adventures in Localizing a
    Cappuccino Application [...]

  8. Community Highlights: Adventures in localizing a Cappuccino Application » Cappuccino project Says:

    [...] post is John Fox, creator of MemoryMiner, a digital storytelling application for Mac OS X. John writes about the steps he took to localize his Cappuccino application, a web viewer for [...]

Leave a Reply