ページの別の言語への動的翻訳

こんにちは、Habr。



今日は、インスタントページ変更の分野での私の成果、つまり動的言語の変更についてお話します。 私はこのことをごく最近必要とし、サードパーティの実装を信頼していないので(どういうわけかそれらを見つけられませんでした)、私は独自のものを書かなければなりませんでした。 使用中(約6か月)、最も目立つバグをすべて修正しました(ただし、これはもはやバグが存在しないという意味ではありません:))。



誰かがクライアントで転送することはお勧めできないと言うでしょうが、私はこれまでと変わらない状況に陥りました。 言語の動的な変更の場合、要素のテキストは単に置き換えられ、作業が続行されます。 「設定は再起動時に適用されます」と私を悩ますのはそれだけではなかったと思います。 私の実装は少し複雑ですが、この問題を解決します。



混乱しないように、この記事では次の用語リストを定義します。

辞書は、特定の言語でローカライズにアクセスするために使用されるキーのリポジトリです。 実際、これは通常のJavaScriptオブジェクトであり、プロパティはアクセスキーであり、値は翻訳された文字列です。

ハッシュは、辞書の順序付きマージの結果であるオブジェクトです。 後に翻訳が選択される一般的な辞書。



より詳細に。





読み上げを高速化するために、 ソースコードインターフェイスの説明の章をスキップすることをお勧めします 。 ソフトウェアの観点から会話の主題の説明を提供します。 希望する場合は、記事に興味があれば後で利用できます。



ソースコード



すぐにソースコードを提案してください。 まだ詳細に触れないでください。しかし、我々はそれに戻ります。

lang = (function init_lang() { var BODY = document.body, //  LANG_HASH = {}, //,   LANG_HASH_LIST = [], //   LANG_HASH_INDEX = {}, //   LANG_HASH_USER = {}, //  LANG_HASH_SYSTEM = {}, //  LANG_QUEUE_TO_UPDATE = [], //    LANG_PROPS_TO_UPDATE = {}, //    LANG_UPDATE_LAST = -1, //    LANG_UPDATE_INTERVAL = 0, //  LANG_JUST_DELETE = false; //     var hash_rebuild = function hash_rebuild() { //   var obj = {}; obj = lang_mixer(obj, LANG_HASH_USER); for (var i = 0, l = LANG_HASH_LIST.length; i < l; i++) obj = lang_mixer(obj, LANG_HASH_LIST[i]); LANG_HASH = lang_mixer(obj, LANG_HASH_SYSTEM); }, lang_mixer = function lang_mixer(obj1, obj2) { //   for (var k in obj2) obj1[k] = obj2[k]; return obj1; }, lang_update = function lang_update(data) { //,   switch (typeof data) { default: return; case "string": LANG_PROPS_TO_UPDATE[data] = 1; break; case "object": lang_mixer(LANG_PROPS_TO_UPDATE, data); } LANG_UPDATE_LAST = 0; if (!LANG_UPDATE_INTERVAL) LANG_UPDATE_INTERVAL = setInterval(lang_update_processor, 100); }, lang_update_processor = function lang_update_processor() { //  var date = new Date; for (var l = LANG_QUEUE_TO_UPDATE.length, c, k; LANG_UPDATE_LAST < l; LANG_UPDATE_LAST++) { c = LANG_QUEUE_TO_UPDATE[LANG_UPDATE_LAST]; if(!c) continue; if (!c._lang || !(c.compareDocumentPosition(BODY) & 0x08)) { LANG_QUEUE_TO_UPDATE.splice(LANG_UPDATE_LAST, 1); LANG_UPDATE_LAST--; if (!LANG_QUEUE_TO_UPDATE.length) break; continue; } for (k in c._lang) if (k in LANG_PROPS_TO_UPDATE) lang_set(c, k, c._lang[k]); if (!(LANG_UPDATE_LAST % 10) && (new Date() - date > 50)) return; } LANG_PROPS_TO_UPDATE = {}; clearInterval(LANG_UPDATE_INTERVAL); LANG_UPDATE_INTERVAL = 0; }, lang_set = function lang_set(html, prop, params) { //   html[params[0]] = prop in LANG_HASH ? LANG_HASH[prop].replace(/%(\d+)/g, function rep(a, b) { return params[b] || ""; }) : "#" + prop + (params.length > 1 ? "(" + params.slice(1).join(",") + ")" : ""); }; var LANG = function Language(htmlNode, varProps, arrParams) { //    var k; if (typeof htmlNode != "object") return; if (typeof varProps != "object") { if (typeof varProps == "string") { k = {}; k[varProps] = [htmlNode.nodeType == 1 ? "innerHTML" : "nodeValue"]. concat(Array.isArray(arrParams) ? arrParams : []) varProps = k; } else return; } if (typeof htmlNode._lang != "object") htmlNode._lang = {}; for (k in varProps) { if (!(Array.isArray(varProps[k]))) varProps[k] = [varProps[k]]; htmlNode._lang[k] = varProps[k]; lang_set(htmlNode, k, varProps[k]); } if (LANG_QUEUE_TO_UPDATE.indexOf(htmlNode) == -1) LANG_QUEUE_TO_UPDATE.push(htmlNode); }; lang_mixer(LANG, { get: function get(strProp) { //    return LANG_HASH[strProp] || ("#" + strProp); }, set: function set(strProp, strValue, boolSystem) { //    //   var obj = !boolSystem ? LANG_HASH_USER : LANG_HASH_SYSTEM; if (typeof strValue != "string" || !strValue) delete obj[strProp]; else obj[strProp] = strValue; hash_rebuild(); lang_update(strProp + ""); return obj[strProp] || null; }, load: function load(strName, objData) { // () switch (typeof strName) { default: return null; case "string": if (LANG_HASH_INDEX[strName]) { LANG_JUST_DELETE = true; LANG.unload(strName); LANG_JUST_DELETE = false; } LANG_HASH_LIST.push(objData); LANG_HASH_INDEX[strName] = objData; break; case "object": objData = {}; for (var k in strName) { if (LANG_HASH_INDEX[k]) { LANG_JUST_DELETE = true; LANG.unload(k); LANG_JUST_DELETE = false; } LANG_HASH_LIST.push(strName[k]); LANG_HASH_INDEX[k] = strName[k]; objData[k] = 1; } } hash_rebuild(); lang_update(objData); return typeof strName == "string" ? objData : strName; }, unload: function unload(strName) { // () var obj, res = {}, i; if (!(Array.isArray(strName))) strName = [strName]; if (!strName.length) return null; for (i = strName.length; i--;) { obj = LANG_HASH_INDEX[strName[i]]; if (obj) { LANG_HASH_LIST.splice(LANG_HASH_LIST.indexOf(obj), 1); delete LANG_HASH_INDEX[strName[i]]; res[strName[i]] = obj; if (LANG_JUST_DELETE) return; } } hash_rebuild(); lang_update(obj); return strName.length == 1 ? res : obj; }, params: function params(htmlElem, strKey, arrParams) { if (typeof htmlElem != "object" || !htmlElem._lang || !htmlElem._lang[strKey]) return false; htmlElem._lang[strKey] = htmlElem._lang[strKey].slice(0, 1).concat(Array.isArray(arrParams) ? arrParams : []); lang_set(htmlElem, strKey, htmlElem._lang[strKey]); return true; } }); return LANG; })();
      
      







階層



まず、実装では、いくつかのタイプの辞書を強調したことに注意してください。

1) ダウンロード可能

このような辞書のキーは、ユーザーが個別に変更することはできません。それらのキーは、完全にのみダウンロードまたはアンロードできます。 このような辞書は、他のダウンロード可能な辞書よりも常に優先されます。 名前付き。

2) カスタム

組み込み辞書。ダウンロードされた辞書の優先度よりも常に優先度が低くなります。 全体をロードまたはアンロードすることはできず、個々のキーに対してのみ変更されます。 その意味は、ユーザーが指定した値を残りとは別に保存することです。 ダウンロード可能なすべての辞書をアンロードしてもユーザー辞書に影響はなく、その値は引き続き使用可能です。

3) システム

意味に関しては、ユーザーを完全に繰り返しますが、最も優先順位が高くなります。





図1-ダウンロード可能な辞書(角かっこ内)のリスト、およびユーザー辞書とシステム辞書を優先度の高い順に並べたもの



そのため、ユーザー辞書またはシステム辞書を変更するとき、およびロードされた辞書のリストを変更するとき、ハッシュが更新されます。 アルゴリズムは次のとおりです。

1)オブジェクトのキーは、指定された数字の逆の順序でハッシュオブジェクトにコピーされます。 優先度の降順。

2)そのようなキーがすでにハッシュに存在する場合、コピーは行われません。



キー\辞書 [ユーザー] 共通 システム App1 App2 App3 [システム] ハッシュ
わかった わかった わかった わかった
キャンセル キャンセルする キャンセルする キャンセルする キャンセルする
完了 完了 完了 完了
STRING ひも ひも


表1-優先度の高い辞書からハッシュへのキーの移行。



デフォルトでは、辞書はロードされません。 ユーザー辞書とシステム辞書は空です。 ところで、辞書の存在しないプロパティを呼び出すと、空の文字列は返されませんが、ハッシュ記号の後にアクセスされたキーの名前が続く「#key」という形式の文字列が返されます。 これにより、存在しないキーがすぐに画面に表示されます。



インターフェースの説明



ソースコードを実行すると、グローバル変数langが使用可能になります。その値は、要素の属性をキーを介してハッシュに関連付ける関数です。 次のように引数を渡すことができます。

 lang(htmlTextNode,strKey); lang(htmlTextNode,strKey,arrParams); lang(htmlTextNode,objKeys); lang(htmlElement,objKeys);
      
      



ここで:

htmlTextNode-バインディングが発生するテキストノード。

strKey-呼び出しが発生する辞書のキー。

arrParams-翻訳に代入されたパラメーター(詳細は後述)。

objKeys-プロパティに呼び出しが発生するキーを含むオブジェクトと、valuesにバインドする属性(文字列として)を含むオブジェクト。 また、値の変換で置換するパラメーターを指定することもできます。 このため、値は配列である必要があります。最初の要素はバインドする属性で、残りはパラメーター値です。

lang変数には独自の関数プロパティget、set、load、unload、paramsがあります。



getを使用:

 lang.get(strKey);
      
      



ここで:

strKeyは、値を取得するキーです。

渡されたキーに関連付けられた翻訳文字列を返します。



セットを使用して:

 lang.set(strKey, strValue,boolSystem);
      
      



ここで:

strKey-値を設定する必要があるキー。

strValueは設定する値です。 空または文字列でない場合、対応するキーは辞書から削除されます。

boolSystem-このパラメーターがtrueに変換される場合 、レコードはシステムディクショナリで発生します。それ以外の場合-ユーザーディクショナリで発生します。

記録された値を返します。キーが削除された場合はnullを返します。



負荷の使用:

 lang.load(strName,objData);
      
      



ここで:

strName-辞書の名前(文字列)または多くのプロパティを持つオブジェクト。これは辞書の名前であり、その意味は辞書そのものです。

objData-最初の引数が文字列の場合、この引数は辞書です。

ロードされた辞書を返します。



アンロードの使用:

 lang.unload(strName);
      
      



ここで:

strName-アンロードする辞書の名前(文字列)または名前(配列)。

ダウンロードした辞書を取得します。



paramsの使用:

 lang. params(htmlElem,strKey,arrParams);
      
      



ここで:

htmlElem-辞書に既に関連付けられている要素。

strKey-辞書アクセスキー。

arrParams-新しいパラメーターの配列。

新しいパラメーターが設定されている場合はtrueを返し、そうでない場合はfalseを返します。

次の章の例は、インターフェイスの理解を明確にする必要があります。



要素との相互作用



辞書と対話できるようにするには、特定の要素またはテキストノードを、この要素の属性と辞書キーのいずれかを介してハッシュにバインドする必要があります。 この属性はinnerHTML 、またはtitle、altなどのいずれかです。テキストノードがバインドされている場合、デフォルトのバインドはtextContentを通過します 。 以下にその理由を説明します。

例を考えてみましょう:

 lang(document.body.appendChild(document.createElement("button")),{"just_a_text":"innerHTML" }); //      innerHTML   just_a_text
      
      



ボタンは、まだ存在しないキーを介して接続されます(したがって、テキストは「#just_a_text」です)。 技術的な実装に関しては、 _langプロパティが要素に作成され、そのプロパティはハッシュキーであり、値は配列です。最初の要素はこの要素の属性であり、ハッシュ値が書き込まれ、残りは翻訳に転送されるパラメータです。

これで、キーを辞書に書き込むための3つのオプションがあります。ダウンロードした辞書のリストに新しい辞書を追加するか、ユーザー辞書またはシステム辞書に値を割り当てます。 すべてのオプションを優先度の高い順に検討してください(コンソールでこの行を1行ずつ実行することをお勧めします)。

 lang.set("just_a_text","  "); //    lang.load("def",{"just_a_text":"   "}); lang.set("just_a_text","    ",1); //   
      
      



ボタンのテキストは、コードで指定された各ステップで変わります。 したがって、この場合、3種類すべての辞書が使用されます。

次に、辞書からキーを逆の順序で削除し、宣言された階層が実際に機能することを確認します。

 lang.set("just_a_text","",1); //    lang.unload("def");//    def lang.set("just_a_text",""); //    
      
      



これで、ボタン上のテキストは再び「#just_a_text」になります。



パラメータ



多くの場合、辞書のテキストを置き換えるだけでなく、いくつかのパラメーターを辞書に渡す必要があったため、パラメーターを置き換えるオプションも実装されています。 要素をキーに関連付けるときに指定することも、lang.params関数を使用してバインドした後に指定することもできます。 行内のパラメータの位置を示すために、構成/%\ d + /が使用されます。ここで、数字は送信されたパラメータの番号を示します。 パラメータが渡されなかった場合、空の文字列が置換されます。

特に、置換はinnerHTMLとともに明示されます。

 lang(b=document.body.appendChild(document.createElement("button")), {"pr1":"innerHTML" }); lang.set("pr1"," 1: <b>%1</b>.  2: <b>%2</b>. "); lang.params(b,"pr1",[100,500]);
      
      



これで、ボタン内で、転送されたパラメーターが強調表示されます。 リンク



性能



負荷を分散するために(これについては既にここで説明しました )、辞書に関連付けられた要素の配列を処理する間隔関数を使用します。

次に、要素でinnerHTMLをできるだけ使用しないようにし、代わりにこれらの要素のテキストノードでtextContentを使用します。 innerHTMLは、この属性のセッターが渡された値をHTMLコードとして解析するため、 20以上遅くなります。 textContentはHTMLの解析については考えませんが、テキストをそのまま挿入します(<、>を&lt ;,&gt;に変更することさえできません)が、残念ながら、これは特に前の例では必ずしも適用できません。



IE8で実行する



少し考えてみると、このことは8番目のIEで実行できることに気付きました。 これを行うには、いくつかの「ダーティ」ハックに頼らなければなりません。

 if (typeof Array.isArray != 'function') Array.isArray = function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } if (typeof Array.prototype.indexOf != 'function') Array.prototype.indexOf = function (value, offset) { offset = parseInt(offset); for (var i = offset > 0 ? offset : 0, l = this.length; i < l; i++) if (this[i] === value) return i; return -1; }; //        IE8 Text.prototype._lang = null; //    ,     if(typeof Element.prototype.compareDocumentPosition!="function") Text.prototype.compareDocumentPosition = Element.prototype.compareDocumentPosition = function compareDocumentPosition(node) { // Compare Position - MIT Licensed, John Resig function comparePosition(a, b) { return a.compareDocumentPosition ? a.compareDocumentPosition(b) : a.contains ? (a != b && a.contains(b) && 16) + (a != b && b.contains(a) && 8) + (a.sourceIndex >= 0 && b.sourceIndex >= 0 ? (a.sourceIndex < b.sourceIndex && 4) + (a.sourceIndex > b.sourceIndex && 2) : 1) + 0 : 0; } return comparePosition(this,node); };
      
      





実際、IEの第8バージョンまでのラインアップは、長い間埋められる必要があると思います。 インターネットは私たちのものです。



おわりに



このことを設計するとき、可能な限り最適化しようとしました。たとえば、要素内のテキストへの変更を最小限に抑えました。 キー値が変更されていない場合、更新されません。 その結果、10000ボタン内のテキストの変更には( textContentを使用してプロファイリングを有効にして)約1秒かかります。



小さなマイナス点があります。DOMから要素を「プル」すると、そのデータは更新されなくなる可能性があります。 問題を解決するには、それをハッシュに再度関連付ける必要があります。



原則として、この実装はページ言語を変更するためだけに使用することはできません。 その主な目的は、辞書を変更するときに指定された属性を変更することです。 つまり スコープは、タイトルに示されているよりもはるかに広いです。



実装はおそらく最も成功していません。 コメントでは、評価と改善のヒントをお待ちしています。 そして、短所を主張してください。 ああ、MITライセンス。



UPD。 例はここにあります 。 インターネットがオンのときにページを開くだけです。



UPD2。 本当の道についての指示と欠点を指摘してくれたlahmatiyに感謝します。



All Articles