PHP: Smarty und die Unterstützung mehrerer Sprachen / Multilingualität
Smarty ist die Templateengine für PHP schlechthin. Smarty ist sehr schnell und kann einfach erweitert werden. In diesem Artikel geht es darum, wie man Smarty mehrsprachig betreiben kann bzw. die Templates sprachunabhängig umsetzen kann. Am Ende des Artikels kann der Code heruntergeladen werden.
(Hinweis: In diesem Artikel wird PHP5 verwendet und vorrausgesetzt!)
Mit Smarty und ein paar Tricks ist es im Grunde sehr einfach, ein Projekt mehrsprachig zu gestalten. Dazu liest man alle nötigen Strings aus Sprachdateien aus und fügt sie an den jeweiligen Stellen ein. Freiwillige Übersetzer erstellen dann unterschiedliche Sprachdateien und man kann das Projekt auch in anderen Sprachen veröffentlichen.
Bei Smarty bedient man sich für diesen Zweck eines Prefilters. Ein solcher Prefilter verändert die Templates noch bevor sie von Smarty “kompiliert” werden.
So ist es z.B. möglich, ein Template wie folgt zu gestalten:
<table border="0"> <tr> <td>##user_form_username##</td> <td><input type="text" name="user" value="" /></td> </tr> <tr> <td>##user_form_password##</td> <td><input type="password" name="pass" value="" /></td> </tr> </table>
Die von Rauten (#) umschlossenen Zeichenketten spezifizieren einen bestimmten Sprach-String, dessen Ersetzung in den jeweiligen Sprachdateien angegeben wird. Eine deutsche Sprachdatei könnte etwa so aussehen:
user_form_username=Benutzername user_form_password=Password
Der Prefilter sucht also nach Sprachstrings und fügt an ihrer Stelle die entsprechende Ersetzung ein – je nachdem, welche Sprache eingestellt ist (z.B. in einer serverseitigen Konfigurationsdatei oder innerhalb der übermittelten Browserdaten des Clients usw.).
Für diese Funktionalität empfehle ich eine Klasse, die das Auslesen der Sprachdateien übernimmt. Der Einfachheit nenne ich sie “Lang“.
Ich muss aber darauf hinweisen, dass ich diese Klassen aus einem meiner eigenen Projekte entnommen habe. Daher kann es sein, dass ich teilweise vergessen habe, den Code an diesen Artikel anzupassen. Wenn also irgendetwas von “OBST” oder “XDB” dasteht, wisst ihr Bescheid.
Die Klasse Lang:
<?php /** * @author: Robert Nitsch */ class Lang { public $lng; protected $lng_strings; /** * Lädt alle Sprachstrings aus der entsprechenden Sprachdatei. * @param string $lng Die Sprache, die geladen werden soll, als Länderkürzel. */ function load_lng($lng) { $this->lng = $lng; /* DIR_ROOT muss außerhalb dieser Datei deklariert werden und muss den Pfad zum Hauptverzeichnis des Webprojekts beinhalten Außerdem setzt diese Klasse eine gewisse Verzeichnisstruktur vorraus... (/include/lng) */ $path = DIR_ROOT.'/include/lng/'.$lng.'.txt'; if(file_exists($path)) { $fh = fopen($path, 'r'); $matches = array(); while($zeile = fgets($fh)) { if(preg_match('/^([^;\s]+)\s*=([^;]*).*$/', $zeile, $matches)) { $this->lng_strings[$matches[1]] = $matches[2]; } } } else { throw new Exception("language file '$path' doesnt exist"); } } /** * Gibt einen Sprachstring zurück. * @param string $name Der Name des Sprachstrings. */ function get($name) { return $this->lng_strings[$name]; } }; /* -> singleton pattern (immer nur eine einzige Instanz dieser Klasse zulassen; Instanz(en) werden ausschließlich über diese Funktion erstellt) */ function getLangInstance($language) { static $lang_instance; if(!is_object($lang_instance)) { global $obst; $lang_instance = new Lang(); $lang_instance->load_lng($language); } return $lang_instance; } ?>
Die angepasste Smarty-Klasse nenne ich LangSmarty. Dem Konstruktor muss natürlich eine Instanz der Klasse Lang übergeben werden. Alles Weitere steht in den Kommentaren.
<?php /** * @author: Robert Nitsch */ class LangSmarty extends Smarty { /** * Speichert die Instanz der Klasse Lang ab. * @var Lang Instanz der Klasse Lang */ protected $lang; /** * Konstruktor * @param Lang $lang Eine Instanz der Klasse Lang, die die jeweiligen Sprachdaten beinhaltet. */ function LangSmarty(Lang &$lang) { $this->lang = $lang; // muss an das jeweilige Webprojekt angepasst werden $this->template_dir = './template_dir'; $this->compile_dir = './compile_dir'; $this->cache_dir = './cache_dir'; // den prefilter registrieren $this->register_prefilter(array(&$this, 'translate_prefilter')); } /** * Diese sogenannte Callback-Funktion wird für jedes Auftauchen von ##(...)## innerhalb von Templates * aufgerufen und gibt die jeweilige Ersetzung zurück. * @param string $string Die konkrete Übereinstimmung. Z.B. "##user_form_username##" */ function _translate_callback($string) { $string = substr($string[0], 2, strlen($string[0])-4); return $this->lang->get($string); } /** * Dies ist der Smarty-Prefilter. Er führt die Ersetzung der Sprachstrings ##(...)## * durch die Callback-Funktion _translate_callback() durch. */ function translate_prefilter($tpl_source, &$smarty) { return preg_replace_callback("/##[^#]*##/", array(&$this, '_translate_callback'), $tpl_source); } /** * Jede sprache soll einen eigenen Satz an Compiled- und Cached-Files haben. * Dafür wird das jeweilige Länderkürzel einfach in den Dateinamen dieser Dateien eingebaut. */ function fetch($_smarty_tpl_file, $_smarty_cache_id = null, $_smarty_compile_id = null, $_smarty_display = false) { $_smarty_compile_id = $this->lang->lng.$_smarty_compile_id; $_smarty_cache_id = $_smarty_compile_id; return parent::fetch( $_smarty_tpl_file, $_smarty_cache_id, $_smarty_compile_id, $_smarty_display); } } ?>
Anwendung:
<?php $lang = getLangInstance('de'); // instanz der Lang-Klasse erstellen $smarty = new LangSmarty($lang); // instanz der LangSmarty-Klasse erstellen, dem Konstruktor wird eine Lang-Instanz übergeben $smarty->display('startseite.tpl'); // das Template startseite.tpl kompilieren und das Ergebnis dessen Ausführung (ja, bei Smarty werden Templates ausgeführt) an den Client schicken // Sprachstrings werden automatisch eingefügt, sofern im Template vorhanden ?>
Das Ganze lässt sich aber noch optimieren. Die Lösung mit den oben gezeigten Sprachdateien ist zwar einfach, aber bei großen Projekten kann es in viel Arbeit ausarten alle Sprachdateien aktuell zu halten. Auch wenn man diese Arbeit an freiwillige Übersetzer delegieren kann, so empfiehlt es sich, das System generell zu verbessern.
Dies kann mit Hilfe von gettext erreicht werden.
Zitat Wikipedia:
GNU gettext ist die GNU-Internationalisierungsbibliothek. Normalerweise wird sie zur Entwicklung von mehrsprachigen Programmen genutzt. Die derzeit aktuelle Version ist 0.16.1.
Und ja, auch in PHP kann man auf gettext zugreifen: Gettext-Funktionen
Dank der objektorientieren Programmierung müssen wir nur die Klasse Lang an gettext anpassen. Das werde ich in diesem Artikel aber nicht mehr bis ins Detail erklären. Letztendlich läuft es darauf hinaus, dass man nur ein paar Zeilen verändert (nicht aber wesentliche Strukturen).
Der größte Aufwand steckt allerdings in der Gewöhnung an gettext. Bevor man auf gettext umsteigt, sollte man sich daher etwas einlesen. Mir hat der Text ONLamp.com — Gettext sehr gefallen…
Code herunterladen
Es macht keinen Sinn, den Code direkt aus dem Beitrag herauszukopieren, da verschiedene Zeichen von WordPress ungünstig umgewandelt werden. Daher hier der Download des ZIP-Archivs mit den Klassen Lang und LangSmarty:
PHP-Smarty-Mehrsprachig.zip herunterladen
Hinweis: Die Dateien sind UTF-8-kodiert. Linux-Benutzer werden damit keine Probleme haben, Windows-Nutzer sollten auf den Notepad++ – Editor zurückgreifen.

Hallo, tolle Lösung!!!
Ich hab das Ganze mal umgesetzt für das Zend Framework (ZF).
Zur Integration von Smarty in das ZF gibt es eine View-Klasse, die Smarty entsprechend dem Zend_View_Interface wrapped. Dort habe ich den Filter und die Callback-Funktion eingebaut:
/**
* Diese sogenannte Callback-Funktion wird für jedes Auftauchen von ##…## innerhalb von Templates
* aufgerufen und gibt die jeweilige Ersetzung zurück.
* @param string $string Die konkrete Übereinstimmung. Z.B. “##user_form_username##)”
*/
function _translate_callback($string) {
$string = substr($string[0], 2, strlen($string[0])-4);
return Zend_Registry::get(‘lang’)->translate($string);
}
/**
* Dies ist der Smarty-Prefilter. Er führt die Ersetzung der Sprachstrings ##…##
* durch die Callback-Funktion _translate_callback() durch.
*/
function translate_prefilter($tpl_source, &$smarty) {
return preg_replace_callback(“/##.*##/”, array($this, “_translate_callback”), $tpl_source);
}
Dann habe ich noch die funktion render() von meiner Zend_View-Implementierung verändert:
public function render($tpl_source) {
// Jede Sprache soll einen eigenen Satz an Compiled- und Cached-Files haben.
// Dafür wird das jeweilige Länderkürzel einfach in den Dateinamen dieser Dateien eingebaut.
if (strpos($this->_smarty->compile_id, Zend_Registry::get(‘lang’)->getLocale()) === false) {
$this->_smarty->compile_id = Zend_Registry::get(‘lang’)->getLocale().$this->_smarty->compile_id;
$this->_smarty->cache_id = $this->_smarty->compile_id;
}
return $this->_smarty->fetch($tpl_source);
}
Ich würde nun gern das in Gettext übliche _(…) als Stringkennzeichnung benutzen.
Weiterhin bekomme ich einen merkwürdigen Fehler. In der ersten Zeile meines Templates wird der String nur ersetzt, wenn die hintere Begrenzung weggelassen wird:
##Benutzer ##zurück zur Liste
fasse ich die Strings richtig ein:
##Benutzer## ##zurück zur Liste##
Entsteht das:
Benutzer##
##zurück zur Liste
Kommentar by Axel — 29.06.2007 @ 14:19:23
Der Code im Artikel enthält meines Erachtens keinen Fehler, da ich ihn selbst so verwende. Der Fehler muss irgendwo anders liegen. Jedoch kann ich das nicht ermitteln, da ich keinen Zugriff auf ihr Projekt habe. Außerdem ist das auch nicht meine Aufgabe.
Versichern kann ich aber ernsthaft, dass die Codes im Artikel korrekt sind. Vielleicht gibt es irgendwelche PHP-Einstellungen, die sich auf die Auswertung regulärer Ausdrücke in einem Maße auswirken, dass solche Fehler wie bei dir auftreten.
Der Underscore “_” lässt sich ganz einfach einbauen, indem du translate_prefilter und _translate_callback anpasst. Das ist sehr einfach, darum erspare ich uns die Beschreibung.
MfG, Robert Nitsch
Kommentar by Robert Nitsch — 06.07.2007 @ 11:58:38
Hallo
Ersteinmal klasse post da man sehr gut davon lernen kann.
Jedoch hab ich ein kleines Problem. Habe deine lang.php genommen und alles gemacht wie beschrieben. Leider bekomme ich immer diese meldung
Parse error: syntax error, unexpected T_STRING, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or ‘}’ in /var/www/vhosts/meinedomain.de/httpdocs/kci/core/lang.php on line 13
Hast du ne idee?
Gruss
Dragon
Kommentar by Dragon — 12.10.2007 @ 19:59:15
Hallo Dragon,
danke erstmal.
Auf den ersten Blick fällt mir nur ein, dass PHP in Version 5 benötigt wird (das habe ich jetzt auch im Artikel ergänzt), weil der Code auf Sprachelemente zurückgreift (z.B. Datenkapselung: “public”, “private” usw.), die erst seit PHP5 unterstützt werden.
Ansonsten fällt mir noch ein, dass es derzeit leider sehr schwierig ist, Quelltexte aus meinen Artikeln zu kopieren. Der Link “Show Plain Text” funktioniert nämlich nicht so, wie er eigentlich sollte. Am besten gehst du ganz sicher, dass du den Code 100% richtig kopiert hast.
Wenn du möchtest kannst du mir die Datei, die du auf deinem Webspace eingebunden hast, auch gerne per Mail zuschicken, damit ich sie mir mal ansehe. Meine Addresse hierfür ist dev at robertnitsch dot de.
MfG, Robert Nitsch
Kommentar by Robert Nitsch — 12.10.2007 @ 20:15:02
ok werds dir morgen mittag mal schicken. PS gibet das auch in php4? weil wenn ich erlioch bin finde das man immer so es machen sollte das es auf 4 und 5 leüft.
Aber soll kein vorwurf sein trozdem klasse arbeit. Hab selten so ein gutes verständliches tutorial gelesen
Kommentar by Dragon — 13.10.2007 @ 00:23:31
Der Support für PHP4 wird offiziell Ende des Jahres 2007 eingestellt. Mit Anfang 2008 wird also nur noch PHP5 weiterentwickelt usw.usf.
Und wir dürfen nicht vergessen, dass wir noch nicht wissen, ob es wirklich an der PHP-Version liegt. WENN es aber an der PHP_Version liegt, dann ersetze einfach alle Klassenvariablendefinitionen wie z.B. public/protected/private $variable; mit:
Das war jedoch nicht der Grund dafür, das Tutorial an PHP5 “anzupassen”. Nein, ich habe es einfach aus Gewohnheit so gemacht – also eher unbewusst.
var $variable;
Public, protected und private sind nämlich Bestandteile von PHP5. In PHP4 hat man stattdessen einfach nur “var” genommen.
Ich bin mir jedoch derzeit nicht ganz sicher, wie es sich mit “parent” verhält:
return parent::fetch( $_smarty_tpl_file, $_smarty_cache_id, $_smarty_compile_id, $_smarty_display);
Diese Anweisung bereitet evtl. Probleme bei der Portierung zu PHP4. Zur Not muss man eben nicht nur die Syntax des Codes an PHP4 anpassen, sondern auch die Logik…
Aber im Moment ist es mir etwas zu spät dafür. Ich hau mich ins Bett. =)
MfG, Robert Nitsch
Kommentar by Robert Nitsch — 13.10.2007 @ 01:43:19
hm also bei mir funzt es selbt mit php5 nicht bekomm nochmehr errors
so sieht der code aus bei mir
lng = $lng;
/* DIR_ROOT muss außerhalb dieser Datei deklariert werden und muss den Pfad
zum Hauptverzeichnis des Webprojekts beinhalten
Außerdem setzt diese Klasse eine gewisse Verzeichnisstruktur vorraus… (/include/lng) */
$path = DIR_ROOT.‘/include/lng/’.$lng.‘.txt’;
if(file_exists($path))
{
$fh = fopen($path, ‘r’);
$matches = array();
while($zeile = fgets($fh))
{
if(preg_match(‘/^([^;\s]+)\s*=([^;]*).*$/’, $zeile, $matches))
{
$this->lng_strings[$matches[1]] = $matches[2];
}
}
}
else
{
throw new Exception(“language file ‘$path’ doesnt exist”);
}
}
/**
* Gibt einen Sprachstring zurück.
* @param string $name Der Name des Sprachstrings.
*/
function get($name)
{
return $this->lng_strings[$name];
}
};
/*
-> singleton pattern
(immer nur eine einzige Instanz dieser Klasse zulassen;
Instanz(en) werden ausschließlich über diese Funktion erstellt)
*/
function getLangInstance($language) {
static $lang_instance;
if(!is_object($lang_instance)) {
global $obst;
$lang_instance = new Lang();
$lang_instance->load_lng($language);
}
return $lang_instance;
}
?>
Kommentar by Dragon — 13.10.2007 @ 14:26:03
Schick mir bitte den Code als >Datei< per Mail. So hat das keinen Sinn, ich blicke da keinen Meter durch.
Außerdem solltest du die Fehlermeldungen dazuschreiben.
MfG, Robert Nitsch
Kommentar by Robert Nitsch — 17.10.2007 @ 17:48:33