wxPython-Programme internationalisieren

Es ist gar nicht so schwierig, wie ich mir am Anfang gedacht hatte. Man muss die zu übersetzenden Strings im Programm in "_()" einschließen, wxPython mitteilen welche Übersetzung verwendet werden soll und man muss Übersetzungen in einen Ordner legen.

Das sind sie die wichtigsten Punkte. Ich werde in diesem Erfahrungsbericht auf diese Punkte eingehen und anhand eines Beispielprojektes zeigen wie ich meine ersten Erfahrungen damit gemacht habe.

Nicht übersetztes Beispielprogramm

Hier sieht man, wie bei mir so ein nicht übersetztes Beispielprogramm mit einem Menü und einem Button aussehen könnte.

not_translated_frame.gif
#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
"""
Nicht übersetzte Beispielanwendung

Requirements
    - Python: http://www.python.org/
    - wxPython: http://wxpython.org/
"""

import wx

wx.SetDefaultPyEncoding("iso-8859-15")


class MyFrame(wx.Frame):

    def __init__(
        self, parent = None, id = -1, title = "Not Translated Example",
        size = wx.Size(300, 200)
    ):
        """
        Initialisiert das Frame. Menü und Widgets erstellen
        """

        wx.Frame.__init__(self, parent, id, title, size = size)

        panel = wx.Panel(self)

        vbox_main = wx.BoxSizer(wx.VERTICAL)
        panel.SetSizer(vbox_main)

        # Menüleiste erstellen
        menubar = wx.MenuBar()

        # Menü: File
        mnu_file = wx.Menu()
        mnu_f_open = mnu_file.Append(wx.ID_OPEN, u"&Open...")
        self.Bind(wx.EVT_MENU, self.open_file, mnu_f_open)
        mnu_f_exit = mnu_file.Append(wx.ID_EXIT, u"E&xit")
        self.Bind(wx.EVT_MENU, self.close_frame, mnu_f_exit)
        menubar.Append(mnu_file, u"&File")

        # Menüleiste an Frame binden
        self.SetMenuBar(menubar)

        # Irgendein Button in die Mitte des Panels setzen
        btn = wx.Button(panel, -1, u"Click me...")
        vbox_main.Add((0, 0), 1)
        vbox_main.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10)
        vbox_main.Add((0, 0), 1)
        btn.Bind(wx.EVT_BUTTON, self.show_message)

        # Layout
        panel.Layout()


    def open_file(self, event = None):
        """
        Öffnet eine Datei
        """

        wx.MessageBox(u"Open a file...", u"Open")


    def close_frame(self, event = None):
        """
        Fenster schließen
        """

        self.Close()


    def show_message(self, event = None):
        """
        Zeigt irgendeine Meldung an
        """

        wx.MessageBox(u"This is a Message!", u"Message in a Box")


class MyApp(wx.PySimpleApp):

    def OnInit(self):
        """
        Anwendungslogik
        """

        # MyFrame anzeigen
        self.myframe = MyFrame()
        self.myframe.Center()
        self.myframe.Show()

        # MainLoop
        self.MainLoop()
        return True


def main():
    """Testing"""

    app = MyApp()


if __name__ == "__main__":
    main()

Wie man sieht, sind die Texte in Englisch gehalten, da mehr Übersetzer Englisch als Deutsch können. Es ist aber auch kein Problem, wenn man deutsche Texte hin schreibt. Die Anwendung kann dann aber nur mehr von Leuten übersetzt werden, die Deutsch beherrschen.

Vorbereitungen für die Übersetzung

Das Programm muss ein wenig angepasst werden, damit Python und wxPython wissen, welche Übersetzung verwendet werden soll und welche Texte übersetzt werden sollen.

Zuerst einmal ist es wichtig, dass man sich bewusst ist, in welchem Encoding das Python-Modul gespeichert wurde. Das muss Python durch ein "Encoding Cookie" mitgeteilt werden. Mehr dazu unter: http://www.python-forum.de/topic-5095.html

Ich arbeite im Moment unter Windows XP und habe mir angewöhnt, Python-Programme, die ich unter XP entwickle mit dem "Encoding Cookie" als "iso-8859-15" zu kennzeichnen. Wenn du unter Windows entwickelst und nicht weißt, welches Encoding du einsetzt, dann bist du mit der Kennzeichnung als "iso-8859-15" nicht schlecht beraten.

# -*- coding: iso-8859-15 -*-

Dann ist es auch noch wichtig, dass man wxPython mitteilt in welchem Encoding Strings an wxPython-Funktionen übergeben werden, wenn diese nicht als Unicode übergeben werden. Ich probiere so oft wie möglich an das "u" vor einem String zu denken, aber es kann ja passieren, dass ich es vergesse und in diesem Fall soll wxPython wissen, mit welchem Encoding es zu tun hat.

Das wird durch diese Anweisung erledigt:

wx.SetDefaultPyEncoding("iso-8859-15")

Texte markieren

Jetzt kommt der erste ungewöhnliche Schritt. Alle Strings, die übersetzt werden sollen, müssen mit "_()" gekennzeichnet werden. "_" ist ein Alias für eine Funktion. An diese Funktion wird der zu übersetzende Text als Parameter übergeben und sie liefert den übersetzten Text zurück. -- Falls der zu übersetztende Text in einer Übersetzungsdatei gefunden wurde.

Die Funktion wird so definiert:

_ = wx.GetTranslation

Das genügt. Später wird von einem Programm der gesamte Quelltext nach dieser Funktion durchsucht und daraus eine Datei erstellt, die von Übersetzern verwendet wird um die Anwendung zu übersetzen. Sollte das so nicht funktionieren, dann kann man den Unterstrich auf diese Art an die Funktion wx.GetTranslation binden:

import __builtin__
__builtin__.__dict__['_'] = wx.GetTranslation

Beispiele für solche Texte:

wx.MessageBox(_(u"Open a file"), _(u"File Open"))
message1 = _(u"Close the program.")
message2 = _(u"Your name is %(first_name)s %(last_name)s.") % {
    "first_name": "Gerold",
    "last_name": "Penz"
}

Wie man sieht ist es gar nicht so schwer die Texte zum Übersetzen zu markieren. Man sollte aber darauf achten, dass zu ersetzende Textstellen mit benannten Platzhaltern gekennzeichnet werden (z.B. %(first_name)s). Man will ja, dass der Übersetzer sich leicht tut und keine Fehler beim Übersetzen macht.

Dafür ist "Template" aus dem "string"-Modul auch hervorragend geeignet. Damit muss man sich keine Gedanken über %-Zeichen im Text machen.

from string import Template

message2 = Template(
    _(u"Your name is ${first_name} ${last_name}.")
).safe_substitute({"first_name": u"Gerold", "last_name": u"Penz"})

Pfad zu den Übersetzungen und Sprache festlegen

Wenn man alle Texte im Programm markiert hat, dann muss man wxPython noch mitteilen, wo die übersetzten Texte zu finden sind und welche Sprache verwendet werden soll.

Beim Erstellen der wx.Locale-Instanz kann man wxPython mitteilen, welche Sprache verwendet werden soll. Gibt man hier wx.LANGUAGE_DEFAULT an, dann wird die vom Betriebssystem eingestellte Sprache verwendet. Wenn dein Windows oder dein Linux auf die Sprache "Deutsch" eingestellt ist, dann werden die deutschen Übersetzungen verwendet. Ist deine Arbeitsoberfläche auf die Sprache "Finnisch" eingestellt, dann wird das Programm, vorausgesetzt es gibt eine finnische Übersetzung, mit finnischen Texten versehen. Gibt es keine Übersetzung für die eingestellte Sprache, dann werden die Texte nicht übersetzt.

self.locale = wx.Locale(wx.LANGUAGE_DEFAULT)

Man kann die Sprache aber auch manuell festlegen. Dafür sind die wx.LANGUAGE-Konstanten zuständig. (wx.LANGUAGE_ABKHAZIAN bis wx.LANGUAGE_ZULU)

Die Übersetzungen steckt man normalerweise in einen Ordner mit dem Namen "locale". Unterhalb dieses Ordners befinden sich dann je ein Ordner für die Sprachen und unterhalb dieser Sprachordner ("de", "en", "fr", ...) sollte sich der Ordner "LC_MESSAGES" befinden. In diesem Ordner sucht wxPython nach einer Datei mit einem speziellen, eindeutigen Namen für das Programm.

Diese Ordnerstruktur hat wxPython von "GNU GetText" übernommen. Dieses Programm gilt als der Standard für Übersetzungen von OpenSource-Programmen. Diese Struktur hat auch den Vorteil, dass Übersetzungen von vielen Programmen gemeinsam in einer einzigen Ordnerstruktur liegen können.

Wir müssen wxPython also den Pfad zum "locale"-Ordner und einen eindeutigen Namen für dieses Programm übergeben. Diesen Namen nennt man "Domain" oder unter wxPython "Catalog".

APPDIR = os.path.dirname(os.path.abspath(__file__))
LOCALEDIR = os.path.join(APPDIR, "locale")
LOCALEDOMAIN = "i18n_example"

self.locale.AddCatalogLookupPathPrefix(LOCALEDIR)
self.locale.AddCatalog(LOCALEDOMAIN)

Übersetztes Beispielprogramm

Hier sieht man das für die Übersetzung vorbereitete Programm. Es läuft auch ohne die Übersetzungen.

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
"""
Übersetzte Beispielanwendung

Requirements
    - Python: http://www.python.org/
    - wxPython: http://wxpython.org/
"""

import wx
import os

APPDIR = os.path.dirname(os.path.abspath(__file__))
LOCALEDIR = os.path.join(APPDIR, "locale")
LOCALEDOMAIN = "i18n_example"

wx.SetDefaultPyEncoding("iso-8859-15")
_ = wx.GetTranslation


class MyFrame(wx.Frame):

    def __init__(
        self, parent = None, id = -1, title = u"Not Translated Example",
        size = wx.Size(300, 200)
    ):
        """
        Initialisiert das Frame. Menü und Widgets erstellen
        """

        wx.Frame.__init__(self, parent, id, title, size = size)

        panel = wx.Panel(self)

        vbox_main = wx.BoxSizer(wx.VERTICAL)
        panel.SetSizer(vbox_main)

        # Menüleiste erstellen
        menubar = wx.MenuBar()

        # Menü: File
        mnu_file = wx.Menu()
        mnu_f_open = mnu_file.Append(wx.ID_OPEN, _(u"&Open..."))
        self.Bind(wx.EVT_MENU, self.open_file, mnu_f_open)
        mnu_f_exit = mnu_file.Append(wx.ID_EXIT, _(u"E&xit"))
        self.Bind(wx.EVT_MENU, self.close_frame, mnu_f_exit)
        menubar.Append(mnu_file, _(u"&File"))

        # Menüleiste an Frame binden
        self.SetMenuBar(menubar)

        # Irgendein Button in die Mitte des Panels setzen
        btn = wx.Button(panel, -1, _(u"Click me..."))
        vbox_main.Add((0, 0), 1)
        vbox_main.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10)
        vbox_main.Add((0, 0), 1)
        btn.Bind(wx.EVT_BUTTON, self.show_message)

        # Layout
        panel.Layout()


    def open_file(self, event = None):
        """
        Öffnet eine Datei
        """

        wx.MessageBox(_(u"Open a file..."), _(u"Open"))


    def close_frame(self, event = None):
        """
        Fenster schließen
        """

        self.Close()


    def show_message(self, event = None):
        """
        Zeigt irgendeine Meldung an
        """

        wx.MessageBox(_(u"This is a Message!"), _(u"Message in a Box"))


class MyApp(wx.PySimpleApp):

    def OnInit(self):
        """
        Anwendungslogik
        """

        # Locale
        self.locale = wx.Locale(wx.LANGUAGE_DEFAULT)
        self.locale.AddCatalogLookupPathPrefix(LOCALEDIR)
        self.locale.AddCatalog(LOCALEDOMAIN)

        # MyFrame anzeigen
        self.myframe = MyFrame(title = _(u"Not Translated Example"))
        self.myframe.Center()
        self.myframe.Show()

        # MainLoop
        self.MainLoop()
        return True


def main():
    """Testing"""

    app = MyApp()


if __name__ == "__main__":
    main()

Vorlage für die Übersetzer

Übersetzer sind keine Programmierer. Deshalb muss man ihnen auch etwas in die Hand geben was sie übersetzen können, ohne am Quelltext etwas ändern zu müssen.

Dafür erstellt man mit dem Program "pygettext.py" eine POT-Datei. Diese POT-Datei dient den Übersetzern als Vorlage. Daraus erstellen sie dann eine PO-Datei für die jeweilige Sprache. Dieses Programm ist unterhalb des Python-Ordners, im Ordner Tools/i18n zu finden.

Um dieses Programm von überall aus verwenden zu können, kann man den Ordner zum Systempfad hinzu fügen. (Unter Windows: Start --> Einstellungen --> Systemsteuerung --> System --> Erweitert --> Umgebungsvariablen) Das muss aber nicht sein, wenn man ständig den gesamten Pfad zum Programm angibt, oder wenn man so wie ich, mit einem kleinen Python-Programm arbeitet das die Übersetzungsvorlage erstellt.

Hier das kleine Programm, mit dem ich die Vorlage aktuell halte:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
"""
Erstellt die POT-Datei (Übersetzungsvorlage).

Requirements
    - Python: http://www.python.org/
"""

import os
import sys
import subprocess
from uebersetzt import APPDIR, LOCALEDIR, LOCALEDOMAIN


PYGETTEXT = os.path.join(sys.exec_prefix, "Tools/i18n/pygettext.py")
EXECUTABLE = sys.executable

# Argumente
args = [
    EXECUTABLE,
    PYGETTEXT,
    "--default-domain=%s" % LOCALEDOMAIN,
    "--add-location",
    "--output-dir=%s" % LOCALEDIR,
    "--output=%s.pot" % LOCALEDOMAIN,
    "--verbose",
    # Dateien
    os.path.join(APPDIR, "uebersetzt.py")
]

# Ausführen
subprocess.call(args = args, executable = EXECUTABLE, cwd = APPDIR)

print "Fertig"

Damit erstelle ich also die POT-Datei. Das ist praktisch, wenn man mehrere Dateien im Projekt zu übersetzen hat. Diese POT-Datei kann sich jetzt jeder Übersetzer kopieren und in seine Sprache übersetzen.

PoEdit

PoEdit ist ein Programm, mit dem man recht komfortabel eine PO-Datei aus einer POT-Datei erstellt. Es ist also das Programm für Übersetzer.

poedit-clearlooks.png

Übersetzen mit PoEdit

Neuen Katalog anlegen

Mit "Datei --> Neuer Katalog aus POT-Datei" erstellt man eine neue PO-Datei. PoEdit nennt PO-Dateien "Kataloge". Darauf hin erscheint das Fenster "Öffne Katalogvorlage". Hier muss man den Pfad zur vorher erstellten POT-Datei angeben. Diese sollte sich in unserem Beispiel unterhalb des "locale"-Ordners befinden.

Katalogoptionen - Projektinfo

Hier muss man allgemeine Informationen über das Übersetzungsprojekt angeben. Das Land sollte man nur angeben, wenn man eine länderspezifische Übersetzung erstellen möchte. Z.B. Deutsch/Schweiz oder Englisch/England.

Projektname und Version:
wxPython Localisation Example
Sprache:
German
Zeichensatz:
utf-8
Zeichensatz des Quellcode:
iso-8859-15

Pfade

Der Basispfad ist nicht der Pfad zum Programm, sondern zum Pfad, in dem alle oder mehrere Programme entwickelt werden. Ich entwickle alle meine Programme im Ordner P:\dev. Dieser Ordner gehört als Basispfad angegeben. Bitte fragt mich nicht warum. Ich weiß es nicht. Das Beispielprogramm liegt bei mir im Ordner P:\dev\wxpython_i18n. Dieser Ordner gehört als neuer Pfad in die Pfade-Liste.

In meinem Fall sieht das also so aus:

Basispfad:
C:\dev
Pfade:
P:\dev\wxpython_i18n

Schlüsselwörter

Hier müssen wir nichts eingeben.

Mit einem Klick auf die Schaltfläche "OK" wird das neue Projekt erstellt. Wenn die Frage nach dem Speicherort zur PO-Datei auftaucht, dann muss dieser unbedingt korrekt angegeben werden.

Die PO-Datei muss in den Ordner locale\de\LC_MESSAGES und muss in unserem Fall i18n_example.po heißen.

Der volle Pfad lautet also für dieses Beispiel: P:\dev\wxpython_i18n\locale\de\LC_MESSAGES\i18n_example.po

Übersetzen

Nach dem Erstellen der PO-Datei (des Kataloges), sollte PoEdit in etwa so aussehen:

poedit_vor_dem_uebersetzen.gif

Oben sieht man die zu übersetzenden Texte. In der Mitte links, sieht man den zu übersetzenden Text vollständig und links darunter kann man nun den deutschen Text eingeben.

Nach dem Übersetzen, sieht das Fenster in etwa so aus:

poedit_nach_dem_uebersetzen.gif

Speichert man nun die Übersetzungsdatei, dann wird automatisch auch eine MO-Datei erstellt. Das ist eine optimierte Binärdatei, die schneller gelesen werden kann als die PO-Datei. Diese MO-Datei wird von nun an von wxPython gelesen und verwendet.

Wenn man das Programm jetzt startet, dann sollte es so aussehen:

translated_frame.gif

Aktualisieren der Übersetzung

Natürlich ändert sich ab und an etwas am Programm. Das ist kein Problem. Man muss nur die POT-Datei mit unserem Skript aktualisieren und die PO-Datei in PoEdit öffnen. Über den Menüeintrag "Katalog --> Aus POT-Datei aktualisieren", lässt sich die Übersetzung auf den aktuellen Stand bringen. Beim Speichern wird automatisch auch die MO-Datei aktualisiert.

Happy translating! :-)