サイトおよびプロジェクトで多言語をどのように実装したか

オープンソースプロジェクトを作成してサポートしたので、プロジェクトとサイトの両方の多言語サポートで起こりうるすべての問題をすぐに解決したいと思います。 私は、デスクトッププログラムから始めて、非常に長い間、さまざまなプロジェクトで多言語サポートに出会いました。 したがって、考えられるニーズを把握した上で、提案されたソリューションに精通し始めました。 はい、ほとんどすべてのSaaSサービスはオープンソースプロジェクトを無料で使用できますが、基本的にはすべてが文字列リソースの翻訳に集中しています。 しかし、サイトとドキュメントはどうですか? 残念ながら、私は適切なものを見つけられず、独立した実装を開始しました。 私は結果に満足しており、ほぼ半年間システムを使用しているとすぐに言わなければなりませんが、これは大量の完成したソリューションではなく、むしろ私のニーズのための具体的な実装であると警告していますが、いくつかのアイデアが他の開発者にも役立つことを願っています



まず、将来の子孫に対して設定した要件をリストします。



  1. .jsにJSONの形式で保存されたプロジェクトのリソースと、サイト上のすべてのテキストとドキュメントの両方をローカライズする必要があります。
  2. リソースには他の言語への翻訳がない場合があります。 つまり、たとえば、ロシア語でテキストを蓄積し、翻訳者に渡すことができます。ロシア語版のサイトでは、これらのテキストはすでに利用可能になっています。
  3. ユーザーが自分の言語に翻訳されていないリソースを翻訳したり、新しいリソース(テキスト)を作成したり、既存のテキストを母国語でチェックおよび編集できるように、サイトに便利なシステムが必要です。 これは次のようになります-ユーザーはアクション(翻訳、検証)、母国語(翻訳の場合は元の言語)、および希望するボリュームを選択します。 これらのパラメーターに基づいて、リソースが検索され、翻訳または編集のためにユーザーに提供されます。 当然、ユーザーアクションのログを保持し、実行された作業の統計を蓄積する必要があります。
  4. サイトには言語を選択する必要がありますが、各ページには、このページの翻訳が既に存在する言語のみを表示する必要があります。
  5. 同じ行を複数の場所で使用できます。 たとえば、文字列は.jsおよびドキュメントで使用されます。 つまり、リソースは1つのインスタンスに存在する必要があり、リソースが変更された場合は、JSONとドキュメントの両方で変更する必要があります。
  6. 理想的には、何らかの自動調整システムがあるはずですが、現時点では、公開に関する個人的な決定に集中できます。


リアルタイムで変更を表示することは私には関係がなかったため、内部キッチン全体でいくつかの中間テーブルを作成し、コマンドでJSONを構築してサイト自体のページを生成することにしました。 実際、4つのテーブルで十分です。

テーブル構造
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 ;
      
      





ネイティブ、iso639という3つのフィールド名を持つ言語テーブル。 記録例: ロシア語、ロシア語、ル



別のコメントとタイプを指定できるlangidリソースのテキスト識別子の表。 私はすべてのリソースをいくつかのタイプに分けました:JSON文字列、サイトページ、プレーンテキスト、MarkDown形式のテキスト。 もちろん、独自のタイプを使用できます。

例: cancelbtn、[キャンセル]ボタンのテキスト、JSON



テキストリソーステーブルの言語(langid、language、text、prev)。 識別子、言語、テキスト自体へのリンクを保存します。

最後のprevフィールドは、編集時のテキストのバージョン管理を保証し、リソースの前のバージョンを指します。



すべての変更は、langlogログテーブル(iduser、idlangres、action)に記録されます。 アクションフィールドには、作成、編集、検証といった完璧なアクションが表示されます。



ユーザーとの作業を停止することはありません。翻訳または修正を送信するときにユーザーが自動的に登録すると言うだけです。 電子メールはオプションであるため、ユーザーにはユーザー名とパスワードがすぐに通知されます。 彼が行ったすべての変更は、アカウントに関連付けられます。 将来、彼は電子メールやその他のデータを示すか、単にこの登録を忘れることができます。



テーブル間のすべての関係をよりよく理解できるように、図を作成しました。

画像



リソースを他のリソースに挿入する機能が必要なので、#identifier#という形式のマクロを追加しました。 たとえば、最も単純な場合、リソース名= "Name"がある場合、リソースでそれを使用できますentername = "Specify your#name#"。これは、生成中にSpecify your Nameに置き換えられます

ここで、サイトページを生成するには、すべての言語とリソースを適切なタイプで処理し、特別な置換関数を使用して各テキストを処理し、完成したページを含む別のテーブルに結果を書き込みます。 さらに、現在の言語で#識別子#が見つからない場合、他の言語で検索されるように処理が行われます。 この処理を実行する再帰関数(ループ保護付き)のスケッチを次に示します。

PHPルックアップ関数の例
  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; }
      
      







#name#という形式のマクロを置き換えることに加えて、MarkDownマークアップをすぐにHTMLに変換し、独自のディレクティブを処理します。 たとえば、異なる言語のスクリーンショットを1つのレコードに掛けることができる写真のテーブルがあり、テキストでタグ[img "/ file /#* indexs#"]を指定した場合、必要なものと名前のインデックスの画像を置き換えます舌。 しかし、最も重要なことは、さまざまな目的のアップロードを任意の形式で生成できることです。 例として、JSONファイルを生成するためのコードを示しますが、真実はありますが、不要なため、識別子置換関数は使用されません。

RUおよびENのJSONファイル生成
 function jsonerror( $message ) { print $message; exit(); } function save_json( $filename ) { global $db, $original; preg_match("/^\w*_(?<lang>\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<br>"; 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<br><a href='/tmp/locale.zip'>ZIP file</a>";
      
      







したがって、それほど多くの労力を費やさなかったので、私は私が望んだほとんどすべてを実現しました。 サイトでのアクティビティが少ないために現時点では関係のないものだけが未実現のままでした。 ただし、使用プロセスで必要な追加機能が追加されました。 たとえば、翻訳が必要なリソースを含むテキストファイルを受信し、翻訳されたテキストを逆ロードします。

ご希望の方は、ユーザーが私のプロジェクトの新しいリソースを翻訳、編集、作成できる作業ページをご覧ください



画像







All Articles