在之前使用vue-i18n
组件的时候,都是比较简单的使用$t('label')
进行翻译而已。这次接到一个需求,国际化会比较复杂,于是乎好好研究了下vue-i18n
组件,竟然发现有很多其它好用的方法。
需求描述
[时间] [设备名称] 已上线
[时间] [设备名称] 已下线
[时间] [操作人]将[xx设备(红色字体)]添加到白名单
[时间] [操作人]将[xx设备(红色字体)]移出白名单
[时间] [操作人]修改白名单中[xx设备(红色字体)]的[xx属性/xx属性]
这次的需求是做一个类似于系统公告的功能。比较麻烦的地方在于会有非常多种类型的操作,每种操作上也会有对应的变量需要替换。其中尤其需要注意的是
- 不同变量使用的颜色可能是不同的
- 在修改类型的操作中,修改的属性可能是多个,用 / 进行分隔,而且属性名称也需要进行 国际化
最后需要实现的效果大致是这样的。
在接到需求后的第一想法:
每个操作类型定义一个模板,和后端约定好对应的变量名称进行替换。变量需要展示不同颜色,那就用span
标签包起来然后指定对应颜色。将这整个国际化消息使用v-html
指令插入到DOM上。
// zh-CN.js
export default {
// 系统公告,这里的key是操作类型
systemNotice: {
// 设备上下线
'305': '{createTime} {deviceName}已{deviceStatus}',
'306': '{createTime} {deviceName}已{deviceStatus}',
// 白名单相关
'601': '{createTime} {operatorName}将{uavName}添加到白名单',
'602': '{createTime} {operatorName}修改白名单中{uavName}的{updateFields}',
'603': '{createTime} {operatorName}将{uavName}移出白名单',
}
}
但是这样会有几个比较明显的问题:
- 会存在XSS漏洞风险
- 修改的属性要进行二次国际化会比较麻烦
- 每个模板的变量个数不一致,实现起来也达不到要求
如果我们能避免XSS漏洞,然后用v-for的方式插入变量就非常好了。这个时候就把目光移向了官方文档,果然发现了更好的解决办法。
Component Interpolation(组件插值)
先简单介绍下这种用法,更详细的信息可以查看👉 官网
假设我们需要翻译下面一段话,
<p>I accept xxx <a href="/term">Terms of Service Agreement</a></p>
原始做法
先不使用组件插值的做法应该是这样的,分两段进行分别翻译。
定义国际化文件
// 国际化文件
const messages = {
en: {
term1: 'I Accept',
term2: 'Terms of Service Agreement'
}
}
<p>{{ $t('term1') }}<a href="/term">{{ $t('term2') }}</a></p>
组件插值(列表)
使用组件插值的做法是这样的(能避免XSS漏洞):
定义国际化文件
// 国际化文件
const messages = {
en: {
term: 'I Accept {0}',
tos: 'Terms of Service Agreement',
}
}
<i18n-t keypath="term" tag="P" class="className">
<a :href="url" target="_blank">{{ $t('tos') }}</a>
</i18n-t>
组件说明
使用组件的方式进行国际化
keypath
指向的是在国际化文件中的属性名,可以使用多级,例如:system.notice.name
tag
是组件最后生成的标签,不指定的话默认是Fragments
<i18n-t>
组件上的其它属性都会添加到最后生成的标签上,例如上面就可以添加自定义样式- 子元素会按照顺序依次插入到模板中的变量中,例如:例子中第一个子元素
<a>
对应模板中的{0}
组件插值(Slot)
组件插值还可以使用命令Slot的方式进行传递,这种方式针对命令参数会更加方便。
// 国际化文件
const messages = {
en: {
term: 'I Accept {content1} and {content2}',
content1: 'xx1',
content2: 'xx2',
}
}
<i18n-t keypath="term" tag="P" class="className">
<template v-slot:content>
<a :href="url" target="_blank">{{ $t('content1') }}</a>
</template>
<!-- 两种方式都可以 -->
<template #content2>
<a :href="url" target="_blank">{{ $t('content2') }}</a>
</template>
</i18n-t>
这样的话对应的命令插槽中的内容会替换到模板的同名变量中去。
实现需求
// 后端返回值示例
{
"data": [
{
"id": 1285249044751450112,
"operationType": 602,
"content": {
"fieldList": [
{
"name": "operatorName",
"value": "00000000001-Pad",
"colourType": 1
},
{
"name": "uavWhiteName",
"value": "001-3464563",
"colourType": 1
},
{
"name": "updateFields",
"value": [
"usage",
"vendor"
],
"colourType": 0
},
{
"name": "createTime",
"value": "2024-06-25T03:42:42.141535",
"colourType": 0
}
]
},
},
{
"id": 1285225551280340992,
"operationType": 603,
"content": {
"fieldList": [
{
"name": "operatorName",
"value": "00000000001-Pad",
"colourType": 1
},
{
"name": "uavWhiteName",
"value": "001-3464563",
"colourType": 1
},
{
"name": "createTime",
"value": "2024-06-25T02:11:32.256318",
"colourType": 0
}
]
},
},
],
"pageTotal": 3,
"pageIndex": 1,
"pageSize": 30
}
上面是和后端约定好的返回值结构,在下面使用Slot的方式实现。
- 在
<i18n-t>
组件内部,每个变量都使用Slot
进行插入,变量名是动态的,使用方式是#[field.name]
- 颜色通过
span
标签上的style
具体指定 - 针对模板里面的特殊变量,例如修改内容字段和设备上下线等需要二次进行国际化的字段,特殊处理。修改属性可能会存在多个,所以使用
v-for
进行遍历 - 在修改内容字段里,多个属性之间用
/
进行分隔。这个实现我是用m-n12n-item__updateLabel
类的伪元素进行实现的,然后用last-child
去掉最后一个/
。这样避免在template里面用过多的判断逻辑。
<template>
<!-- 使用组件的方式 -->
<i18n-t :keypath="template" tag="p" class="m-n12n-item">
<!-- 每个变量使用插槽方式替换: https://vue-i18n.intlify.dev/guide/advanced/component.html -->
<template v-for="field in fields" :key="field.name" #[ field.name ] >
<span class="m-n12n-item__label" :class="field.name" :style="{ color: ColorMap[field.colourType] }">
<!-- 修改内容字段 -->
<template v-if="field.name === 'updateFields' && field.value">
<span class="m-n12n-item__updateLabel" v-for="label in field.value" :key="label">{{ t(`systemNotice.updateFields.${label}`) }}</span>
</template>
<!-- 设备上下线 -->
<template v-else-if="field.name === 'deviceStatus' && field.value">
{{ t(`systemNotice.deviceStatus.${field.value}`) }}
</template>
<!-- 其它内容 -->
<template v-else>
{{ field.value }}
</template>
</span>
</template>
</i18n-t>
</template>
// vue3 JS
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
})
// 操作类型,对应国际化模板
const template = computed(() => `systemNotice.${props.data.operationType}`)
// 变量列表
const fields = computed(() => {
const list = props.data.content.fieldList || []
return list.map((item) => {
if (item.name === 'createTime') {
item.value = utcTime2localtime(item.value)
}
return item
})
})
// SCSS
.m-n12n-item {
&__updateLabel {
padding-right: 6px;
display: inline-block;
position: relative;
&::after {
content: '/';
position: absolute;
top: 2px;
right: 1px;
}
&:last-child {
padding-right: 0;
&::after {
content: '';
}
}
}
}
我的实现方法就是这样,如果有更好的实现方法,欢迎交流🙌