为支持两个语言版本,我基于谷歌翻译API写了一款自动翻译的 webpack 插件
大家好,我是若川。持續(xù)組織了6個(gè)月源碼共讀活動(dòng),感興趣的可以點(diǎn)此加我微信 ruochuan12?參與,每周大家一起學(xué)習(xí)200行左右的源碼,共同進(jìn)步。同時(shí)極力推薦訂閱我寫的《學(xué)習(xí)源碼整體架構(gòu)系列》?包含20余篇源碼文章。歷史面試系列
本文來自讀者@漫思維 投稿授權(quán)
原文鏈接:https://juejin.cn/post/7072677637117706270
1前言
以下我會(huì)列舉出我業(yè)務(wù)中遇到的問題難點(diǎn)及相對(duì)應(yīng)的解決方法,解釋簡繁體插件怎么誕生的整個(gè)過程
2背景
目前開發(fā)工作有大量的營銷活動(dòng)需要編寫,特點(diǎn)是小而多,同時(shí)現(xiàn)階段項(xiàng)目需要做大陸與港臺(tái)兩個(gè)版本
3現(xiàn)階段實(shí)現(xiàn)的方案
先做完大陸版本,最后再復(fù)刻一份代碼, 改成港臺(tái)版本
將項(xiàng)目中的漢字、價(jià)格、登錄方式進(jìn)行替換。
4存在的問題
首先復(fù)制來復(fù)制去就不是一個(gè)很好的方案,容易復(fù)制出問題,其次兩個(gè)版本都是需要同一個(gè)時(shí)間點(diǎn)上線,復(fù)刻代碼的代碼的時(shí)機(jī)存在問題,如果復(fù)刻的過早,如果提測階段大陸版本有bug, 那么就需要修改兩份bug, 如果復(fù)刻的過晚那么會(huì)存在港臺(tái)版本測試時(shí)間不足,也易導(dǎo)致問題發(fā)生。
簡繁體轉(zhuǎn)換,都是將簡體手動(dòng)復(fù)制到谷歌翻譯網(wǎng)頁端中翻譯好,再手動(dòng)替換,繁瑣且工程量大, 登錄方式需要單獨(dú)的復(fù)制一份。
5兩個(gè)版本之間存在以下不同點(diǎn)
登錄方式的不同, 大陸主要是用賬號(hào)密碼登錄,而港臺(tái)使用谷歌、臉書、蘋果登錄
價(jià)格、單位不同,¥ 與 NT$
漢字的形式不同,中文簡體與中文繁體
核心問題在于復(fù)刻出一份項(xiàng)目存在的工作量與潛在風(fēng)險(xiǎn)較大,所以需要將兩個(gè)項(xiàng)目合成一個(gè)項(xiàng)目,怎么解決?
6解決方案
1. 將兩個(gè)項(xiàng)目合并成一個(gè)項(xiàng)目
如果需要將兩個(gè)項(xiàng)目合成一個(gè)項(xiàng)目,并解決以上分析出來的不同點(diǎn),那么顯而易見,需要有個(gè)一標(biāo)識(shí)去區(qū)分,那么使用環(huán)境變量解決這個(gè)問題是非常合適的,以vue項(xiàng)目舉例, 可以編寫對(duì)應(yīng)的環(huán)境變量配置。
大陸版本生產(chǎn)環(huán)境:.env
VUE_APP_ENV=prod VUE_APP_PUBLIC_PATH=/mainland大陸版本開發(fā)環(huán)境:.env
VUE_APP_ENV=dev VUE_APP_PUBLIC_PATH=/mainland港臺(tái)版本開發(fā)環(huán)境:.env.ht
VUE_APP_ENV=ht VUE_APP_PUBLIC_PATH=/ht NODE_ENV=productionpackage.json
"serve":?"vue-cli-service?serve", "build":?"vue-cli-service?build", "build:ht":?"vue-cli-service?build?--mode?ht",可以看到這里使用了一個(gè)自定義變量 VUE_APP_ENV, 在項(xiàng)目代碼中就可以使用 process.env.VUE_APP_ENV 去做區(qū)分當(dāng)前是大陸還是港臺(tái)了,同時(shí)為什么不使用NODE_ENV作為變量,因?yàn)樵撟兞客鶗?huì)有其他用途,如當(dāng)NODE_ENV設(shè)置為production 時(shí),打包時(shí)會(huì)做一些如壓縮等優(yōu)化操作。
注: 港臺(tái)版本不做測試環(huán)境的區(qū)分,因?yàn)橥箨懓娴倪壿嫑]有問題,港臺(tái)版的就沒有問題,所以只需要基于大陸版開發(fā),港臺(tái)版只需要最后打包一次即可 **(測試環(huán)境可選,只需要多添加一個(gè)配置即可)**。
其他注意點(diǎn): process.env.VUE_APP_ENV通常只能在node環(huán)境下才能訪問的,但是vue-cli創(chuàng)建項(xiàng)目會(huì)自動(dòng)將.env里的變量注入到運(yùn)行時(shí)環(huán)境中,也就是使用一個(gè)全局變量存起來,通常是使用webpack的define-plugin插件實(shí)現(xiàn)的。
解決了環(huán)境變量的問題,接下來的工作就比較好進(jìn)行了。
2. 解決登錄方式的不同
將兩套登錄封裝成兩個(gè)不同的組件,因?yàn)榈卿浲婕暗揭恍┤譅顟B(tài),項(xiàng)目一般都會(huì)使用vuex等全局狀態(tài)管理工具,所以默認(rèn)使用vuex儲(chǔ)存狀態(tài),把整個(gè)包含登錄邏輯的代碼制作成一個(gè)項(xiàng)目的基礎(chǔ)模板,使用自定義腳手架拉取即可,同時(shí)注意使用vuex時(shí),為登錄相關(guān)的狀態(tài),放置到一個(gè)module下,這樣基于該模板創(chuàng)建項(xiàng)目后, 每個(gè)項(xiàng)目的其它狀態(tài)單獨(dú)再寫module即可,避免修改登錄的module。
自定義腳手架:交互式創(chuàng)建項(xiàng)目,輸入一些選項(xiàng),如項(xiàng)目名稱,項(xiàng)目描述之類的,再從gitlab等遠(yuǎn)程倉庫拉取已經(jīng)寫好的模板,將模板中的一些特定變量,使用模板引擎將模板中的項(xiàng)目名稱等替換,最終產(chǎn)生一個(gè)新的項(xiàng)目。(腳手架還有其他用途,這里只描述使用它創(chuàng)建一個(gè)簡單的項(xiàng)目)
沒有腳手架那就只能使用git clone 下來后再修改項(xiàng)目名稱之類的東西,會(huì)增加一點(diǎn)額外的工作,但不影響不大。
封裝的部分邏輯:
比如大陸的登錄組件叫做 mainlandLogin, 港臺(tái)的登錄組件叫 htLogin,再寫一個(gè) login組件將他們整合,通過環(huán)境變量進(jìn)行區(qū)分引入不同的組件,使用component動(dòng)態(tài)加載對(duì)應(yīng)的登錄組件如下:
login.vue:
<component?:is="currentLogin"?@sure="sure"?cancel="cancel"></component>data:{return?{currentLogin:?process.env.VUE_APP_ENV?===?'ht'???'mainlandLogin'?:?'htLogin'} }, components:?{mainlandLogin:?()?=>?import("./components/mainlandLogin.vue"),htLogin:?()?=>?import("./components/htLogin.vue"), }, method:{sure(){this.$emit('sure')},cancel(){this.$emit('cancel')} }注意: 引入組件的方式使用動(dòng)態(tài)加載,打包時(shí)會(huì)將兩個(gè)組件打包成兩個(gè)單獨(dú)的chunk, 因?yàn)榇箨懓姹九c港臺(tái)版本只會(huì)用到一種登錄,另一個(gè)用不到的不需要引入
經(jīng)過如上操作將登錄的組件封裝好以后使用起來就很簡單了
<login?@sure="sure"?cancel="cancel"></login>3. 解決價(jià)格不一致問題
與登錄一樣,根據(jù)環(huán)境變量區(qū)分即可,在原來大陸版本的商品JSON中加入一個(gè)字段即可如htPrice
const?commodityList?=?[{id:?1name:?"xxx",count:1,price:1,htPrice:?2} ]遍歷的時(shí)候還是根據(jù)process.env.VUE_APP_ENV === 'ht'進(jìn)行顯示對(duì)應(yīng)價(jià)格與單位
{{?isHt???`${commodity.htPrice}?NT$`?:?`${commodity.price}?¥`?}}data()?{return?{isHt:?process.env.VUE_APP_ENV?===?'ht'} }4. 簡繁體轉(zhuǎn)換
解決了兩個(gè)項(xiàng)目合并成一個(gè)項(xiàng)目和登錄、價(jià)格、單位不一致的問題,最后只剩下簡體轉(zhuǎn)繁體,也是最難解決的一部分,經(jīng)過了多次技術(shù)調(diào)研沒有找到合適的方案,最后只能自己寫一套。
1. 使用i18n, 維護(hù)兩套語言文件
優(yōu)點(diǎn): 國際化使用的最多的一個(gè)庫,不用改動(dòng)代碼中的文字,使用變量替換,只需維護(hù)兩套語言文件,改動(dòng)點(diǎn)集中在一個(gè)文件中
缺點(diǎn): 使用變量進(jìn)行替換一定程度上增加了代碼的復(fù)雜性,無法省去手動(dòng)復(fù)制簡體去翻譯在額外寫入特定的語言文件這一過程,對(duì)于這個(gè)場景不是一個(gè)最好的方案
2. 采用:language-tw-loader
優(yōu)點(diǎn): ? 看似 可以自動(dòng)化將簡體轉(zhuǎn)換成繁體,方便快捷
缺點(diǎn): 在使用時(shí)發(fā)現(xiàn)一個(gè)致命的缺點(diǎn), 無法準(zhǔn)確替換,原因: 不同的詞組,同一個(gè)詞可能對(duì)應(yīng)多個(gè)字形,如:聯(lián)系 -> 聯(lián)繫, 系鞋帶 -> 系鞋帶。
基本原理: 列舉常用的中文簡體與繁體,一一對(duì)應(yīng),逐一替換, 如下圖所示:
image.png3. 采用 v-google-translate優(yōu)點(diǎn): 運(yùn)行時(shí)采用谷歌翻譯,自動(dòng)將網(wǎng)頁的簡體翻譯成繁體
缺點(diǎn): 因?yàn)槭沁\(yùn)行時(shí)轉(zhuǎn)義,所以頁面始終會(huì)先展示簡體,過一段時(shí)間再顯示繁體
綜上所述: 現(xiàn)有的一些方案存在以下幾個(gè)問題
需要維護(hù)額外的語言文件,使用變量替換文字
編譯時(shí)轉(zhuǎn)換無法正確轉(zhuǎn)換,運(yùn)行時(shí)轉(zhuǎn)換有延時(shí)
為了解決以上問題:
1. 無需寫多套語言文件,正常開發(fā)使用中文進(jìn)行編寫即可
需要一個(gè)翻譯的API,且翻譯要準(zhǔn)確,經(jīng)測試簡繁體轉(zhuǎn)換谷歌翻譯是最準(zhǔn)確的。
2. 在編譯時(shí)轉(zhuǎn)換
編寫打包工具的plugin,這里主要以webpack為打包工具,所以需要編寫一個(gè)webpack的plugin。
翻譯API
需要一個(gè)免費(fèi)、準(zhǔn)確、且不易掛的翻譯服務(wù),但是谷歌翻譯API是需要付費(fèi)的,有錢付費(fèi)的很方便就能享受這個(gè)服務(wù),但是為了一個(gè)簡體轉(zhuǎn)繁體產(chǎn)生額外的支出,不太現(xiàn)實(shí)。
開源項(xiàng)目中有很多的免費(fèi)谷歌API, 但都是去嘗試模擬生成其加密token,進(jìn)行請(qǐng)求,服務(wù)很容易掛掉,所以很多 直接變成了沒有。
但是!!!你要記得,谷歌翻譯是提供免費(fèi)的網(wǎng)頁版的!
所以只需要打開一個(gè)瀏覽器,填入需要翻譯的文字,獲取翻譯后的文字即可,只不過需要程序自動(dòng)幫我們打開一個(gè)瀏覽器,你沒想錯(cuò),已經(jīng)有很成熟的方案puppeteer 就是干這件事情的。
所以最終采用: 基于puppeteer的訪問谷歌https://translate.google.cn 獲得翻譯結(jié)果,比其他方案都要穩(wěn)定。
同時(shí)已有大佬寫了一個(gè)基于puppeteer的轉(zhuǎn)換服務(wù) translateer,感興趣的可以看看其源碼,也不復(fù)雜。
但是注意,基于 translateer 啟動(dòng)API服務(wù), 存在幾個(gè)可以優(yōu)化的點(diǎn):
先看下為什么需要優(yōu)化, 首先我們得要知道谷歌翻譯網(wǎng)頁端最大支持多少字符,測試得知如下最大支持一頁最大支持 5000字符,超過的部分可以翻頁。
再以上左側(cè)輸入框內(nèi)輸入源文本,該網(wǎng)頁會(huì)發(fā)送一個(gè)post請(qǐng)求,一小會(huì)延遲右側(cè)出現(xiàn)翻譯后的內(nèi)容,同時(shí)注意導(dǎo)航欄上的鏈接會(huì)變成如下形式:
https://translate.google.cn/?sl=zh-CN&tl=zh-TW&text=哈哈哈&op=translate上面幾個(gè)參數(shù)分別的含義
sl:?源語言;?tl:?目標(biāo)語言;?text:?翻譯的文本;?op:?translate?(翻譯)如果直接使用以上鏈接進(jìn)行請(qǐng)求,經(jīng)過測試,將text值替換為'1'.repeat(16346), 16346 個(gè)字符時(shí) (該數(shù)值不包括url上其它字符,算上其它字符,那么總的url長度是16411) ,谷歌接口會(huì)返回400錯(cuò)誤。
image.png值得提的是: 看了很多的文章都說chrome的get請(qǐng)求最大字符長度限制是2048或8182,但是都不太準(zhǔn)確,上述測試就可以證明,總長度少于16411 谷歌翻譯依舊可以正常訪問,超過以后還是由谷歌翻譯對(duì)應(yīng)的后臺(tái)服務(wù)器拋出的400 錯(cuò)誤。
參考了GET請(qǐng)求的長度限制, 以下幾點(diǎn)是可以知道的:
1、首先即使有長度限制,也是限制的是整個(gè)URI長度,而不僅僅是你的參數(shù)值數(shù)據(jù)長度。
2、HTTP協(xié)議從未規(guī)定GET/POST的請(qǐng)求長度限制是多少
3、所謂的請(qǐng)求長度限制是由瀏覽器和web服務(wù)器決定和設(shè)置的,瀏覽器和web服務(wù)器的設(shè)定均不一樣
所以瀏覽器到底限制的是多少字符呢,暫時(shí)還沒有找到正確答案,有知道的大佬可以幫忙解釋一下
測試所用的谷歌瀏覽器版本: 98.0.4758.102(正式版本)(64 位)
分析了以上基本的限制,接下來看看translateer 的實(shí)現(xiàn):
translateer 服務(wù)啟動(dòng)時(shí)創(chuàng)建一個(gè) PagePool頁面池,開啟5個(gè)tab頁面并且都跳轉(zhuǎn)至https://translate.google.cn/, 以下為刪減后的部分代碼:
export?default?class?PagePool?{private?_pages:?Page[]?=?[];private?_pagesInUse:?Page[]?=?[];constructor(private?browser:?Browser,?private?pageCount:?number?=?5)?{pagePool?=?this;}public?async?init()?{this._pages?=?await?Promise.all([...Array(this.pageCount)].map(()?=>this.browser.newPage().then(async?(page)?=>?{await?page.goto("https://translate.google.cn/",?{waitUntil:?"networkidle2",});return?page;})));} }然后使用fastify啟動(dòng)一個(gè)Node服務(wù)器,對(duì)外提供一個(gè)get請(qǐng)求API。以下為刪減后的部分代碼:
fastify.get("/",async?(request,?reply)?=>?{const?{?text,?from?=?"auto",?to?=?"zh-CN",?lite?=?false?}?=?request.query;const?page?=?pagePool.getPage();await?page.evaluate(([from,?to,?text])?=>?{location.href?=?`?sl=${from}&tl=${to}&text=${encodeURIComponent(text)}`;},[from,?to,?text]);//?translating...await?page.waitForSelector(`span[lang=${to}]`);//?get?translated?textlet?result?=?await?page.evaluate((to)?=>(document.querySelectorAll(`span[lang=${to}]`)[0]?as?HTMLElement).innerText,to); }傳入sl: 源語言; tl: 目標(biāo)語言; text: 翻譯的文本 這幾個(gè)參數(shù),location.href 跳轉(zhuǎn)至
?sl=${from}&tl=${to}&text=${encodeURIComponent(text)} 從而獲得右側(cè)輸入框的返回結(jié)果。
分析了其基本的實(shí)現(xiàn)原理,接下來分析其中存在的坑點(diǎn)。
location.href 是個(gè)get請(qǐng)求,經(jīng)過上面的分析暫時(shí)不知道瀏覽器get請(qǐng)求的字符長度限制,但是已經(jīng)知道谷歌后臺(tái)服務(wù)的對(duì)請(qǐng)求長度的限制為16411, 再粗略減去411個(gè)字符作為url的其他字符長度, 那么每次的翻譯文本最大支持長度就為16000個(gè)字符。
而如上代碼對(duì)text進(jìn)行encodeURIComponent 編碼 (get請(qǐng)求默認(rèn)也會(huì)對(duì)中文及其它特殊字符進(jìn)行編碼)
需要注意的是中文一個(gè)字符編碼后為9個(gè)字符 聯(lián) => %E8%81%94, 那么16000 / 9 約等于 1777個(gè)漢字
階段總結(jié):
由于谷歌翻譯網(wǎng)頁版的一些限制,直接使用get請(qǐng)求,一次最大支持翻譯1777個(gè)漢字, 而在輸入框內(nèi)模擬輸入漢字無字符長度限制,一頁最大支持5000 字符,超出的部分可進(jìn)行翻頁。
需要達(dá)到的效果是一次翻譯最少要能翻譯5000個(gè)字符,盡量少請(qǐng)求次數(shù),能減少翻譯的時(shí)間,進(jìn)而加快插件編譯的速度,所以需要開始改進(jìn) translateer:
使用fastify創(chuàng)建一個(gè)新的post請(qǐng)求API
跳轉(zhuǎn)時(shí)只添加參數(shù)sl源語言與tl目標(biāo)語言不加text參數(shù)
選中谷歌翻譯頁面左側(cè)的文本輸入框,并將需要翻譯的文本賦值給輸入框,并且需要使用page.type鍵入一個(gè)空字符,觸發(fā)一次文本框的input事件,網(wǎng)頁才會(huì)執(zhí)行翻譯。
這里有個(gè)坑點(diǎn),就是 page.type 是模擬用戶輸入所以他會(huì)一個(gè)字一個(gè)字的輸入,一開始的時(shí)候我使用它去給文本輸入框賦值,文本過長時(shí),輸入的時(shí)間巨長,當(dāng)時(shí)不知道怎么處理,為此我還專門提了個(gè)issue, 被指導(dǎo)后才改寫成現(xiàn)在的寫法: ?issues
總結(jié):
前面提到,超過5000字符可以進(jìn)行翻頁,這里沒有進(jìn)行翻譯處理,目前限制就每次請(qǐng)求翻譯5000個(gè)字符已經(jīng)夠用,超過5000再請(qǐng)求一次翻譯接口 (后續(xù)可處理一下翻頁,不管多長的字符都一次翻譯完畢, 不過還需要進(jìn)一步對(duì)比兩者的所用時(shí)間長短)
最后以上修改過的代碼github地址: Translateer
translate-language-webpack-plugin
解決了翻譯API的問題,剩下的事情就只剩將代碼中的中文簡體轉(zhuǎn)換成繁體了,由于打包工具使用的webpack, 所以編寫webpack plugin 進(jìn)行讀取中文并替換, 同時(shí)需要支持webpack5.0與webpack4.0版本,以下以5.0版本為例:
首先理一下該插件的思路
編寫webpack插件
讀取代碼中所有的中文
請(qǐng)求翻譯API, 獲得翻譯后的結(jié)果
將翻譯后的結(jié)果寫入至代碼中
額外的功能:將每次讀取的源文本與目標(biāo)文本輸出至日志中, 特別是在翻譯返回的文本長度與源文本長度不一致時(shí)用于對(duì)照。
接下來一步步實(shí)現(xiàn)上述功能
1. 第一步需要編寫一個(gè)插件,怎么寫?這是個(gè)問題
4.0版本 和 5.0版本 的鉤子是不一樣的,而且很多,這里不會(huì)介紹webpack plugin中每個(gè)鉤子的含義,不是兩句話能說的清楚的, 網(wǎng)上有很多介紹的如揭秘webpack插件工作流程和原理,要想快速的寫一個(gè)插件,那么最快的方式就是參考現(xiàn)有的成熟的插件,我在編寫的時(shí)候就是直接參照的html-webpack-plugin, 4.0版本與5.0版本都是參照其對(duì)應(yīng)版本寫的。
tips: ?看開源項(xiàng)目的源碼的意義就在于此,可以學(xué)到很多的成熟的解決方案,可以稍微少踩一點(diǎn)坑, 所以最基本也需要學(xué)會(huì)如何找入口文件,如何調(diào)試代碼。
部分代碼如下,參考如下注釋:
const?{?sources,?Compilation?}?=?require('webpack'); //?日志輸出文件 const?TRANSFROMSOURCETARGET?=?'transform-source-target.txt'; //?谷歌翻譯一次最大支持字符 const?googleMaxCharLimit?=?5000; //?插件名稱 const?pluginName?=?'TransformLanguageWebpackPlugin';class?TransformLanguageWebpackPlugin?{constructor(options?=?{})?{//?默認(rèn)的一些參數(shù)const?defaultOptions?=?{?translateApiUrl:?'',?from:?'zh-CN',?to:?'zh-TW',?separator:?'-',?regex:?/[\u4e00-\u9fa5]/g,?outputTxt:?false,?limit:?googleMaxCharLimit,};//?translateApiUrl?翻譯API必須傳if?(!options.translateApiUrl)throw?new?ReferenceError('The?translateApiUrl?parameter?is?required');//?將傳入的參數(shù)與默認(rèn)參數(shù)合并this.options?=?{?...defaultOptions,?...options?};}//?添加apply方法,供webpack調(diào)用apply(compiler)?{const?{separator,?translateApiUrl,?from,?to,?regex,?outputTxt,?limit}?=?this.options;//?監(jiān)聽compiler?的thisCompilation?鉤子compiler.hooks.thisCompilation.tap(pluginName,?(compilation)?=>?{//?監(jiān)聽compilation?的processAssets?鉤子compilation.hooks.processAssets.tapAsync({name:?pluginName,// stage 代表資源處理的階段, PROCESS_ASSETS_STAGE_ANALYSE:analyze the existing assets.stage:?Compilation.PROCESS_ASSETS_STAGE_ANALYSE,},//?assets?代表所有chunk文件`路徑及內(nèi)容async?(assets,?callback)?=>?{// TODO:在此處填充要實(shí)現(xiàn)的功能})})} }以上為該插件的基本結(jié)構(gòu), webpack5.0中processAssets鉤子用于處理文件,我們主要看一下 Compilation.PROCESS_ASSETS_STAGE_ANALYSE階段assets 中有什么. 以提供的github倉庫中提供的例子為例
可以看到assets就是最終會(huì)輸出的文件,根據(jù)需要做的事選擇不同的stage, 這里選擇PROCESS_ASSETS_STAGE_ANALYSE的原因是,需要處理index.htm中的中文,所以需要選擇一個(gè)非常靠后的鉤子,其他鉤子參考 (相關(guān)文檔)
2. 讀取代碼中所有的中文
首先需要寫一個(gè)函數(shù),用于匹配相鄰的中文字符,如,源碼中含有<p>失聯(lián)</p><div>系鞋帶</div>, 返回:['失聯(lián)', '系鞋帶']。將返回的字符數(shù)組,以分隔符分隔,如['失聯(lián)', '系鞋帶'] => 失聯(lián)'-'系鞋帶' , 分隔的原因:如中文簡體 => 中文繁體(存在多形字):失聯(lián)系鞋帶 => 失聯(lián)繫鞋帶, 而正確的結(jié)果應(yīng)該是 失聯(lián)系鞋帶, 失聯(lián)是一個(gè)詞組,系鞋帶是一個(gè)詞組,轉(zhuǎn)換后不會(huì)有變化的, 而聯(lián)系在一起的時(shí)候就會(huì)變成 聯(lián)繫
/***?@description?返回中文詞組數(shù)組, 如:?<p>你好</p><div>世界</div>, ?返回:?['你好', '世界']*?@param?{*}?content?打包后的bundle文件內(nèi)容*?@returns*/ function?getLanguageList(content,?regex)?{let?index?=?0,termList?=?[],term?=?'',list;?//?遍歷獲取到的中文數(shù)組while?((list?=?regex.exec(content)))?{if?(list.index?!==?index?+?1?&&?term)?{termList.push(term);term?=?'';}term?+=?list[0];index?=?list.index;}if?(term?!==?'')?{termList.push(term);}return?termList; }在以上代碼TODO: 的位置繼續(xù)編寫, 獲取所有chunk中的中文并保存至chunkAllList數(shù)組中
let?chunkAllList?=?[]; //?先將所有的chunk中的`指定字符詞組`存起來 for?(const?[pathname,?source]?of?Object.entries(assets))?{//?只讀取js與html文件中的中文,其他的文件不需要if?(!(pathname.endsWith('js')?||?pathname.endsWith('.html')))?{continue;}//?獲取當(dāng)前chunk的源代碼字符串let?chunkSourceCode?=?source.source();//?獲取chunk中所有的中文。const?chunkSourceLanguageList?=?getLanguageList(chunkSourceCode,?regex);//?如果小于0,說明當(dāng)前文件中沒有?`指定字符詞組`,不需要替換if?(chunkSourceLanguageList.length?<=?0)?continue;chunkAllList.push({//?原文本數(shù)組chunkSourceLanguageList,// separator為分割符默認(rèn)為:?-chunkSourceLanguageStr:?chunkSourceLanguageList.join(separator),//?chunk原代碼chunkSourceCode,//?chunk的輸出路徑pathname,}); }3. 請(qǐng)求翻譯API, 獲得翻譯后的結(jié)果
因?yàn)橛行ヽhunk中中文是很少的, 比如一個(gè)chunk中只有2個(gè)字,另一個(gè)chunk中只有3個(gè)字,那么就沒必要請(qǐng)求兩次翻譯接口,為了減少請(qǐng)求次數(shù),先將所有chunk中的中文合成一個(gè)字符串,并用_分隔開用于區(qū)分是屬于那個(gè)chunk中的內(nèi)容。
const?chunkAllSourceLanguageStr?=?chunkAllList .map((item)?=>?item.chunkSourceLanguageStr).join(`_`);合成一個(gè)字符串以后,還需要進(jìn)行切割,因?yàn)橐淮巫畲笾С址g5000個(gè)字符
//?合理的分割所有chunk中讀取的字符,供谷歌API翻譯,不能超過谷歌翻譯的限制 const?sourceList?=?this.getSourceList(chunkAllSourceLanguageStr,?limit);getSourceList(sourceStr,?limit)?{let?len?=?sourceStr.length;let?index?=?0;if?(limit)?{}const?chunkSplitLimitList?=?[];while?(len?>?0)?{let?end?=?index?+?limit;const?str?=?sourceStr.slice(index,?end);chunkSplitLimitList.push(str);index?=?end;len?=?len?-?limit;}return?chunkSplitLimitList; }切割完成后,最后使用Promise.all去請(qǐng)求所有的接口,所有的翻譯成功才能算成功
//?翻譯 const?tempTargetList?=?await?Promise.all(sourceList.map(async?(text)?=>?{return?await?transform({translateApiUrl:?translateApiUrl,text:?text,from:?from,to:?to,});}) );4. 將翻譯后的結(jié)果寫入至代碼中
得到了所有chunk中的中文簡體翻譯后的繁體,最后遍歷chunk數(shù)組chunkAllList,將源代碼中的
for?(let?i?=?0;?i?<?chunkAllList.length;?i++)?{const?{chunkSourceLanguageStr,chunkSourceLanguageList,pathname,chunkSourceCode,}?=?chunkAllList[i];let?sourceCode?=?chunkSourceCode;//?將簡體轉(zhuǎn)換為繁體targetList[i].split(separator).forEach((phrase,?index)?=>?{sourceCode?=?sourceCode.replace(chunkSourceLanguageList[index],phrase);});//?if?(outputTxt)?{writeContent?+=?this.writeFormat(pathname,chunkSourceLanguageStr,targetList[i]);}compilation.updateAsset(pathname,?new?sources.RawSource(sourceCode)); }以上代碼為不完全代碼,完整代碼及插件使用方式請(qǐng)參考:translate-language-webpack-plugin
5. 輸出對(duì)照文本
如下:主要是輸出每個(gè)chunk中的中文用于對(duì)照,如果說頁面沒有其它動(dòng)態(tài)的文字,且這些文字需要應(yīng)用特殊的字體,也可以使用這些讀取出來的字打包一個(gè)字體文件,比一整個(gè)字體文件小很多很多。
image.png7總結(jié)
注意:會(huì)將頁面上包括js中的中文全部替換,但是接口返回的文字是無法轉(zhuǎn)換的,由后端返回對(duì)應(yīng)繁體
至此一個(gè)完整的業(yè)務(wù)需求就已經(jīng)優(yōu)化的七七八八了,翻譯插件理論上支持任意語言互轉(zhuǎn),但是由于翻譯的語義不同,往往翻譯出來的意思不是我們想要的,適用于簡體繁體互轉(zhuǎn)。
·················?若川簡介?·················
你好,我是若川,畢業(yè)于江西高校。現(xiàn)在是一名前端開發(fā)“工程師”。寫有《學(xué)習(xí)源碼整體架構(gòu)系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會(huì)寫一篇年度總結(jié),已經(jīng)寫了7篇,點(diǎn)擊查看年度總結(jié)。
同時(shí),最近組織了源碼共讀活動(dòng),幫助3000+前端人學(xué)會(huì)看源碼。公眾號(hào)愿景:幫助5年內(nèi)前端人走向前列。
識(shí)別上方二維碼加我微信、拉你進(jìn)源碼共讀群
今日話題
略。分享、收藏、點(diǎn)贊、在看我的文章就是對(duì)我最大的支持~
總結(jié)
以上是生活随笔為你收集整理的为支持两个语言版本,我基于谷歌翻译API写了一款自动翻译的 webpack 插件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [html] 如何通过表单下载文件?
- 下一篇: android 自定义唤醒词,星星1号语