bestimmte Sprache aus mehrsprachigem String filtern
bearbeitet von 1unitedpower> > Ich habe deinen Lösungsweg ehrlichgesagt nicht verstanden.
>
> Es geht um HTML-Dateien mit Platzhaltern, die von PHP-Programmen gelesen, ausgefüllt und an den Client ausgeliefert werden.
Okay, mein Rat ist weiterhin PHPs interne String-Interpolation zu benutzen.
Wenn du an deinem Ansatz festhälst, möchte ich trotzdem versuchen dir zu helfen. Tabellenkalk hat schon richtig erkannt, dass das wesentliche Problem an deinem Ansatz ist, dass du die selbe Zeichenkette `###` für den Array-Anfang und das Array-Ende benutzt. Der Ansatz hat zwei, drei weitere kleine Probleme: Du benutzt die selbe Zeichenkette auch noch als Trennsymbol zwischen den Array-Elementen. In den übersetzten Texten darf die Zeichenkette `###` nicht auftauchen und übersetzte Text dürfen nicht mehrzeilig sein.
Ich würde vorschlagen, du überdenkst die Syntax nochmal. Zum Beispiel könntest du eckige Klammern für den Beginn und das Ende des Arrays benutzen und Kommata als Trennsymbole zwischen den Array-Elementen. Wenn eines dieser Sonderzeichen innerhalb eines übersetzten Textes vorkommt, kann man es durch einen Backslash maskieren.
Also, wird aus deinem Beispiel:
~~~php
$zeile = "<p>textA: ###de-1###en-1###nl-1###fr-1###es-1### textB: ###deutsch###english###Nederlands###francais###espanol### textC: ###de-2###en-2###nl-2###fr-2###es-2###</p>";
~~~
Nun folgendes:
~~~php
zeile = "<p>textA: [de-1,en-1,nl-1,fr-1,es-1] textB: [deutsch,english,Nederlands,francais,espanol] textC: [de-2,en-2,nl-2,fr-2,es-2]</p>";
~~~
Obendrein wird das doch viel besser lesbar.
Im Prinzip baust du eine eigene Mini-Programmiersprache. Ein Compiler für so eine Programmiersprache durchläuft konzeptionell drei Phasen: Tokenization, Parsen und Code-Generation. Tokenization beschäftigt sich mit der lexikalischen Strukut der Programmiersprache, Parsen beschäftigt sich mit der syntaktischen Struktur. Die Code-Generierung schließlich macht aus dem Zwischenformat, dass ihm der Parser liefert, eine Repräsentation im Zielformat (hier also ein PHP-String.)
Wir bauen also eine Funktion:
~~~php
function compile(int $language, string $input) : string
{
return evaluate($language, parse(tokenize($input)));
}
~~~
Ein Aufruf wird später so aussehen:
~~~php
echo compile(1, '<p>textA: [de-1,en-1,nl-1,fr-1,es-1] textB: [deutsch,english,Nederlands,francais,espanol] textC: [de-2,en-2,nl-2,fr-2,es-2]</p>');
// <p>textA: en-1 textB: english textC: en-2</p>
~~~
Fangen wir mit der `tokenize`-Funktion an. Aufgabe der Tokenize-Funktion ist den Eingabe-String in einzelne Stücke sogenannte Token zu zerlegen, und zwar so, dass jedes Sonderzeichen zu einem eigenen Stück wird und alle Zeichenketten, die kein Sonderzeichen enthalten, sollen zusammen bleiben. Wenn ein Sonderzeichen maskiert wurde, soll es auch es keinen eigenen Token bekommen, sondern zum vorherigen dazugezählt werden.
Wir haben also 4 Arten von Token: Eine öffnende Klammer, eine schließende Klammer, ein Komma, und rohen Text ohne Sonderzeichen. In PHP könnte eine Token-Klasse so aussehen:
~~~php
final class Token
{
const OPEN = 0;
const CLOSE = 1;
const COMMA = 2;
const PLAINTEXT = 3;
private $type;
private $content;
public function __construct(int $type, string $content)
{
$this->type = $type;
$this->content = $content;
}
public function getContent() : string
{
return $this->content;
}
public function getType() : int
{
return $this->type;
}
}
~~~
Kommen wir jetzt zur eigentlichen `tokenize`-Funktion. Die Funktion muss den Eingabe-String Zeichen für Zeichen einlesen und jeweils entscheiden, ob es sich um ein Sonderzeichen handelt oder nicht. Wenn ein Sonderzeichen vorliegt, dann geben wir ein entsprechendes Token dieser Art aus. Wenn kein Sonderzeichen vorliegt, schreiben wir das Zeichen in einen Zwischenspeicher, einen sogenannten Buffer. Sobald wir wieder ein Sonderzeichen lesen oder am String-Ende angelangt sind, machen wir aus dem Buffer ein PLAINTEXT-Token und setzen den Buffer anschließend zurück. Und wie gesagt, Sonderzeichen können maskiert werden, dafür müssen wir Sorge tragen. Die Funktion könnte dann so aussehen:
~~~php
function tokenize(string $input) : Generator
{
$buffer = '';
$escape = false;
for ($i = 0; $i < mb_strlen($input); $i++) {
$char = mb_substr($input, $i, 1);
if ($escape) {
$buffer .= $char;
$escape = false;
} else {
switch ($char) {
case '[':
yield new Token(Token::PLAINTEXT, $buffer);
$buffer = '';
yield new Token(Token::OPEN, $char);
break;
case ']':
yield new Token(Token::PLAINTEXT, $buffer);
$buffer = '';
yield new Token(Token::CLOSE, $char);
break;
case ',':
yield new Token(Token::PLAINTEXT, $buffer);
$buffer = '';
yield new Token(Token::COMMA, $char);
break;
case '\\':
$escape = true;
break;
default:
$buffer .= $char;
}
}
}
if ($buffer !== '') {
yield new Token(Token::PLAINTEXT, $buffer);
}
}
~~~
Kommen wir als nächstes zur `parse`-Funktion. Diese Funktion bekommt die Tokens als Eingabe und macht daraus eine neue Liste von Elementen, die dieses mal nicht Token genannt werden, sondern Knoten. Ein Knoten ist entweder ein Rohtext ohne Übersetzungen oder ein Sammlung von Übersetzungs-Texten.
~~~php
interface Node {
}
final class PlainText implements Node {
private $content;
public function __construct($content) {
$this->content = $content;
}
public function getContent() {
return $this->content;
}
}
final class TranslatedText implements Node
{
private $dictionary;
public function __construct($dictionary)
{
$this->dictionary = $dictionary;
}
public function translate($key) {
return $this->dictionary[$key];
}
}
~~~
Der Parser arbeitet so ähnlich wie der Lexer. Nochmal zur Verdeutlichung: Der Lexer geht den Eingabestring Zeichen für Zeichen durch und macht daraus eine Liste von Tokens. Der Parser geht nun diese neue Liste Token für Token durch und macht daraus eine Liste von Knoten. Das machen wir deshalb, weil nicht jedes Komma und jede eckige Klammer auch tatsächlich eine Sonderrolle spielt. Kommata, zum Beispiel, haben nur innerhalb von eckigen Klammern eine besondere Bedeutung. Auf der anderen Seite hat die öffnende Klammer innerhalb eines übersetzten Textes keine spezielle Bedeutung mehr. Aufgabe des Parsers ist es diese Fälle zu unterscheiden. Wir brauchen also zumindest eine Variable, in der wir uns merken, ob wir zwischen eckigen Klammern stecken. Außerdem brauchen wir diesmal zwei Buffer, in denen wir uns unvollständige Rohtexte bzw. übersetzte Texte merken.
~~~php
function parse(Generator $tokens) : Generator
{
$insideBrackets = false;
$textBuffer = '';
$languageBuffer = [];
foreach ($tokens as $token) {
switch ($token->getType()) {
case Token::OPEN:
if ($insideBrackets) {
$textBuffer .= $token->getContent();
} else {
yield new PlainText($textBuffer);
$insideBrackets = true;
$textBuffer = '';
}
break;
case Token::CLOSE:
if ($insideBrackets) {
$languageBuffer[] = $textBuffer;
yield new TranslatedText($languageBuffer);
$insideBrackets = false;
$textBuffer = '';
$languageBuffer = [];
} else {
$textBuffer .= $token->getContent();
}
break;
case Token::COMMA:
if ($insideBrackets) {
$languageBuffer[] = $textBuffer;
$textBuffer = '';
} else {
$textBuffer .= $token->getContent();
}
break;
case Token::PLAINTEXT:
$textBuffer .= $token->getContent();
break;
default:
yield new Exception('Unkown type of token:' . $token->getType());
}
}
if ($insideBrackets) {
yield new Exception('Missing closing square bracket.');
} elseif ($textBuffer !== '') {
yield new PlainText($textBuffer);
}
}
~~~
Das war der komplizierte Teil. Wir haben jetzt also eine Liste von Knoten, manche speichern verschiedene Übersetzungen, andere nur einen Rohtext. Jetzt kommt der einfache Teil: Je nach Sprache, möchten wir aus dieser Liste von Texten einen zusammenhängenden Text in der jeweiligen Zielsprache machen:
~~~php
function evaluate(int $language, iterable $nodes) : string
{
$buffer .= '';
foreach ($nodes as $node) {
if ($node instanceof PlainText) {
$buffer .= $node->getContent();
} elseif ($node instanceof TranslatedText) {
$buffer .= $node->translate($language);
} elseif ($node instanceof Exception) {
throw $node;
}
}
return $buffer;
}
~~~
Damit wäre der Code komplett, ich hab ihn aber nur sehr wenig getestet. Ich hoffe die Vorgehensweise wurde einigermaßen deutliche. Das sieht zugegebenermaßen ziemlich komplex aus, ist es auch. Deswegen nochmal mein Rat, benutze lieber PHP-Interpolation oder eine fertige Templating-Engine anstelle von etwas eigenem.