日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

为支持两个语言版本,我基于谷歌翻译API写了一款自动翻译的 webpack 插件

發(fā)布時(shí)間:2023/12/9 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 为支持两个语言版本,我基于谷歌翻译API写了一款自动翻译的 webpack 插件 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

大家好,我是若川。持續(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=production

    package.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.png

    3. 采用 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

  • export?const?post?=?((fastify,?opts,?done)?=>?{fastify.post('/',async?(request,?reply)?=>?{...more...});done(); });
  • 跳轉(zhuǎn)時(shí)只添加參數(shù)sl源語言與tl目標(biāo)語言不加text參數(shù)

  • await?page.evaluate(([from,?to])?=>?{location.href?=?`?sl=${from}&tl=${to}`;},[from,?to]);
  • 選中谷歌翻譯頁面左側(cè)的文本輸入框,并將需要翻譯的文本賦值給輸入框,并且需要使用page.type鍵入一個(gè)空字符,觸發(fā)一次文本框的input事件,網(wǎng)頁才會(huì)執(zhí)行翻譯。

  • await?page.waitForSelector(`span[lang=${from}]?textarea`);const?fromEle?=?await?page.$(`span[lang=${from}]?textarea`);await?page.evaluate((el,?text)?=>?{el.value=?text},fromEle,?text)//?模擬一次輸入觸發(fā)input事件,使得谷歌翻譯可以翻譯await?page.type(`span[lang=${from}]?textarea`,?'?');//?translating...await?page.waitForSelector(`span[lang=${to}]`);//?get?translated?textconst?result?=?await?page.evaluate((to)?=>(document.querySelectorAll(`span[lang=${to}]`)[0]?as?HTMLElement).innerText,to);

    這里有個(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.png

    7總結(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)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。