[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
dasxgettext
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 undmain.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 dasutf-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
undde
des Wurzelelements
translations/ cah der Datei LC_MESSAGES/hello_world.moErstellen 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.netFUNDGRUBE
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.