XRC and I18n

(originally written by Stefan Hudac, stefan@datax.biz )

There is not much written about wxPython, XRC and internationalization. At least I wasn't able to find it easily. That's why I want to share my recent knowledge on the topic. So lets take it from the ground up.

1. Build your XRC file

A comprehensive list of wx GUI designers can be found in an article UsingXmlResources.

I tried several GUI builders for wxPython. I am not very fond of XRCed. I liked wxGlade much more. However the editor of choice for me is DialogBlocks. Among other things DialogBlocks allows you to have several projects open at the same time so you can copy paste parts of GUI from one to another without any problem. You just need to search for the open multiple projects settings in configuration. So find out which which one better suites your taste.

As for any unexperienced GUI programmer (as I certainly am), it is a great thing to look up examples in any wx GUI designer. It is also valuable to look for other samples or real life projects. It saves a lot of time not to reinvent a wheel and learn from other peoples' experience.

{i} So now, that you have GUI designer of your liking and spent some time with it, create your XRC file with frames, dialogs, etc.

2. Create room for localization in your application

All the projects I looked at have a directory named locale in root directory of an application. I am not sure if it is a convention, but seems fairly logical so I am not going to think up anything else.

{i} Create locale directory in your application root directory. This directory will become home for the localized message files.

Now you are free to create any number of subdirectories for localized languages in locale directory. Each subdirectory should keep a name of the language according to a localization standard and a LC_MESSAGES folder inside it with the translation. For example: en, es, fr, de, pl, sk, ru, ... Once done you are ready for next step.

3. Create .POT file

After you've created your XRC file you need to extract translatable phrases out of it. As an XRC file is just a plain XML it is fairly easy to parse it and get what you want. We are looking for content of tags <title> and <label>. This needs to be put into file which is usually put into locale directory with an extension .pot (portable object template - i guess)

POT file has certain structure which needs to be adhered to.

msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: Fri Feb 18 16:51:55 2005\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: xrc_i18n.py\n"
msgid "Basic Info"
msgstr ""
msgid "Title:"
msgstr ""
msgid "Street:"
msgstr ""
msgid "City:"
msgstr ""
msgid "Postal Code:"
msgstr ""

{i} So, create the file ./locale/name_of_your_project.pot by extracting the content of tags <title> and <label> from your .xrc file.

I would lie to tell you right now that I know all the header meaning. I just used it as I saw it. I believe to examine it further later on.

4. Create PO file

(From here on, you can also use poEdit. --FilipeFunenga)

Now that we have POT file we need to create our translation.

{i} Copy ./locale/name_of_your_project.pot into the desired subdirectory and change its extension to .po: ./locale/en/name_of_your_project.po. Start translating.

It is pretty common for some operating system to save files for you in weird encodings. This may become a problem later when you find out that your application does not display messages properly on different platform/system. The right encoding for portability reasons is UTF-8. My personal preference for editor is Eclipse with PyDev module. It allows me to set the output encoding for a file on any platform.

5. Compile MO files

{i} Once done editing you need to compile po files you created into mo files which are used by wx or gettext.

The standard Python installation comes with a Tools directory where you can find a script called msgfmt.py:

$ curl -O http://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz
$ tar -xf Python-2.7.3.tgz
$ cd Python-2.7.3
$ cd Tools/i18n/

Now you just need to call the msgfmt.py and it will create the .mo file for you:

python msgfmt.py locale/fr/messages.po locale/fr/messages.mo

The created .mo file is ready to being used in your application.

Workflow and Improvements to this Approach

As anyone know software development is endless process. The moment you create your app you can be sure it is unavoidable that you will have to fix something later at any part of it. This means that you will have to, at some point, make changes to XRC file. This change has to propagate further into POT and PO files logically. Once the messages in those files are already translated it is a nightmare to keep track of what needs to be transfered from POT file into PO file or cleaned up manually.

Therefor I created a tool which makes those tasks easy to handle. The script does following things:

  1. Extracts all the messages from XRC file
  2. Backups POT file if it exists.
  3. Creates new POT file.
  4. Searches for all PO files in locale subtree.
  5. Backups PO files.
  6. Syncs each PO file with messages extracted from XRC. Translated messages which are part of updated XRC file messages are kept, while messages which are no longer used are excluded.
  7. Compiles MO file for each PO file.

The script needs to be placed together with msgfmt.py mentioned above as it uses msgfmt.py to compile MO files. Usage is following:

python xrc_i18n.py xrc_file_location locale_dir_location

Download the script here: xrc_i18n.py (this script depends on msgfmt --Filipe Funenga)

(I intend to continue improving this tool. I have already some ideas on enhancing it. Keep me posted with your ideas. I'll see what I can do)

With this tool I am able to do the changes to an app at any time not worrying about loosing its parts. Of course code repository as CVS or SVN comes in handy for these kinds of mistakes anyway.

Examples

For the following examples, the directory structure of the application looks like this:

MyApp.py
MyApp.xrc
locale/
locale/MyApp.pot
locale/en/
locale/en/LC_MESSAGES/MyApp.mo
locale/en/LC_MESSAGES/MyApp.po
locale/fr/
locale/fr/LC_MESSAGES/MyApp.mo
locale/fr/LC_MESSAGES/MyApp.po

Example 1: localization implementation

So the only thing left, is to implement the created localization in our code. A good place to do this is at the OnInit method of the wx.App object. (Caution! if i18n is done before the OnInit method is called, some kind of error happens On Windows. On linux is ok. --FilipeFunenga)

import gettext
import os
import wx


class MyApp(wx.App):
    def OnInit(self):
        # initialize i18n
        self.i18n = self.initI18n()

        # create a handle to the XML resource file
        self.xrcres = wx.xrc.EmptyXmlResource()
        self.xrcres.Load('MyApp.xrc')
        self.mainWindow = MainWindow(None)
        self.SetTopWindow(self.mainWindow)
        self.mainWindow.Show()
        return True

    def initI18n(self):
        lang = "fr"
        basedir = os.getcwd()
        langdir = os.path.join(basedir, "locale")

        # This allows the usage of gettext inside the application with the
        # function _("string to translate").
        transl = gettext.translation("MyApp", langdir, [lang])
        transl.install()

        # The following code will set the appropriate variables so that
        # the import of XRC code can be translated.
        wx.Locale.AddCatalogLookupPathPrefix(langdir)
        i18n = wx.Locale(wx.LANGUAGE_FRENCH)
        i18n.AddCatalog("MyApp")

        # Do not forget to keep this object "alive". Example: keep it as an
        # attribute of the wx.App.
        return i18n

Among other components (classes) of your application you can get access to self.xrcres by calling app = wx.GetApp() which will return reference to the application instance. And calling the xrcres further is a snap - app.xrcres - and you can draw your dialogs across the entire program of yours...

Example 2: Alternative approach using Python's global catalog

There is also a different way of localizing your XRC gui's by using the installed gettext function _() if it is already defined by your python scripts.

def txtLocalize( match_object ):
    return _(match_object.group(1))


class GuiXrcApp():

    def __init__(self, wx_app=None, wx_frame=None):
        # create an empty xrc resource
        self.xrc_ressource = xrc.EmptyXmlResource()

        # localize the gui to avoid using specific wx stuff, using the global python _

        # get the text from the file
        xrc_file = open('your_xrc_file.xrc')
        xrc_data = xrc_file.read()
        xrc_file.close()

        # decode it to unicode
        xrc_data = xrc_data.decode('your-decoder') # for example iso-8859-1

        # replace the gettext strings with the available translation
        xrc_data = re.sub('_\([\'"](.*?)[\'"]\)', txtLocalize, xrc_data)

        # encode to original encoding
        xrc_data = xrc_data.encode('your-encoder') # for example iso-8859-1
        self.xrc_ressource.LoadFromString(xrc_data)

See also

XRCAndI18N (last edited 2012-12-27 11:24:21 by bl12-209-241)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.