美文网首页
前端国际化

前端国际化

作者: cd2001cjm | 来源:发表于2023-06-27 09:17 被阅读0次

前言

随着公司业务的飞速发展,我们的市场覆盖范围已扩大至港澳台和欧洲地区。为了满足多语言需求和提升用户体验,我们需要对web运行平台进行国际化支持和改造。下面分享一下我们在国际化的过程中,遇到的问题以及解决方案。其过程主要分为四个部分:

  • 词条抽取

  • 词条管理

  • 词条引入

  • 词条调整

词条抽取

我们遇到的第一个问题就是应该提取哪些内容作为词条,以及如何提取。

目前待改造的页面有2000+,纯靠人工的话,耗时长效率低还比较容易遗漏。所以我们想尽可能靠工具解决80%的问题,人工解决剩下的20%。基于这个目标,我们的思路是:对各个前端工程进行扫描,寻找vue文件以及js文件并进行扫描,把里面包含中文的部分提取成词条,并最终输出到excel文件。

对于js文件的处理:

首先通过@babel/parser对文本内容进行ast转换,然后判定是否包含中文,是的话就纳入词条,同时替换成多语言方式。

比如:let message = "提醒",转换后变为

let message = this.$t("XXXXXX","提醒"),XXXXXX代表词条标识,如果词条没匹配到,就用第二个参数(默认值)

其各个处理步骤代码如下:

1,对ast的node内容进行判定,看看是否包含中文

exports.containZH = function(content){
    if(!content)return false
    return /.*[\u4e00-\u9fa5]+.*$/.test(content)
}

2,将内容"提醒"转换成: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,替换节点

下面有几种场景需要考虑:

  • 作为表达式:

    let message = "提醒"
    
  • 作为判定条件:

    if(test === "提醒"){}
    
  • 作为方法入参:

    let result = getData("提醒")
    
  • es5的this变换:

    var that = this
    getData().then(function(){
         var message = "提醒"
        //此时应替换成that.$t("xxxxxx","提醒")  
    }) 
    

当然除了上述场景外,还要结合需要转换的代码的新旧程度以及实际情况进行一些兼容,同时我们也会把一些判断不准的,输出到日志进行人工处理,节点替换代码如下:

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("请人工确认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
                }             
}

除了上述场景,还有一个情况是比较难处理的:

let message = "第" + i + "行"

类似上面这种在循环里的拼接字符串,会被分割成两个词条。由于我们在代码中全局搜索后发现,这种情况并不多所以也是采用人工处理的方式。当然ast能做但不太好做,所以对我们来说得不偿失。

对于vue文件的处理

主要是通过vue-template-compiler提取出vue文档的template部分,script部分。script部分的处理和上面类似不再累述,我们重点描述一下template部分:

提取template的html字符串后,首先利用parse5进行ast转换:

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内容空白,程序忽略,请人工确认!")
    }

    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,
    }
}

转换后,同样对html的节点进行替换,需要考虑的场景有下面几个:

  • 忽略注释中的中文,而非作为词条的一部分

    // author is 张三
    
  • 替换标签文本部分的内容

    <i title="张三"></i>
    //替换成,注意属性前要加冒号
    <i :title="$t('xxxxx','张三')"></i>
    
  • 替换属性部分的内容

    <i>张三</i>
    //替换成
    <i>$t('xxxxx','张三')</i>
    

其整体代码实现如下:

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)
        })
    }

}

我们通过上述工具,基本能自动转换80%的代码,其余的就需要人工介入修改检查。词条抽取后,下一步我们要对抽取的内容进行管理。

词条管理

这部分涉及的内容不多。在应用层面,我们对前端词条进行了区分以减少重复词条的数量,主要有三类:

  • 公共词条:跨工程页面都会重复使用的词汇,比如:查询

  • 业务公共词条:某一块业务经常出现的词汇,比如:促销

  • 词条:具体页面个性化的词汇

另外我们也需要保证词条在维护的时候,不会重复不会影响他人。同时由于词条的数量比较多,都决定了我们无法用文件的形式进行词条管理,必须依赖数据库。

基于上述原因,我们构建了一个管理页面,来对相应的表进行数据录入和处理。在词条提取部分我们提到过,提取的词条最终会生成excel,所以在词条管理页面,也支持该excel的直接导入,减少词条录入时间,提高录入效率。有了这些词条,我就可以使用了:

词条引入

一说起vue的国际化,我们首先就会想到vue-i18n。如果只有几个页面十几个页面,甚至几十个页面,直接使用vue-i18n都没太大的问题。

但我们有2000+的页面需要国际化,词条也会超过10万+,所以我们会变通的方式使用vue-i18n从而满足以下几点要求:

  • 词条变更不需要重新编译发布

  • 可以充分利用浏览器缓存

  • 语言切换的时候页面内容不会晃动

最终我们的方案如下:

1,通过监控程序,从词条管理接口获取数据,分别生成公共词条,关键应用维度的业务词条的静态js文件

2,web框架根据当前关键应用信息,加载公共词条以及关键应用下的静态js文件,并将内容放入全局变量localeMessage,i18n根据全局变量初始化:

let messages = {}

//部分代码省略

messages[window.locale] = Object.assign(window.localeMessage,_locale)

// Create VueI18n instance with options
const i18n = new VueI18n({
  locale: window.locale,
  messages
})

这样当我们在词条管理页面维护词条信息后,线上也能一定时间内同步最新的词条信息。

词条调整

到目前为止,不管是词条抽取,管理,引入都是偏开发视角。考虑下面一个场景:如果一个非开发人员要纠正词条信息,比如产品经理或翻译人员,他不知道一段文字到底对应哪个词条key,需要找开发人员去问,然后再去词条管理维护修改,这样一圈下来效率太低。

所以我们想给非开发人员提供一种在线编辑词条的能力,如图:

image.png

其特性主要有以下两点:

  • 可以通过定位可以快速找到词条位置

  • 可以修改词条内容并保存

这样的话,非开发人员都可以在线上快速进行词条的编辑修正,特别是在非中文的场景下,从而大大提高了词条的纠正效率。

其实现主要分为两部分:

1,如何收集页面到底使用了哪些词条?

我们可以用mixin的方式劫持$t方法,然后收集页面依赖的词条。

2,如何标记词条位置?

要建立dom和词条的关联,需要在dom上生成一个包含词条信息的属性。

比如一个词条"xxxx-yyyy",然后增加属性 data-term-xxxx-yyyy="xxxx-yyyy"。当点击定位的时候,就可以根据属性去查找节点。当然在标记的时候要注意:内容和属性的词条都加到节点上。

我们通过webpack,loader插件的方式实现,插件内容大致如下:

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')
}

另外在实现的过程中我们发现,html转ast的库很多,但从ast转html的比较少,最后就找到了parser5,转换的时候有点小问题,就是会把标签转成小写造成部分转换后的代码不能正确执行,也算一个小插曲。

结语

以上就是我们在国际化项目中遇到的一些问题和解决方案,只是描述了其中大概的部分,而在实际的过程中还有很多和历史代码相关的零星问题。所以对于一些项目,特别是老项目,国际化并非一件容易的事情,我们会继续积累经验,把国际化做的更完善,从而更好的服务我们的业务。

相关文章

  • 【整理】前端国际化小结

    近期在做国际化的改造,做了相应的调研,简单做下项目前端国际化的小结 国际化可以分为前端国际化和后端国际化,也可以是...

  • jQuery实现资源国际化

    jQuery实现资源国际化 1、jQuery之前端国际化jQuery.i18n.properties 1. jQu...

  • JAVA8新特性Stream

    实现前端实现多国语言切换 = 实现前端页面的资源国际化,需要依赖jQuery.i18n.properties插件[...

  • 前端国际化

    前言 从10年接触编程就开始接触国际化这个概念,然而这些年全面用到国际化的项目并不是很多,且都是些螺丝钉式的工作。...

  • 前端国际化

    20200731 先下结论: 国际化(i18n)和可访问性(a11y) 都是大坑。 首先说一下背景,仍然是一个SA...

  • 前端国际化

    github上的文章angularjs种的解决方案官网Formatjs

  • 前端国际化

    需要注意的点 1 .最基本的要求:文字替换.label,placeholder,字段校验提示信息,超链接2 .页面...

  • 评估工作流程引擎的30个关键技术点

    1.1:支持国际化 1. 如果使用工具包的模式开发,支持国际化的工作由自己完成的。 2. 如果使用Ccbpm的前端...

  • SpringBoot的国际化使用

    在项目中,很多时候需要国际化的支持,这篇文章要介绍一下springboot项目中国际化的使用。 在这个项目中前端页...

  • vue中如何使用i18n实现国际化

    一、前言 项目中需要实现多语言切换,这时候接触到国际化,前端框架无数,其中几种热门的框架都有相匹配的国际化插件工具...

网友评论

      本文标题:前端国际化

      本文链接:https://www.haomeiwen.com/subject/ehzgydtx.html