Wie ich Mehrsprachigkeit auf der Website und im Projekt implementiert habe

    Nachdem ich ein Open Source-Projekt erstellt und unterstützt habe, möchte ich sofort alle möglichen Probleme der mehrsprachigen Unterstützung sowohl für das Projekt als auch für die Site lösen. Ich bin in verschiedenen Projekten schon sehr lange auf Unterstützung für Mehrsprachigkeit gestoßen, angefangen bei Desktop-Programmen. Als ich eine Vorstellung von den möglichen Bedürfnissen hatte, begann ich mich mit den vorgeschlagenen Lösungen vertraut zu machen. Ja, fast alle SaaS-Dienste bieten eine kostenlose Nutzung für Open-Source-Projekte, aber im Grunde ist alles auf die Übersetzung von String-Ressourcen ausgerichtet. Aber was ist mit der Site und der Dokumentation? Leider habe ich nichts passendes gefunden und mit der eigenständigen Umsetzung begonnen. Ich muss sofort sagen, dass ich mit dem Ergebnis zufrieden bin und das System seit fast einem halben Jahr benutze, obwohl ich Sie warne, dass dies keine vollständige Lösung ist, sondern eine konkrete Implementierung für meine Bedürfnisse, aber ich hoffe

    Zunächst werde ich die Anforderungen auflisten, die ich für den zukünftigen Nachwuchs festgelegt habe.

    1. Sie müssen sowohl die Ressourcen für das Projekt lokalisieren, die in Form von JSON in .js gespeichert sind, als auch alle Texte und Dokumentationen auf der Site.
    2. Die Ressource wurde möglicherweise nicht in andere Sprachen übersetzt. Das heißt, ich kann zum Beispiel Texte auf Russisch sammeln und sie dann einem Übersetzer übergeben, und in der russischen Version der Website sind diese Texte bereits verfügbar.
    3. Auf der Website sollte ein praktisches System vorhanden sein, mit dem der Benutzer nicht in seine Sprache übersetzte Ressourcen übersetzen, eine neue Ressource (Text) erstellen oder vorhandene Texte in seiner Muttersprache prüfen und bearbeiten kann. Es sollte ungefähr so ​​aussehen - der Benutzer wählt die Aktion (Übersetzung, Überprüfung), die Muttersprache (und im Falle der Übersetzung die Originalsprache) sowie das gewünschte Volumen aus. Basierend auf diesen Parametern wird eine Ressource gesucht und dem Benutzer zur Übersetzung oder Bearbeitung angeboten. Natürlich sollte ein Protokoll der Benutzeraktionen geführt und Statistiken über die durchgeführten Arbeiten gesammelt werden.
    4. Die Site sollte eine Auswahl an Sprachen haben, aber auf jeder Seite sollten nur die Sprachen angezeigt werden, für die es bereits eine Übersetzung dieser Seite gibt.
    5. Die gleiche Zeile kann an mehreren Stellen verwendet werden. Beispielsweise wird die Zeichenfolge in .js und in der Dokumentation verwendet. Das heißt, die Ressource muss sich in einer Instanz befinden, und wenn sie sich ändert, muss sie sich sowohl in JSON als auch in der Dokumentation ändern.
    6. Idealerweise sollte es eine Art automatisch moderiertes System geben, aber Sie können sich vorerst darauf konzentrieren, persönliche Entscheidungen über das Veröffentlichen zu treffen.

    Das Anzeigen von Änderungen in Echtzeit war für mich nicht relevant, und ich entschied mich, mehrere Zwischentabellen mit der gesamten internen Küche zu erstellen und dann auf Befehl JSON zu erstellen und Seiten der Site selbst zu generieren. In der Tat sind vier Tische genug.
    Tabellenstruktur
    CREATE TABLE IF NOT EXISTS `languages` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `_owner` smallint(5) unsigned NOT NULL,
      `name` varchar(32) NOT NULL,
      `native` varchar(32) NOT NULL,
      `iso639` varchar(2) NOT NULL,
      PRIMARY KEY (`id`),
      KEY `_uptime` (`_uptime`)
    ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;
    CREATE TABLE IF NOT EXISTS `langid` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `_owner` smallint(5) unsigned NOT NULL,
      `name` varchar(96) NOT NULL,
      `comment` text NOT NULL,
      `restype` tinyint(3) unsigned NOT NULL,
      `attrib` tinyint(3) unsigned NOT NULL,
      PRIMARY KEY (`id`),
      KEY `_uptime` (`_uptime`),
      KEY `name` (`name`),
      KEY `restype` (`restype`)
    ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;
    CREATE TABLE IF NOT EXISTS `langlog` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `_owner` smallint(5) unsigned NOT NULL,
      `iduser` int(10) unsigned NOT NULL,
      `idlangres` int(10) unsigned NOT NULL,
      `action` tinyint(3) unsigned NOT NULL,
      PRIMARY KEY (`id`),
      KEY `_uptime` (`_uptime`),
      KEY `iduser` (`iduser`,`idlangres`)
    ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;
    CREATE TABLE IF NOT EXISTS `langres` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `_owner` smallint(5) unsigned NOT NULL,
      `langid` smallint(5) unsigned NOT NULL,
      `lang` tinyint(3) unsigned NOT NULL,
      `text` text NOT NULL,
      `prev` mediumint(9) unsigned NOT NULL,
      `verified` tinyint(3) NOT NULL,
      `size` mediumint(9) unsigned NOT NULL,
      PRIMARY KEY (`id`),
      KEY `_uptime` (`_uptime`),
      KEY `langid` (`langid`,`lang`),
      KEY `size` (`size`)
    ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;
    

    Sprachtabelle mit drei Feldern name, native, iso639. Eingabebeispiel : Russisch, Russisch, ru

    Tabelle mit Text-IDs für langwierige Ressourcen, in der Sie auch einen Kommentar und einen Typ angeben können. Ich habe alle Ressourcen für mich in verschiedene Typen unterteilt: JSON-Zeichenfolge, Site-Seite, Nur-Text, Text im MarkDown-Format. Sie können natürlich Ihre eigenen Typen verwenden.
    Beispiel: сancelbtn, Schaltfläche Text für Abbrechen, JSON-

    Textressourcentabellensprache (langid, language, text, prev). Wir speichern Links zu dem Identifikator, der Sprache und dem Text selbst.
    Das letzte vorherige Feld stellt die Versionierung des Texts beim Bearbeiten sicher und verweist auf die vorherige Version der Ressource.

    Alle Änderungen werden in der Langlog-Protokolltabelle aufgezeichnet (iduser, idlangres, action). Das Aktionsfeld zeigt die perfekte Aktion an - Erstellung, Bearbeitung, Überprüfung.

    Ich werde nicht aufhören mit Benutzern zu arbeiten, sondern nur sagen, dass der Benutzer sich automatisch registriert, wenn er eine Übersetzung oder Korrektur sendet. Da E-Mail optional ist, wird der Benutzer sofort über den Benutzernamen und das Passwort informiert. Alle von ihm vorgenommenen Änderungen werden an sein Konto gebunden. Zukünftig kann er seine E-Mail-Adresse und andere Daten angeben oder diese Registrierung einfach vergessen.

    Ich habe ein Diagramm gezeichnet, damit Sie alle Beziehungen zwischen den Tabellen besser verstehen.
    Bild

    Da ich Ressourcen in andere Ressourcen einfügen muss, habe ich Makros der Form # identifier # hinzugefügt. Wenn wir beispielsweise im einfachsten Fall einen Ressourcennamen = "Name" haben, können wir ihn in der Ressource entername = "Specify your # name #" verwenden, der durch Specify your Name während der Generierung ersetzt wird .
    Um nun Websiteseiten zu erstellen, müssen Sie nur alle Sprachen und Ressourcen mit dem entsprechenden Typ durchgehen, jeden Text mit einer speziellen Ersetzungsfunktion verarbeiten und das Ergebnis in eine separate Tabelle mit den fertigen Seiten schreiben. Darüber hinaus erfolgt die Verarbeitung so, dass, wenn # bezeichner # nicht in der aktuellen Sprache gefunden wird, in anderen Sprachen gesucht wird. Hier ist eine Skizze einer rekursiven Funktion (mit Schleifenschutz), die diese Verarbeitung ausführt.
    Beispiel für eine PHP-Suchfunktion
        public function proceed( $input, $recurse = false )
        {
            global $db, $syslang;
            if ( !$recurse )
                $this->chain = array();
            $result = '';
            $off = 0;
            $start = 0;
            $len = strlen( $input );
            while ( ($off = strpos( $input, '#', $off )) !== false && $off < $len - 2 )
            {
                $end = strpos( $input, '#', $off + 2 );
                if ( $end === false )
                    break;
                if ( $end - $off > $this->lenlimit )
                {
                    $off = $end - 1;
                    continue;
                }
                $name = substr( $input, $off + 1, $end - $off - 1 );
                $langid = $db->getone("select id from langid where name=?s", $name );
                if ( $langid && !in_array( $langid, $this->chain ))
                {
                    $langres = $db->getrow("select _uptime, id,text from langres where langid=?s && verified>0
                                                                order by if( lang=?s, 0, 1 ),lang",  $langid, $this->lang );
                    if ( $langres )
                    {
                        if ( $langres['_uptime'] > $this->time )
                            $this->time = $langres['_uptime'];
                        $result .= substr( $input, $start, $off - $start );
                        $off = $end + 1;
                        $start = $off;
                        array_push( $this->chain, $langid );
                        $result .= $this->proceed( $langres['text'], true );
                        array_pop( $this->chain );
                        if ( $off >= $len - 2 )
                            break;
                        continue;
                    }
                }
                $off = $end - 1;
            }
            if ( $start < $len )
                $result .= substr( $input, $start );
            return $result;
        }
    


    Zusätzlich zum Ersetzen von Makros der Form "Name" konvertiere ich MarkDown-Markups sofort in HTML und verarbeite meine eigenen Anweisungen. Ich habe zum Beispiel eine Tabelle mit Bildern, in der Screenshots für verschiedene Sprachen auf einem Datensatz abgelegt werden können. Wenn ich das Tag [img "/ file / # * indexes #"] im Text spezifiziere, ersetze ich ein Bild durch die Namensindizes durch das, das ich benötige Zunge. Vor allem aber kann ich Uploads für verschiedene Zwecke in jedem Format erstellen. Als Beispiel gebe ich den Code zum Generieren von JSON-Dateien an, obwohl die Wahrheit darin besteht, dass die Identifier-Substitutionsfunktion nicht verwendet wird, da sie unnötig ist.
    JSON-Dateigenerierung für RU und EN
    function jsonerror( $message )
    {
        print $message;
        exit();
    }
    function save_json( $filename )
    {
        global $db, $original;
        preg_match("/^\w*_(?\w*)\.js$/", $filename, $matches );
        if ( empty( $matches['lang'] ))
            jsonerror( 'No locale' );
        $lang = $db->getrow("select * from languages where iso639=?s", $matches['lang'] );
        if ( !$lang )
            jsonerror( 'Unknown locale '.$matches['lang'] );
        $list = $db->getall("select lng.name, r.text from langid as lng
            left join langres as r on r.langid = lng.id
            where  lng.restype=5 && verified>0 && r.lang=?s
            order by lng.name", $lang['id'] );
        $out = array();
        foreach ( $list as $il )
            $out[ $il['name']] = $il['text'];
        if ( $lang['id'] == 1 )
            $original = $out;
        else
            foreach ( $original as $ik => $io )
                if ( !isset( $out[ $ik ] ))
                    $out[ $ik ] = $io;
        $output = "/* This file is automatically generated on eonza.org.
       Use http://www.eonza.org/translate.html to edit or translate these text resources.
    */
    var lng = {
    \tcode: '$lang[iso639]',
    \tnative: '$lang[native]',
    ";
        foreach ( $out as $ok => $ov )
        {
            if ( strpos( $ov, "'" ) === false )
                $text = "'$ov'";
            elseif (strpos( $ov, '"' ) === false )
                $text = "\"$ov\"";
            else
                jsonerror( 'Wrong text:'.$text );
            $output .= "\t$ok: $text,\r\n";
        }
        $output .= "\r\n};\r\n";
        $jsfile = dirname(__FILE__)."/i18n/$lang[iso639].js";
        if ( file_exists( $jsfile ))
            $output .= file_get_contents( $jsfile );
        if (file_put_contents( HOME."tmp/$filename", $output ))
            print "Save: ".HOME."tmp/$filename
    "; else jsonerror( 'Save error:'.HOME."tmp/$filename" ); } $original = array(); $files = array( 'en', 'ru'); foreach ( $files as $if ) save_json( "locale_$if.js" ); $zip = new ZipArchive(); print $zip->open( HOME."tmp/locale.zip", ZipArchive::CREATE ); foreach ( $files as $f ) print $zip->addFile( HOME."tmp/locale_$f.js", "locale_$f.js" ); print $zip->close(); print "Finish
    ZIP file";


    Nachdem ich nicht so viel Mühe aufgewendet hatte, realisierte ich fast alles, was ich wollte. Nur Dinge, die momentan aufgrund der geringen Aktivität auf der Site nicht relevant sind, blieben unrealisiert. Es wurden jedoch zusätzliche Funktionen hinzugefügt, die im Nutzungsprozess benötigt wurden. Beispiel: Empfangen einer Textdatei mit Ressourcen, die übersetzt werden müssen, und Zurückladen des übersetzten Texts.
    Wer möchte, kann einen Blick auf die Arbeitsseite werfen, auf der Benutzer Ressourcen für mein Projekt übersetzen, bearbeiten und neu erstellen können.

    Bild

    Jetzt auch beliebt: