Chapter 25. Internationalizing an Application

For more than a century people have been uttering the platitude that the world is getting smaller all the time. That's nonsense: it's getting bigger. Although most computer users are still able to work with English-only applications, even speakers of really obscure languages, like Limbu, own computers and would like some applications in their own language.

An open-source effort like KDE offers more-or-less complete translations of the entire desktop, including all applications in dozens of languages. And, for a consideration, you can get a version of Windows in your own language, too, even if that language is Basque.

Of course, there are other aspects to the internationalization of an application, like date and number formats, currency, keyboard, preferred dialog layout and so on. Some of these aspects are handled by Qt - like reversing the dialog layout if the current script is right-to-left. Others, like the date and number formats are handled by Python's locale module - which is alas severely underdocumented.

Translating texts on screen can be handled either by PyQt - using the QTranslator class, or by Python itself - using the gettext module. PyQt's QTranslator is far more convenient in use, but gettext is based on the wide-spread GNU gettext library, which is also used by KDE for its translations.

Translating screen texts

The first task is to surround all translatable text with the method self.tr() - every QObject - derived class has that method. You don't have to do that manually with designs you have generated with the Designer module or Qt Designer. However, for Kalam, it's a fair bit of work - I'll only show a fragment here:

# fragment from kalamapp.py
 ...
    def initActions(self):
        self.actions = {}
        self.actions["fileNew"] = \
              QAction(self.tr("New"),
                      QIconSet(QPixmap(filenew)),
                      self.tr("&New"),
                      QAccel.stringToKey(self.tr("CTRL+N",
                                                 "File|New"))
                      self)
        self.connect(self.actions["fileNew"],
                     SIGNAL("activated()"),
                     self.slotFileNew)


        self.actions["fileOpen"] = \
              QAction(self.tr("Open"),
                      QIconSet(QPixmap(fileopen)),
                      self.tr("&Open"),
                      QAccel.stringToKey(self.tr("CTRL+O",
                                                 "File|Open")),
                      self)
        self.connect(self.actions["fileOpen"],
                     SIGNAL("activated()"),
                     self.slotFileOpen)
 ...
    

You must not only mark all text that will appear on screen, but also all accelerator keys, otherwise translators won't be able to translate them. The extra argument to tr() gives the translator some extra context.

The tr() serves two purposes: at first, it used as a recognition point for a small utility that extracts the strings to create message catalogs - files full of translatable text that you can send your Limbu speaking friends to translate for you.

Secondly, when you run the application, tr() looks in a message database to find the right string. This is a very fast operation, so you don't have to worry about performance loss.

After you've marked all translatable strings, you can use a utility to generate translatable message files. Qt's utility—either lupdate or findtr—can only work with strings marked with tr(), and only with double-quoted strings.

There is a significant, though quite esoteric, difference between the way Qt2 and Qt3 handle the tr(). This means that when you use a version of PyQt designed to work with Qt 2, the tr() doesn't work out of the box. You need to add a tr() to all your classes that calls qApp.translate(). This is what is done in the current Kalam code, because I wrote and developed the book using PyQt 2.5.

Another important difference: in Qt 3, you can also use trUtf8(), if the source text is in the utf-8 encoding. That means that if your translators produce utf-8 encoded files, instead of plain two-byte Unicode text, you should use this function, instead of tr(). With PyQt 3 for Qt 3, trUtf8*() will be used automatically by pyuic.

You can also tell pyuic to use another function instead of tr() - for instance, the Python pygettext.py default _(). If you do that, with the command:

pyuic -tr _ frmsettings.ui 
      

there will be one important difference: by default, the translation function tr() has class-local scope, i.e. it is prefixed with self. But a custom translation function has global scope - exactly what you need for the Python implementation of gettext.

So, you can either do:

boud@calcifer:~/doc/pyqt/ch19/kalam > pygettext.py --keyword=tr kalamapp.py
    

Which creates a file called messages.pot, or:

boud@calcifer:~/doc/pyqt/ch19/kalam > findtr kalamapp.py  
    

The resulting files are almost identical - except for the minor detail of order. You should make a copy of these files for every language you need a translation for, and send them to your translators. They can use any editor, or a specialised application like KBabel to translate the text, and send it back in the form of a translated .pot file.

KBabel

The result can be compiled to .mo files using the msgfmt.py utility which should hide somewhere in you Python installation.

Finally, you can use these message catalog by loading it and installing a global function _(). (That should have been the function you used to mark your strings):

import gettext
gettext.install('kalam')
    

Or for message catalogs in the Unicode encoding:

import gettext
gettext.install('kalam', '/usr/share/locale', unicode=1)
    

Here, the path should point to a locale directory where all message files can be found.

If you are working with Qt 3.0, you can also use a new tool: Qt Linguist. This extracts the messages to a special, xml-based, format, and you can create message catalogs with a nice GUI frontend.

To use Qt Linguist, you need to make a Qt project file containing the following text:

SOURCES = configtest.py \
dlgfindreplace.py \
dlgsettings.py \
docmanager.py \
docmanagertest.py \
edmund.py \
frmfindreplace.py \
frmsettings.py \
kalamapp.py \
kalamconfig.py \
kalamdoc.py \
kalamview.py \
macromanager.py \
macromanagertest.py \
main.py \
resources.py \
sitecustomize.py \
startup.py 

TRANSLATIONS = kalam_nl.ts

    

And run the following command:

boud@calcifer:~/doc/pyqt/ch19/kalam > lupdate kalam.pro
    

After spewing out a lot of warnings (this tool expects C++, not python) a file in xml format is created which you can edit with an editor or with Qt Linguist.

The Qt Linguist screen

If the translator is finished, he or she can choose "release" in the menubar and generate a .qm message catalog.

Using this catalog in your application is a simple matter of installing the appropriate translator:

Example 25-1. Installing the translator

#!/usr/bin/env python
"""
main.py - application starter

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
import sys, locale

from qt import *

from kalamapp import KalamApp
from kalamdoc import KalamDoc
from kalamview import KalamView
import kalamconfig

from resources import TRUE, FALSE

def main(args):
    app=QApplication(args)

    translator = QTranslator(app)
    translator.load("kalam_" + locale.getlocale()[0] + ".qm",
                    kalamconfig.get("libdir","."))
    app.installTranslator(translator)

    kalam = KalamApp()
    app.setMainWidget(kalam)
    kalam.show()
    if len(args) > 1:
        for arg in args[1:]:
            document=KalamDoc()
            document.open(arg)
            kalam.docManager.addDocument(document, KalamView)
    app.exec_loop()
    
if __name__=="__main__":
    main(sys.argv)
    

Two remarks: note how we use the locale module to determine the language of the user. This returns a tuple containing a language code and a character set that correspond the user locale, as set by the operating system: ['en_US', 'ISO8859-1']. If you always use the language code as the second part for your filename, then Qt will be able to determine which translation file to load.

Note also that the location of that message file is determined by a configuration option. Standard Unix .mo files tend to go into /usr/share/locale/, but there is no corresponding standard for Qt .qm messages, and you might as well put those in the application installation directory. Where that is, will be determined in the next chapter.