Lupus: Sprachverwaltungs-System für WebApp

Guten Tag,

ich starte in kürze ein WebApp-Projekt, das ich mehrsprachig anbieten möchte. Ich habe mir natürlich schon Gedanken darüber gemacht, wie sich das am besten umsetzten liesse, wollte mich aber lieber noch einmal erkundigen ob es bereits andere, bzw. bessere, Methoden gibt, um bei einem mehrsprachigen WebApp die Sprachen möglichst einfach zu verwalten. Gibt eine ein Schema das hierfür oft verwendet wird; oder sogar Standards?

Hier wie ich es machen würde:

<?php  
  
  // .../languages/de.php  
  
  $language = array();  
  
  $language['pleaselogin'] = 'Bitte loggen Sie sich ein.';  
  $language['loggedout'] = 'Sie wurden erfolgreich abgemeldet.';  
  //...  
  
?>  
  
<?php  
  
  // .../languages/en.php  
  
  $language = array();  
  
  $language['pleaselogin'] = 'Please log in.';  
  $language['loggedout'] = 'You\'ve been logged out successfully.';  
  //...  
  
?>  
  
<?php  
  
  // .../index.php  
  
  switch(isset($_POST['lang'])? $_POST['lang'] : @$_COOKIE['lang']) {  
      case 'en':  
          setcookie('lang', 'en');  
          include '.../languages/en.php';  
          break;  
      default:  
          setcookie('lang', 'de');  
          include '.../languages/de.php';  
          break;  
  }  
  
  echo $language['pleaselogin'];  
  // <?=$language['pleaselogin']?>  
  
?>

Was meint ihr dazu? Würdet ihr das komplett anders lösen?

Ich freue mich über jeden Ratschlag.
Herzlichen Dank,
Lupus

  1. Hallo Lupus,

    der Klassiker fuer Uebersetungen ist eigentlich gettext. PHP hat dafuer die schoene Faulenzerfunktion _(). In diesem Zusammenhang ist poedit sowas wie ein Standardeditor fuer die Uebersetzungsdateien. Im Prizip funktioniert das so, dass du eine Uebersetzungsdatei hast mit jeweils Wertepaaren in, sagen wir, Englisch und Deutsch. Du rufst die Function auf mit _('Some string') und das ergibt in deiner Applikation 'Irgendeine Zeichenkette', falls die Uberseztung vorhanden ist oder eben 'Some string', falls nicht.

    Gettext ist erst einmal etwas gewoehnungsbeduerftig, zum anderen nicht auf allen Servern vorhanden.

    Alternativ kann man das auch so machen, dass man alle Uebersetzungen in eine Ini-Datei schreibt und von dort ausliest. Wir machen das in der Firma etwas aufwaendiger mit einer Datenbank, und das klappt recht gut.

    Unsere Tabelle sieht so aus:

    iniKey | en     | de        | fr        | ...  
    -----------------------------------------------------  
    cancel | Cancel | Abbrechen | Annuller  | ...
    

    Diese Tabelle laesst sich mit phpMyAdmin gut warten und man vermeidet Probleme mit fehlerhaften/fehlenden Keys etc.

    Daraus generieren wir regelmaessig fuer alle Sprachen je eine Datei 'locale.ini.php', die so aussieht:

    ;<? die() ?> -> vermeidet diekten Aufruf via HTTP
    ;ö           -> macht dem Editor klar, dass es sich um UTF-8 handelt
    [vocabulary]
    cancel = "Annuller"

    Diese Datei wird beim Applicationstart mit parse_ini_file() eingelesen. In den Userkommentaren zu dieser Funktion findest du auch einige brauchbare Funktionen um ini-Files zu schreiben, das musst du natuerlich wissen, wenn du die Daten aus einer DB einlesen willst.

    Egal, ob du den Umweg ueber eine DB nimmst oder nicht, hast du jetzt ein Array (sagen wir $vocabulary) mit allen Uebersetzungen einer Sprache. Bei uns sind das so ca. 200, das geht problemlos.

    Jetzt zu den etwas komplizierteren Faellen - Du wirst Uebersetzungen in der Art 'Found 35 results' vs. '35 Ergebnisse gefunden' haben, wo du also eine Variable '35' mit in die Uebersetzung einbauen willst, deren Position von der Sprache abhaengig ist. Deine Uebersetzung koennte jetzt so aussehen:
    found_num_results = "Found %d results"

    Fuer solche Faelle muesste Deine Funktion sprintf() koennen.

    Eine Beispielfunktion waere etwa so, man nennt die haeufig __() (2 underscores)

    function __($key) {  
      global $vocabulary;//nee, mit gefaellt das global auch nicht, aber ich will es hier einfach halten  
      if(!isset($vocabulary[$key])) {  
        return $key; // key nicht vorhanden, return $key  
      }  
      $args = func_get_args();  
      return call_user_func_array('sprintf', $args);  
    }
    

    Diese kannst du mit __('cancel'), oder aber __('found_num_results', 35) aufrufen (oder eben noch mehr Argumenten, sie nochmal sprintf();

    Jetzt gibt es noch ein paar Dinge, auf die ich dich aufmerksam machen moechte. Anstatt von 'lang' solltest du vielleicht lieber von 'locale' sprechen, Wikipedia kann das nett erklaeren. Wenn du in deiner Uebersetung Apostroph meinst, solltest du auch Apostroph sagen und nicht '. Das Zeichen kriegst du mit ALT + 0146. Allerdings musst du deine Anwendung dann ganz in UTF-8 halten, aber das ist sowieso eine gute Idee.

    Dein Locale-Detector ist an sich gut gedacht, aber was ist mit _GET und _SESSION? Und das @ vor _COOKIE ist hoffentlich nur ein Vertipper, ansonsten waere es a.) unnoetig und b.) schlechter Stil.

    Du koenntest das auch etqa so machen:

    if(!empty($_GET['locale'])) {  
      $locale = $_GET['locale'];  
    }  
    else if ... POST COOKIE SESSION...  
      
    if(is_file('pfad/zu/inis/'. $locale . '/locale.ini.php')) {  
      $locale_file = 'pfad/zu/inis/'. $locale . '/locale.ini.php';  
    }  
    else {  
      $locale_file = 'pfad/zu/inis/en/locale.ini.php';// default Sprachdatei  
    }  
    $vocabulary = parse_ini_file($locale_file)
    

    Das war jetzt etwas langatmig, aber ich hoffe, es hilft Dir weiter

    Dieter

    1. Moin Moin!

      Wir machen das in der Firma etwas aufwaendiger mit einer Datenbank, und das klappt recht gut.

      Unsere Tabelle sieht so aus:

      iniKey | en     | de        | fr        | ...


      cancel | Cancel | Abbrechen | Annuller  | ...

      
      >   
      > Diese Tabelle laesst sich mit phpMyAdmin gut warten und man vermeidet Probleme mit fehlerhaften/fehlenden Keys etc.  
        
        
      Woher kenne ich das nur? ;-) Irgendwie endet man immer mehr oder weniger bei so einer Tabelle. Mein letztes mehrsprachiges Projekt hatte das etwas mehr durchnormalisiert, mit Spalten für Begriff, Sprache und übersetztem Begriff, aber das ist nur ein Detail. In exakt Deinem Format wurde das Zeug aus der DB in Excel exportiert, ins Übersetzngsbüro gegeben und dort bis zu Unkenntlichkeit in neue Sprachen übersetzt. Dann ging es wieder zurück in die DB.  
        
      Die Anwendung hat dann für jeden User ein `SELECT term,translated FROM translations WHERE language_id=XX`{:.language-sql} gemacht, um einen Hash mit den Übersetzungen zu füllen.  
        
      Fehlende Übersetzungen wurden durch den Originalterm ersetzt (nicht schön, aber lesbar) und erzeugten eine Warnung in den Logs.  
        
      
      > Jetzt zu den etwas komplizierteren Faellen - Du wirst Uebersetzungen in der Art 'Found 35 results' vs. '35 Ergebnisse gefunden' haben, wo du also eine Variable '35' mit in die Uebersetzung einbauen willst, deren Position von der Sprache abhaengig ist. Deine Uebersetzung koennte jetzt so aussehen:  
      > found\_num\_results = "Found %d results"  
        
      Richtig garstig wird das, wenn Dinge wie Singular, [Dual](http://de.wikipedia.org/wiki/Dual_(Grammatik)), [Paukal](http://de.wikipedia.org/wiki/Paukal) und Plural ins Spiel kommen, z.B. bei "keine Eier gefunden" / "ein Ei gefunden", "zwei Eier gefunden" / "%d Eier gefunden". (Dual-Beispiele bitte aus der Wikipedia fischen!) gettext kümmert sich darum, genauer die [ngettext](http://us2.php.net/manual/en/function.ngettext.php)-Funktion.  
        
      Auch schön sind Texte, in denen mehr als eine Variable vorkommt, die je nach Sprache in verschiedenen Reihenfolgen vorliegen, gerne auch kombiniert mit den verschiedenen Numerus-Varianten: "Der Begriff %s wurde %d mal in %s gefunden." vs. "In %s, %s was found %d times." Da ist ein normales (s)printf überfordert, weil die Platzhalter nicht unterschieden werden können. Abhilfe schaffen da nur numerierte oder benannte Platzhalter.  
        
      Alexander
      
      -- 
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so".
      
      1. Hi Alexander,

        Bei uns sind die Tabellen in Wirklichkeit auch etwas komplexer mit Kategorien usw. Mit den Uebersetzungen haben wir es in Luxembourg relativ leicht, weil immer irgendwer in house Franzoesisch oder sowas spricht und gleichzeitig mit phpMyAdmin umgehen kann. Solange sich die Tabellen auf Standardfloskeln beschraenken, geht das problemlos.

        Und mit Dualen oder Paukalen haben wir auch eher weniger zu tun... Mit den Pluralen machen wir das so, dass die Keys die Endung -p bekommen, -s (wie in sprintf) weisst auf Variablen hin. Alles nicht so todsicher, aber wie gesagt, ist der weitaus groesste Teil des Vokabulars von der Sorte 'Name', 'Vorname' oder 'Ihre Email wurde gesendet'.

        Alles in allem nicht der Weisheit letzter Schluss, aber es tut, was es soll. Und vor allem ist es wiederverwertbar.

        Gruss

        Dieter

    2. Hallo,

      wow, erstmal herzlichen Dank an alle, besonders Dieter. Ich denke, dass ich nun schon ein ziemliches Stück weiter bin. Vielleicht sollte ich noch erwähnen, dass es sich bei meinem WebApp nicht um ein Projekt handelt, dass schlussendlich auch weltweit und in sämtlichen Sprachen verfügbar sein wird; sondern ich bin ein Schüler, der an diesem Projekt üben und neue Techniken kennen lernen möchte - unter anderem eben auch das einbinden von mehreren Sprachen in ein WebApp. Da möchte ich mir natürlich keine falschen, schlechte oder halbpatzige Techniken aneignen.

      Die Methode von Dieter gefällt mir eigentlich sehr gut; sie ist ja auch recht nahe an meinem ursprünglichen Gedanken. Die Methode mit gettext(), ist mir doch etwas zu mirakulös. Da mag ich die andere Methode mit den Vokabular-Files doch schon viel leiber, weil ich so eher den Eindruck habe den Überblick über meinen Code zu behalten. Ich verstehe aber nicht ganz, was der Vorteil ist, wenn ich das Vokabular in in einer INI-Datei festlege, anstatt es in einer herkömmlichen PHP-Datei mit einem Array festzulegen. Ich könnte mir vorstellen, dass das einlesen der INI-Datei in PHP mittels parse_ini_file() deutlich länger dauert, wie wenn das Vokabular-Array direkt in der PHP-Datei steht.
      Auch vertehe ich noch nicht ganz, weshalb man, wenn das ganze Vokabular in einer Datenbank erfasst wird, den Umweg machen muss, das ganze Vokabular erst in einer INI-Datei abzulegen, wenn man es doch auch direkt mit einer SQL-Abfrage an PHP übergeben könnte, bzw. es so direkt in ein Array speichern könnte. Mir ist schon klar, dass die Datei nich bei jedem Aufruf der Application neu erstellt wird. Dauert ein eine Datenbank-Abfrage tatsächlich länger als parse_ini_file()?! Das ganze Vokabular in einer Datenbank abzulegen erscheint mir hingegen wieder als sehr sinnvoll, weil so eben fehlende Keys vermieden werden können.

      [...] Du wirst Uebersetzungen in der Art 'Found 35 results' vs. '35 Ergebnisse gefunden' haben [...]

      Danke für den Hinweis; das wollte ich eigentlich noch fragen. Aber auch dieser Fall ist mit der kleinen Funktion die Dieter mir vorgeschlagen hat recht einfch zu lösen. Hier nochmals die einwenig von mir angepasste Funktion.

      <?php  
      function __($key) {  
          global $locale;  
          if(!isset($locale[$key])) {  
              return $key;  
          } else {  
              $args = func_get_args();  
              $args[0] = $locale[$key];  
              return call_user_func_array('sprintf', $args);  
          }  
      }  
      ?>
      

      Mich würde aber interessieren, wie ich diese Funktion anpassen muss, dass damit auch etwas kompliziertere Fäll übersetzt werden können.

      Auch schön sind Texte, in denen mehr als eine Variable vorkommt, die je nach Sprache in
      verschiedenen Reihenfolgen vorliegen, gerne auch kombiniert mit den verschiedenen
      Numerus-Varianten: "Der Begriff %s wurde %d mal in %s gefunden." vs. "In %s, %s was
      found %d times." Da ist ein normales (s)printf überfordert, weil die Platzhalter nicht
      unterschieden werden können. Abhilfe schaffen da nur numerierte oder benannte Platzhalter.

      In diesem Beispiel könnte ich mir ja noch vorstellen, wie man so etwas lösen könnte. In der Datei mit dem Vokabular für eine Sprache (es spielt hier ja keine Rolle, ob es nun, eine INI- oder eine PHP-Datei ist), die zu ersetzenden Sätze mit Nummerierten "Pseudo-Variablen" definieren und diese dann mit einem Regex (vielleicht reicht ja sogar str_replace) in der richtigen Reihenfolge ersetzten. Beispiel:

      de.php: found_a_in_b_x_times = 'Der Begriff $arg1 wurde $arg2 mal in $arg3 gefunden.'
      en.php: found_a_in_b_x_times = 'In $arg3, $arg1 was found $arg2 times.'

      In der __()-Funktion müsste man dann eben mit einem Regex oder str_replace die durchnummerierten $arg[1-9]'s mit den dazugehörigen Argumenten ersetzen. Ich weiss jetzt zwar nicht, ob das besonders elegant ist aber funktionieren müsste es eigentlich.

      Richtig garstig wird das, wenn Dinge wie Singular, Dual, Paukal und Plural ins Spiel
      kommen, z.B. bei "keine Eier gefunden" / "ein Ei gefunden", "zwei Eier gefunden" / "%d
      Eier gefunden". (Dual-Beispiele bitte aus der Wikipedia fischen!) gettext kümmert sich
      darum, genauer die ngettext-Funktion.

      Zum Glück muss ich nicht auf Dinge wie Dual oder Paukal achten, weil ich bei meinem kleinen Test-Projekt nur mit Deutsch, Französisch und Englisch rumspielen werde. Aber mit dem Singular und Plural ist es ja im Prinzip ähnlich. Ich habe leider keine Ahnung, wie ich das Eier-Beispiel lösen würde. Vielleicht etwas in der Art...(?):

      de.php: x_eggs_found = 'Es [wurde|wurden] $arg1 [Ei|Eier] gefunden.'

      Und im Regex dann den entsprechend so filtern, dass entweder wurde, order wurden - bzw. Ei, oder Eier - verwendet wird, jenachdem, was $arg1 ist. In diesem Falls müsste dann wahrscheinlich noch eine extra abfrage gemacht werden, ob $arg1 überhaupt eine Zahl ist, bzw. dass es kein String ist.
      Hat jemand bessere Ideen, wie man so etwas elegant lösen würde? Oder ist es hierfür dann wirklich notendig, gettext() zu verwenden, weil es sonst extrem kompliziet werden würde?!

      Jetzt gibt es noch ein paar Dinge, auf die ich dich aufmerksam machen moechte.Anstatt
      von 'lang' solltest du vielleicht lieber von 'locale' sprechen, Wikipedia kann das nett erklaeren.
      Wenn du in deiner Uebersetung Apostroph meinst, solltest du auch Apostroph sagen und
      nicht '. Das Zeichen kriegst du mit ALT + 0146. Allerdings musst du deine Anwendung
      dann ganz in UTF-8 halten, aber das ist sowieso eine gute Idee.

      Okay, danke. Das werde ich gerne beherzigen. Ich verwende sowieso UTF-8 als Charset bei all meinen Dateien, den HTML- und PHP-Header. Von dem her ist es kein Problem, richtige Apostrophe zu verwenden anstatt single-quote. Notfalls, was aber bei UTF-8 eh nicht notwendig sein wird, kann ich die sonderzeichen ja noch HTML-encoden. Also zB "&rsquo;" statt "’" verwenden.

      Dein Locale-Detector ist an sich gut gedacht, aber was ist mit _GET und _SESSION?

      Ich verstehe nicht ganz, wozu ich zusätzlich noch _GET und _SESSION benötige. Ich möchte in mein WebApp ein <select>-Element einbauen, mit dem man die gewünschte Sprache auswählen kann. Die Sprachauswahl wird dann mit method="post" an PHP übergeben und dann in einem Cookie abgelegt. Wozu brauche ich dann noch _GET in meinem Detector, oder sogar "?locale=de" in jeder URL, wenn die Sprache sowieso im Cookie abgelegt ist. Wozu die Sprache zusätzlich in der Session, wenn sie schon im Cookie gespeichert ist? Oder übersehe ich hier etwas?

      Und das @ vor _COOKIE ist hoffentlich nur ein Vertipper, ansonsten waere es a.) unnoetig und b.) schlechter Stil.

      Naja, eigentlich nicht. Ich will mit dem "@" bloss die Fehlermeldung vermeiden, wenn man die Seite zum ersten Mal aufruft und das Cookie noch nicht gesetzt ist. Dass dies ein schlechter Stil ist und auch recht langsam sein sollte, habe ich auch schon gehört, aber mit was könnte man das "@" ersetzten? Klar, mit einer verschachtelten If-Else-Abfrage würde es funktionieren, aber wenn man _POST, _GET, _SESSION und _COOKIE einbaut wird diese Abfrage dann doch recht tief.

      Herzlichen Dank,
      Lupus

      1. Hallo Lupus,

        bin ich ja froh, wenn du weiterkommst.

        Da möchte ich mir natürlich keine falschen, schlechte oder halbpatzige Techniken aneignen.

        Das ist immer schon mal eine sehr gute Idee

        Ich verstehe aber nicht ganz, was der Vorteil ist, wenn ich das Vokabular in in einer INI-Datei festlege. Ganz einfach, ich mag INI-Dateien, die sind leicht zu warten. Zudem kann man die meist auch Programmier-Laien anvertrauen, zB. dem Uebersetungsbuero.

        Die Zeit, die parse_ini_file() verwendet, kannst du bei diese Groesenordnung getrost vergessen.

        Was deine Frage mit dem Umweg ueber die Datenbank angeht, das hat mehr organisatorische Gruende. Das kannst anhand unseres Workflows sehen:

        • Die DB liegt auf einem zentralen Server, auf den die meisten Kollegen Zugriff hat. Dort werden aber nur Standardfloskeln verwaltetet. Ich mache jetzt einen neuen Eintrag in Deutsch und Englisch. Danach nimmt sich zB. die franzoesische Praktikantin die fehlenden Uebersetungen vor.
        • Jede Nacht wird das gesamte Framework aus dem SVN exportiert, danach werden die locale-files aus der DB generiert
        • Wenn jetzt ein Programmierer ein neues Projekt anfaengt, hat er eine aktuelle Kopie des Frameworks mit aktuellem (Standard-)Vokabular.
        • Zu jedem Projekt gehoeren aber noch andere, projektspezifische, Ubersetzungsdateien. Die werden erstmal von Hand geschrieben. I.d.R macht das jeder Progammierer in Englisch und seiner Muttersprache, d.h. hier muessen regelmaessig Abgleiche gemacht werden un das wiederum kann die Praktikantin machen.
        • Die Uebersetzungsklasse nimmt sich jetzt alles mit parse_ini_file vor, was im Ordner /locale/fr (en/de...) liegt. Da hat es wenig Sinn zu sagen, nimm dieses und jenes aus der DB und alles andere aus Dateien.

        Was die komplizierten Faelle angeht, bis jetzt konnte ich das mit sprintf ganz gut loesen. Fall das nicht klappen sollte, muesste ich mir auch erstmal eine Loesung zurechtzimmern.

        Zu GET und SESSION
        Wir machen unsere URIs so: server/locale/alles-andere und uebersetzen das mit mod_rewrite zu locale=de&path=alles-andere, das heisst, wir haben, ausser beim ersten Aufruf, immer ein GET-Parameter. Das ist auch eine ziemlich gaengige Praxis. Ich wuerde deine Selectform auch eher auf GET setzen, dann entfaellt der laestige Post-Hinweis beim zurueckblaettern. Und beim naechsten Projet wird es garantiert irgendwie anders sein, weil dem Designer oder dem Kunden irgendwas zum Thema einfallen wird... Na gut, du macht eine Schuelerprojekt, aber du hast ja, was den Stil angeht, offenbar gute Vorsaetze. Insofern wuerde ich an deiner Stelle das Problem ein fuer alle Male loesen und alle Moeglichkeiten der Locale-Uebergabe von vornherein implementieren. Du sprichst in deinem Posting mehrfach die Zeit an, die bestimmte Funktionalitaeten beanspruchen. Klar kosten zusaetzliche Checks Performance, aber das ist trivial, verglichen mit den zukuenftigen Problemen, die entstehen, wenn du solche Loesungen nicht konsequent durchprogarmmierst.

        Gruss

        Dieter

    3. Hallo Dieter,

      der Klassiker fuer Uebersetungen ist eigentlich gettext.

      was sagst Du zu diesem Resümee? http://forum.de.selfhtml.org/archiv/2009/6/t187540/#m1246865

      Gruß aus Berlin!
      eddi

      --
      Könnte bitte jemand mal langsam dafür sorgen, dass da draußen nicht dauernd die Filmrolle "Planet der Affen" abgedudelt wird? Danke!
      1. Hallo Eddi,

        was sagst Du zu diesem Resümee? http://forum.de.selfhtml.org/archiv/2009/6/t187540/#m1246865

        Ich benutze gettext() selbst eher selten und finde es, wie ich bereits geschrieben habe, ebenfalls etwas gewoehnungsbeduerftig (oder mirakulös, wie Lupus das so schoen sagt).
        Der Archivbeitrag, auf den du verweist, klingt fuer mich mich allerdings sehr nach 'keinen Bock auf RTFM' und reichlich uebertrieben.

        Gruss

        Dieter

        1. Re:

          was sagst Du zu diesem Resümee? http://forum.de.selfhtml.org/archiv/2009/6/t187540/#m1246865
          Ich benutze gettext() selbst eher selten und finde es, wie ich bereits geschrieben habe, ebenfalls etwas gewoehnungsbeduerftig (oder mirakulös, wie Lupus das so schoen sagt).
          Der Archivbeitrag, auf den du verweist, klingt fuer mich mich allerdings sehr nach 'keinen Bock auf RTFM' und reichlich uebertrieben.

          Okay, danke. Bis jetzt habe ich mich nämlich um gettext immer herumgedrückt. Wenn es aber nicht so schlimm ist, wie das Archiv zu erzählen weiß, werde ich mir das auch mal zu Gemüte führen.

          Gruß aus Berlin!
          eddi

          --
          Könnte bitte jemand mal langsam dafür sorgen, dass da draußen nicht dauernd die Filmrolle "Planet der Affen" abgedudelt wird? Danke!
  2. Moin Moin!

    Was meint ihr dazu? Würdet ihr das komplett anders lösen?

    Es gibt fertige Lösungen dafür, such mal nach L10N und I10N, oder stumpf nach GNU gettext.

    Wenn Du das Rad unbedingt noch einmal selbst erfinden mußt, denk mal über eine Datenbank nach, statt tonnenweise PHP-Kot äh -Code zu schreiben. Es muß ja nicht gleich ein RDBMS sein, File-DBs wie z.B. cdb, Berkeley-DB, JSON oder notfalls CSV oder XML reichen vollkommen aus.

    Alexander

    --
    Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so".
  3. @@Lupus:

    nuqneH

    ich starte in kürze ein WebApp-Projekt, das ich mehrsprachig anbieten möchte.

    Dann ist es angebracht, Sprachvereinbarung (language negotiation) einzusetzen.

    Weiterlesen: Content Negotiation: why it is useful, and how to make it work.

    Qapla'

    --
    Alle Menschen sind klug. Die einen vorher, die anderen nachher. (John Steinbeck)