📜 ⬆️ ⬇️

Dynamic translation of the page into another language

Hi, Habr.

Today I will talk about my achievements in the field of instant page change - dynamic language change. I needed this thing quite recently, and since I don’t trust third-party implementations (I didn’t even find them), I had to write my own. During its use (about six months), I fixed all the most noticeable bugs (but this does not mean that they are no longer there :)), and now I am presenting a working version.

Someone will say that it is impractical to make a transfer on the client, but I got the situation that can’t be done otherwise: for the server transfer, you have to forcibly close web applications on the page in order not to lose data; in the case of a dynamic language change, the texts on the elements are simply replaced and the work continues. I think, not one annoyed me "Settings will be applied when rebooting." My implementation, though complicated, solves this problem.
')
In order not to be confused, I will define the following list of terms for this article:
Dictionary - a repository of keys by which access to localization in a given language. In fact, it is a regular JavaScript object, where properties are access keys, and their values ​​are translated strings.
A hash is an object that is the result of an ordered merge of dictionaries; general dictionary, from which the translation is subsequently sampled.

Now in more detail.


To speed up reading, I recommend skipping the chapters Source Code and Interface Description , as they give the description of the object of conversation from the program point of view. If you wish, you can read them later if you are interested in the article.

Source


Immediately suggest the source code. Do not go into details yet, but we will come back to it.
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; })(); 


Hierarchy


First, I want to note that in my implementation I selected several types of dictionaries:
1) downloadable :
The keys of such a dictionary cannot be changed by the user individually: they can be downloaded or unloaded only as a whole. Such a dictionary always takes precedence over other downloadable dictionaries. Named.
2) custom :
Built-in dictionary, the priority of which is always lower than the priority of any of the loaded dictionaries. Cannot be loaded or unloaded entirely, changes only by individual keys. Its meaning is to keep the values ​​that were specified by the user separately from the rest. If you download all downloaded dictionaries, it will not affect the user dictionary and its values ​​will still be available.
3) systemic :
According to the meaning, it repeats the user one completely, but it has the highest priority.


Figure 1 - List of downloadable dictionaries (in square brackets), as well as user and system dictionaries, arranged in order of increasing priority.

So, when changing the user or system dictionary, as well as changes in the list of loaded dictionaries, the hash is updated. The algorithm is as follows:
1) keys from objects are copied into the hash object in the reverse of the specified figure, i.e. in order of lower priority;
2) if such a key is already in the hash, then copying does not occur.

Key \ Dictionary[User]CommonSystemApp1App2App3[System]Hash
OkOkOkOk
CANCELCancelCancelCancelCancel
DONEDoneIs doneIs done
STRINGStringString

Table 1 - Transfer of keys to the hash from higher priority dictionaries.

By default, no dictionary is loaded; User and system dictionaries are empty. By the way, a call to a non-existent dictionary property returns not an empty string, but a string of the form "#key", where the hash character is followed by the name of the key to which the call was made. This is done so that it is immediately visible on the screen which keys do not exist.

Interface Description


After executing the source code, the global variable lang will become available, the value of which is the function that associates the attributes of the element with the hash through the key. You can pass arguments to it as follows:
 lang(htmlTextNode,strKey); lang(htmlTextNode,strKey,arrParams); lang(htmlTextNode,objKeys); lang(htmlElement,objKeys); 
Where:
htmlTextNode - the text node with which the binding occurs;
strKey - the key in the dictionary, which will be addressed;
arrParams - parameters substituted into the translation (more on this later);
objKeys is an object containing, in its properties, the keys by which the call should occur, and in values ​​the attribute to bind (as a string). In the value you can also specify the parameters inserted into the translation. For this, the value must be an array, where the first element is the attribute to bind, and the rest are the parameter values.
The variable lang has its own function-functions get, set, load, unload, params .

Use get :
 lang.get(strKey); 
Where:
strKey is the key whose value you want to get.
Returns the translation string associated with the passed key.

Using set :
 lang.set(strKey, strValue,boolSystem); 
Where:
strKey - the key whose value is to be set;
strValue is the value to set. If it is empty or is not a string, then the corresponding key is deleted from the dictionary;
boolSystem - if this parameter is set to true , then the entry occurs in the system dictionary, otherwise - in the user dictionary.
Returns the recorded value or null if the key has been deleted.

Use load :
 lang.load(strName,objData); 
Where:
strName - the name of the dictionary (string) or an object with a set of properties that represent the names of the dictionaries, and their meaning is the dictionaries themselves;
objData - if the first argument is a string, then this argument is a dictionary.
Returns the dictionary (s) that (e) was (s) loaded (s).

Using unload :
 lang.unload(strName); 
Where:
strName - the name (string) or names (array) of dictionaries that should be unloaded.
Grows uploaded dictionary (s).

Using params :
 lang. params(htmlElem,strKey,arrParams); 
Where:
htmlElem - an item that has already been associated with a dictionary;
strKey - dictionary access key;
arrParams - an array of new parameters.
Returns true if new parameters were set, or false otherwise.
The example in the next chapter should bring clarity to the understanding of the interface.

Interaction with elements


In order to be able to interact with dictionaries, you need to bind a specific element or text node to a hash through one of the attribute of this element and the key of the dictionary. This attribute can be both innerHTML , and title, alt , etc. If a text node is bound, then the default binding passes through textContent . Below I will describe why.
Consider an example:
 lang(document.body.appendChild(document.createElement("button")),{"just_a_text":"innerHTML" }); //      innerHTML   just_a_text 
The button is connected via a key that does not exist yet (therefore the text is "#just_a_text"). As for the technical implementation, the _lang property is created on the element, the properties of which are the hash keys, and the values ​​are the arrays, where the first elements are the attributes of this element, into which the hash values ​​will be written, and the rest are the parameters passed to the translation.
Now there are 3 options to write the key to the dictionary: add to the list of loaded dictionaries with a new dictionary or assign a value to the user or system dictionary. Consider all the options in order of increasing priority (I would advise doing it line by line in the console):
 lang.set("just_a_text","  "); //    lang.load("def",{"just_a_text":"   "}); lang.set("just_a_text","    ",1); //    
The text on the button will change with each step to the one specified in the code. Thus, in this case, all 3 types of dictionaries are used.
Now we remove the keys from the dictionaries in the reverse order and make sure that the declared hierarchy really works:
 lang.set("just_a_text","",1); //    lang.unload("def");//    def lang.set("just_a_text",""); //     
Now the text on the button will again become "#just_a_text".

Options


Often I had to not only substitute the text from the dictionary, but also pass some parameters into it, so the possibility of parameter substitution is also implemented. They can be specified when associating an element with a key, or after binding with the help of the lang.params function. To indicate the position of a parameter within a line, the /% \ d + / construct is used, where digits indicate the number of the parameter being passed. If the parameter was not passed, then an empty string is substituted.
Especially good replacement manifests itself in conjunction with 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]); 
Now inside the button, the passed parameters will be highlighted. Link

Performance


In order to distribute the load (I already wrote about this here ), I use an interval function that processes the array of elements associated with the dictionary.
Secondly, try to use as little as possible innerHTML on elements, instead use textContent on the text nodes of these elements. innerHTML works 20+ times slower, because the setter of this attribute parses the passed value as HTML code. textContent does not think about parsing HTML, but inserts text as it is (you can even <,> not change), but, unfortunately, this is not always applicable, in particular, in the last example.

Run under IE8


A little thought, I realized that this thing can be run under the eighth IE. To do this, you will have to resort to a pair of dirty hacks:
 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); }; 

Although in fact I believe that the IE line up to the eighth version inclusive should be buried for a long time. Internets are belong to us.

Conclusion


When designing this thing, I tried to optimize it as much as possible, for example, minimized text changes inside the element. If the value of the key has not changed, then it is not updated. As a result, it turned out that changing the text inside 10,000 buttons takes (with profiling enabled, via textContent ) for about a second.

There is a small minus: if you “pull out” an element from the DOM, then the data on it can no longer be updated. To solve the problem, you will have to re-associate it with the hash.

In principle, this implementation can be used not only to change the page language. Its main goal is to change the specified attributes when changing the dictionary. Those. the scope is much broader than that specified in the title.

The implementation is probably not the most successful. In the comments I am waiting for ratings and tips for improvement. And please argue the cons. Oh yes, MIT license.

UPD. An example can be found here . Just open the page when the internet is on.

UPD2. Special thanks to lahmatiy for the instructions on the true path and for pointing out the shortcomings.

Source: https://habr.com/ru/post/138492/


All Articles