当前位置: 首页 > 科技观察

现有Vue.js项目快速实现多语言切换的方法

时间:2023-03-18 23:50:23 科技观察

Web项目多语言(i18n,即国际化)是比较普遍的需求,常规做法如下:page,适用于CMS等网站分离多语言文本和页面结构,并在运行时动态替换。适用于单页应用(SPA)直接使用网页翻译插件,机器翻译。这种效果并不理想,而且有一定的局限性(后面会提到)。每种方案都有其优点和局限性,具体项目应根据实际情况选择。最近工作中遇到的需求是在已有项目的基础上快速推出多语言版本。项目基于Vue.js开发,迭代了多个版本。其实一开始是打算做多语言的,还引入了vue-i18n插件。本插件就是上面的第二种方案,使用JSON文件管理多语言文本资源,在Vue组件模板中通过键名引用文本。但是管理这些英文键名很麻烦,命名起来很头疼。而且在阅读代码时,很难从键名快速识别出对应的中文。后来发现VSCode有相关的插件,可以显示对应的中文,但是找代码还是有点麻烦。此外,该产品的多语言版本也没有被提上日程。时间长了就麻烦了,慢慢的直接在模板里写中文。该来的还是来了。产品突然说英文版很快就上线了,以后还会有其他语言的。最初的想法是直接使用Chrome浏览器自带的谷歌翻译功能,越快越好。但是经过一番测试,发现了很多问题。首先机器翻页的效果肯定要打折扣,不过这还是在可以接受的范围内。最关键的是会影响功能的使用。有什么问题?由于项目是用Vue.js开发的单页应用,所以页面内容完全用JS动态渲染。对话框中的某些文本会被Google翻译忽略。另外,谷歌翻译只处理DOM文本节点,输入框中的文本(包括占位符)会被忽略。最严重的问题是谷歌翻译处理的DOM元素已经失去了Vue的响应能力。数据变化后,DOM中的文字不会更新!如果你想继续使用浏览器谷歌翻译解决方案,你必须解决这个问题SomeProblems。通过调试,发现谷歌翻译使用的JS脚本嵌入在浏览器VM中,通过HTTP调用翻译服务,然后修改DOM元素。JS脚本经过压缩和混淆,格式化后很难看。想找到更新DOM的代码,然后用自己的逻辑覆盖它吗?你瞎了,算了吧。GoogleTranslateJS代码鉴于以上原因,浏览器自带的GoogleTranslate解决方案基本不予考虑。现在只剩下第二个选项了,语言配置文件与页面结构分离。前面说了vue-i18n用的不彻底。如果所有的组件都重新归一化,工作量会太大。有没有办法在不修改现有代码的情况下实现文本翻译?很自然的就想到了谷歌翻译的思路,直接翻译页面渲染结果。自行翻译的好处是可以精细控制DOM操作,比如还可以翻译输入框的文本和占位符。同时经过研究发现,Vue组件通过数据绑定渲染的DOM元素中包含的文本内容,无法通过innerHTML或innerText直接修改,会导致响应失败。解决方法是操作它的子元素,即文本节点(nodeType为3的节点),修改它的textContent属性。多语言配置映射表和谷歌翻译的区别在于我们采用静态翻译,即通过多语言配置文件进行映射。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.jsexportdefault{customerName:{en:'CustomerName',},//动态文本,后面我们会讲'剩余{0}台矿机未注册':{en:'{0}unregistered',},xxxx:{en:'XXX',}}DOM操作类似于谷歌翻译,我们也会在翻译后更新DOM。由于是单页应用,DOM会随着用户的操作不断更新。最初的想法是监听整个body的变化,在回调中更新DOM。有一个原生API可用于监控DOM变化,它是MutationObserver。mounted(){this.observeDOM(document.body);},methods:{observeDOM(el){letmutationTimer;constvm=this;constobserver=newMutationObserver(()=>{//类似debounce的效果,调用merge多次clearTimeout(mutationTimer);mutationTimer=setTimeout(()=>{if(!vm.mutationFromTrans){translate();vm.mutationFromTrans=true;setTimeout(()=>{vm.mutationFromTrans=false;},300);}},100);});constoptions={childList:true,//监听节点subtree的直接子节点的变化:true,//监听所有子节点attributes的变化:true,//监听thechangesofnodeattributescharacterData:true,//监听指定目标节点或子节点树中节点包含的字符数据的变化。};if(this.language==='en'){observer.observe(el,options);}},},但是试了之后会导致死循环,因为没有判断是否改变的DOM来自用户操作或翻译本身。于是在代码后面加了一个判断,结果还是不太理想。这种操作代价太大,页面性能影响很大。而且还有一个明显的问题,就是在进入新界面的时候,会闪一下,从中文变成英文。这种经历太可怕了。以后有办法改进。翻译我们先来看看翻译过程。翻译就是从多语言配置对象中找到匹配的属性名,得到对应语言的属性值。这个对于静态文本来说比较简单,直接使用属性名即可。但是如何处理动态文本呢?由于中文和英文的表达方式不同,这种文本不能简单地拆分成多个部分分别处理,而必须在英文表达方式中替换动态数据。我的方法是使用格式化的键名,例如像{0}这样的占位符。搜索时,固定文本优先。因为大部分情况都是固定文本,而这个匹配是O(1)的时间复杂度,优先判断会提高性能。当匹配失败时,遍历预先构造的正则列表中的匹配,如果成功则提取正则匹配组替换动态数据。如果失败,说明没有对应的翻译,直接返回原字符串即可。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);文本Nodes.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更新的时间。特别是当动态文本比较多的时候,涉及到正则匹配的遍历,比较耗时。如果您有更好的解决方案,欢迎留言!本文转载自微信公众号「1024翻译站」,可通过以下二维码关注。转载请联系1024翻译站公众号。

猜你喜欢