template

Index :: PHP/MySQL :: Multilinguale Site

Siehe auch: Anhang :: Multilinguale Site (Mechanismen zur Sprachauswahl)

Die aktuelle Sprache wird als ID [0...N-1] in der globalen Variablen $LangID gespeichert. Ihre Initialisierung erfolgt beim Laden der Seite durch Auslesen einer entsprechenden Session-Variablen. Ist noch kein solcher Wert vorhanden, wird die SprachenID auf einen Default-Wert gesetzt: entweder auf 0 (womit die erste Sprache aktiviert wird, in den folgenden Beispielen also Deutsch) oder auf 1 (womit unsere Sekundärsprache Englisch aktiviert wird).
Im Grunde genügt es, die Sprachauswahl (in Form anklickbarer Flaggensymbole) nur auf der Start- oder Hauptseite anzubieten, wo auch das Auslesen des Cookies erfolgt.

Für das Einfügen von Resourcen ins Dokument sorgt eine eigene Funktion WRSC() - für write resource, die anhand einer globalen SprachenID und der übergebenen ResourceID den benötigten Text ermittelt und per echo ausgibt. Damit auch Platzhalter (Siehe Eingebettete Platzhalter) ersetzt werden können, lassen sich optional zusätzliche Parameter angeben.

Hier werden im BODY-Bereich die Resourcen 13 und 14 eingefügt. Letztere besitzt einen Platzhalter, an dessen Stelle die Variable $count stehen soll:
<br>
<?php WRSC(13); ?><br>
<br>
<p align='center'><?php WRSC(14,$count); ?></p>
Woher die Ausgabefunktion den geforderten Text holt, hängt letztlich davon ab, in welcher Weise die Resourcen gespeichert sind:

Auslesen des Cookies

Die Sprachauswahl-Seite (hier: home.php) holt sich - wie alle anderen Seiten auch - die aktuelle SprachenID aus einer SESSION-Variablen namens 'LID'.
Zuerst wird jedoch geprüft, ob eine SprachenID als URL-Parameter vorliegt. Dies passiert, wenn Javascript die Seite erneut anfordert (z.B. weil der Benutzer die Sprache wechselt). Wenn ja, wird die neue SprachenID in der SESSION gespeichert; ansonsten wird die alte SprachenID aus der SESSION geholt.

Falls die SprachenID weder als URL-Parameter noch als SESSION-Variable vorliegt, produziert PHP eine leere HTML-Seite, welche lediglich einen Javascript-Block zum Auslesen des Cookies und zum Zurücksenden der SprachenID (per URL-Parameter) enthält.
Ist noch kein Cookie gesetzt, wird von der Funktion MyLangID() per navigator-Objekt die Sprachkennung ermittelt: Bei 'de' wird 0 geliefert, bei 'fr' wird 2 geliefert, und für den Rest (z.B. 'en', 'it' oder unbekannt) wird 1 geliefert.
Php Einleitung
//---- evtl. neue ID aus URL, ansonsten -1 $LangID = strlen($_GET['lid'])? min(max(intval($_GET['lid']),0),2): -1; session_start(); if (session_is_registered('LID')) { // alte ID vorhanden if ($LangID<0) $LangID = intval($_SESSION['LID']); // alte ID benutzen else $_SESSION['LID'] = $LangID; // neue ID speichern } else if ($LangID>-1) { // ID aus URL session_register('LID'); $_SESSION['LID'] = $LangID; // speichern }
Javascript im Header
function CooGet(key) { var c=document.cookie,s=key+'=',val='',p; if ((p=c.indexOf(s))>-1) { p+=s.length; var p2=c.indexOf(';',p); if (p2<0) p2=c.length; val=unescape(c.substring(p,p2)); } return val; }
Liegt noch keine SprachenID vor?
<?php if ($LangID<0) { ?>
Dann Cookie-Wert holen und Seite reloaden ...
function MyLangID() { var lid=CooGet('LangID'); if (lid.length) return parseInt(lid); if (!(lid=((navigator&&(navigator.language||navigator.userLanguage))||'').substr(0,2))) lid=1; else switch (lid) { case 'de': lid=0; break; case 'fr': lid=2; break; default: lid=1; } document.cookie='LangID='+lid; return lid; } self.location.href='home.php?lid='+MyLangID(); </SCRIPT>
Dokument schließen ...
</head><body></body>
... und fertig.
<?php exit; } ?>
sonst: restlichen HEAD (Javascript) ausgeben ...
Flaggen-Klick verarbeiten
function SetLang(lid) { if (lid==parseInt(CooGet('LangID')||0)) return; document.cookie='LangID='+lid; self.location.href='home.php?lid='+lid; }
Weiter unten, im BODY, die Flaggensymbole:
<img src='img/flgde.gif' onClick='SetLang(0)' style='CURSOR:pointer'> <img src='img/flgen.gif' onClick='SetLang(1)' style='MARGIN-LEFT:2;CURSOR:pointer'> <img src='img/flgfr.gif' onClick='SetLang(2)' style='MARGIN-LEFT:2;CURSOR:pointer'>
Alle anderen Skripte lesen direkt die SESSION-Variable (falls bereits registriert) oder benutzen den Standardwert 0:
Php Einleitung
session_start(); $LangID = session_is_registered('LID')? intval($_SESSION['LID']): 0;

Lokales Array

Die (hier 2-sprachige) Resource liegt in Form eines einfachen String-Arrays vor, wobei jeweils 2 aufeinanderfolgende Elemente (in deutsch und englisch) einen Datensatz bilden:
$Lang=array(
'Menü','Menu',
'Suche','Search',
...
... weitere Zeilen ...
...
'Siehe auch','See also', 'Technische Administration','technical administration' );
Die Ausgabefunktion holt den Text am berechneten Offset aus dem Array, ersetzt evtl. Platzhalter und fügt ihn per echo ein:
function WRSC($rid) {
  global $Lang,$LangID;
  $s=$Lang[$rid*2+$LangID]; $i=func_num_args();
  while (--$i) { $v=func_get_arg($i); $s=str_replace("%$i%",strval($v),$s); }
  echo htmlentities($s);
}

Textdatei

Die Datensätze werden zeilenweise hinterlegt, wobei am Zeilenanfang die ResourceID steht, gefolgt von 2 oder mehr Textabschnitten in den verschiedenen Sprachen. Als Trennzeichen zwischen den Elementen dient entweder ein Tabulator oder ein anderes Zeichen, das im Nutztext nicht vorkommt (hier: ein einfaches Hochkomma).

Ob nun jedes Dokument seine eigene Resourcedatei haben soll, oder ob alle Resourcen in einer einzigen, großen Datei zusammengefasst werden, ist im Grunde eine Geschmacksfrage und hängt auch davon ab, wie umfangreich die Site ist und wie die Entwicklung/Pflege der Resourcen organisiert wird.

In beiden Fällen ist der Rahmenablauf der selbe:
Um die Performance bei den Lesezugriffen zu erhöhen, sollten die Zeilen der Resourcedatei indexiert werden (Siehe Textdatei-Zeilenindexierung). Bei einer zentralen Textdatei muss diese Indexierungsfunktion jedoch modifiziert werden, um ausschließlich den relevanten Zeilenbereich zu ermitteln.

Textdatei: individuell

Individuelle Resourcen liegen normalerweise in einem eigenen Unterordner (hier: lang), wobei ihr Dateiname dem Zieldokument entspricht, allerdings mit typbedingter Extension wie 'rsc' oder 'dat':
index.php -> lang/index.rsc
search.php -> lang/search.rsc
impressum.php -> lang/impressum.rsc

So könnte eine index.rsc aussehen:
0'Menü'Menu
1'Suche'Search
...
... weitere Zeilen ...
...
22'Siehe auch'See also
23'Technische Administration'technical administration
Im Vorfeld muss die Textdatei geöffnet und das globale Filehandle versorgt werden. Dies erledigt die Funktion OpenRSC(), welche aus dem aktuellen Dokumentnamen den Resourcenamen ermittelt:
$LangFH=0;
OpenRSC();
...
function OpenRSC() { global $LangFH; $i=strrpos($doc=basename($_SERVER['PHP_SELF']),46); $LangFH=fopen('lang/'.substr($doc,0,$i+1).'rsc','r')); }
Am Ende des Skripts wird die Textdatei mit fclose($LangFH); wieder geschlossen.

Die Ausgabefunktion holt den Text aus der Datei, ersetzt evtl. Platzhalter und fügt ihn per echo ein:
function WRSC($rid) {
  global $LangFH,$LangID;
  $txt=''; fseek($LangFH,0);
  while (!feof($LangFH)) if (intval($s=fgets($LangFH,4096))==$rid) {
    $s=explode("'",$s); $txt=trim($s[1+$LangID]); break;
  }
  $i=func_num_args();
  while (--$i) { $v=func_get_arg($i); $s=str_replace("%$i%",strval($v),$txt); }
  echo htmlentities($txt);
}

Textdatei: zentral

Als zentrale Resource könnte eine Textdatei namens lang.rsc dienen. Vor jedem Dokumentbereich steht dann als Marke der reine Dateiname (oder eine entsprechende ID):
#index
0'Menü'Menu
1'Suche'Search
...
... weitere Zeilen ...
...
22'Siehe auch'See also
23'Technische Administration'technical administration
#search
0'Suche'Search
1'Eingabemaske'Input mask
...
... weitere Zeilen und Dokumente ...
Im Vorfeld muss die Textdatei geöffnet und das globale Filehandle versorgt werden. Um die spätere Lese-Performance zu erhöhen, wird ausserdem der Offset auf den relevanten Resource-Block festgehalten. Dies erledigt die Funktion OpenRSC(), welche anhand des aktuellen Dokumentnamens die Resource-Marke ermittelt:
$LangFH=$LangOff=0;
OpenRSC();
...
function OpenRSC() { global $LangFH,$LangOff; $i=strrpos($doc=basename($_SERVER['PHP_SELF']),46); $doc='#'.substr($doc,0,$i); $LangFH=fopen('lang.rsc','r')); $LangOff=0; while (!feof($LangFH)) if (trim(fgets($LangFH,4096))==$doc) { $LangOff=ftell($LangFH); break; } }
Am Ende des Skripts wird die Textdatei mit fclose($LangFH); wieder geschlossen.

Die Ausgabefunktion holt den Text aus der Datei, ersetzt evtl. Platzhalter und fügt ihn per echo ein:
function WRSC($rid) {
  global $LangFH,$LangOff,$LangID;
  $txt=''; fseek($LangFH,$LangOff);
  while (!feof($LangFH)) if (intval($s=fgets($LangFH,4096))==$rid) {
    $s=explode("'",$s); $txt=trim($s[1+$LangID]); break;
  }
  $i=func_num_args();
  while (--$i) { $v=func_get_arg($i); $s=str_replace("%$i%",strval($v),$txt); }
  echo htmlentities($txt);
}

Include-Datei

Resource-Module liegen normalerweise in einem eigenen Unterordner (hier: lang), wobei ihr Dateiname dem Zieldokument entspricht, allerdings mit typbedingter Extension wie 'rsc' oder 'dat':
index.php -> lang/index.rsc
search.php -> lang/search.rsc
impressum.php -> lang/impressum.rsc

So könnte eine index.rsc aussehen:
<?php $Lang=array(
'Menü','Menu',
'Suche','Search',
...
... weitere Zeilen ...
...
'Siehe auch','See also',
'Technische Administration','technical administration'
); ?> 
Im Vorfeld wird das Modul eingebunden:
include('lang/index.rsc');
Die Ausgabefunktion holt den Text am berechneten Offset aus dem Array, ersetzt evtl. Platzhalter und fügt ihn per echo ein:
function WRSC($rid) {
  global $Lang,$LangID;
  $s=$Lang[$rid*2+$LangID]; $i=func_num_args();
  while (--$i) { $v=func_get_arg($i); $s=str_replace("%$i%",strval($v),$s); }
  echo htmlentities($s);
}

MySQL-Tabelle

In der Datenbank reicht eine Tabelle, die sich bei Bedarf um weitere Spalten (Sprachen) erweitern lässt. Zum Auffinden eines bestimmten Satzes werden FileID und ResourceID benötigt. Beide Felder können optional indexiert werden, evtl. mit einem Index über beide Spalten:

lang
 FeldnameTyp 
0FIDsmallint[INDEX]FileID
1RIDsmallintResourceID
2rscDEtext(deutsch)
3rscENtext(englisch)
4rscFRtext(französisch)

Alternativ könnten FileID und ResourceID auch zusammengefasst werden:

lang
 FeldnameTyp 
0RIDint[INDEX] FileID*65536 + ResourceID
1rscDEtext(deutsch)
2rscENtext(englisch)
3rscFRtext(französisch)

Im Vorfeld finden das Öffnen der Datenbank, die Definition der Feldnamen und die Festlegung der globalen DateiID (hier z.B. 7) statt:
$db=mysql_connect(DB_HOST,DB_USER,DB_PASS); mysql_select_db(DB_NAME);
$LangStr=array('DE','EN','FR');
$FileID=7;
Die Ausgabefunktion holt den Text aus der Datenbank, ersetzt evtl. Platzhalter und fügt ihn per echo ein:
function WRSC($rid) {
  global $LangStr,$FileID,$LangID;
  $row=mysql_fetch_row(mysql_query("SELECT rsc".$LangStr[$LangID]." FROM lang WHERE FID=$FileID AND RID=$rid LIMIT 1"));
  $i=func_num_args();
  while (--$i) { $v=func_get_arg($i); $row[0]=str_replace("%$i%",strval($v),$row[0]); }
  echo htmlentities($row[0]);
}
Dokumente, an deren Übersetzung noch gearbeitet wird, sollte man natürlich nicht online stellen, weil die Resourcen noch unvollständig sind. Falls aber zumindest die Texte der Basissprache schon in der Datenbank stehen, könnte man immerhin provisorisch veröffentlichen:
Die Ausgabefunktion muss nur prüfen, ob im Falle eines Fremdsprachentextes der Eintrag noch aussteht. Wenn ja, wird die Resource in der Basissprache ausgegeben.
Ausserdem wird hier bei komplett fehlendem Eintrag (FileID oder ResourceID unbekannt) eine Fehlermeldung geschrieben, und auch Umbruch-Symbole (hier Pipe-Zeichen) werden durch <br> ersetzt:
function WRSC($rid) {
  global $LangStr,$FileID,$LangID;
  $row=mysql_fetch_row(mysql_query("SELECT rsc".$LangStr[$LangID]." FROM lang WHERE FID=$FileID AND RID=$rid LIMIT 1"));
  if (!$row) { echo "<b>ERROR(FID:$FileID,RID:$rid)</b>"; return; } // unbekannte FileID oder ResourceID
  if ($LangID&&!strlen($row[0])) // Fremdsprache noch ohne Eintrag
    $row=mysql_fetch_row(mysql_query("SELECT rsc".$LangStr[0]." FROM lang WHERE FID=$FileID AND RID=$rid LIMIT 1"));
  $i=func_num_args();
  while (--$i) { $v=func_get_arg($i); $row[0]=str_replace("%$i%",strval($v),$row[0]); }
  echo str_replace("\n",'<br>',htmlentities($row[0])); // Zeilenumbrueche
}

Manche Textelemente (wie z.B. 'Index', 'vorherige Seite' oder 'Kontakt') sind redundant, weil sie auf vielen Seiten benötigt werden.
Es handelt sich quasi um globale Resourcen, die an keine spezielle Datei gebunden sind. Daher weist man ihnen in der Datenbank die FileID 0 zu und gibt sie mit einer separaten Funktion GRSC() - für global resource - aus. Diese Funktion ist aufgebaut wie WRSC(); der einzige Unterschied besteht darin, dass beim Lesen nicht der Wert von $FileID, sondern immer FileID 0 benutzt wird:
function GRSC($rid) {
  global $LangStr,$LangID;
  $row=mysql_fetch_row(mysql_query("SELECT rsc".$LangStr[$LangID]." FROM lang WHERE FID=0 AND RID=$rid LIMIT 1"));
  if (!$row) { echo "<b>ERROR(global,RID:$rid)</b>"; return; } // unbekannte ResourceID
  if ($LangID&&!strlen($row[0])) // Fremdsprache noch ohne Eintrag
    $row=mysql_fetch_row(mysql_query("SELECT rsc".$LangStr[0]." FROM lang WHERE FID=0 AND RID=$rid LIMIT 1"));
  $i=func_num_args();
  while (--$i) { $v=func_get_arg($i); $row[0]=str_replace("%$i%",strval($v),$row[0]); }
  echo str_replace("\n",'<br>',htmlentities($row[0])); // Zeilenumbrueche
}

Index :: PHP/MySQL


template