Yuzhibo program notes 33 - existing Vue An idea of fast multi language switching in JS project

Multilingualism (i18n, i.e. internationalization) is a common requirement for Web projects. There are probably the following common practices:

  1. Each language develops pages separately, which is suitable for websites such as CMS

  2. Multilingual text and page structure are separated and replaced dynamically at runtime. For single page applications (SPA)

  3. Directly use web translation plug-ins and machine translation. This effect is not ideal and has some limitations (which will be discussed later)

problem

Each scheme has its own advantages and limitations, and the specific project should be selected according to the actual situation. Recently, the demand encountered in work is to quickly launch multilingual versions based on existing projects.

The project is based on Vue JS development, has iterated many versions. In fact, at the beginning, it was planned to be multilingual, and the {vue-i18n} plug-in was also introduced. This plug-in is the second scheme above. It uses JSON files to manage multilingual text resources and references text through key names in Vue component templates.

However, it is troublesome to manage these English key names, and naming is a headache. And when reading the code, it is difficult to quickly identify the corresponding Chinese from the key name. Later, I found that VS Code has relevant plug-ins, which can display the corresponding Chinese, but it is still a little troublesome to find the code. In addition, the multilingual version of the product has not been put on the agenda for a long time, so it is troublesome to write Chinese directly in the template slowly.

As a result, it's time to come. The product suddenly said that it would quickly launch the English version recently, and there would be other languages in the follow-up. The initial idea was to directly use the Google translation function of Chrome browser, how fast and how to come. But after some tests, many problems were found.

First of all, the effect of machine turning must be discounted, but it is still acceptable. The most important thing is that it will affect the use of functions. What's the problem? Because the project uses Vue JS development of a single page application, the page content is completely rendered dynamically with JS. The text in some dialog boxes is ignored by Google translation.

In addition, Google translation only deals with DOM text nodes, and the text in the input box (including placeholder) is ignored. The most serious problem is that the DOM element after Google translation has lost the Vue responsive feature, and the text in the DOM will not be updated after the data changes!

If we want to continue to adopt the solution of browser Google translation, we must solve these problems. Through debugging, it is found that the JS script for Google translation is embedded in the browser VM, call the translation service through HTTP, and then modify the DOM element. JS script is compressed and confused, and it is also ugly after formatting. Want to find the code that updates the DOM and overwrite it with your own logic? I'm blind. Forget it.

Google Translate JS code

In view of the above reasons, the Google translation scheme provided by the browser is basically not considered.

Now there is only the second scheme, which separates the language configuration file from the page structure. As mentioned earlier, vue-i18n it is not used completely. If all components are normalized again, the workload will be too heavy. Is there any way to realize text translation without modifying the existing code? It is natural to think of the idea of Google translation and translate the page rendering results directly. The advantage of self translation is that you can finely control DOM operations. For example, you can translate the text in the input box and placeholder.

At the same time, through research, it is found that the DOM elements rendered by Vue components through data binding cannot be modified directly through # innerHTML or innerText, which will lead to reactive failure. The solution is to manipulate its child element, that is, the text node (node with nodeType 3), and modify its textContent attribute.

Multilingual configuration mapping table

The difference from Google translation is that we use static translation, that is, mapping through multilingual configuration files. vue-i18n# is to prepare a JSON file for each language. The attribute name is in English and the namespace (multi-level object) is used to avoid naming conflict. I simplified it directly, using a JS object to store all language versions, and the key name is the Chinese used in the page. With the development iterations accumulated over time, these Chinese characters are scattered in hundreds of files... My approach is to use VS Code global regular search, copy the search results, and write a JS method to process these strings into JS objects.

Search Chinese
Regular matching Chinese (not comprehensive enough, some mixed with other symbols):

[A-Z]*[\u4e00-\u9fa5][,,!! 0-9a-zA-Z\u4e00-\u9fa5]*

Copy the results to the translation tool for translation, and then write a function to merge these texts into objects and save them to labels JS file.

var kv = dist.reduce((acc,cur, index) => {
acc[cur]=en[index] || cur;return acc;
},{})

The structure of the object is roughly as follows:

// labels.js
export default {
  Customer sex name: {
    en: 'Customer Name',
  },
 //Dynamic text, which will be discussed later
 'surplus{0}This miner is not registered': {
    en: '{0} unregistered',
  },
  xxxx: {
    en: 'XXX',
  }
}

Operation DOM

Similar to Google translation, we also update the DOM afterwards. Because it is a single page application, the DOM will be constantly updated with the user's operation. The initial idea was to monitor the changes of the whole} body and update the DOM in the callback. There is a native API available for monitoring DOM changes, which is {MutationObserver.

mounted() {
  this.observeDOM(document.body);
},
methods: {
  observeDOM(el) {
    let mutationTimer;
    const vm = this;
    const observer = new MutationObserver(() => {
      //Similar to the effect of debounce, multiple calls are combined into one
      clearTimeout(mutationTimer);
      mutationTimer = setTimeout(() => {
        if (!vm.mutationFromTrans) {
          translate();
          vm.mutationFromTrans = true;
          setTimeout(() => {
            vm.mutationFromTrans = false;
          }, 300);
        }
      }, 100);
    });
    const options = {
      childList: true, //Monitor the changes of direct child nodes of node
      subtree: true, //Monitor the changes of all descendants of node
      attributes: true, //Monitor changes in node properties
      characterData: true, //Monitor the changes of character data contained in the specified target node or child node tree.
    };
    if (this.language === 'en') {
      observer.observe(el, options);
    }
  },
},

But after trying, I found that this will lead to wireless loop, because I didn't judge whether the DOM change came from the user operation or the translation itself. So judgment is added to the code, but the result is still not ideal. This operation is too expensive and the page performance is greatly affected. There is also an obvious problem, that is, when entering the new interface, it will flash from Chinese to English. The experience was terrible. There are ways to improve.

translate

Let's first look at the process of translation. Translation is to find the matching attribute name from the multilingual configuration object and obtain the attribute value of the corresponding language. This is relatively simple for static text. Just use the attribute name directly. But what about dynamic text? Due to the different expressions between Chinese and English, this text can not be simply divided into multiple parts for separate processing, but to replace dynamic data in the English expression.

My approach is to use formatted key names, such as placeholders like {0}. When searching, priority is given to matching fixed text. Because most cases are fixed text, and this matching is O(1) time complexity, priority judgment will improve performance. When the matching fails, traverse the matching in the regular list constructed in advance. If it succeeds, extract the regular matching group to replace the dynamic data. If it fails, it indicates that there is no corresponding translation. Just return the original string directly.

const keys = Object.keys(words);
//Cache regular in advance to avoid repeated execution and performance consumption
const regExps = keys.reduce((acc, key) => {
  //Template key name
  if (key.indexOf('{0}') > -1) {
    const reg = new RegExp(key.replace('{0}', '(.+)'));
    acc.push({
      expression: reg,
      key,
    });
  }
  return acc;
}, []);
export function translate(el = document.body, lang = 'en') {
  const kv = words;
  if (!el.querySelectorAll) {
    return;
  }
  const _trans = label => {
    const text = label?.trim?.();
    if (!text) {
      return label;
    }
    if (kv[text]?.[lang]) {
      return kv[text]?.[lang];
    }
    for (let index = 0; index < regExps.length; index++) {
      const regItem = regExps[index];
      const m = text.match(regItem.expression);
      if (m) {
        return kv[regItem.key][lang].replace('{0}', m[1]);
      }
    }
    return text;
  };
  [...el.querySelectorAll('*')].forEach(node => {
    //You cannot directly modify node InnerText, which will lead to Vue reactive failure
    // node.innerText = kv[node.innerText?.trim?.()] || node.innerText;
    if (node.nodeName === 'INPUT' && node.type === 'text') {
      node.value = _trans(node.value);
      node.placeholder = _trans(node.placeholder);
    }
    const textNodes = [...node.childNodes].filter(n => n.nodeType === 3);
    textNodes.forEach(textNode => {
      textNode.textContent = _trans(textNode.textContent);

  groupId>org.springframework.boot</groupId>
  
  <artifactId>spring-boot-starter-freemarker</artifactId>
  
  * 1. The www.yachengyl.cn protocol www.fudayulpt.cn configured www.ued3zc.cn the client is inconsistent with the protocol of the server.
  
  * eg: consumer www.baichuangyule.cn www. jinmazx.cn www.bhylzc.cn protocol www.jintianxuesha.com= dubbo, provider only has other protocol services(rest).
  
  * 2. The registration www.xinhuihpw.com center www.wanyayuue.cn not robust and pushes illegal specification data.
  
  if (CollectionUtils.www.feihongyul.cn isEmptyMap(newUrlInvokerMap)) www.qiaoheibpt.com{
  
  logger.error(new www.shengrenpt.com IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size www.baishenjzc.cn :0.
  
  List<Invoker<www.huanhua2zhuc.cn>> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values(www.baihuayl7.cn)));
  
  // pre-route and build cache, notice www.shengrenyp.cn that route cache should build on www.shengrenyp.cn original Invoker list.
  
  // toMergeMethodInvokerMap(www.xinchenptgw.cn ) will wrap some invokers www.xinhuihpw.com having www.youxiu2pt.cn different groups, those wrapped invokers not should be routed.
  
  routerChain.setInvokers(www.haojuptzc.cn newInvokers);
  
  this.invokers =www.wujiu5zhuce.cn

 

Improved DOM operation

As mentioned earlier, if the translation is performed after DOM rendering, the page performance is very poor. So I thought about the rendering process of Vue itself. Can I intercept the rendering process of Vue components and insert some additional logic? Through the source code, it is found that there is one on the Vue prototype__ patch__ Method, which will be executed every time the DOM is updated. Starting from here, rewrite this method to perform translation operations on DOM elements that have not been mounted to the document tree.

const __patch__ = Vue.prototype.__patch__;
Vue.prototype.__patch__ = function() {
  const elm = __patch__.apply(this, arguments);
  if (this.$store?.getters?.language) {
    translate(elm, this.$store?.getters?.language);
  }
  return elm;
};

So far, multilingual translation has been basically completed. After weighing and comparing, this scheme is relatively time-saving and labor-saving, and can meet the requirements. Of course, this scheme has a certain impact on page performance more or less. After all, it increases the time of DOM update. Especially when there are many dynamic texts, it involves traversing regular matching, which is time-consuming. If you have a better plan, welcome to leave a message!

Posted by mrjameer on Tue, 17 May 2022 14:33:46 +0300