1unitedpower: bestimmte Sprache aus mehrsprachigem String filtern

Beitrag lesen

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:

$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:

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 Struktur der Programmiersprache, Parsen beschäftigt sich mit der syntaktischen Struktur. Die Code-Generierung schließlich macht aus dem Zwischenformat, das ihm der Parser liefert, eine Repräsentation im Zielformat (hier also ein PHP-String.)

Wir bauen also eine Funktion:

function compile(int $language, string $input) : string
{
    return evaluate($language, parse(tokenize($input)));
}

Ein Aufruf wird später so aussehen:

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 es 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, zusammenhängend bleiben. Wenn ein Sonderzeichen maskiert wurde, soll es auch keinen eigenen Token bekommen, sondern zum vorherigen dazugezählt werden.

Wir haben also viert 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:

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:

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 eine Sammlung von Übersetzungs-Texten.

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 uns zwischen eckigen Klammern befinden. Außerdem brauchen wir diesmal zwei Buffer, in denen wir uns unvollständige Rohtexte bzw. übersetzte Texte merken.


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:

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 deutlich. 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.