vue項目實現多語言切換的思路
Web 項目多語言(i18n,即國際化)是比較常見的需求,常規的做法大概有以下幾種:
每種語言單獨開發頁面,適用于 CMS 之類的網站 多語言文本和頁面結構分離,運行時動態替換。適用于單頁應用(SPA) 直接用網頁翻譯插件,機器翻譯。這種效果不太理想,同時有一些局限性(后面會講到)問題
每一種方案都有各自的優點和局限性,具體項目應該根據實際情況選擇。最近在工作中碰到的需求是要在現有的項目基礎上快速推出多語言版本。項目是基于 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 對象。
匹配中文的正則(不夠全面,有些還夾雜了其他符號):
[A-Z]*[u4e00-u9fa5][,,!! 0-9a-zA-Zu4e00-u9fa5]*
將結果復制到翻譯工具翻譯,再寫一個函數把這些文本合并成對象,并保存到labels.js文件中備用。
var kv = dist.reduce((acc,cur, index) => {acc[cur]=en[index] || cur;return acc;},{})
對象的結構大致如下:
// labels.jsexport default { 客戶性名: { en: ’Customer Name’, }, // 動態文本,后面會講到 ’剩余{0}臺礦機未登記’: { en: ’{0} unregistered’, }, xxxx: { en: ’XXX’, }}
操作 DOM
跟 Google 翻譯類似,我們也采取事后更新 DOM 的方式來進行翻譯。由于是單頁應用,隨著用戶的操作,會不停地更新 DOM。一開始的想法是監聽整個 body的變化,在回調里再更新 DOM。監聽 DOM 變化有一個原生的 API 可用,就是 MutationObserver。
mounted() { this.observeDOM(document.body);},methods: { observeDOM(el) { let mutationTimer; const vm = this; const observer = new MutationObserver(() => { // 類似于 debounce 的效果,多次調用合并為一次 clearTimeout(mutationTimer); mutationTimer = setTimeout(() => { if (!vm.mutationFromTrans) { translate(); vm.mutationFromTrans = true; setTimeout(() => { vm.mutationFromTrans = false; }, 300); } }, 100); }); const options = { childList: true, // 監視node直接子節點的變動 subtree: true, // 監視node所有后代的變動 attributes: true, // 監視node屬性的變動 characterData: true, // 監視指定目標節點或子節點樹中節點所包含的字符數據的變化。 }; if (this.language === ’en’) { observer.observe(el, options); } },}
但是試過之后發現這會導致無線循環,因為沒有判斷 DOM 的變化來自用戶操作還是翻譯本身。所以代碼里后面加了判斷,但是結果依然不理想。這種操作代價太大了,頁面性能受了很大影響。而且還有個很明顯的問題,就是進入到新的界面會閃一下,從中文變成英文。這個體驗太糟糕了。后面有改進辦法。
翻譯
先來來看下翻譯的過程。翻譯就是從多語言配置對象里查找匹配的屬性名,獲取對應語言的屬性值。這對于靜態文本來說比較簡單,直接用屬性名就好了。但是對于動態的文本怎么處理呢?由于中英文表達方式不一樣,這種文本不能簡單地拆分成多個部分單獨處理,而是要在英文的表達方式里替換動態數據。我的做法是使用帶格式的鍵名,比如{0}這樣的占位符。在查找的時候,優先匹配固定文本。因為大部分情況是固定文本,而且這種匹配是O(1)時間復雜度的,優先判斷會提高性能。匹配失敗的時候才去提前構造好的正則列表里遍歷匹配,成功則提取正則匹配的group用于替換動態數據。如果失敗,說明沒有對應的翻譯,直接返回原始字符串就行了。
const keys = Object.keys(words);// 提前緩存正則,避免重復執行消耗性能const regExps = keys.reduce((acc, key) => { // 模板型鍵名 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 => { // 不能直接修改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); } const textNodes = [...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() { const elm = __patch__.apply(this, arguments); if (this.$store?.getters?.language) { translate(elm, this.$store?.getters?.language); } return elm;};
至此,基本完成了多語言翻譯。經過權衡對比,這個方案算是比較省時省力又能完成需求的了。當然,這種方案或多或少對頁面性能有一定影響,畢竟增加了 DOM 更新的時間。尤其是動態文本較多的情況,涉及到遍歷正則匹配,比較耗時。如果大家有更好的方案,歡迎留言!
以上就是vue項目實現多語言切換的思路的詳細內容,更多關于vue項目多語言切換的資料請關注好吧啦網其它相關文章!
相關文章: