手写《自动化提词和回填工具》实现前端国际化(语言切换)
前端国际化的功能要求 :下拉选择语言,然后对应切换语言,先支持中英文。(目前有10+项目,要逐步处理完)
技术工具 :(项目是vue工程) vue-i18n,需要把所有的中文提出来,并且还要回填
难点 : 项目老旧、庞大,手动提词太笨了,效率低(而且有10+项目),此时需要一个工具,能够自动完成提词和回填
实现大纲:
- vue-i18n 的使用介绍
- 开发自动化提词和回填工具分析
- 提词碰到的难点
- 回填碰到的难点
- 加入工具平台,让小组成员能够很容易使用
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步:
- 有一个前端工具平台(内部前端工具平台搭建),可以执行命令,类似比如 终端输入:eslint 或者 webpack,可以执行对应的工具库
- (好处)目的是,组员们可以很轻松的使用,比如
npm i -g feTools
,然后fe lan /src/views
就可以执行这个脚本。而不是用copy源文件去使用(copy大法也不好管理版本)
- (好处)目的是,组员们可以很轻松的使用,比如
提词
用正则匹配,拿到所有的中文,然后生成zh.json和en.json,如下// zh.json
{
"确定", "确定",
"取消", "取消",
...
}
// en.json 后续把这个文件交给产品去翻译,或者自己翻译,翻译的结果写到对应的value内
{
"确定", "",
"取消", "",
...
}
回填
用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)
}
最终总结:
- 思考问题的过程中,不要以为一条正则就能搞定所有问题,这样容易走入死胡同。可以多几次正则 或者 为正则结果做过滤,思路要打开
- 场景会很复杂,还是会有些多提取的情况(比如注释内有<>尖括号这种),但不会少提取。很难做到100%的准确性,起码保证不会少就行
4. 回填碰到的难点
情况很多,一次正则是不可能处理完的
情况有:
- 内的标签子元素替换:
<span>查询</span> 替换成=> <span>{{$t('查询')}}</span>
解法:
```jsx // 复杂情况有:
查询
// 此处的正则要匹配换行符查询xxx信息 // 此处的正则要用到非贪婪模式 查询{{obj.name}}信息 // 此处要额外正则处理
// 实际处理 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')"`<br />经过上一步处理后,placeholder="任务ID" 已经变成了 placeholder="$t('任务ID')"<br />解法: <br />异常情况: ```javascript 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 => "')")