vue国际化实践
我们在做国际化时会面临的问题:
- 语言翻译
-
静态文案翻译(前端静态模板文案)
-
动态文案翻译(server端下发的动态数据)
- 样式
-
不同语言文案长度不一样造成的样式错乱
-
图片的替换
-
map表维护
-
第三方服务
- SDK
- 本地化
-
货币单位
-
货币汇率
-
时间格式
- 打包方案
-
运行时
-
编译后
vue-i18n是一个针对于vue的国际化插件,本文基于vue-i18n实现vue项目运行时的国际化,并对其中常见的疼点给出相应的解决办法。
常规方案
新项目
我们根据vue-i18n的官方文档,将国际化的插件运用于我们的项目:
// 引入插件和语言包
import VueI18n from 'vue-i18n'
import zh from '@/i18n/langs/zh'
import en from '@/i18n/langs/en'
Vue.use(VueI18n)
// 从本地存储中取,如果没有默认为中文,
// 这样可以解决切换语言后,没记住选择的语言,刷新页面后还是默认的语言
function getLocal() {
const cookies = document.cookie ? document.cookie.split('; ') : [];
let locale = cookies.find((cookie) => decodeURIComponent(cookie).split('=')[0] === 'locale') || 'locale=zh';
locale = locale.split('=');
return locale[1];
}
const locale = getLocal();
//实例化vue-i18n
const i18n = new VueI18n({
locale,
messages: {
'zh': zh, // 中文语言包
'en': en // 英文语言包
}
})
// 将i18n实例挂载到vue上
new Vue({
el: '#app',
i18n,
router,
store,
template: '<App/>',
components: { App }
})
接着我们需要创建中、英文翻译文件,这里一般会按照模块或文件路径将翻译文件进行模块化:
//zh.js
export default {
nav: {
home: '首页',
monitor: '监控',
analyze: '分析',
alarm: '报警',
maintenance: '运维',
config: '配置',
device: '设备',
scada: '画面'
},
confirm: {
ok: '确认',
cancel: '取消',
content: '你确认要退出系统吗?',
logout: '退 出'
},
}
//en.js
export default {
nav: {
home: 'Home', //首页
monitor: 'Monitor', //监控
analyze: 'Analyze', //分析
alarm: 'Alarm', // 报警
maintenance: 'Maintenance', //运维
config: 'Config', //配置
device: 'Device', //设备
scada: 'Scada' //画面
},
confirm: {
ok: 'Logout', //退出
cancel: 'Cancel', //取消
content: 'Are you sure you want to quit the system?', //你确认要退出系统吗
logout: 'Logout' //
}
}
然后在项目中使用:
<span v-text="$t('nav.home')"></span>
or
<span></span>
老项目
对于大多数项目,其实并不是一开始就会做国际化,往往是在项目迭代一段时间后才会有国际化的需求。此时,我们需要将项目中已经存在的中文进行替换:
1,先使用脚本来替换常规写法
2,对于非常规写法,则需要我们一个一个文件进行查漏补缺
至于到底怎么去替换,需要结合项目的实际情况来具体分析,这里不再讨论。给大家看一个简单的例子,里面的$spt()
函数下文会讲到:
存在的问题
一切看起来都那么美好,然而在我们后续的维护和开发中发现这种方式存在这些问题:
1,在新增页面的时候,需要将页面中的展示文字添加到对应的中文、英文翻译文件中。现在只有两种语言,如果之后需要支持的语言增多,则需要同步增加到所有文件,这样不仅增加工作量且容易造成遗漏。
2,使用的映射key不直观,在写代码时不能直接知道其含义,代码易读性低下。
3,映射key值不好取,取名一直是令人头疼的事,一般使用中文对应的英文或者直接取中文的拼音,然而不管那种都需要消耗开发的时间。
4,中英文在于不同的文件,每次修改或查找都不方便,且翻译人员翻译时还需要对照两个文件。
下面我们就来一个个解决它们。
优化方案
中、英文翻译集中
首先,我们来看问题4,对于翻译文件,我们完全可以把所有翻译文件集中在一起。为了使翻译文件更加简洁清晰我们使用yml文件来储存翻译的映射关系,类似下面这样:
// translationFile.yml
nav.home
zh: 首页
en: home
nav.monitor
zh: 监控
en: monitor
...
当然这样做了以后需要进行相应的适配:
const translationData = require('json-loader!yaml-loader!./translationFile.yml');
function getYmlData() {
const enObj = {};
const cnObj = {};
Object.entries(translationData).forEach(([key, val]) => {
const enStr = val.en;
const cnStr = val.zh;
enObj[key] = enStr || '';
cnObj[key] = cnStr || '';
});
return {
enObj,
cnObj
};
}
const { enObj, cnObj } = getYmlData();
const i18n = new VueI18n({
locale,
messages: {
'zh': cnObj, // 中文语言包
'en': enObj // 英文语言包
}
})
这样使用:
<span v-text="$t('nav.home')"></span>
or
<span></span>
中文做key
接着来看问题2和问题3,都是因为需要用字母做key才导致的,那我们是不是可以考虑用中文来做key:
// translationFile.yml
nav.首页
zh-CN: 首页
en: home
nav.监控
zh-CN: 监控
en: monitor
...
这样使用:
<span v-text="$t('nav.首页')"></span>
or
<span></span>
是不是可读性高了点,但是前缀nav.
总感觉有点瑕疵,可不可以去掉呢?
如果对于同一个中文,给出的翻译是一致的,那就可以去除前缀的限制。于是我们查阅了项目中对于同一个中文给出的翻译,发现对于我们的项目,同一个中文的英文翻译基本是相同的,偶尔不同也是大小写的区别是同义不同词的翻译。
所以我们决定以中文作为key,但由于中文中可能会存在标点符号,而标点符号在yml文件中可能会引起格式不正确,这里我们对中文做一次hash再当作key:
// translationFile.yml
f203d577d14268cd853ce37418cb14f8e268c2bce88220ae131c761e2255954a:
zh-CN: "首页"
en: "home"
fac2a67ad87807c4112af2b9201ef929d8c1c80214a6ad5198423398369371a4:
zh-CN: "监控"
en: "monitor"
同样需要进行相应的适配:
const SHA256 = require('crypto-js/sha256');
const i18n = new VueI18n({
locale,
messages: {
'zh': cnObj, // 中文语言包
'en': enObj // 英文语言包
}
})
// 在原来函数的基础上做一层封装,如果没有翻译默认返回中文
Vue.prototype.$spt = i18n.$spt = (str, ...arg) => {
const val = i18n.t(SHA256(str), ...arg);
return val || str;
};
这样使用,是不是代码可读性提高了很多:
<span v-text="$spt('首页')"></span>
or
<span></span>
最后,在需要使用同一个中文对应不同的翻译时,可以为其中某个中文添加额外信息(这里需要在返回中文翻译时去除相关的额外信息):
<span></span>
or
<span v-text="$spt('首页(导航栏)')"></span>
生成的翻译文件:
// translationFile.yml
f203d577d14268cd853ce37418cb14f8e268c2bce88220ae131c761e2255954a:
zh-CN: "首页"
en: "home"
fac2a67ad87807c4112af2b9201ef929d8c1c80214a6ad5198423398369371a4:
zh-CN: "首页(导航栏)"
en: "Home"
自动生成翻译文件
我们回过头再来看第问题1,我们每次增加了新的内容都需要手动添加到翻译文件吗?
不,我们拒绝这份重复而繁重的工作。
只需约定在添加展示文案时使用$spt()
来包裹中文,我们就可以轻易的找出项目中所有需要翻译的中文,脚本代码如下:
const fs = require('fs');
const path = require('path');
const SHA256 = require('crypto-js/sha256');
const YAML = require('yamljs');
const ymlData1 = YAML.parse(fs.readFileSync(path.resolve('./system/i18n/translationFile.yml'), 'utf-8'));
const exitObj = {}; // 用来保存当前文件中存在的翻译key
// 获取对应的翻译文字
function getValue(hash) {
const obj1 = ymlData1[hash] || {};
return {
enStr: obj1.en || ''
};
}
function transferFile(filePath) {
// 类似:yi-components.yiParamEditor.yiParamEditor
const fileKey = filePath.replace(/^.+\/system\//, '').replace(/\.vue/g, '').replace(/\//g, '.');
const file = fs.readFileSync(filePath, 'utf-8');
// 匹配除去'和"的其余所有标点符号
const reg = /\$spt\((['"])([\u4e00-\u9fa5a-zA-Z0-9\s\,\.\/\;\[\]\\\`\-\=\<\>\?\:\{\}\|\~\!\@\#\$\%\^\&\*\(\)\_\+\,\。\/\;\‘\【\】\、\·\-\=\《\》\?\:\“\”\「\」\|\~\!\@\#\¥\%\…\&\*\(\)\—\+]+)['"]/g;
let result = reg.exec(file);
while (result) {
const zhCnStr = result[2];
const hashStr = SHA256(zhCnStr);
const { enStr } = getValue(hashStr);
if (!exitObj[hashStr]) {
const obj = {
enStr,
zhCnStr
};
exitObj[hashStr] = obj;
} else if (!exitObj[hashStr].enStr) {
exitObj[hashStr].enStr = enStr;
}
result = reg.exec(file);
}
}
function readDirSync(dirPath) {
const pa = fs.readdirSync(dirPath);
pa.forEach((ele) => {
const currentPath = path.join(dirPath, ele);
const info = fs.statSync(currentPath);
// 文件夹则继续递归
if (info.isDirectory()) {
readDirSync(currentPath);
} else if ((ele.endsWith('.vue') || ele.endsWith('.js')) && (ele !== 'translation.js')) {
// .vue、.js文件
transferFile(currentPath);
}
});
}
function geneFile() {
// system目录下进行递归遍历
readDirSync(path.resolve('./system'));
let targetStr = '';
Object.entries(exitObj).forEach(([key, val]) => {
const { enStr, zhCnStr } = val;
targetStr += `${key}:\n zh-CN: "${zhCnStr}"\n en: "${enStr}"\n`;
});
fs.writeFileSync('./system/i18n/translationFile.yml', targetStr, 'utf-8');
}
geneFile();
然后将执行语句写入package.json
中:
"scripts": {
"translation": "node system/i18n/translation.js"
}
团队规范
基于我们的方案,我们可以确立如下规则:
1,中文需要使用$spt( '中文')
函数包裹起来,js文件和vue文件中都需要
2,$spt()
函数包裹的中文不能包含英文的单引号、双引号即:’和”,其余符号都可以使用
3,$spt()函数不得换行,若下所示:
$spt(
'中文' // 不能换行
)
需要改为
$spt('中文')
这是由于我们的脚本没有匹配\n
,不做这个匹配是因为希望统一格式。
4,$spt()
函数使用单引号包裹中文:$spt( '中文')
5,每次添加或修改中文后需要执行:npm run translation
来生成新的翻译文件
6,如果需要使用同一个中文对应不同的英文翻译,可以在中文中加上相应的解释:
$spt( '总览')
$spt( '总览(导航栏)')
后续问题
在后续开发中,我们发现如果文字中包含 \
,使用我们前面的脚本会出现问题。
首先,我们查询了 yml 文件中值的写法有几种方式:
1. k: v:字面直接来写;
字符串默认不用加上单引号或者双引号;
2. "":双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思
name: "zhangsan \n lisi":输出;zhangsan 换行 lisi
2. '':单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据
name: 'zhangsan \n lisi':输出;zhangsan \n lisi
接着,我们来复现这个问题 ,比如:
// 需要使用转义符来转义'\'
<div></div>
...
// 获取到的参数为:'如果使用“@”符号,请使用转译符号,如“\@”'
Vue.prototype.$spt = i18n.$spt = (str, ...arg) => {
const val = i18n.t(SHA256(str), ...arg); // 注意,这里是对:'如果使用“@”符号,请使用转译符号,如“\@”' 进行hash计算
return val || str;
};
...
const zhCnStr = result[2]; // '如果使用“@”符号,请使用转译符号,如“\\@”'
const hashStr = SHA256(zhCnStr); // 注意,这里是对:'如果使用“@”符号,请使用转译符号,如“\\@”' 进行hash计算
...
34869c6ac5bf72cef373c2f818b43b5b93725d001015d379bfab485c5e468611:
zh-CN: "如果使用“@”符号,请使用转译符号,如“\\@”"
en: ""
我们发现翻译文件中生成 hash 的字符串和翻译函数中进行 hash 计算的字符串是不同的,导致翻译失败。所以我们进行如下修改:
const zhCnStr = result[2]; // '如果使用“@”符号,请使用转译符号,如“\\@”'
const hashStr = SHA256(zhCnStr.replace(/\\\\/g, '\\')); // 现在正确了,是使用:'如果使用“@”符号,请使用转译符号,如“\@”' 进行hash计算
ok,现在页面能正确显示了:
如果使用“@”符号,请使用转译符号,如“\@”