Internationalisierung von Webseiten

Aus THM-Wiki
Wechseln zu: Navigation, Suche

Zur Internationalisierung von Webseiten gibt es verschiedene Techniken. Die fünf verbreitetsten werden hier vorgestellt und deren Anwendung kurz erklärt.

Einleitung

Seit Menschengedenken gibt es unterschiedliche Sprachen, ob sie nun vom Turmbau zu Babel herrühren oder einfach von den sich verstreut entwickelten Kulturen und Völkern, ist jedem selbst überlassen. In jedem Falle bilden Sprachen oft eine Barriere, die es als Webentwickler zu überwinden gilt. Die kommt daher, dass Webseiten weltweit aufgerufen werden können und so auch für Menschen anderer Länder lesbar sein müssen.

Man könnte meinen, dass es beliebig viele Ansätze gibt, um eine Mehrsprachigkeit zu realisieren. Tatsächlich findet man aber nur wenige, immer wiederkehrende Muster, welche eine Kategorisierung in einige (teilweise durchaus ähnliche) Techniken zulässt. Hierbei ist es wichtig, zwischen Aufwand, Komplexität und Zielsetzung abzuwägen. So sollte ein großes Projekt absolut strukturiert internationalisiert werden. Während eine Seite, die einfach ein Hotel mit Zimmerpreisen und Fotos darstellt, wahrscheinlich eher „quick & dirty“ internationalisiert werden würde.

Im Internet-Sprachgebrauch wird die Internationalisierung oft auch als i18n bezeichnet. Dieser Ausdruck entspricht den Anfangs- und Endbuchstaben "i" und "n" des englischen Begriffs internationalization und durch die 18 werden die dazwischen befindlichen 18 Buchstaben "nternationalizatio" repräsentiert.

Techniken

Internationalisierte HTML-Seiten

Dies ist die programmiertechnisch sicherlich einfachste Variante. Über Aufruf-Parameter, Session-Angabe, Cookie oder Browsereinstellung wird die aktuell vom Besucher eingestellte Sprache erkannt. Passend dazu wird die HTML-Seite ausgewählt. Ist eine Seite in der gewünschten Sprache bereitgestellt, wird sie geladen und zur Anzeige ausgeliefert. Ist jedoch der Text in dieser Sprache nicht vorhanden, so wird er in der voreingestellten Standardsprache dargestellt. Für den ein oder anderen Zweck mögen komplett internationalisierte HTML-Seiten noch sinnvoll sein (Ausgabe der http-Fehlerseiten), bei veränderbaren Menüs scheitert diese Technik dann aber schon an der nicht mehr gegebenen Übersichtlichkeit.

  • Vorteil: einfache Umsetzung der Technik
  • Nachteile: Dynamische Ausgaben sowie Trennung von Inhalt und Layout sind kaum realisierbar. Die Folgen sind unwartbare Projektstrukturen

Array-basierte Sprachsnippets

Eine etwas verfeinerte Variante, der komplett internationalisierten HTML-Seiten, ist es nur übersetzte Text-Schnipsel (engl. "snippets") zu verwenden.

Hierbei wird eine Datei angelegt, die exakt ein Array enthält. Dessen Schlüssel sind die Platzhalter für die internationalisierten Snippets, die zugeordneten Werte die entsprechenden Ausdrücke in der jeweiligen Sprache. Um nun eine Seite in einer bestimmten Sprache anzuzeigen, wird das Array mit den Platzhaltern geladen. Dort werden die Platzhalter, mit den gesetzten Werten in der Sprache und die nicht im Array enthaltenen Schlüssel mit Werten der Standardsprache, ersetzt. Die komplettierte Seite wird dann dem Benutzer ausgeliefert.

Leider ergeben sich bei dieser Technik andere Nachteile im Vergleich zu der Komplett-Variante. So programmierte Systeme sind schwer wartbar für Außenstehende, da diese wissen müssen wie die jeweiligen Platzhalter heißen und wofür sie stehen.

Zusätzlich müssen Dritte ein gewisses Syntax-Verständnis aufweisen - zumindest soweit, wie die Textfragmente in ein Array entsprechend der Syntax der verwendeten Programmiersprache geschrieben werden. Dabei schleichen sich schnell Fehler ein.

Außerdem ist zu beachten, dass innerhalb der einzelnen Text-Schnipsel Formatierungsangaben (wie fettgedruckter Text oder veränderte Schriftfarbe) vorhanden sein müssen, wodurch das Design nur teils im Template und teils eben in den Schnipseln verwirklicht wird. Dadurch stellt sich, im Vergleich zur zusammenhängenden Komplett-Technik, die Frage, inwiefern man so Formatierung und Inhalt sinnvoll zusammenführen kann und überhaupt will. Dies kann eigentlich kein Ziel sein, da Übersetzer doch schnell die Formatierung zu übertragen vergessen. Bei einer anderen Sprache kann diese Verteilung, durch Satzbau und Grammatik, genauso wie Datums-und Währungsformatierungen, doch sehr von der Ursprungssprache unterscheiden. Dadurch entstehen weitere Fehlerquellen.

Zu beachten ist, dass wenn der Übersetzer bestimmte Snippets auslässt, diese nur in der Ausgangssprache oder schlechtestenfalls garnicht vorhanden sind.

Dies kann zu sogenannten Lückentexten auf einer Seite führen, wodurch der Inhalt unbrauchbar wird. Natürlich gibt es auch Möglichkeiten dies zu vermeiden, allerdings sind diese, aus Aufwand- und Performancegründen, nicht zu empfehlen und leider auch selten implementiert.

  • Vorteil: losere Kopplung von Formatierung und Inhalt als bei der Verwedung von kompletten internationalisierten Webseiten -> verbessertes Konzept
  • Nachteile: schlechte Wartbarkeit für Dritte, unklare Trennung von Formatierung und Inhalt, Lücken bei fehlenden Übersetzungsschnipseln

Gettext

GNU Gettext ist die GNU-Internationalisierungsbibliothek. Sie wird zur Entwicklung von mehrsprachigen Programmen genutzt. Diese, vor allem bei der Open Source-Gemeinde, seit langem etablierte Implementationserleichterung erzeugt immer eine .po-Datei ('Portable Object'), welche direkt verwendbar ist, auch an einem anderen Ort.

Die erzeugte .po-Datei enthält die reine, übersetzte Information. Zusätzlich findet man in ihr noch noch folgende Elemente:

  • Daten zur übersetzten Textstelle
  • Meta-Informationen
  • Einfügungsstellen für dynamische Textelemente (z.B. Wochentag)

Der Originaltext ist dort in kleine Absätze unterteilt, welche jeweils übersetzt werden können. Nicht vorhandene Übersetzungen von Absätzen sind nicht problematisch, da die ursprünglichen Absätze ebenfalls in der .po-Datei enthalten sind und dann in der Ausgangssprache angezeigt werden können.

Beispiel einer .po-Datei "test.po" mit übersetzten Schnipseln:

 #TITEL: IMPRESSUM
 #COPYRIGHT (C) 2007
 #STEFFEN ELLER
 #this file is distributed under the same license as the ... package
 #FIRST AUTHOR: STEFFEN ELLER<STEFFEN.ELLER@MNI.FH-GIESSEN.DE>, 2006
 #
 #, fuzzy
 msgid""
 msgstr""
 "Project-Id-Version:1.0\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date 2006-08-07 12:45+0200\n"
 "POT-Revision-Date 2006-08-08 15:22+0200\n"
 "Last-Translator:STEFFEN ELLER<STEFFEN.ELLER@MNI.FH-GIESSEN.DE>\n"
 "Language-Team:GERMAN<germany@languages.com>\n"
 "MIME-Version:1.0\n"
 "Content-Type:text/plain;charset=ISO-8859-1\n"
 "Content-Transfer-Encoding:8bit\n"
 
 #: impressum.php:36
 msgid "My name is Steffen.\n"
 msgstr "Mein Name ist Steffen.\n"
 
 #: impressum.php:42
 msgid "How are you?\n"
 msgstr "Wie geht es dir?\n"

Zum Schluss werden die .po-Dateien in binäre .mo-Dateien ('Machine Object') übersetzt. Diese können nun mit dem Softwarepaket ausgeliefert werden.

Im Zusammenhang mit PHP funktioniert diese Variante auf die folgende Weise:

Zuallererst wird die Sprache festgelegt, in die übersetzt werden soll. Diese wird mittels setlocale() ins System exportiert. Hierbei ist zu beachten, dass bei mehreren gleichzeitig aufgerufenen Scripten es zu Nebenwirkungen in der Quasi-Nebenläufigkeit kommen kann. Danach wird eine Verknüpfung auf die vorher definierten .mo-Datei für diese Internationalisierung (gettext-"Domain") gesetzt. Mit diesen Angaben ist es nun möglich die gettext-Funktion auf Strings anzuwenden. Die zu übersetzenden Texte können entweder mit _("Mein Text") oder mit gettext("Mein Text") angeben werden.

Im Quellcode sieht das folgendermaßen aus:

 <?php
  // Sprache festlegen (hier: auf Deutsch)
  $language = "de_DE";
  putenv("LANG=$language");
  setlocale(LC_ALL, $language);
 
  // Ort der Ueberetzungstabellen angeben
  $domain="test";
  bindtextdomain($domain, "./locale");
  textdomain($domain);
 
  // Uebersetzung wird nun aus ./locale/de_DE/LC_MESSAGES/test.mo geholt
 
  // Zu Uebersetzender Teil
  echo _("My name is Steffen.\n");
  echo "<br />";
  echo gettext("How are you?\n");
 ?>

Dies führt zur Ausgabe:

 Mein Name ist Steffen.\n
 <br />
 Wie geht es dir?\n

Bei Betrachtung dieser Technik, welche mächtig, kompakt und verständlich ist, stellt sich die Frage, warum noch andere Techniken verfolgt werden. Dies ergibt sich aus der Historie der Bibliothek. Sie wurde nicht eigens für das Zusammensetzen von Seiten entwickelt, so belasten die immer wieder zu tätigende Berechnungen den Server und verursachen hohen Traffic. Sinn macht die Verwendung also nur in Verbindung mit einem Cache-System, welches bereits berechnete Seiten direkt ausliefert.

  • Vorteile: mächtige Technik, kompakt, verständlich
  • Nachteile: setlocale()-Nebenwirkungen, Berechnungsaufwand

Stringlisten

Der Ansatz der Nutzung von Stringlisten kommt den Techniken der Array-basierten Sprachkonzepten und Gettext-Bibliothek sehr nahe. Im Prinzip werden die übersetzten Snippets wie bei der Array-Variante in eine Datei ausgelagert, wo sie in der Form "Bezeichner=übersetzter Inhalt" angegeben werden. Diese allgemeine Form (auch "ini-Fomat" oder, im Java-Umfeld, "Properties-Datei" genannt) macht den Vorteil gegenüber der Liste mit Array-Inhalten klar: es ist kein Syntaxverständnis notwendig, da diese so einfach wie möglich gehalten wurde.

Beispiel einer Stringlistendatei "demo.list":

 my.name.is=Mein Name ist 
 date.time=Die aktuelle Uhrzeit ist %s, das aktuelle Datum lautet %s

Nachteilig ist allerdings, dass es bei längeren Textstücken durchaus Sinn machen würde, diese mehrzeilig abzulegen. Dies ist aber im betrachteten Fall nicht möglich. Allerdings sollte bei jedem größeren Projekt die Übersetzung mittels einer geeigneten Oberfläche realisiert werden, was dieses Problem höchstwahrscheinlich elegant umschifft.

Vorteilhaft zeigt sich diese Variante vor allem bei Projekten, die in mehreren Programmiersprachen erstellt werden (sogenannte heterogene Projekte). Durch die Sprachunabhängigkeit ist hier eine hohe Kompatibilität erreichbar.

Auch hier empfiehlt sich wieder ein Cache-Verwendung, um zuletzt "übersetzte" Textteile performant wiederverwenden zu können.

Die Implementierung dieser Variante kann mit einer Streamwrapper-Klasse geschehen, die wie im folgenden aufgebaut sein kann (Quelle: PHP-Magazin 1/2007):

 <?php
 
 class dtdStream {
 
   private $uri;
   private $fp;
   
   const BASEDIR='/webspace/i18n';
 
   public static $lang;
 
   // nicht implementierte Funktionalitaet abfangen
   public function __call($name,$args) {
     trigger_error("Funktion '$name' nicht implementiert.");
   }
 
   // Wrapper Registrierung
   static function register($n='locale') {
     stream_wrapper_register($n,  "dtdStream");
     return true;
   }
 
   // Pfad Uebersetzung
   private function translate($path) {
     // Regex zum URI RFC #2396 - Regex result array:
 
     $tmp=array();
     preg_match("=^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?=",
                $path, $tmp);
 
     // realen Dateisystempfad ablegen
     return self::BASEDIR.'/'.self::$lang.'/'.$tmp[4].$tmp[5];
   }
 
   public function stream_open($path,$mode,$options,&$opened_path) {
     // realen Dateisystempfad ablegen
     $this->uri=$this->translate($path);
 
     // Gibt's die Datei?
     if (!file_exists($this->uri)) { return false; }
 
     // Datei Oeffnen
     $this->fp=fopen($this->uri, $mode, $options);
     if (!$this->fp) { // irgendwas ging schief...
       return false;
     }
 
     return true;
   }
 
   public function stream_close() {
     fclose($this->fp);
     $this->fp=0;
     return;
   }
 
   public function stream_read($count) {
     return fread($this->fp,$count);
   }
 
   public function stream_eof() {
     return feof($this->fp);
   }
 
   public function stream_tell() {
     return ftell($this->fp);
   }
 
   public function stream_seek($offset,$whence){
     return fseek($this->fp,$offset,$whence);
   }
 
   public function stream_stat() {
     return fstat($this->fp);
   }
 
   public function url_stat($path,$flags) {
     return stat($this->translate($path));
   }
 
   public function stream_flush() {
     // readonly, also nichts zu tun
     return true;
   }
 
 } // dtdStream
 
 ?>

Diese Stringwrapper-Klasse wird nun im Aufruf verwendet:

 <?php
 
   // Klasse streamwrapper einbinden, um Methoden verwenden zu koennen
   require('./streamwrapper.inc.php');
 
   // Wrapper registrieren
   dtdStream::register();
 
   // Sprache auf Deutsch einstellen
   dtdStream::$lang='de_DE';
 
   /* Klasse stringbundle einbinden:
    * stellt die Stringlisten-Paare (Bezeichner=Sprachspezifischer Kontent)
    * zur Verfuegung
    */
   require('./stringbundle.php');
 
   // Instanz anlegen
   $strBndl=stringBundle::getInstance();
 
   // Stringlisten-Datei laden
   $strBndl->load('locale://strings/demo.list');
 
   // String holen, je nach voreingestellter $lang
   echo $strBndl->getString('my.name.is');
   echo "<br/>";
 
   // Formatierten String holen, je nach voreingestellter $lang
   echo $strBndl->getFormattedString('date.time',strftime('%X'),strftime('%x'));
 
 ?>
  • Vorteile: von Programmiersprache unabhängig, einfache Syntax
  • Nachteile: fehlende Mehrzeiligkeit

Entities

Im Vergleich zu den Gettext- sowie Array- und Stringlisten-Varianten, welche primär für die dynamische, kontextbezogene Ausgabe von Inhalt geeignet sind, ist es bei HTML-Schnipseln naturgemäß oft der Fall, dass viele Teile statisch sind. Diese Vorraussetzung benötigt eine weitere Möglichkeit der Implementierung.

Naheliegend ist der Gedanke, eine Seite im XHTML-Format zu erstellen, da hierdurch eine gute Basis für die Validierung durch den enthaltenen XML-Content geschaffen wird.

Der Ansatz dabei ist der folgende: selbstdefinierte Entitäten werden beim Parsen der Seite je nach gewünschter Sprache durch die übersetzten Elemente ersetzt. Schönerweise lässt sich auf diesem Wege die bei den anderen Techniken schmerzlich vermisste Trennung von Layout und Inhalt verwirklichen, da jeweils die verschiedenen Entitäten ersetzt werden.

Da in einem gültigen Dokument schon die W3C-DTD für XHTML im Kopf enthalten sein muss, muss man eine eigene DTD über einen Umweg mittels DOM einbinden. Dies ist jedoch mit wenigen Handgriffen unter der Benutzung der oben genannten Stream-Wrapper-Klasse möglich:

 <?php
 
  // streamwrapper laden
     require('./streamwrapper.inc.php');
 
  // Wrapper registrieren
     dtdStream::register();
 
  // Sprache auf Deutsch einstellen
     dtdStream::$lang='de';
 
  // DomDocument-Instanz erzeugen
     $src=new DomDocument;
 
  // Entity ersetzen aktivieren
     $src->substituteEntities = true;
     $src->resolveExternals   = true;
 
  // HTML Quelldatei laden (Template)
     $src->load('test.html');
 
  // leeres xhtml Dokument erzeugen
     $doctype = DOMImplementation::createDocumentType("html",
         "-//W3C//DTD XHTML 1.0 Transitional//EN",
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd");
     $html = DOMImplementation::createDocument(null, null, $doctype);
 
  // Quell-Daten einbinden
     $html->appendChild($html->importNode($src->documentElement,true));
 
  // Ergebnis ausgeben
     echo $html->saveHtml();
 
 ?>

Der genannte Code konstruiert folgendes: er aktiviert nach der Initialisierung und Einstellung des Stream-Wrappers ein neues DomDocument ($src) und lädt die DTD zu folgendem Template:

 <?xml version="1.0" encoding="utf-8" ?>
 <!DOCTYPE html [
   <!ENTITY % localeDTD SYSTEM "locale://dtd/test.dtd"> 
   %localeDTD; 
 ]>
 <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
      <title>&test.title;</title>
    </head>
    <body>
      <p>&test.abschnitt;</p>
    </body>
 </html>

Hierbei werden dann die Entities durch die ihnen entsprechenden ersetzt, das Dokument muss noch formatiert werden und kann danach zur Ausgabe bereitgestellt werden. Ab PHP-Version 5.2 gibt es die Methode AdoptNode, welche ein Umhängen der DOM-Struktur statt eines Kopierens, wie im o.g. Fall im appendChild-Abschnitt verwendet, ermöglicht. Dadurch wird diese Variante noch einmal extrem schneller und ressourcenschonender. Bekannte Anwendungen, die mit diesem System arbeiten sind die Übersetzungen der Mozilla-Produkte sowie die offzielle PHP-Dokumentation.

  • Vorteile: klare Trennung von Layout und Inhalt, Performance, einfache Validation
  • Nachteile: Probleme bei nicht übersetzten Entities

Beispiel: Strukturierte Internationalisierung einer zu entwickelnden Webseite

[ Erläuterung bei Präsentation ]

Beispiel: Strukturierte Internationalisierung einer vorhandenen Webseite (anhand von eStudy)

[ Erläuterung bei Präsentation ]

Fazit

Aus der Vielzahl der Möglichkeiten um eine Homepage zu internationalisieren gilt es immer eine Abschätzung zu treffen. Aufwand und Mächtigkeit müssen sinnvoll abgewogen werden, um die passende Technik für den jeweiligen Verwendungszweck auszuloten.

Dabei gilt es den Rahmen nicht aus den Augen zu verlieren. So sichert man eine performante, sichere und strukturierte Programmierung, die spätestens bei den nächsten Arbeiten an den Quelldateien mit Übersicht dankt.

Kein Konzept muss zwingend für sich alleine stehen, gerade miteinander kombinierte Techniken erfüllen in der Praxis den erwünschten Zweck.

Literatur und Quellen

PHP Magazin 01/2007: "Sprachwunder: Mehrsprachige Projekte mit PHP realisieren"

Wikipedia (de): Internationalisierung (Softwareentwicklung)

Wikipedia (de): GNU gettext

php bar: Gettext

Unterlagen zum Download

Präsentation zum Vortrag als PDF-Dokument: Media:I18n_praesentation.pdf

Thesenpapier zum Vortrag als PDF-Dokument: Media:I18n_thesenpapier.pdf

Konzeptidee für eStudy zum Vortrag als PDF-Dokument: Media:I18n_estudy.pdf


--Eller-SEl 12:10, 15. Mär. 2007 (CET)