Linuchs: bestimmte Sprache aus mehrsprachigem String filtern

Moin,

HTML-Platzhalterdateien können mehrsprachig sein. Die Anzahl der Sprachen ist von Programm zu Programm variabel. Alle Programme haben de, en, nl - aber ich schließe nicht aus, dass neue Programme auch fr, es, ... können und die alten nach und nach erweitert werden.

Bisher konnten die Sprachen pro Zeile nur einmal vorkommen, in der neuen Funktion dürfen sie beliebig oft sein, hier zweimal:

<p>textA: ###de###en###nl### textB: ###deutsch###english###Nederlands###</p>

Ich übergebe der php-Funktion also die Textzeile, die Anzahl der Sprachen und den Sprach-Index 2 für en:

$zeile = "<p>textA: ###de###en###nl### textB: ###deutsch###english###Nederlands###</p>";
echo waehleSprache( $zeile, 3, 2 );

Der Return-String soll so aussehen:

<p>textA: en textB: english</p>

Ich zerlege also die Zeile in ein Array und benötige mit Sicherheit den ersten und letzten Wert, aber dazwischen ist unklar:

$arr = explode( '###', $zeile );
$string   =  $arr[0]; // vorlaufender Text
for ( $i=1; $i<count($arr); $i++ ) {
  $string .= ...;  // siehe Frage
}
$string .= $arr[$i]; // nachlaufender Text

Irgendwie habe ich gerade ein Brett vorm Kopf, deshalb die Frage:

Ich weiß, dass ich bei drei Sprachen und dem Sprach-Index 2 diese Sub-Arrays benötige:

0, 2, [6, 10, 14, 18, ...,] count($arr) -1

Bei fünf Sprachen und dem Sprach-Index 4 wären es:

0, 4, [10, 16, ...,] count($arr) -1

Mir will einfach nicht einfallen, wie ich diese Zahlenfolge mit einer Formel generieren kann.

Wer kann helfen? Oder gibt es einen besseren Lösungsansatz?

Gruß, Linuchs

Edit: 0, 2, [6, 10, 14, 18, ...,] count($arr) -1 ist falsch, ergibt

textA: endeutsch

  1. Hallo,

    HTML-Platzhalterdateien können mehrsprachig sein. Die Anzahl der Sprachen ist von Programm zu Programm variabel. Alle Programme haben de, en, nl - aber ich schließe nicht aus, dass neue Programme auch fr, es, ... können und die alten nach und nach erweitert werden.

    bis hier kann ich dir folgen.

    Bisher konnten die Sprachen pro Zeile nur einmal vorkommen, in der neuen Funktion dürfen sie beliebig oft sein, hier zweimal:

    <p>textA: ###de###en###nl### textB: ###deutsch###english###Nederlands###</p>
    

    Auch das verstehe ich noch, aber ich finde die Datenorganisation alles andere als gut.

    Ich übergebe der php-Funktion also die Textzeile, die Anzahl der Sprachen und den Sprach-Index 2 für en:

    $zeile = "<p>textA: ###de###en###nl### textB: ###deutsch###english###Nederlands###</p>";
    echo waehleSprache( $zeile, 3, 2 );
    

    Ähm, mit Index 2 komme ich auf nl (Niederländisch): 0->de, 1->en, 2->nl.

    Der Return-String soll so aussehen:

    <p>textA: en textB: english</p>

    Ich zerlege also die Zeile in ein Array und benötige mit Sicherheit den ersten und letzten Wert, aber dazwischen ist unklar:

    $arr = explode( '###', $zeile );
    $string   =  $arr[0]; // vorlaufender Text
    for ( $i=1; $i<count($arr); $i++ ) {
      $string .= ...;  // siehe Frage
    }
    $string .= $arr[$i]; // nachlaufender Text
    

    Irgendwie habe ich gerade ein Brett vorm Kopf, deshalb die Frage:

    Ich weiß, dass ich bei drei Sprachen und dem Sprach-Index 2 diese Sub-Arrays benötige:

    0, 2, [6, 10, 14, 18, ...,] count($arr) -1

    Also sei s die Anzahl der Sprachen und i der ab 1 zählende Index, dann brauchst du ns+0 und ns+i, wobei n ab 0 läuft und solange erhöht wird, bis die Array-Grenze überschritten ist.

    Wer kann helfen? Oder gibt es einen besseren Lösungsansatz?

    Ich würde beim bisherigen Ansatz bleiben und nur ein Element pro String verwenden. Und das dann als mehrstufiges Array organisieren:

    str = definition[index][lang]

    Hierbei ist index die fortlaufende Nummer des Textfragments, lang der 0-basierte Index der gewünschten Sprache.

    Ciao,
     Martin

    --
    Ein Tag, an dem du nicht wenigstens einmal gelacht hast, ist ein verlorener Tag.
    1. Hi Martin,

      schön, Dich wieder hier zu lesen.

      Pit

  2. HTML-Platzhalterdateien können mehrsprachig sein. Die Anzahl der Sprachen ist von Programm zu Programm variabel. Alle Programme haben de, en, nl - aber ich schließe nicht aus, dass neue Programme auch fr, es, ... können und die alten nach und nach erweitert werden.

    <p>textA: ###de###en###nl### textB: ###deutsch###english###Nederlands###</p>
    

    Ich habe deinen Lösungsweg ehrlichgesagt nicht verstanden. Aber PHP hat auch einen eigenen Mechanismus um Platzhaler in Templates zu ersetzen. Die Syntax ist etwas anders als bei dir und vielleicht etwas gewöhnungsbedürftig, aber dafür musst du das Template nicht mehr selber nach Platzhaltern durchsuchen und sie ersetzen. Damit ließe sich sehr einfach eine ähnliche Übersetzungsfunktion bauen:

    final class Lang
    {
        const DE = 0;
        const EN = 1;
        const NL = 2;
    }
    
    $translate = function (int $key, string ...$dictionary) : string {
        return $dictionary[$key];
    }
    
    function render(int $lang) : string {
        return <<<HTML
            <p>
                textA: {$translate($lang, 'de', 'en', 'nl')}
                textB: {$translate($lang, 'deutsch', 'english', 'Netherlands')}
            </p>
            HTML;
    }
    
    render(Lang::DE);
    

    $translate muss in diesem Fall ein Closure sein, weil normale Funktionen innerhalb von String-Platzhaltern nicht aufgerufen werden können.

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

      Ursprünglich hatte ich für fünf Sprachen und jedes Programm je eine Platzhalter-Datei. Bei Änderungen mussten fünf Dateien manuell geändert werden, das führte zu Fehlern.

      Deshalb habe ich sprach-abhängige Texte als "Array" in die HTML-Datei geschrieben und die "Arrays" mit den Zeichen ### markiert. Bisher pro Zeile nur ein "Array".

      Nun geht es darum, mehrere "Arrays" pro Zeile zuzulassen, wobei die Anzahl der Sprachen von Programm zu Programm variieren kann, sie muss als Parameter also mitgegeben werden.

      Hier habe ich 5 Sprachen und möchte die französische Version (4):

      $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>";
      echo displaySprache( $zeile, 5, 4 );
      

      Ausgabe: textA: fr-1 textB: francais textC: fr-2

      Habe das inzwischen so gelöst, hoffe aber, dass das vielleicht schneller geht. Pro Zeile der Platzhalter-Datei so eine Schleife zu duchlaufen hält auf:

      function displaySprache( $text, $anzahl_sprachen, $lg_ndx ) {
        $anzahl_trenner   = substr_count ( $text, "###" ); // Prtüfung, ob paarig
        // Trenner vorhanden und teilbar durch anzahl_sprachen +1?
        if ( $anzahl_trenner > 0 && $anzahl_trenner % ($anzahl_sprachen +1) == 0 ) {
          $arr    = explode( '###', $text );   // text - #de - #en - #nl - #text - #de - #en - #nl - #text
          $string   =  $arr[0];   // vorlaufender text
          for ( $i=1; $i<count($arr); $i++ ) {
            if ( 
                $i %  ($anzahl_sprachen +1) == 0        // vorlaufender Text und Texte zwischen den Sprachen
            ||  $i ==  $lg_ndx                          // 1. Vorkommen der Sprache
            ||  $i %  ($anzahl_sprachen +1) == $lg_ndx  // weitere Sprach-Elemente
            ) {
              $string .=  $arr[$i];
            }
          }
          $string .=  $arr[$i];   // nachlaufender text
        } else {
          $string = $text;
        }
        return $string;
      }
      

      Linuchs

      1. Wenn die "Array"-Markierung nicht aufgeht, also Programmier-Fehler in der Platzhalter-Datei, würde ich gerne die fehlerhafte Zeilen-Nummer ausgeben.

        PHP kennt mit seinen Fehlermeldungen ja die Zeilen-Nr, kann ich die als Programmierer auch wissen?

        1. Tach!

          Wenn die "Array"-Markierung nicht aufgeht, also Programmier-Fehler in der Platzhalter-Datei, würde ich gerne die fehlerhafte Zeilen-Nummer ausgeben.

          PHP kennt mit seinen Fehlermeldungen ja die Zeilen-Nr, kann ich die als Programmierer auch wissen?

          Ja, __LINE__ enthält die Zeilennummer. Auch andere magische Konstanten existeren, beispielsweise __FILE__. Das wird dir nur nicht viel nützen, weil das ja die aktuelle Code-Zeile ist und nicht die mit den fehlerhaften Daten. Ein "wo wurden die Daten definiert?" gibt es nicht. Du kannst aber die fehlerhaften Daten selbst ausgeben und danach eine Text-Suche starten.

          dedlfix.

      2. Hallo,

        Nun geht es darum, mehrere "Arrays" pro Zeile zuzulassen, wobei die Anzahl der Sprachen von Programm zu Programm variieren kann, sie muss als Parameter also mitgegeben werden.

        Du hast also sowohl unbekannte Anzahl Sprachen als auch unbekannte Anzahlan Texten. Wäre es da dann nicht sinnvoll, mit zwei verschiedenen Trennern (z.b. ### & $$$) zu arbeiten und dann mit 2 verschachtelten Arrays zu hantieren?

        Gruß
        Kalk

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

  3. Hallo,

    Wer kann helfen? Oder gibt es einen besseren Lösungsansatz?

    Wenn ich das richtig durchschaut haben sollte, brauchst du die Anzahl der Sprachen nicht zu übergeben. Sie berechnet sich aus der Arraylänge.

    AnzSprachen = ( array.len - 3 ) ÷ 2

    Die zusammengehörenden Texte haben dann einen Abstand von AnzSprachen+1

    Gruß
    Kalk

    1. Wenn ich das richtig durchschaut haben sollte, brauchst du die Anzahl der Sprachen nicht zu übergeben. Sie berechnet sich aus der Arraylänge.

      Ja, wenn ich genau 1 Array mit Fremdsprachen habe, ich möchte aber mehrere Arrays. Wie soll ich das unterscheiden?

      Array mit 7 Sprachen (9 Elemente):

      <p>###deutsch###english###Nederlands###francais###espanol###russki###plattdütsch###</p>

      2 Arrays mit je 3 Sprachen (auch 9 Elemente):

      <p>Sprache: ###deutsch###english###Nederlands### &nbsp; Gruß: ###Guten Morgen###Good morning###Goedemorgen###</p>

      1. Hallo,

        Wie soll ich das unterscheiden?

        Z.B. so:

        Array mit 7 Sprachen (9 Elemente):

        <p>###deutsch###english###Nederlands###francais###espanol###russki###plattdütsch$$$</p>

        2 Arrays mit je 3 Sprachen (auch 9 Elemente):

        <p>Sprache: ###deutsch###english###Nederlands$$$ &nbsp; Gruß: ###Guten Morgen###Good morning###Goedemorge$$$</p>

        Jeweils 1 zusätzliches Hilfsarray für das Abschluss-Element

        Gruß
        Kalk

  4. Nun, der Sinn von Templates ist, es so zu machen, daß im Template nur die Platzhalter stehen und die Werte erst mit dem Rendern da reinkommen. Beispw. haben wir {{platzhalter}} und nach dem Rendern steht dann, je nach Sprache English oder Deutsch an der Stelle wo sich der Platzhalter befand.

    Auf diese Art und Weise sind Erweiterungen um weitere Sprachen nicht vom Template abhängig. MFG

    1. Nun, der Sinn von Templates ist, es so zu machen, daß im Template nur die Platzhalter stehen und die Werte erst mit dem Rendern da reinkommen. Beispw. haben wir {{platzhalter}} und nach dem Rendern steht dann, je nach Sprache English oder Deutsch an der Stelle wo sich der Platzhalter befand.

      Auf diese Art und Weise sind Erweiterungen um weitere Sprachen nicht vom Template abhängig. MFG

      Mein Konzept ist ein anderes. Eine Platzhalter-Datei kann als Formular gesehen werden, in das Daten eingefügt werden. Aber anstatt mehrere Formulare für mehrere Sprachen zu haben, packe ich die Sprachen in ein gemeinsames Formular.

      Vor Jahren hatte ich fünf Dateien für fünf Sprachen. Multipliziert mit mehreren dutzend Programmen. Das ist bei Änderungen nicht mehr händelbar.

      1. Mein Konzept ist ein anderes. Eine Platzhalter-Datei kann als Formular gesehen werden, in das Daten eingefügt werden. Aber anstatt mehrere Formulare für mehrere Sprachen zu haben, packe ich die Sprachen in ein gemeinsames Formular.

        Genauso funktioniert das mit Templates. Hier beschrieben.

        Template'engines sind übrigens stark auf dem Vormarsch und stehen mittlerweile in sehr vielen PLs zur Verfügung. Mustache'templates kannst Du sowohl für JS als auch für PHP nutzen. MFG

  5. Oder gibt es einen besseren Lösungsansatz?

    Du betreibst sozusagen eine Zweckentfremdung. In template.h sähe das so aus:

    <tmpl_if name="de">
      <p> hir rede deusch </p>
    </tmpl>
    
    <tmpl_if name="en">
      <p> speaking english </p>
    </tmpl>
    
    <tmpl_if name="nl">
      <p> je word ouder </p>
    </tmpl>
    
    

    Und es gibt tatsächlich Kollegen die das so machen. Was natürlich umständlich ist und bei Spracherweiterungen müssen die Templates bearbeitet werden. Hier schrieb ich gestern wie das zweckmäßiger geht.

    MFG