手写《自动化提词和回填工具》实现前端国际化(语言切换)

手写《自动化提词和回填工具》实现前端国际化(语言切换)

前端国际化的功能要求 :下拉选择语言,然后对应切换语言,先支持中英文。(目前有10+项目,要逐步处理完)

技术工具 :(项目是vue工程) vue-i18n,需要把所有的中文提出来,并且还要回填

难点 : 项目老旧、庞大,手动提词太笨了,效率低(而且有10+项目),此时需要一个工具,能够自动完成提词和回填

实现大纲:

  1. vue-i18n 的使用介绍
  2. 开发自动化提词和回填工具分析
  3. 提词碰到的难点
  4. 回填碰到的难点
  5. 加入工具平台,让小组成员能够很容易使用

1. vue-i18n 的使用介绍

// 入口 main.js, 引入
import i18n from './i18n'
...

new Vue({
  i18n,
  ...  
})
复制代码
// i18n.js
import Vue from 'vue'
import iView from 'view-design'
import VueI18n from 'vue-i18n'
import zhView from 'view-design/dist/locale/zh-CN'
import enView from 'view-design/dist/locale/en-US'
import zh from './langs/zh.json'
import en from './langs/en.json'

Vue.use(VueI18n)
Vue.locale = () => {}

const messages = {
  en: Object.assign(enView, en), // 将自己的英文包和iview提供的结合
  zh: Object.assign(zhView, zh) // 将自己的中文包和iview提供的结合
}

let locale = localStorage.getItem('lan')
if (!locale) {
  if (navigator.language === 'zh-CN') { // 获取浏览器的语言
    locale = 'zh'
  } else {
    locale = 'en'
  }
}
const i18n = new VueI18n({
  locale: locale, // 设置语言,如果本地存储了则用本地的,没有则默认 'en'
  messages
})

Vue.use(iView, {
  i18n: (key, value) => i18n.t(key, value)
})

export default i18n
复制代码
// 对应的切换语言按钮 的逻辑
<Select @on-change="languageChange" placeholder="Language" size="small">
  <Option value="zh">简体中文</Option>
  <Option value="en">English</Option>
</Select>

languageChange(lang) {
  localStorage.setItem('lan', lang)   
  location.reload()
}
复制代码

vue-i18n 现在已经配置好了,还需要资源zh.json, en.json, 还有回填

  • vue-i18n注册完后,会有一个全局的 $t,处理语言的切换
// zh.json
{
    "确定", "确定",
    "取消", "取消",
    ...
}
// en.json
{
    "确定", "confirm",
    "取消", "cancel",
    ...
}

// xx.vue  回填,    vue-i18n注册完后,会有一个全局的 $t,处理语言的切换
<template>
    原来的写法:<button>确定</button>
    回填后:   <button>{{$t('确定')}}</button>
</template>
<script>
    export default {
        data() {
            return {
                msg: '取消',         // 原来的写法
                msg: this.$t('取消') // 回填后
            }
        }
    }
</script>
复制代码

2. 开发自动化提词和回填工具分析

根据上面的vue-i18n使用介绍,我们还需要资源zh.json, en.json, 还有回填

目标是:写一个脚本,执行一条命令,比如lan /src/views, 就可以得到/src/views目录内的 zh.json, en.json,并且回填好

解耦开发分3步:

  1. 有一个前端工具平台(内部前端工具平台搭建),可以执行命令,类似比如 终端输入:eslint 或者 webpack,可以执行对应的工具库

    • (好处)目的是,组员们可以很轻松的使用,比如 npm i -g feTools,然后fe lan /src/views 就可以执行这个脚本。而不是用copy源文件去使用(copy大法也不好管理版本)
  2. 提词

    用正则匹配,拿到所有的中文,然后生成zh.json和en.json,如下

    // zh.json
    {
        "确定", "确定",
        "取消", "取消",
        ...
    }
    // en.json  后续把这个文件交给产品去翻译,或者自己翻译,翻译的结果写到对应的value内
    {
        "确定", "",
        "取消", "",
        ...
    }
    复制代码
  3. 回填

    用replace配合正则,把中文,替换成 $(”) 的形式

    • 如:<span>查询</span> 替换成=> <span>{{$t('查询')}}</span>
    /* 处理 vue的template 部分 */
    let vueStr = (源代码).replace(re, word => {
        if (zhJson[word.trim()]) {
            return "$t('" + word.trim() + "')"
        }
        return word
    })
    复制代码

3. 提词碰到的难点

匹配中文的正则表达式:/[\u4e00-\u9fa5]{1,}/g,但实际上很多都不是简单的中文

情况有:

1. 中文混杂英文
    <span>http 的content-type采用json格式,建议使用的格式</span>
2. 中文混杂各种标点符号
    <span>应用描述不允许为空 (长度150以内)</span>
3. 中文混杂vue的模板语法:`<span>企业{{obj.name}}图谱</span>`
    <span>企业{{obj.name}}图谱</span>
4. 过滤注释(html注释,js注释)
    // 注释
    <!-- 注释 -->
复制代码

分析问题:

1. 中文混杂英文
    <span>http 的content-type采用json格式,建议使用的格式</span>
2. 中文混杂各种标点符号
    <span>应用描述不允许为空 (长度150以内)</span>
3. 中文混杂vue的模板语法:
    <span>企业{{obj.name}}图谱</span>
    
(考虑的后续的回填)这两种情况最好把标点和英文都一同匹配完整,不然要分太多的段,可能会影响意思的表达
错误示例:(回填后): <span>http{{$t('的')}}content-type{{$t('采用')}}json{{$t('格式')}},{{$t('建议使用的格式')}}</span>
分多段实在太丑了
正确示例:
<span>{{$t('http 的content-type采用json格式,建议使用的格式')}}</span>
    

4. 过滤注释(html注释,js注释)
    // 注释
    /* 注释
     *   注释注释注释注释
     */
    <!-- 注释 -->
    <!-- 
        注释 
    -->
复制代码

最终的正则表达式结果:[\u4e00-\u9fa5|\w|\s|,,.。\/\*::=()()!!?\?\-]{1,}

正则表达式分析:中文 + 英文数字 + 空格或换行符等 + 部分标点符号(因为要过滤掉<>和{},所以就不能直接写\S)

  • 关键点是:不要以为一条正则就能搞定所有问题,因为上面匹配了英文,所以所有的英文也都会匹配出来,需要在写一次正则为结果做过滤
正则字符串:
    <span>企业{{obj.name}}图谱</span>
    <span>http 的content-type采用json格式,建议使用的格式</span>
    // 注释
    <!-- 注释 -->

正则表达式: 
    [\u4e00-\u9fa5|\w|\s|,,.。\/\*::=()()!!?\?\-]{1,}

匹配结果:
    共找到 10 处匹配:
    span
    企业
    obj.name
    图谱
    /span
    span
    http 的content-type采用json格式,建议使用的格式
    /span
    // 注释
    !-- 注释 --

源代码:
    function getChineseList (str) { // 获取所有 中文 混合英文 混合空格 及 标点符号
      const re = /[\u4e00-\u9fa5|\w|\s|,,.。\/\*::=()()!!?\?-]{1,}/g
      return str.match(re).map(e => {
        e = e.trim()
        // 把 英文 和 注释 过滤掉
        if (/[\u4E00-\u9FA5]/.test(e) && !/\/\/|\/\*--/.test(e)) return e
      }).filter(e => e)
    }
复制代码

最终总结:

  1. 思考问题的过程中,不要以为一条正则就能搞定所有问题,这样容易走入死胡同。可以多几次正则 或者 为正则结果做过滤,思路要打开
  2. 场景会很复杂,还是会有些多提取的情况(比如注释内有<>尖括号这种),但不会少提取。很难做到100%的准确性,起码保证不会少就行

4. 回填碰到的难点

情况很多,一次正则是不可能处理完的

情况有:

  1. <template>内的标签子元素替换:<span>查询</span> 替换成=> <span>{{$t('查询')}}</span>

    解法:

    // 复杂情况有:
    <span>  
        查询  
    </span> // 此处的正则要匹配换行符
    
    <span>查询<a href="https://juejin.cn/post/xx">xxx</a>信息</span>  // 此处的正则要用到非贪婪模式
    <span>查询{{obj.name}}信息</span> // 此处要额外正则处理
    
    // 实际处理
    const getZh = /[\u4e00-\u9fa5|\w|\s|,,.。\/\*::=()()!!?\?-]{1,}/g // 匹配中英文加部分标点
    
    let vueStr = (源代码).replace(getZh, word => {
        if (zhJson[word.trim()]) { // zhJson是提词函数拿到的map
            return "$t('" + word.trim() + "')"
        }
        return word
    })
    const textRe = />.*?<|\n\$.*?\n/g // . 不会匹配"\n",  加个 问号, 非贪婪模式
    vueStr = vueStr.replace(textRe, e => {
        if (e.slice(0, 2) === '>$') {
            if (/{{/.test(e)) { // 这种情况: >$t('查询'){{obj.name}}$t('信息')<
                e = e.replace(/\$t\(.*?\)/g, res => {
                    return '{{' + res + '}}'
                })
            } else {
                return `>{{${e.slice(1, e.length - 1)}}}<`
            }
        } else if (e[0] === '\n') {
            return `\n{{${e.slice(1, e.length - 1)}}}\n`
        }
        return e
    })
    复制代码
  2. <template>内的标签的 props 加冒号: placeholder="任务ID" 替换成=> :placeholder="$t('任务ID')"

    经过上一步处理后,placeholder=”任务ID” 已经变成了 placeholder=”$t(‘任务ID’)”

    解法:

    const vueRe = /[a-zA-Z|="'\>\$t\(]{1,}/g
    // 处理props内的, 加上冒号 比如: placeholder="$t('任务ID')"  =>  :placeholder="$t('任务ID')"
    let propsVue = vueStr.match(vueRe).map(e => {
      if (e.includes('="$t(')) return e
    }).filter(e => e)
    
    const map = new Map() // 去重
    propsVue = propsVue.map(e => { // 去重,避免多加了冒号
      const val = e.split('=')[0]
      if (!map.get(val)) {
        map.set(val, 1)
        return val
      }
    }).filter(e => e)
    
    propsVue.forEach(e => {
      vueStr = vueStr.replace(new RegExp(e, 'g'), word => {
        if (word.includes(':')) {
          return word
        } else {
          return ':' + word
        }
      })
    })
    复制代码

    异常情况:

    • 直接上面一个正则流程很难把一些异常处理干净,走下面的“清洗”操作,就会简单很多~
    1. 难免有些情况会异常处理成 多个冒号的情况 ::label-width='70'
    2. 有些三元表达式内,会出现: xx ? '$t('打开')' : '$t('关闭')'  需要把引号去掉 '$t('打开')' => $t('打开')
    
    // !!!! 把一些脏东西洗掉
    vueStr = vueStr.replace(new RegExp(/::/, 'g'), word => ':')
    // '$t('打开')' => $t('打开')
    vueStr = vueStr.replace(new RegExp(/'\$t\('/, 'g'), word => "$t('")
    vueStr = vueStr.replace(new RegExp(/'\)'/, 'g'), word => "')")
    复制代码
  3. <script>内的替换:(可能还有模板字符串拼接情况)

    /* js内: 
     *     columns: [
              {
         (old)  title: '任务名称',
         (new)  title: this.$t('任务名称'),
                key: 'name'
              },
           ]
          还有模板字符串拼接的情况,比较特殊, 需特殊处理
          如:`错误${res.message}`    `${this.$t('错误')}${res.message}` 
     */
    复制代码

    解决:

    function escapeRegExp (str) { // escapeRegExp的作用: 比如str是'变更前(', 此时new RegExp(str) 会报错, 需要转义一下,转成: '变更前\\('
      return str.replace(/[\(\)]/g, '\\$&')
    }
    
    let jsStr = data[1]
    const zhJsonNew = {}
    Object.keys(zhJson).forEach(e => {
      zhJsonNew[`'${e}'`] = e // 此处拿中文+双引号字符串  例如:"消息"
      zhJsonNew[`"${e}"`] = e // 此处拿中文+单引号字符串  例如:'消息'
      zhJsonNew[e] = e // 此处只拿中文字符串(为了匹配模板字符串)  例如:消息
    })
    
    Object.keys(zhJsonNew).forEach(e => {
      // escapeRegExp的作用: 比如e是'变更前(', 此时new RegExp(e) 会报错, 需要转义一下,转成: '变更前\\('
      jsStr = jsStr.replace(new RegExp(escapeRegExp(e), 'g'), word => {
        console.log(word)
        if (/"'/.test(word)) { // 处理非模板字符串的情况
          return 'this.$t(' + word + ')'
        } else { // 处理模板字符串 `错误${res.message}` => `${this.$t('错误')}${res.message}`
          return '${this.$t(' + word + ')}'
        }
      })
    })
    复制代码

5. 加入工具平台,让小组成员能够很容易使用

加入工具平台:内部前端工具平台搭建

安装好工具平台后:(实际使用方法:)

  • 终端输入: fe lan 参数1 [参数2:replace 或 r, [参数3: noOutput 或 n]]

    参数1 (必填):提词的路径

    • 支持 目录, 也支持 文件
    • 推荐绝对路径, 可以用编辑器copy出来. 相对路径也行, 但要保证输入准确

    参数2 (选填):是否回填, 可以简写成r

    • 填的话, 只能填replace 或 r.
    • 参数3 (选填, 必须要有参数2 才能生效): 作用: 只回填, 不输出.json文件

码字不易,点点小赞~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享