前言
隨著公司業(yè)務(wù)的飛速發(fā)展享郊,我們的市場覆蓋范圍已擴(kuò)大至港澳臺(tái)和歐洲地區(qū)。為了滿足多語言需求和提升用戶體驗(yàn),我們需要對(duì)web運(yùn)行平臺(tái)進(jìn)行國際化支持和改造羡玛。下面分享一下我們?cè)趪H化的過程中,遇到的問題以及解決方案全跨。其過程主要分為四個(gè)部分:
詞條抽取
詞條管理
詞條引入
詞條調(diào)整
詞條抽取
我們遇到的第一個(gè)問題就是應(yīng)該提取哪些內(nèi)容作為詞條缝左,以及如何提取。
目前待改造的頁面有2000+浓若,純靠人工的話渺杉,耗時(shí)長效率低還比較容易遺漏。所以我們想盡可能靠工具解決80%的問題挪钓,人工解決剩下的20%是越。基于這個(gè)目標(biāo)碌上,我們的思路是:對(duì)各個(gè)前端工程進(jìn)行掃描倚评,尋找vue文件以及js文件并進(jìn)行掃描,把里面包含中文的部分提取成詞條馏予,并最終輸出到excel文件天梧。
對(duì)于js文件的處理:
首先通過@babel/parser對(duì)文本內(nèi)容進(jìn)行ast轉(zhuǎn)換,然后判定是否包含中文霞丧,是的話就納入詞條呢岗,同時(shí)替換成多語言方式。
比如:let message = "提醒"蛹尝,轉(zhuǎn)換后變?yōu)?/p>
let message = this.$t("XXXXXX","提醒")后豫,XXXXXX代表詞條標(biāo)識(shí),如果詞條沒匹配到突那,就用第二個(gè)參數(shù)(默認(rèn)值)
其各個(gè)處理步驟代碼如下:
1挫酿,對(duì)ast的node內(nèi)容進(jìn)行判定,看看是否包含中文
exports.containZH = function(content){
if(!content)return false
return /.*[\u4e00-\u9fa5]+.*$/.test(content)
}
2愕难,將內(nèi)容"提醒"轉(zhuǎn)換成:this.$t("XXXXXX","提醒")
function createCallExpression(thizz,term,content){
let _objetct = null
if( thizz == 'this'){
_objetct = t.thisExpression()
}else{
_objetct = t.identifier(thizz)
}
let property = t.identifier('$t')
let callee = t.memberExpression(_objetct,property)
let argument = t.stringLiteral(config.appCode +'.'+ term)
let defaultArgument = t.stringLiteral(content)
let callExpression = t.callExpression(callee,[argument,defaultArgument])
return callExpression
}
3,替換節(jié)點(diǎn)
下面有幾種場景需要考慮:
-
作為表達(dá)式:
let message = "提醒"
-
作為判定條件:
if(test === "提醒"){}
-
作為方法入?yún)ⅲ?/p>
let result = getData("提醒")
-
es5的this變換:
var that = this getData().then(function(){ var message = "提醒" //此時(shí)應(yīng)替換成that.$t("xxxxxx","提醒") })
當(dāng)然除了上述場景外早龟,還要結(jié)合需要轉(zhuǎn)換的代碼的新舊程度以及實(shí)際情況進(jìn)行一些兼容惫霸,同時(shí)我們也會(huì)把一些判斷不準(zhǔn)的,輸出到日志進(jìn)行人工處理拄衰,節(jié)點(diǎn)替換代碼如下:
let callExpression = createCallExpression(thizz,term,path.node.value)//t.callExpression(callee,[argument])
let rPath = path.parentPath
if(t.isCallExpression(rPath)){
rPath.node.arguments = [callExpression]
if(thizz == 'this' || thizz == 'that'){
let checklog = "行:"+path.node.loc.start.line+"列:"+path.node.loc.start.column
console.log("請(qǐng)人工確認(rèn)this是否正確:",_path,checklog)
writeCheckLog(_path,checklog)
}
}else if(t.isObjectProperty(rPath)){
rPath.node.value = callExpression
} else if(t.isBinaryExpression(rPath)){
if(t.isBinaryExpression(rPath.parentPath) && t.isBinaryExpression(rPath.parentPath.parentPath)){
let plusPath = path.findParent(path => path.isCallExpression())
if(plusPath)plusPath.node.arguments = [callExpression]
}else{
if(rPath.node.left.hasOwnProperty('value') && rPath.node.left.value == path.node.value){
rPath.node.left = callExpression
}
if(rPath.node.right.hasOwnProperty('value') && rPath.node.right.value == path.node.value){
rPath.node.right = callExpression
}
}
}else if(t.isConditionalExpression(rPath)){
if(rPath.node.consequent.hasOwnProperty('value') && rPath.node.consequent.value == path.node.value){
rPath.node.consequent = callExpression
}
if(rPath.node.alternate.hasOwnProperty('value') && rPath.node.alternate.value == path.node.value){
rPath.node.alternate = callExpression
}
}
除了上述場景它褪,還有一個(gè)情況是比較難處理的:
let message = "第" + i + "行"
類似上面這種在循環(huán)里的拼接字符串,會(huì)被分割成兩個(gè)詞條翘悉。由于我們?cè)诖a中全局搜索后發(fā)現(xiàn)茫打,這種情況并不多所以也是采用人工處理的方式。當(dāng)然ast能做但不太好做妖混,所以對(duì)我們來說得不償失老赤。
對(duì)于vue文件的處理
主要是通過vue-template-compiler提取出vue文檔的template部分,script部分制市。script部分的處理和上面類似不再累述抬旺,我們重點(diǎn)描述一下template部分:
提取template的html字符串后,首先利用parse5進(jìn)行ast轉(zhuǎn)換:
exports.vueParse = function vueParse(_path){
globalThis.termIndex = 0
var fileContent = fs.readFileSync(_path).toString()
var result = compiler.parseComponent(fileContent)
let scriptAst = parser.parse(result.script.content,{sourceType: "module"})
let templateAst = parse5.parse(result.template.content,{sourceCodeLocationInfo:true});
globalThis.hasTerm = false
try{
let templateRoot = templateAst.childNodes[0].childNodes[1].childNodes[0]
makeTerms(templateRoot,_path)
}catch(e){
console.warn(_path+",template內(nèi)容空白,程序忽略,請(qǐng)人工確認(rèn)祥楣!")
}
let html = parse5.serialize(templateAst);
html = html.replace(/<(html|\/*head|body)>/g,'')
html = html.replace(/<\/(body|html)>/g,'')
let scriptData = JsContentParse(_path,result.script.content)
if(!globalThis.hasTerm && !scriptData.hasZH){
globalThis.hasTerm = false
return
}
makeNewFile(_path,html,scriptData.data,result.source)
return {
filePath:_path,
template:templateAst,
script:scriptAst,
result:result,
}
}
轉(zhuǎn)換后开财,同樣對(duì)html的節(jié)點(diǎn)進(jìn)行替換,需要考慮的場景有下面幾個(gè):
-
忽略注釋中的中文误褪,而非作為詞條的一部分
// author is 張三
-
替換標(biāo)簽文本部分的內(nèi)容
<i title="張三"></i> //替換成责鳍,注意屬性前要加冒號(hào) <i :title="$t('xxxxx','張三')"></i>
-
替換屬性部分的內(nèi)容
<i>張三</i> //替換成 <i>$t('xxxxx','張三')</i>
其整體代碼實(shí)現(xiàn)如下:
function makeTerms(templateRoot,_path){
if(templateRoot.nodeName == ' #comment'){
return false;
}
if(templateRoot.nodeName == '#text'){
if(containZH(templateRoot.value)){
globalThis.hasTerm = true
let term = record(_path,templateRoot.sourceCodeLocation,templateRoot.value)
templateRoot.value = '{{$t(\''+config.appCode +'.'+ term+'\')}}'
}
return
}
if(templateRoot['attrs']){
templateRoot.attrs.forEach(attr => {
if(containZH(attr.value)){
globalThis.hasTerm = true
let term = record(_path,templateRoot.sourceCodeLocation,attr.value)
attr.value = '$t(\''+config.appCode +'.'+ term+'\')'
attr.name = (':'+ attr.name)
return true
}
});
}
if(templateRoot['childNodes']){
templateRoot.childNodes.forEach(_node=>{
makeTerms(_node,_path)
})
}
}
我們通過上述工具,基本能自動(dòng)轉(zhuǎn)換80%的代碼兽间,其余的就需要人工介入修改檢查历葛。詞條抽取后,下一步我們要對(duì)抽取的內(nèi)容進(jìn)行管理嘀略。
詞條管理
這部分涉及的內(nèi)容不多恤溶。在應(yīng)用層面,我們對(duì)前端詞條進(jìn)行了區(qū)分以減少重復(fù)詞條的數(shù)量帜羊,主要有三類:
公共詞條:跨工程頁面都會(huì)重復(fù)使用的詞匯咒程,比如:查詢
業(yè)務(wù)公共詞條:某一塊業(yè)務(wù)經(jīng)常出現(xiàn)的詞匯,比如:促銷
詞條:具體頁面?zhèn)€性化的詞匯
另外我們也需要保證詞條在維護(hù)的時(shí)候讼育,不會(huì)重復(fù)不會(huì)影響他人孵坚。同時(shí)由于詞條的數(shù)量比較多,都決定了我們無法用文件的形式進(jìn)行詞條管理窥淆,必須依賴數(shù)據(jù)庫。
基于上述原因巍杈,我們構(gòu)建了一個(gè)管理頁面忧饭,來對(duì)相應(yīng)的表進(jìn)行數(shù)據(jù)錄入和處理。在詞條提取部分我們提到過筷畦,提取的詞條最終會(huì)生成excel词裤,所以在詞條管理頁面刺洒,也支持該excel的直接導(dǎo)入,減少詞條錄入時(shí)間吼砂,提高錄入效率逆航。有了這些詞條,我就可以使用了:
詞條引入
一說起vue的國際化渔肩,我們首先就會(huì)想到vue-i18n因俐。如果只有幾個(gè)頁面十幾個(gè)頁面,甚至幾十個(gè)頁面周偎,直接使用vue-i18n都沒太大的問題抹剩。
但我們有2000+的頁面需要國際化,詞條也會(huì)超過10萬+蓉坎,所以我們會(huì)變通的方式使用vue-i18n從而滿足以下幾點(diǎn)要求:
詞條變更不需要重新編譯發(fā)布
可以充分利用瀏覽器緩存
語言切換的時(shí)候頁面內(nèi)容不會(huì)晃動(dòng)
最終我們的方案如下:
1澳眷,通過監(jiān)控程序,從詞條管理接口獲取數(shù)據(jù)蛉艾,分別生成公共詞條钳踊,關(guān)鍵應(yīng)用維度的業(yè)務(wù)詞條的靜態(tài)js文件
2,web框架根據(jù)當(dāng)前關(guān)鍵應(yīng)用信息勿侯,加載公共詞條以及關(guān)鍵應(yīng)用下的靜態(tài)js文件拓瞪,并將內(nèi)容放入全局變量localeMessage,i18n根據(jù)全局變量初始化:
let messages = {}
//部分代碼省略
messages[window.locale] = Object.assign(window.localeMessage,_locale)
// Create VueI18n instance with options
const i18n = new VueI18n({
locale: window.locale,
messages
})
這樣當(dāng)我們?cè)谠~條管理頁面維護(hù)詞條信息后罐监,線上也能一定時(shí)間內(nèi)同步最新的詞條信息吴藻。
詞條調(diào)整
到目前為止,不管是詞條抽取弓柱,管理沟堡,引入都是偏開發(fā)視角∈缚眨考慮下面一個(gè)場景:如果一個(gè)非開發(fā)人員要糾正詞條信息航罗,比如產(chǎn)品經(jīng)理或翻譯人員,他不知道一段文字到底對(duì)應(yīng)哪個(gè)詞條key屁药,需要找開發(fā)人員去問粥血,然后再去詞條管理維護(hù)修改,這樣一圈下來效率太低酿箭。
所以我們想給非開發(fā)人員提供一種在線編輯詞條的能力复亏,如圖:
其特性主要有以下兩點(diǎn):
可以通過定位可以快速找到詞條位置
可以修改詞條內(nèi)容并保存
這樣的話,非開發(fā)人員都可以在線上快速進(jìn)行詞條的編輯修正缭嫡,特別是在非中文的場景下缔御,從而大大提高了詞條的糾正效率。
其實(shí)現(xiàn)主要分為兩部分:
1妇蛀,如何收集頁面到底使用了哪些詞條耕突?
我們可以用mixin的方式劫持$t方法笤成,然后收集頁面依賴的詞條。
2眷茁,如何標(biāo)記詞條位置炕泳?
要建立dom和詞條的關(guān)聯(lián),需要在dom上生成一個(gè)包含詞條信息的屬性上祈。
比如一個(gè)詞條"xxxx-yyyy"培遵,然后增加屬性 data-term-xxxx-yyyy="xxxx-yyyy"。當(dāng)點(diǎn)擊定位的時(shí)候雇逞,就可以根據(jù)屬性去查找節(jié)點(diǎn)。當(dāng)然在標(biāo)記的時(shí)候要注意:內(nèi)容和屬性的詞條都加到節(jié)點(diǎn)上掉蔬。
我們通過webpack,loader插件的方式實(shí)現(xiàn),插件內(nèi)容大致如下:
function makeTerms(templateRoot){
if(templateRoot.nodeName == ' #comment'){
return false;
}
if(templateRoot.nodeName == '#text'){
if(containZH(templateRoot.value)){
let terms = getTermCode(templateRoot.value)
terms.forEach(term=>{
if(!term.startsWith('T_S_'))return true
let tName = term.replace(/_|-|\./g,'')
templateRoot.parentNode.attrs.push({
value:term,
name:'data-term-'+tName.toLowerCase()
})
})
}
return
}
if(templateRoot['attrs']){
templateRoot.attrs.forEach(attr => {
if(attr.name.startsWith(':') || attr.name.startsWith('v-bind:')){
if(attr.value && attr.value.startsWith('{'))return true
let terms = getTermCode(attr.value)
terms.forEach(term=>{
if(!term.startsWith('T_S_'))return true
let tName = term.replace(/_|-|\./g,'')
templateRoot.attrs.push({
value:term,
name:'data-term-'+tName.toLowerCase()
})
})
return true
}
});
}
if(templateRoot['childNodes']){
templateRoot.childNodes.forEach(_node=>{
makeTerms(_node)
})
}
}
webpack配置部分如下:
{
test: /.vue$/,
loader: path.resolve(__dirname, '../webpack-plugin/teld-term-loader.js')
}
另外在實(shí)現(xiàn)的過程中我們發(fā)現(xiàn)托启,html轉(zhuǎn)ast的庫很多蹭劈,但從ast轉(zhuǎn)html的比較少,最后就找到了parser5,轉(zhuǎn)換的時(shí)候有點(diǎn)小問題,就是會(huì)把標(biāo)簽轉(zhuǎn)成小寫造成部分轉(zhuǎn)換后的代碼不能正確執(zhí)行,也算一個(gè)小插曲。
結(jié)語
以上就是我們?cè)趪H化項(xiàng)目中遇到的一些問題和解決方案瑰艘,只是描述了其中大概的部分芒率,而在實(shí)際的過程中還有很多和歷史代碼相關(guān)的零星問題。所以對(duì)于一些項(xiàng)目偶芍,特別是老項(xiàng)目充择,國際化并非一件容易的事情,我們會(huì)繼續(xù)積累經(jīng)驗(yàn)腋寨,把國際化做的更完善聪铺,從而更好的服務(wù)我們的業(yè)務(wù)。