vue项目实现多语言切换的思路
Web项目多语言(i18n,即国际化)是比较常见的需求,常规的做法大概有以下几种:
- 每种语言单独开发页面,适用于CMS之类的网站
- 多语言文本和页面结构分离,运行时动态替换。适用于单页应用(SPA)
- 直接用网页翻译插件,机器翻译。这种效果不太理想,同时有一些局限性(后面会讲到)
问题
每一种方案都有各自的优点和局限性,具体项目应该根据实际情况选择。最近在工作中碰到的需求是要在现有的项目基础上快速推出多语言版本。项目是基于Vue.js开发的,已经迭代过很多版本了。其实一开始是有规划多语言的,也引进了vue-i18n插件。这个插件就是上面第二种方案,用JSON文件管理多语言的文本资源,在Vue组件模板里通过键名引用文本。但是要管理这些英文键名比较麻烦,命名就很头疼。而且阅读代码的时候也很难从键名快速识别出对应的中文。后面发现VSCode有相关的插件,可以显示出对应的中文,但是代码找起来还是有点麻烦。再加上产品的多语言版本一直没有提上日程,时间久了就嫌麻烦,慢慢地就直接在模板里写中文了。
结果,该来的还是来了。老板突然说最近要推出英文版,后续还有其他语言。一开始的想法是直接用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对象存储所有语言版本,键名就是页面用到的中文。随着日积月累的开发迭代,这些中文散落在几百个文件里……我的做法是用VSCode全局正则搜索,把查找结果复制出来,写一个JS方法把这些字符串处理成JS对象。
匹配中文的正则(不够全面,有些还夹杂了其他符号):
[A-Z]*[\u4e00-\u9fa5][,,!!0-9a-zA-Z\u4e00-\u9fa5]*
将结果复制到翻译工具翻译,再写一个函数把这些文本合并成对象,并保存到labels.js文件中备用。
varkv=dist.reduce((acc,cur,index)=>{ acc[cur]=en[index]||cur;returnacc; },{})
对象的结构大致如下:
//labels.js exportdefault{ 客户性名:{ en:'CustomerName', }, //动态文本,后面会讲到 '剩余{0}台矿机未登记':{ en:'{0}unregistered', }, xxxx:{ en:'XXX', } }
操作DOM
跟Google翻译类似,我们也采取事后更新DOM的方式来进行翻译。由于是单页应用,随着用户的操作,会不停地更新DOM。一开始的想法是监听整个body的变化,在回调里再更新DOM。监听DOM变化有一个原生的API可用,就是MutationObserver。
mounted(){ this.observeDOM(document.body); }, methods:{ observeDOM(el){ letmutationTimer; constvm=this; constobserver=newMutationObserver(()=>{ //类似于debounce的效果,多次调用合并为一次 clearTimeout(mutationTimer); mutationTimer=setTimeout(()=>{ if(!vm.mutationFromTrans){ translate(); vm.mutationFromTrans=true; setTimeout(()=>{ vm.mutationFromTrans=false; },300); } },100); }); constoptions={ childList:true,//监视node直接子节点的变动 subtree:true,//监视node所有后代的变动 attributes:true,//监视node属性的变动 characterData:true,//监视指定目标节点或子节点树中节点所包含的字符数据的变化。 }; if(this.language==='en'){ observer.observe(el,options); } }, }
但是试过之后发现这会导致无线循环,因为没有判断DOM的变化来自用户操作还是翻译本身。所以代码里后面加了判断,但是结果依然不理想。这种操作代价太大了,页面性能受了很大影响。而且还有个很明显的问题,就是进入到新的界面会闪一下,从中文变成英文。这个体验太糟糕了。后面有改进办法。
翻译
先来来看下翻译的过程。翻译就是从多语言配置对象里查找匹配的属性名,获取对应语言的属性值。这对于静态文本来说比较简单,直接用属性名就好了。但是对于动态的文本怎么处理呢?由于中英文表达方式不一样,这种文本不能简单地拆分成多个部分单独处理,而是要在英文的表达方式里替换动态数据。我的做法是使用带格式的键名,比如{0}这样的占位符。在查找的时候,优先匹配固定文本。因为大部分情况是固定文本,而且这种匹配是O(1)时间复杂度的,优先判断会提高性能。匹配失败的时候才去提前构造好的正则列表里遍历匹配,成功则提取正则匹配的group用于替换动态数据。如果失败,说明没有对应的翻译,直接返回原始字符串就行了。
constkeys=Object.keys(words); //提前缓存正则,避免重复执行消耗性能 constregExps=keys.reduce((acc,key)=>{ //模板型键名 if(key.indexOf('{0}')>-1){ constreg=newRegExp(key.replace('{0}','(.+)')); acc.push({ expression:reg, key, }); } returnacc; },[]); exportfunctiontranslate(el=document.body,lang='en'){ constkv=words; if(!el.querySelectorAll){ return; } const_trans=label=>{ consttext=label?.trim?.(); if(!text){ returnlabel; } if(kv[text]?.[lang]){ returnkv[text]?.[lang]; } for(letindex=0;index{ //不能直接修改node.innerText,会导致Vue响应式失效 //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); } consttextNodes=[...node.childNodes].filter(n=>n.nodeType===3); textNodes.forEach(textNode=>{ textNode.textContent=_trans(textNode.textContent); }); }); }
改进后的DOM操作
前面提过,如果在DOM渲染后再执行翻译,页面性能非常差。于是想到了Vue本身的渲染过程,能不能拦截Vue组件渲染过程,插入一些额外的逻辑呢?通过扒源码发现,Vue原型上有个__patch__方法,每次更新DOM的时候都会执行。就从这里入手,重写这个方法,对还没挂载到文档树的DOM元素执行翻译操作。
const__patch__=Vue.prototype.__patch__; Vue.prototype.__patch__=function(){ constelm=__patch__.apply(this,arguments); if(this.$store?.getters?.language){ translate(elm,this.$store?.getters?.language); } returnelm; };
至此,基本完成了多语言翻译。经过权衡对比,这个方案算是比较省时省力又能完成需求的了。当然,这种方案或多或少对页面性能有一定影响,毕竟增加了DOM更新的时间。尤其是动态文本较多的情况,涉及到遍历正则匹配,比较耗时。如果大家有更好的方案,欢迎留言!
以上就是vue项目实现多语言切换的思路的详细内容,更多关于vue项目多语言切换的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。