[A] Mehrsprachige Programme mit GNU/Gettext



  • 1 Einleitung

    1.1 Begriffsklärung

    Die Begriffe «Lokalisierung» und «Internationalisierung» werden oft
    für die unterschiedlichsten Dinge verwendet. In diesem Artikel steht
    Internationalisierung für aktivitäten an der Software, die zum
    Beispiel erlaubt, Texte durch übersetzte Versionen zu ersetzen,
    während Lokalisierung für den Vorgang des Übersetzens und des
    Ladens von Übersetzungen gebraucht wird.

    1.2 Umfang des Artikels

    Die Möglichkeiten beim erstellen von Lokalisierten Programmen sind
    vielseitig und oft sind spezielle Lösungen für einzelne Probleme
    gefragt. Dieser Artikel behandelt die Grundlagen mit dem GNU/Gettext
    System und bildet die Grundlage für Folgeartikel, die unter anderem
    Praxisbeispiele beleuchten.

    2 Warum lokalisieren

    Immer wieder ist zu lesen, dass Lokalisierung eigentlich überflüssig
    ist, da eh jeder Anwender seine Programme als erstes auf «Englisch»
    stellt, um von Tutorials und Hilfestellungen im Internet zu
    profitieren.

    Tatsächlich besteht die Zielgruppe von Übersetzten Anwendungen nicht
    unbedingt aus technikbegeisterten Powerusern, Gelegenheitsuser
    profitieren viel stärker aus der Übersetzung und eine Angepasste
    Version für die eigene Sprache und Kultur erhöht die Akzeptanz von
    Software.

    3 Ein einfaches Beispiel

    3.1 Internationalisierung

    Gehen wir einmal von dem Idealfall für die Übersetzung mit gettext
    aus. Sämmtliche Texte, die übersetzt werden wollen, liegen als Teil
    des Quellcodes vor, idealerweise gut Kommentiert. Einflüsse, wie zum
    Beispiel das Rechtssystem oder die Wirtschaftsordnung spielen für
    unsere Software keine Rolle und auch eine Währungsumrechnung ist nicht
    nötig.

    Folgender Code soll hier als Beispiel dienen, er wird im verlauf des
    Artikels erweitert werden:

    #include <iostream>
    
    int main()
    {
    	std::cout << "Hello, world\n";
    	std::cout.flush();
    }
    

    Jetzt möchten wir gerne den Text in möglichst viele Sprachen
    übersetzen können. Dazu binden wir die nötigen Module ein und
    markieren den Text als übersetzbar:

    #include <iostream>
    
    // gettext Header Datei
    #include <libintl.h>
    
    // std::locale header
    #include <clocale>
    
    // _() hat sich als markierung etabliert, gettext stellt diese Form
    // allerdings nicht per default zur Verfügung um den Namensraum nicht
    // unnötig zu füllen.
    #define _(string) gettext (string)
    
    int main()
    {
    	// Wir setzen alle locale Einstellungen auf den Wert "" dieser
    	// entspricht der im Betriebssystem eingestellten Standardsprache
    	std::setlocale(LC_ALL, "");
    
    	// Hier wird ein eindeutiger Name für unser Programm
    	// definiert. Dies ist besonders dann wichtig, wenn wir unser
    	// Programm im Linux Umfeld installieren wollen, wo alle
    	// Übersetungen in /usr/share/locale landen
    	textdomain("hello_world");
    
    	// Momentan soll gettext die übersetungen im Unterverzeichniss
    	// translations/ suchen
    	bindtextdomain("hello_world", "translations");
    
    	// Durch die _( ) haben wir den string als übersetzbar markiert.
    	std::cout << _("Hello, world\n");
    	std::cout.flush();
    }
    

    Dieser Code sollte sich genauso wie die einfache Version compilieren
    lassen und weiterhin den Text "Hello, world" ausgeben. Um den Text
    jetzt deutsch ausgeben zu lassen, müssen wir ersteinmal Hand anlegen.

    Der erste Schritt besteht daraus, die Texte, die später übersetzt
    werden sollen, aus dem Programmcode zu extrahieren. Dazu verwenden wir
    das xgettext Programm aus der getext Sammlung:

    xgettext -k_ --c++ -o translations/hello_world.pot main.C
    

    Die Option -k_ teilt gettext mit, dass unsere übersetzbaren Strings
    alle mit _() eingeschlossen sind, wer möchte kann dies mit allen
    anderen, erlaubten C++ Namen machen, verwendet man z.B. g("") so muss
    die Option -kg lauten. Möchte man mehrere verschiedene Zeichen für das
    Markieren von gettext Strings verwenden, darf man auch gerne mehrmals
    -k verwenden.

    --c++ teilt gettext die Verwendete Sprache mit, immerhin kann
    man gettext genauso mit Python oder LUA Code verwenden, der sich etwas
    anders verhält als C++ Code, translate/hello_world.pot ist die
    Schablone, nach der später übersetzt wird und main.C
    schließlich ist der Dateiname der zu durchsuchenden Datei.

    Daraus hat xgettext jetzt eine sogenannte Schablone erstellt,
    die als Grundlage für die Übersetzungen dient und in etwa so aussehen
    sollte:

    # SOME DESCRIPTIVE TITLE.
    # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
    #
    #, fuzzy
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: 2009-07-01 19:37+0200\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: 8bit\n"
    
    #: main.C:31
    msgid "Hello, world\n"
    msgstr ""
    

    Man erkennt den in diesem Beispiel noch dominierenden Kopfbereich,
    sowie in den letzten 3 Zeilen unseren String aus der main.C.

    3.2 Lokalisierung

    Um jetzt Übersetzungen zu erstellen stehen uns mehrere Möglichkeiten
    zur Verfügung. So gibt es die GUI Anwendung PoEdit[2] und die
    Webapplikation Pootle[1], aber auch Launchpad[3] stellt einen
    Übersetzungsservice bereit und da die Dateien reiner Text sind, ist
    auch die Bearbeitung mit einem Texteditor wie Emacs oder Vim nicht
    abwegig.

    Für diesen Artikel werde ich von der Übersetzung mit dem Texteditor
    nach Wahl ausgehen, da diese einfach und ohne weitere
    Softwareinstallation durchzuführen ist und die internen Arbeitsabläufe
    am klarsten weitergibt.

    Um jetzt die deutsche Übersetzung anzufertigen erstellen wir eine
    Kopie der Schablone. Aus dem Kopfbereich muss zumindest charset
    ausgefüllt werden, bei mir ist das utf-8 und aus

    #: main.C:31
    msgid "Hello, world\n"
    msgstr ""
    

    wird dann

    #: main.C:31
    msgid "Hello, world\n"
    msgstr "Hallo, Welt"!\n"
    

    Zu guter letzt muss jetzt die Übersetzungsdatei in das Maschinenformat
    übersetzt werden und an der richtigen Stelle abgelegt
    werden. Ausgehend von einer Deutschen (im Unterschied zu
    z.B. Schweizerischen) Installation mit UTF-8 als
    Standard-Zeichenkodierung sucht gettext in den Unterverzeichnissen
    de_DE.UTF-8 , de_DE und de des Wurzelelements
    translations/ cah der Datei LC_MESSAGES/hello_world.mo

    Erstellen wir also translations/de/LC_MESSAGES/ und die
    Übersetzungsdatei hello_world.mo mit folgendem Befehl:

    msgfmt -o translations/de/LC_MESSAGES/hello_world.mo translations/de.po
    

    In diesem Beispiel wurde die Deutsche Übersetzung als
    translations/de.po abgespeichert. Eine sinnvolle, und weit verbreitete
    Alternative ist das Ablegen als
    translations/de/LC_MESSAGES/hello_world.po direkt neben der
    Datei im Maschienenformat.

    Verweise

    [1] http://translate.sourceforge.net/wiki/pootle/index
    [2] http://www.poedit.net/
    [3] http://www.launchpad.net

    FUNDGRUBE

    Alte, potentiell still relevanten Notizen

    So ich hab' ma wieder etwas Zeit.

    Aktuell beschäftige ich mich relativ viel mit GNU/Gettext also gibt's hier mal einen Artikel zu 😉 hoffe, dass es halbwegs interessant wird.

    Das ganze ist bisher noch ziemlich leer - wird schon noch

    • i18n - l10n - Begrifflichkeiten
    • Warum ich mein Programm für die Übersetzung vorbereite
    • Setup - GNU/Gettext
    • Die Software
    • Übersetzen
    • Zusammenstellen
    • Nachwort
    #include <iostream>
    #include <libintl.h>
    #define _(string) gettext (string)
    #include <clocale>
    #include <boost/format.hpp>
    
    int main()
    {
            std::setlocale (LC_ALL, "");
            textdomain ("hello");
            bindtextdomain ("hello", ".");
    
            std::cout << _("Hello this is a GNU/Gettext test.") << std::endl;
            std::cout << gettext("Again testing gettext") << std::endl;
            std::cout << boost::format(_("%1% multiplied by %2% is %3%")) % 6 % 9 % 42 << std::endl;
            std::cout.flush();
    }
    

    g++ -o hello main.cxx

    $ ./hello
    Hello this is a GNU/Gettext test.
    Again testing gettext
    6 multiplied by 9 is 42
    

    xgettext -k_ --c++ --boost -o hello.pot main.cxx

    # SOME DESCRIPTIVE TITLE.
    # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
    #
    #, fuzzy
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: 2008-05-17 20:22+0200\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: 8bit\n"
    
    #: main.cxx:14
    msgid "Hello this is a GNU/Gettext test."
    msgstr ""
    
    #: main.cxx:15
    msgid "Again testing gettext"
    msgstr ""
    
    #: main.cxx:16
    #, boost-format
    msgid "%1% multiplied by %2% is %3%"
    msgstr ""
    

    msginit --locale=de_DE.UTF-8 -i hello.pot

    # German translations for PACKAGE package
    # German messages for PACKAGE.
    # Copyright (C) 2008 THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # Christoph Egger <Christoph.Egger@gmx.de>, 2008.
    #
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: 2008-05-17 20:22+0200\n"
    "PO-Revision-Date: 2008-05-17 20:24+0200\n"
    "Last-Translator: Christoph Egger <Christoph.Egger@gmx.de>\n"
    "Language-Team: German\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=(n != 1);\n"
    
    #: main.cxx:14
    msgid "Hello this is a GNU/Gettext test."
    msgstr ""
    
    #: main.cxx:15
    msgid "Again testing gettext"
    msgstr ""
    
    #: main.cxx:16
    #, boost-format
    msgid "%1% multiplied by %2% is %3%"
    msgstr ""
    

    vim de.pot

    # German translations for PACKAGE package
    # German messages for PACKAGE.
    # Copyright (C) 2008 THE PACKAGE'S COPYRIGHT HOLDER
    # This file is distributed under the same license as the PACKAGE package.
    # Christoph Egger <Christoph.Egger@gmx.de>, 2008.
    #
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: 2008-05-17 20:22+0200\n"
    "PO-Revision-Date: 2008-05-17 20:24+0200\n"
    "Last-Translator: Christoph Egger <Christoph.Egger@gmx.de>\n"
    "Language-Team: German\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=(n != 1);\n"
    
    #: main.cxx:14
    msgid "Hello this is a GNU/Gettext test."
    msgstr "Hallo, das ist ein GNU/Gettext test."
    
    #: main.cxx:15
    msgid "Again testing gettext"
    msgstr "Teste nocheinmal GNU/Gettext"
    
    #: main.cxx:16
    #, boost-format
    msgid "%1% multiplied by %2% is %3%"
    msgstr "%1% mal %2% ist %3%"
    

    mkdir -p de_DE.UTF-8/LC_MESSAGES
    msgfmt -o de_DE.UTF-8/LC_MESSAGES/hello.mo de.po

    $ ./hello
    Hallo, das ist ein GNU/Gettext test.
    Teste nocheinmal GNU/Gettext
    6 mal 9 ist 42
    


  • Du hast noch keinen "Autoren"-Titel, vielleicht willst du es noch nachholen?



  • Was muss ich denn da machen?



  • Nichts, ich kümmere mich drum.



  • Meint ihr der Artikel ist soweit zu einem Interessanten Thema?

    Ich denk' ich hab die Tage 'mal Zeit 'was fertigzumachen



  • Ja logisch, der Artikel wäre auf jeden Fall interessant. 👍



  • Irgend welche Bereiche auf die ich speziell eingehen sollte?

    Sonst bring' ich das ganze jetzt erstmal in Form und schau' dann was noch fehlt



  • Hm, ich würde auf das grundsätzliche Vorgehen mit gnu gettext eingehen und dabei halt - sofern angebracht - Spezialfälle einweben. So dass man einen guten Einstieg in das Thema erhält, aber nicht grad beim ersten "non-standard" Problem aufgeben muss 😉



  • gibt es tools um das schreiben der uebersetzungsdateien zu erleichtern.
    wie sieht es mit unicode aus? muss alles unicode sein oder nur eine bestimmte sprache, etc.
    sind die sprachen in die binary eingkompiliert oder liegen sie extern, bzw. wie mache ich es genau anders rum.
    was sind die standard vorgehensweisen bei mehrzahl/nummerierungs problemen.

    da gibt es vieles was interessant ist 🙂



  • Design technisch finde ich das Thema interessant. Wie sorgt man zum Beispiel dafür, dass man einen Satz auch bei der Übersetzung umdrehen kann und dergleichen? Wie geht man mit Sprachen um welche in einer anderen Richtung gelesen werden? Kann man "zweite" übersetzen wobei die 2 aus einer Laufzeitvariable stammt? All diese kleinen Fragen und Problemchen dessen ich mir nicht mal bewusst bin, interessieren mich.

    Darüber hinaus finde ich das Thema sehr nützlich, da Internationalisierung etwas ist wo man sich nicht ewig rumdrücken kann, aber nicht sonderlich interessant.



  • Wie versprochen hab' ich mich nach dem Abiturstress daran gemacht, das ganze mal etwas weiterzubringen. Da ja mittlerweile Artikelserien hoch im Kurs stehen würde ich noch ein paar Absätze dazuschreiben und dann den Rest auf einen Nachfolgeartikel verschieben.

    Ist alles noch WIP aber auch die Zeit, zu der ich Inhaltlich noch anpassungen vornehmen kann. Wer also Kommentare hat 😉



  • Ich find's gut dass du die Sache step by step durchgehst, so bekommt man einen guten Einstieg. Aber wenn du eine Artikelserie planst, dann achte darauf, dass du pro Artikel ein Art "Ziel" definierst. Somit hat jeder Artikel ein Leitthema 🙂
    Bisher liest sich der Artikel recht flüssig, wenn da nicht der Umbruch nach 80 Spalten wäre 😉



  • Ich bin wohl in letzter Zeit zu viel html / markdown gewohnt und zu wenig Forensoftware. Die Zeilenumbrüche sind mir auch aufgefallen, die werd' ich noch killen müssen.

    Ich denk' Teil I bekommt noch boost::format mit positional format-strings, das sollte relativ zügig zu erklären sein und in Folge können dann so Fälli wie strings außerhalb des Quellcodes behandelt werden.


Anmelden zum Antworten