Multilanguage Class

Psychomentis

Mitglied
Ich arbeite gerade an einem PHP Framework zu lernzwecken und habe in der letzten
Nacht eine Multilanguage Class geschrieben und möchte von Euch der Community ein
Feedback erhalten oder gar Verbesserungsvorschläge bekommmen.

So nun komme ich zum Klassenaufbau:
  • __construct() Durchsucht die Sprachverzeichnisse nach Sprachen und füllt eine Array damit. Danach werden doppelte Einträge gelöscht so wie die Sprache gesetzt.
  • setPath() fügt einen Dateipfad der Array bei.
  • getPath() gibt die Dateipfad Array aus.
  • setLanguage() Die Sprach kann manuel festgelegt werden und wird in die Session gespeichert.
  • getLanguage() Gibt die aktuelle Sprache zurück.
  • getLanguageList() Gibt eine Liste der verfügbaren Sprachen zurück.
  • detectLanguage() Ermittelt die Standard Sprache des Browsers.
  • loadLanguage() Lädt alle Sprachdateien in die Klasse.
  • get() Anhand des indexes wird der Wert aus der Sprachdateiarray ausgelesen und zurückgegeben.
  • set() Einzelne oder mehrere Einträge können hinzugefügt werden.

Desweiteren gibt es eine Fallback Funktion auf die englische Sprache.

So nun aber mal der komplette Code:
index.php
PHP:
session_start();

# Timer for Debugging Info
$time_start = microtime();


ini_set('display_errors', 1);
error_reporting(~0);


define('ROOT', dirname(__FILE__));
define('DS', DIRECTORY_SEPARATOR);


include(ROOT.DS.'System'.DS.'Language.php');

$lang = new Language();


# Debugging Info
echo "Verarbeitungszeit des Skripts: ".sprintf('%.3f', (microtime() - $time_start))." Sekunden"

Language.php
PHP:
class Language
{
	private $_lang = array();
	private $_language = '';
	private $_languages = array();
	private $_language_path = array();
	
	public function __construct()
	{
		$path = ROOT.DS.'lang'.DS;
		$this->setPath($path);
		
		// loading languages
		foreach($this->_language_path as $path)
		{
			if ($handle = opendir($path))
			{
				while (false !== ($file = readdir($handle)))
				{
					if ($file != "." && $file != "..")
					{
						array_push($this->_languages, str_replace(".php", "", $file));
					}
				}
				closedir($handle);
				array_unique($this->_languages);
			}
			else
			{
				continue;
			}
		}
		
		$this->setLanguage($this->detectLanguage());
	}
	
	public function setPath($path)
	{
		if(is_dir($path))
		{
			if(!array_key_exists($path, $this->_language_path))
			{
				array_push($this->_language_path, $path);
			}
		}
		
		$this->loadLanguage();
	}
	
	private function getPath()
	{
		return $this->_language_path;
	}
	
	public function setLanguage($lang)
	{		
		if(in_array($lang, $this->_languages))
		{
			$this->_language = strtolower($lang);
		}
		else
		{
			$this->_language = 'en';
		}
		
		$this->loadLanguage();
	}
	
	public function getLanguage()
	{
		return in_array($this->_language, $this->_languages) ? $this->_language : "en";
	}
	
	public function getLanguageList()
	{
		return $this->_languages;
	}
	
	private function detectLanguage()
	{
		$lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
		
		if(in_array($lang, $this->_languages))
		{
			$this->_language = $lang;
		}
		
		return $lang;
	}
	
	private function loadLanguage()
	{		
		foreach($this->_language_path as $path)
		{
			$file = $path . $this->getLanguage() . '.php';
			
			if(!file_exists($file))
			{
				$file = str_replace($this->getLanguage(), "en", $file);
			}
			
			include($file);
			
			$this->_lang = array_merge($this->_lang, $lang);
		}
	}
	
	public function get($tag)
	{
		return array_key_exists(strtoupper($tag), $this->_lang) ? $this->_lang[strtoupper($tag)] : '';
	}
	
	public function set()
	{
		$args = func_get_args();
		
		if(is_array($args[0]))
		{
			foreach($args[0] as $key => $value)
			{
				if(!array_key_exists(strtoupper($key), $this->_lang))
				{
					$this->_lang[strtoupper($key)] = $value;
				}
			}
		}
		else
		{
			if(!array_key_exists(strtoupper($key), $this->_lang))
			{
				$this->_lang[strtoupper($args[0])] = (isset($args[1])) ? $args[1] : "";
			}
		}
	}
}

en.php
PHP:
if(empty($lang) || !is_array($lang))
{
	$lang = array();
}

$lang = array_merge($lang, array(
	'DE'	=> "German",
	'EN'	=> "English",
	'FR'	=> "French",
));

über das Feedback und Verbesserungsvorschläge freue ich mich bereits jetzt.
 
Zuletzt bearbeitet:
Wenn ich darf ;-)

Also, im Konstruktor schleifst du über alle Einträge in Ordner "lang". Was passiert, wenn ich dort einen Ordner anlege oder evtl. eine komplett andere Datei wie .txt oder .xml hinterlege?

$_SESSION ist ohnehin eine superglobale, nicht notwendig, sie als global zu deklarieren (in Methode getLanguage(), detectLanguage()).

Du hast kaum Fehlerbehandlung, wenn man nur ein Argument an die Methode set() übergibt, werden möglicherweise Daten überschrieben, obwohl es nichts zu überschreiben gibt. Außerdem wird ein Warning über undefined index 1 geworfen.

Möglicherweise kannst du etwas Zeit sparen im Konstruktor, wenn du Caching für die Sprachen verwendest (serialize()/unserialize()).


Das ist mir auf die Schnelle aufgefallen.

Ansonsten aber ganz nett :)
 
Also, im Konstruktor schleifst du über alle Einträge in Ordner "lang". Was passiert, wenn ich dort einen Ordner anlege oder evtl. eine komplett andere Datei wie .txt oder .xml hinterlege?

Den Ort wo du deine Sprachdateien speicherst kann man ja mit setPath bestimmt.
Dem entsprechend werden auch wenn du im "lang" Ordner eine Subdirectory erstellst nach dem setPath die dort vorhandenen Dateien eingelesen.

Zu der schleife die dort abläuft es werden sämtliche Dateien eingelesen nur habe ich bis jetzt nur die PHP-Datei Extension mit str_replace entfernt. Weil ich eigentlich nur mit ".php" dateien im moment arbeite ^^, aber die Idee ist gut andere datei extension auch noch zu filtern.
Werde mir dazu was einfallen lassen.

$_SESSION ist ohnehin eine superglobale, nicht notwendig, sie als global zu deklarieren (in Methode getLanguage(), detectLanguage()).

Ok den habe ich bei meiner Recherche nicht genau aufgepasst xD wurde jetzt aber entfernt.

Du hast kaum Fehlerbehandlung, wenn man nur ein Argument an die Methode set() übergibt, werden möglicherweise Daten überschrieben, obwohl es nichts zu überschreiben gibt. Außerdem wird ein Warning über undefined index 1 geworfen.

Das ist mir so garnicht aufgefallen mag auch ein wenig am Schlafmangel gelegen haben^^.
Werde mir dazu gleich mal was einfallen lassen.

Möglicherweise kannst du etwas Zeit sparen im Konstruktor, wenn du Caching für die Sprachen verwendest (serialize()/unserialize()).

Hättest du da vielleicht einen vorschlag für mich?

Ansonsten aber ganz nett :)

Da bin ich ja froh das es nicht totaler Mist ist. xD
Über die Fehlerbehandlung habe ich mir bisher noch garkeine gedanken gemacht....
Eigentlich will ich später das keine Fehler an den Nutzer gesendet werden sondern separat in einem Logfile landen.
 
Zuletzt bearbeitet:
Normalerweise werden die Übersetzungen aber aus einer DB kommen :p und lass das mit der Session weg.. Das gehört in eine andere klasse. Grundsatz: ein Objekt, eine aufgabe
 
Hallo alxy

Hmm ich finde die Idee die Sprachen aus einer Datenbank auszulesen nicht so schön...
Gehen wir mal davon aus das ich im späteren Verlauf 56 Sprachen anbiete und es nachher etwa
5000 oder mehr... Wörter oder gar Texte gibt die ich pro Sprache in die Datenbank speichern soll.
Ergibt sich nachher eine riesige Menge an Daten die ich aus der Datenbank ziehen muss.
Somit belaste ich den Datenbank Server nicht später mit tausenden von Queries. ^^
Zumal ist noch garnicht die Rede das es überhaupt eine Datenbank geben wird. ^^
Oder habe ich eine falsche denkweise?

Wieso die Session weglassen?
Sie gehört doch schon zu der Aufgabe.
Ich habe mir bisher noch keine gedanken über einen Sessionhandler gemacht.

Sicher kann ich die aktuelle Sprache auch in einer variable der klasse speichern.
 
Zuletzt bearbeitet:
nur habe ich bis jetzt nur die PHP-Datei Extension mit str_replace entfernt. Weil ich eigentlich nur mit ".php" dateien im moment arbeite

Richtig, du entfernst einen Teil-String in einem anderen String, was ist, wenn der Teil-String gar nicht vorkommt? Das ist keine Prüfung auf Plausibilität und daher ein Sicherheitsrisiko. Der kommende Include in der Methode loadLanguage() wird auf einen Fehler laufen, denn eine Datei, die in "lang" liegt, mit dem Namen "english.txt" wird also über "include(pfad . 'lang' . 'english.txt.php);" eingebunden, existiert aber gar nicht ;-)

Hättest du da vielleicht einen vorschlag für mich?

Hatte ich bereits gemacht. Mit serialize() das Ergebnisarray der Schleife serialisieren, und weg speichern. Vor dem Ausführen der Schleife prüfen, ob es eine weggespeicherte Datei gibt, einlesen und de-serialisieren. Z.B. so:

PHP:
    public function __construct()
    {
        $path = ROOT.DS.'lang'.DS;
        $this->setPath($path);
        
        if( file_exists('language_cache.data') )
        {
          $this->languages = unserialize(file_get_contents('language_cache.data'));
        }
        else
        {
          // loading languages
          foreach($this->_language_path as $path)
          {
            if ($handle = opendir($path))
            {
                while (false !== ($file = readdir($handle)))
                {
                    if ($file != "." && $file != "..")
                    {
                        array_push($this->_languages, str_replace(".php", "", $file));
                    }
                }
                closedir($handle);
                array_unique($this->_languages);
            }
            else
            {
                continue;
            }
          }
          file_put_contents( 'language_cache.data', serialize($this->_languages) );
        }
        
        $this->setLanguage($this->detectLanguage());
    }


Über die Fehlerbehandlung habe ich mir bisher noch garkeine gedanken gemacht....

Das solltest du aber ;-) Nicht nur im eigenen Interesse.

Eigentlich will ich später das keine Fehler an den Nutzer gesendet werden sondern separat in einem Logfile landen.

Und dafür brauchts du Plausibilitätsprüfungen. Denn wie willst denn sonst was sinnvolles loggen?
 
Richtig, du entfernst einen Teil-String in einem anderen String, was ist, wenn der Teil-String gar nicht vorkommt? Das ist keine Prüfung auf Plausibilität und daher ein Sicherheitsrisiko. Der kommende Include in der Methode loadLanguage() wird auf einen Fehler laufen, denn eine Datei, die in "lang" liegt, mit dem Namen "english.txt" wird also über "include(pfad . 'lang' . 'english.txt.php);" eingebunden, existiert aber gar nicht ;-)
Ok da werde ich mich morgen mal dran setzen.

Hatte ich bereits gemacht. Mit serialize() das Ergebnisarray der Schleife serialisieren, und weg speichern. Vor dem Ausführen der Schleife prüfen, ob es eine weggespeicherte Datei gibt, einlesen und de-serialisieren. Z.B. so:

PHP:
    public function __construct()
    {
        $path = ROOT.DS.'lang'.DS;
        $this->setPath($path);
        
        if( file_exists('language_cache.data') )
        {
          $this->languages = unserialize(file_get_contents('language_cache.data'));
        }
        else
        {
          // loading languages
          foreach($this->_language_path as $path)
          {
            if ($handle = opendir($path))
            {
                while (false !== ($file = readdir($handle)))
                {
                    if ($file != "." && $file != "..")
                    {
                        array_push($this->_languages, str_replace(".php", "", $file));
                    }
                }
                closedir($handle);
                array_unique($this->_languages);
            }
            else
            {
                continue;
            }
          }
          file_put_contents( 'language_cache.data', serialize($this->_languages) );
        }
        
        $this->setLanguage($this->detectLanguage());
    }
Wenn ich das richtig sehe spare ich damit zwar Zeit doch muss ich mir so wieder Gedanken machen.
Was ist wenn eine neue Sprache hinzugekommen ist nachdem ich den cache angelegt habe.
So muss ich nach jedem hinzufügen einer Sprache den cache löschen.

Das solltest du aber ;-) Nicht nur im eigenen Interesse.
Und dafür brauchts du Plausibilitätsprüfungen. Denn wie willst denn sonst was sinnvolles loggen?

Werde ich mir gleich Morgen mal zur Brust nehmen.
 
Wenn ich das richtig sehe spare ich damit zwar Zeit doch muss ich mir so wieder Gedanken machen.
Richtig, und bei 56 oder mehr Sprachen kann das ein bisschen Zeit ausmachen. Dateisystem-Operationen kosten immer. ;-) Wenn ich nur 2 (file_exists, file_get_contents) statt 60 (opendir, readdir [2x ignorieren für . und ..], closedir) machen muss, ist das schon ein Unterschied ;-)

Was ist wenn eine neue Sprache hinzugekommen ist nachdem ich den cache angelegt habe.
So muss ich nach jedem hinzufügen einer Sprache den cache löschen.

Japp, das ist der Trade-off. Musst du selbst entscheiden - wie oft kann es vorkommen, das eine neue Sprache hinzukommt und steht das im Verhältnis zum Caching (ich denke schon, denn die Instanzierung der Translator-Klasse wird mit Sicherheit öfter statt finden als das hinzufügen einer neuen Sprache).
 
Richtig, und bei 56 oder mehr Sprachen kann das ein bisschen Zeit ausmachen. Dateisystem-Operationen kosten immer. ;-) Wenn ich nur 2 (file_exists, file_get_contents) statt 60 (opendir, readdir [2x ignorieren für . und ..], closedir) machen muss, ist das schon ein Unterschied ;-)

Ja ok in anbetracht das später ein Modulsystem folgen soll ist das definitiv die bessere Praxis.

Japp, das ist der Trade-off. Musst du selbst entscheiden - wie oft kann es vorkommen, das eine neue Sprache hinzukommt und steht das im Verhältnis zum Caching (ich denke schon, denn die Instanzierung der Translator-Klasse wird mit Sicherheit öfter statt finden als das hinzufügen einer neuen Sprache).

Das stimmt habe mir auch schon eine Möglichkeit notiert das System intern zu lösen.
In der Administrationoberfläche eine Funktion zu integrieren die die Sprache downloaded, ins Verzeichnis speichert und den Cache löscht.
 
Zurück