images images images

It’s been a while since I blogged about my latest “hobby” (oh, how my wife would cringe), writing Python-based applets for the Avant Window Navigator. I’ve been spending a lot of time working on my clock/calendar applet lately, but today I’m going to go back to my weather applet because it’s more interesting to write about.

A while ago, I decided it was time to add some alternate (that is, non-English) language support to the applet. The de-facto standard tool to accomplish this is the Python variant of GNU’s gettext. I found a couple of resources that helped guide me through this. The first was the wiki for the “one laptop per child” project, which does a lot of L10N in Python. The second was an excellent post in the “Learning Python” blog (the author doesn’t give his/her name in the Who am I section, otherwise I’d give props). And of course, there’s always the official Python library documentation, too.

The first step is to import the gettext libraries into your Python code, and do some setup work, thusly:

1
2
3
4
5
6
7
8
APP=">"awn-weather-applet"
DIR=os.path.dirname (__file__) + '/locale'
import locale
import gettext
locale.setlocale(locale.LC_ALL, '')
gettext.bindtextdomain(APP, DIR)
gettext.textdomain(APP)
_ = gettext.gettext

(Note: Made an edit to the above source code on Nov 16 2007 – evidently, the DIR parameter needs the full path to work properly)

Now, here’s what we’re actually doing. The call to bindtextdomain is binding the awn-weather-applet domain to the locale subdirectory. When the applet is deployed, it has a locale directory right underneath the main script, weather.py. The directory argument of bindtextdomain is relative, so we’re pointing gettext at that directory. We’re basically telling gettext to look in the locale subdirectory for files named language/LC_MESSAGES/awn-weather-applet.mo, where language is the two-letter language code defined by the environment (more on that later).

It should be noted that, customarily, locale files are stored in a default location which is a more global place in the filesystem, e.g. /usr/share/locale/language/LC_MESSAGES. If you don’t supply a directory to the bindtextdomain method call, gettext will use the default directory for the system. I opted not to use the default filesystem location because I wanted non-superusers to be able to easily use language files that I supply with the applet, using the standard AWN applet installation mechanism. Requiring the user to put .mo files in the /usr/share directory tree isn’t an option.

The call to textdomain sets the global domain to awn-weather-applet. Essentially we’re telling gettext that all future calls into gettext should use the awn-weather-applet domain as its source for translations.

Lastly, the line that reads _ = gettext.gettext() defines a convenient alias that we will use to identify strings in our code that need translation. If you’re familiar with i18n of C applications, this will look quite familiar; in the C implementation, an identically named macro is used for the same purpose.

The next step is to read through the code to identify the literal strings that will be translated. Generally, any UI element that a user will see is a candidate for translation. Things like log and console output are not as important. When we find a candidate string, we wrap it in _( ). For example, this:

1
self.dialog.set_title("Forecast")

becomes

1
self.dialog.set_title(_("Forecast"))

Because of the alias at the top of the source file, what we’re really doing is this:

1
self.dialog.set_title(gettext.gettext("Forecast"))

It’s a good thing we have the shorthand version!

As I alluded to earlier, gettext does its magic by reading files that ends in an .mo extension. At runtime, it reads the string that’s passed into it (in the above example, “Forecast”), then looks up that string in the proper .mo file to see if a translation is available. If it finds an entry, it returns the translated string, otherwise it just returns the string that was passed into it. So our next task is to create the .mo files.

Creating .mo files is a three step process. First, we need to run a tool that extracts all of the strings to be translated into a usable text file that a translator can edit. The tool to do this is called xgettext. The command line to run looks like this:

xgettext --language=Python --keyword=_ --output=awn-weather-applet.pot *.py</pre>

This command generates an output file named awn-weather-applet.pot which acts as a template for translators to do their translation. The file has a bunch of lines that look like this:

1
2
3
#: weather.py:127
msgid "Forecast"
msgstr ""

So, let’s say we want to make a Spanish translation of the weather applet. First, we’d make a copy of this file, and name it something sensible like awn-weather-applet.po (note, you can also use the msginit tool, which does a few other housekeeping things for you like fill in the e-mail address). We’d find all the lines that start with msgid, translate it, and put the result in the following line’s msgstr, like this:

1
2
3
#: weather.py:127
msgid "Forecast"
msgstr "PronĂ³stico"

Next, we need to “compile” the awn-weather-applet.po file into an awn-weather-applet.mo file, so that it’s usable by gettext at runtime. This is done using the msgfmt command, like this:

msgfmt awn-weather-applet.po -o awn-weather-applet.mo

That’s it! Now all you need to do is ensure that the awn-weather-applet.mo file ends up in the (base of weather applet)/locale/es/LC_MESSAGES directory, and Spanish translations work!