Web 项目多语言(i18n,即国际化)是比较常见的需求,常规的做法大概有以下几种:

  1. 每种语言单独开发页面,适用于 CMS 之类的网站
  2. 多语言文本和页面结构分离,运行时动态替换。适用于单页应用(SPA)
  3. 直接用网页翻译插件,机器翻译。这种效果不太理想,同时有一些局限性(后面会讲到)

问题

每一种方案都有各自的优点和局限性,具体项目应该根据实际情况选择。最近在工作中碰到的需求是要在现有的项目基础上快速推出多语言版本。项目是基于 Vue.js 开发的,已经迭代过很多版本了。其实一开始是有规划多语言的,也引进了 vue-i18n 插件。这个插件就是上面第二种方案,用 JSON 文件管理多语言的文本资源,在 Vue 组件模板里通过键名引用文本。但是要管理这些英文键名比较麻烦,命名就很头疼。而且阅读代码的时候也很难从键名快速识别出对应的中文。后面发现 VS Code 有相关的插件,可以显示出对应的中文,但是代码找起来还是有点麻烦。再加上产品的多语言版本一直没有提上日程,时间久了就嫌麻烦,慢慢地就直接在模板里写中文了。

结果,该来的还是来了。老板突然说最近要推出英文版,后续还有其他语言。一开始的想法是直接用 Chrome 浏览器自带的 Google 翻译功能,怎么快怎么来。但经过一番测试,发现了不少问题。首先机翻的效果肯定是要打折扣的,但这还在接受范围内。最关键的是会影响到功能使用。什么问题呢?由于项目是用 Vue.js 开发的单页应用,页面内容完全是用 JS 动态渲染的。有些对话框内的文字 Google 翻译就忽略了。另外,Google 翻译只处理了 DOM 文本节点,input输入框内的文字(包括placeholder)被忽略了。最严重的问题是,经过 Google 翻译处理后的 DOM 元素,竟然失去了 Vue 响应式特性,数据变化后 DOM 内的文字不会更新了!

如果要继续采用浏览器 Google 翻译的方案,就要解决这几个问题。通过调试发现 Google 翻译用的 JS 脚本是嵌入到浏览器 VM 里的,通过 HTTP 调用翻译服务,然后修改 DOM 元素。JS 脚本是压缩混淆过的,格式化后也很难看。想要找到更新 DOM 的代码,然后用自己的逻辑去覆盖?眼睛都看瞎了,还是算了。

鉴于以上原因,浏览器自带的 Google 翻译方案基本不考虑了。

现在只剩下第二种方案了,语言配置文件和页面结构分离。前面提过,vue-i18n用得不彻底,如果把所有组件重新规范化,工作量太大了。有没有办法不修改现有代码,也能实现文本翻译呢?很自然地就想到了 Google 翻译的思路,直接对页面渲染结果进行翻译。自己翻译的优势就是,可以精细地控制 DOM 操作,比如可以把输入框里的文本和placeholder也翻译出来。同时,经过研究发现,Vue 组件通过数据绑定渲染出来的 DOM 元素,包含的文本内容不能直接通过 innerHTML或者innerText修改,这样会导致响应式失效。解决办法是操作它的子元素,也就是文本节点(nodeType为3的节点),修改它的 textContent属性。

多语言配置映射表

跟 Google 翻译不同之处在于,我们采用静态翻译,也就是通过多语言配置文件映射。 vue-i18n 是每种语言准备一个 JSON 文件,属性名用英文,用命名空间(多层级对象)的方式避免命名冲突。我直接简化了,用一个 JS 对象存储所有语言版本,键名就是页面用到的中文。随着日积月累的开发迭代,这些中文散落在几百个文件里……我的做法是用 VS Code 全局正则搜索,把查找结果复制出来,写一个 JS 方法把这些字符串处理成 JS 对象。



匹配中文的正则(不够全面,有些还夹杂了其他符号):

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

将结果复制到翻译工具翻译,再写一个函数把这些文本合并成对象,并保存到labels.js文件中备用。

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

对象的结构大致如下:

  1. // labels.js
  2. export default {
  3. 客户性名: {
  4. en: 'Customer Name',
  5. },
  6. // 动态文本,后面会讲到
  7. '剩余{0}台矿机未登记': {
  8. en: '{0} unregistered',
  9. },
  10. xxxx: {
  11. en: 'XXX',
  12. }
  13. }

操作 DOM

跟 Google 翻译类似,我们也采取事后更新 DOM 的方式来进行翻译。由于是单页应用,随着用户的操作,会不停地更新 DOM。一开始的想法是监听整个 body的变化,在回调里再更新 DOM。监听 DOM 变化有一个原生的 API 可用,就是 MutationObserver

  1. mounted() {
  2. this.observeDOM(document.body);
  3. },
  4. methods: {
  5. observeDOM(el) {
  6. let mutationTimer;
  7. const vm = this;
  8. const observer = new MutationObserver(() => {
  9. // 类似于 debounce 的效果,多次调用合并为一次
  10. clearTimeout(mutationTimer);
  11. mutationTimer = setTimeout(() => {
  12. if (!vm.mutationFromTrans) {
  13. translate();
  14. vm.mutationFromTrans = true;
  15. setTimeout(() => {
  16. vm.mutationFromTrans = false;
  17. }, 300);
  18. }
  19. }, 100);
  20. });
  21. const options = {
  22. childList: true, // 监视node直接子节点的变动
  23. subtree: true, // 监视node所有后代的变动
  24. attributes: true, // 监视node属性的变动
  25. characterData: true, // 监视指定目标节点或子节点树中节点所包含的字符数据的变化。
  26. };
  27. if (this.language === 'en') {
  28. observer.observe(el, options);
  29. }
  30. },
  31. },

但是试过之后发现这会导致无线循环,因为没有判断 DOM 的变化来自用户操作还是翻译本身。所以代码里后面加了判断,但是结果依然不理想。这种操作代价太大了,页面性能受了很大影响。而且还有个很明显的问题,就是进入到新的界面会闪一下,从中文变成英文。这个体验太糟糕了。后面有改进办法。

翻译

先来来看下翻译的过程。翻译就是从多语言配置对象里查找匹配的属性名,获取对应语言的属性值。这对于静态文本来说比较简单,直接用属性名就好了。但是对于动态的文本怎么处理呢?由于中英文表达方式不一样,这种文本不能简单地拆分成多个部分单独处理,而是要在英文的表达方式里替换动态数据。我的做法是使用带格式的键名,比如{0}这样的占位符。在查找的时候,优先匹配固定文本。因为大部分情况是固定文本,而且这种匹配是O(1)时间复杂度的,优先判断会提高性能。匹配失败的时候才去提前构造好的正则列表里遍历匹配,成功则提取正则匹配的group用于替换动态数据。如果失败,说明没有对应的翻译,直接返回原始字符串就行了。

  1. const keys = Object.keys(words);
  2. // 提前缓存正则,避免重复执行消耗性能
  3. const regExps = keys.reduce((acc, key) => {
  4. // 模板型键名
  5. if (key.indexOf('{0}') > -1) {
  6. const reg = new RegExp(key.replace('{0}', '(.+)'));
  7. acc.push({
  8. expression: reg,
  9. key,
  10. });
  11. }
  12. return acc;
  13. }, []);
  14. export function translate(el = document.body, lang = 'en') {
  15. const kv = words;
  16. if (!el.querySelectorAll) {
  17. return;
  18. }
  19. const _trans = label => {
  20. const text = label?.trim?.();
  21. if (!text) {
  22. return label;
  23. }
  24. if (kv[text]?.[lang]) {
  25. return kv[text]?.[lang];
  26. }
  27. for (let index = 0; index < regExps.length; index++) {
  28. const regItem = regExps[index];
  29. const m = text.match(regItem.expression);
  30. if (m) {
  31. return kv[regItem.key][lang].replace('{0}', m[1]);
  32. }
  33. }
  34. return text;
  35. };
  36. [...el.querySelectorAll('*')].forEach(node => {
  37. // 不能直接修改node.innerText,会导致Vue响应式失效
  38. // node.innerText = kv[node.innerText?.trim?.()] || node.innerText;
  39. if (node.nodeName === 'INPUT' && node.type === 'text') {
  40. node.value = _trans(node.value);
  41. node.placeholder = _trans(node.placeholder);
  42. }
  43. const textNodes = [...node.childNodes].filter(n => n.nodeType === 3);
  44. textNodes.forEach(textNode => {
  45. textNode.textContent = _trans(textNode.textContent);
  46. });
  47. });
  48. }

改进后的 DOM 操作

前面提过,如果在 DOM 渲染后再执行翻译,页面性能非常差。于是想到了 Vue 本身的渲染过程,能不能拦截 Vue 组件渲染过程,插入一些额外的逻辑呢?通过扒源码发现,Vue 原型上有个__patch__方法,每次更新 DOM 的时候都会执行。就从这里入手, 重写这个方法,对还没挂载到文档树的 DOM 元素执行翻译操作。

  1. const __patch__ = Vue.prototype.__patch__;
  2. Vue.prototype.__patch__ = function() {
  3. const elm = __patch__.apply(this, arguments);
  4. if (this.$store?.getters?.language) {
  5. translate(elm, this.$store?.getters?.language);
  6. }
  7. return elm;
  8. };

至此,基本完成了多语言翻译。经过权衡对比,这个方案算是比较省时省力又能完成需求的了。当然,这种方案或多或少对页面性能有一定影响,毕竟增加了 DOM 更新的时间。尤其是动态文本较多的情况,涉及到遍历正则匹配,比较耗时。如果大家有更好的方案,欢迎留言!

这个图的信息量太大了,你们猜猜是什么

现有 Vue.js 项目快速实现多语言切换的一种思路的更多相关文章

  1. 让现有vue前端项目快速支持多语言 - 用.net core程序快速替换中文为资源Key,咱不干体力活

    前言 最近应公司上层要求,需要将现有项目尽快支持多语言,而中文内容可以找专业人员翻译.那么咱们说干就干,首先我们项目的前端是用vue写的spa程序且组件方面用的element ui,那么自然而然想到用 ...

  2. vue.js项目安装

    Vue.js 安装 NPM 方法安装vue.js项目 npm 版本需要大于 3.0,如果低于此版本需要升级它: # 查看版本 $ npm -v 2.3.0 #升级 npm npm install np ...

  3. Vuejs实例-01使用vue-cli脚手架搭建Vue.js项目

    [TOC] 1. 前言 vue-cli 一个简单的构建Vue.js项目的命令行界面 整体过程: $ npm install -g vue-cli $ vue init webpack vue-admi ...

  4. vue.js项目构建

    这里构建的vue.js项目依赖node服务器运行. 项目搭建完整步骤: 安装node.js ,转至nodeJs网站http://nodejs.cn/ 下载nodeJs进行安装. 安装完毕检查nodeJ ...

  5. vscode下面开发vue.js项目

    vscode下面开发vue.js项目   https://blog.csdn.net/linzhiqiang0316/article/details/79176651 vscode下面开发vue.js ...

  6. Vue (一) --- vue.js的快速入门使用

    =-----------------------------------把现在的工作做好,才能幻想将来的事情,专注于眼前的事情,对于尚未发生的事情而陷入无休止的忧虑之中,对事情毫无帮助,反而为自己凭添 ...

  7. 建立多页面vue.js项目

    介绍 根据需求,我们希望建立一个多页面的vue.js项目,如何改造单页面vue.js项目为多页面项目?跟着我的步伐看下去吧. 1.创建单页面vue.js项目 简单的记录一下创建步骤: --安装cnpm ...

  8. vue-cli脚手架搭建Vue.js项目

    前提条件: 一.node.js 下载 https://nodejs.org/zh-cn/download/ 二.webpack 安装 npm install webpack -g   PS:-g 就是 ...

  9. 如何把vue.js项目部署到服务器上

    如何把vue.js项目部署到服务器上面,我用的是tomcat服务器 1-改一下config/index.js文件,如下图,把assetsPublicPath: './', productionSour ...

随机推荐

  1. 机器学习:支持向量机(SVM)

    SVM,称为支持向量机,曾经一度是应用最广泛的模型,它有很好的数学基础和理论基础,但是它的数学基础却比以前讲过的那些学习模型复杂很多,我一直认为它是最难推导,比神经网络的BP算法还要难懂,要想完全懂这 ...

  2. C语言文件读写命令fprintf和fscanf

    以向文件中读取和写入二维数组为例. 以下是fprintf的使用:向文件中写入10*10的二维数组,数组元素为1~100之间的随机数. #include <stdlib.h> #includ ...

  3. Spring Security-获取当前登录用户的详细信息

    在Spring框架里面,可以通过以下几种方式获取到当前登录用户的详细信息: 1. 在Bean中获取用户信息 Authentication authentication = SecurityContex ...

  4. linux驱动之内核空间几种长延时的实现策略的优劣评估

    本文转载自http://blog.chinaunix.net/uid-23769728-id-3084737.html 这里所谓的长延时,是指其实现时间延时的粒度可以在HZ这一水准上.<深入Li ...

  5. CSP-J2019 把8个同样的球放在同样的5个袋子里,允许有的袋子空着不放,问共有多少种不同的分法?

    把8个同样的球放在同样的5个袋子里,允许有的袋子空着不放,问共有多少种不同的分法? 提示:如果8个球都放在一个袋子里,无论是放哪个袋子,都只算同一种分法. 解析: 把问题合成,先思索5个袋子都不空的状 ...

  6. css基本样式设置

    div中文字居中 如何让一个div中的文字水平和垂直居中?设置如下: 给定该div的长宽(或者二者只给出其一也可) .box{ height: 100px; width: 30%; text-alig ...

  7. 数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解

    数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解 对数组有不了解的可以先看看我的另一篇文章,那篇文章对数组有很多详细的解析,而本篇文章则着重讲动态数组,另一篇文章链接 ...

  8. 华为云服务器基于hadoop2.7.5安装hive-2.3.7

    1. Hive简介 The Apache Hive data warehouse software facilitates reading, writing, and managing large d ...

  9. OC基础--字符串

    前言 做iOS开发有3年了,从当初的小白到现在,断断续续看过很多资料,之前也写过一些博文来记录,但是感觉知识点都比较凌乱.所以最近准备抽时间把iOS开发的相关知识进行一个梳理,主要分为OC基础.UI控 ...

  10. Redis学习2:Redis的数据类型和常用操作

    1.常用命令 命令 说明 返回值 时间复杂度 keys */[pattern] 遍历所有符合条件的key,一般不在生产环境使用 所有key O(n) dbsize 计算key的总数 n O(1) ex ...