前端國際化

前言

隨著公司業(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ā)人員提供一種在線編輯詞條的能力复亏,如圖:

image.png

其特性主要有以下兩點(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ù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末萄窜,一起剝皮案震驚了整個(gè)濱河市铃剔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌查刻,老刑警劉巖键兜,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異穗泵,居然都是意外死亡普气,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門佃延,熙熙樓的掌柜王于貴愁眉苦臉地迎上來现诀,“玉大人夷磕,你說我怎么就攤上這事∽醒兀” “怎么了坐桩?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長封锉。 經(jīng)常有香客問我绵跷,道長,這世上最難降的妖魔是什么成福? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任碾局,我火速辦了婚禮,結(jié)果婚禮上奴艾,老公的妹妹穿的比我還像新娘净当。我一直安慰自己,他們只是感情好握侧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布蚯瞧。 她就那樣靜靜地躺著,像睡著了一般品擎。 火紅的嫁衣襯著肌膚如雪埋合。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天萄传,我揣著相機(jī)與錄音甚颂,去河邊找鬼。 笑死秀菱,一個(gè)胖子當(dāng)著我的面吹牛振诬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衍菱,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赶么,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了脊串?” 一聲冷哼從身側(cè)響起辫呻,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎琼锋,沒想到半個(gè)月后放闺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缕坎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年怖侦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡匾寝,死狀恐怖搬葬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旗吁,我是刑警寧澤踩萎,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站很钓,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏董栽。R本人自食惡果不足惜码倦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锭碳。 院中可真熱鬧袁稽,春花似錦、人聲如沸擒抛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽歧沪。三九已至歹撒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诊胞,已是汗流浹背暖夭。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撵孤,地道東北人迈着。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像邪码,于是被迫代替她去往敵國和親裕菠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 需要注意的點(diǎn) 1 .最基本的要求:文字替換.label,placeholder,字段校驗(yàn)提示信息,超鏈接2 .頁面...
    skoll閱讀 1,543評(píng)論 0 1
  • 關(guān)于國際化 一個(gè)項(xiàng)目發(fā)展到一定的環(huán)境或者一開始就是為多國打造的闭专,就需要考慮國際化了奴潘。簡單來說,就是一套頁面喻圃,多套語...
    XboxYan閱讀 289評(píng)論 0 0
  • 由于在去年的大部分時(shí)間都在做出海項(xiàng)目萤彩,分享一套Web國際化的方案。在出海項(xiàng)目大多轉(zhuǎn)冷的今天斧拍,給去年的瘋狂留個(gè)念想雀扶。...
    pengji閱讀 739評(píng)論 0 0
  • 國際化基礎(chǔ)知識(shí) 國際化與本地化 國際化與本地化,或者說全球化,其目的是讓你的站點(diǎn)支持多個(gè)國家和區(qū)域愚墓。其中國際化是指...
    jsAllen閱讀 5,907評(píng)論 0 3
  • 需求 項(xiàng)目基于Vue進(jìn)行開發(fā)浪册,使用了ant-design-vue框架扫腺,然后需要做國際化。此時(shí)做國際化需要考慮兩方面...
    world_7735閱讀 3,483評(píng)論 0 0