手写《自动化提词和回填工具》实现前端国际化(语言切换)
前端国际化的功能要求 :下拉选择语言,然后对应切换语言,先支持中英文。(目前有10+项目,要逐步处理完)
技术工具 :(项目是vue工程) vue-i18n,需要把所有的中文提出来,并且还要回填
难点 : 项目老旧、庞大,手动提词太笨了,效率低(而且有10+项目),此时需要一个工具,能够自动完成提词和回填
实现大纲:
- vue-i18n 的使用介绍
- 开发自动化提词和回填工具分析
- 提词碰到的难点
- 回填碰到的难点
- 加入工具平台,让小组成员能够很容易使用
1. vue-i18n 的使用介绍
// 入口 main.js, 引入import i18n from './i18n'...new Vue({i18n,...})
// i18n.jsimport 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图谱/spanspanhttp 的content-type采用json格式,建议使用的格式/span// 注释!-- 注释 --源代码:function getChineseList (str) { // 获取所有 中文 混合英文 混合空格 及 标点符号const re = /[\u4e00-\u9fa5|\w|\s|,,.。\/\*::=()()!!?\?-]{1,}/greturn 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 => "')")
