文章目录
- 介绍
- 背景
- 现象1
- 解决办法
- 现象2
- 原因分析
- 解决办法
- 最终方案
介绍
大家或多或少都用过别人封装的组件库,甚至有人或者公司内有自行封装的一些公用组件库,而国际化翻译现在已经是各大项目中必不可少的一个插件了,但组件库中使用 i18n
进行翻译的时候可能遇到过以下这种问题,希望我的一点分析可以帮到你们
背景
因公司历史项目使用 Angular
框架原因,所以在做架构迁移时,我们采用了 vue-class-component
(github链接) 和 vue-property-decorator
(github链接) 平滑的迁移到了 Vue
框架
部分代码如下:
<template>
<div class="pager">
<span class="grid-pager">
{{$t('pager.total', {total: totalRows})}}
</span>
<div class="grid-pager">
<a-pagination
size="small"
v-model:current="index"
:total="totalRows"
v-model:pageSize="size"
:pageSizeOptions="pageSizeOptions"
:showSizeChanger="showSizeChanger"
:showQuickJumper="showQuickJumper"
:simple="simple"
@change="change" />
</div>
</div>
</template>
<script lang="ts">
import { Options, Vue, setup } from 'vue-class-component';
import { Composer, useI18n } from 'vue-i18n';
import { Emit, Prop, Watch } from 'vue-property-decorator';
@Options({
name: 'pager'
})
export default class Pager extends Vue {
@Prop({ type: Number, }) public totalRows: number;
@Prop({ type: Number, }) public pagerSize: number;
@Prop({ type: Number, }) public pageIndex: number;
@Prop({ type: Array, default: ['20', '50', '100']}) public pageSizeOptions: Array<number>;
@Prop({ type: Boolean, default: false }) public showSizeChanger: boolean;
@Prop({ type: Boolean, default: true }) public showQuickJumper: boolean;
@Prop({ type: Boolean, default: false }) public simple: boolean;
public index: number = 1;
public size: number = 20;
/**
* change事件
*
* @param {number} index 下标
* @param {number} size 每页条数
* @returns {{index: number, size: number}} 返回分页器当前页码与当前每页条数
*/
@Emit()
public change(index: number, size: number): {index: number, size: number} {
return {
index: index - 1,
size
};
}
/**
* 监听 pageIndex 属性变化
*
* @param {number} newValue 新值
*/
@Watch('pageIndex')
public pageIndexChange(newValue: number): void {
if (newValue >= 0) {
this.index = newValue + 1;
}
}
/**
* 监听 pagerSize 属性变化
*
* @param {number} newValue 新值
*/
@Watch('pagerSize', { immediate: true })
public pageSizeChange(newValue: number): void {
if (newValue) {
this.size = newValue;
}
}
/**
* 钩子函数
*/
public mounted(): void {
if (this.pageIndex || this.pageIndex === 0) {
this.index = this.pageIndex + 1;
}
}
}
</script>
<style lang="scss" scoped></style>
因此我们封装的组件库也是基于此的,若您的项目不是使用该种方式开发的,那我觉得以下部分的内容对您的帮助应该是不多的
同时我们也没有做像现在主流的UI组件库的国际化翻译处理,如 Element
UI组件库中既可以兼容 vu-i18n 又可以自行进行翻译(可参考这里)
目前我们只是简单的封装了一个组件到另一个库里,国际化翻译是依赖于本地项目中安装的 vue-i18n 插件进行翻译的,正是因为这个原因才会出现我下面说的情况(后期可能会像组件库一样有自行的翻译方法)
现象1
就以上面的代码为例,若引用该库的项目国际化文件中没有 pager.total
字段时,浏览器控制台就会报错
Not found 'pager.total' key in 'zh_CN' locale messages.
且无法正常翻译
这是因为在全局国际化翻译文件中没有找到该key值才翻译有误
解决办法
在 @options
注解中可以直接添加组件级的i18n配置,如下:
@Options({
name: 'pager',
i18n: {
messages: {
zh_CN: {
'pager.total': '共{total}条'
},
zh_TW: {
'pager.total': '共{total}條'
},
en_US: {
'pager.total': '{total} entries in total'
}
}
}
})
使用这种方式确实可以使用了,因此我们也没多注意,其实它只是在引用这个组件的项目中也使用 vue-class-component
时才可以生效
现象2
基于上面的修改,直到有一天我们有个新的项目使用了
vue3 + vite + setup
技术栈时,它又没有正常翻译了,浏览器控制台报错和现象1一致
然后就是和同事找原因,issue、google都找遍了,没有发现遇到同样问题的,无奈只好自行查阅源码分析原因
原因分析
首先使用 @options
进行注解对象中的内容,会挂载在 Vue实例的 $options
上,如下:
因此在注解中设置i18n时可以肯定的是可以传给Vue实例的,那么就得看看I18n中是如何处理这部分内容的了(因源码较复杂,以下只关注$options部分)
可以看到这里 就是处理Vue实例上$options的i18n配置的
可以看到是 这里 引入了该Mixin,因使用的是v9版本,查看 defineMixinNext
调用位置
可以看到注释以及if判断都明确指出是 Legacy模式下才会进入这部分处理逻辑,那么现在翻译不生效的原因就非常明了了
但是现在本地项目使用的是 Vue3 + Vite + setup suger
技术栈,那么就必选找一个可以兼容的方式呀,不然这组件库不是白封装了吗 (艹皿艹)
然后就是漫长的翻阅 vue-class-component
github issue 的过程
解决办法
关注到这个 issue ,里面提到:
Composition functions are available in class property initializers by wrapping setup helper.
也就是说,在V8版本中我们是可以使用 setup 的
而翻阅 Vue-i18n 官网,可以看到在组合式API部分可以在setup中设置组件级的翻译内容
这样的话,方法就显而易见了,只要在 setup 中设置组件级的翻译内容即可,如下:
public componentSetup = setup(() => {
const i18n: Composer = useI18n({
locale: 'zh_CN',
messages: {
zh_CN: {
'pager.total1': '共{total}条'
},
zh_TW: {
'pager.total': '共{total}條'
},
en_US: {
'pager.total': '{total} entries in total'
}
},
});
return {
i18n
};
});
结果一试,还是原来的问题,控制台报错,页面没有正常显示,继续翻文档可以看到 这里 提到:
大致意思就是如果你在 useI18n 中 设置了那些字段时,添加 useScope: 'global'
配置将会将这些字段合并到全局作用域中,这不就是我们想要的吗! (✪ω✪)
public componentSetup = setup(() => {
const i18n: Composer = useI18n({
locale: 'zh_CN',
messages: {
zh_CN: {
'pager.total1': '共{total}条'
},
zh_TW: {
'pager.total': '共{total}條'
},
en_US: {
'pager.total': '{total} entries in total'
}
},
useScope: 'global'
});
return {
i18n
};
});
运行项目并打印全局i18n实例,你会发现组件中的翻译已成功加入全局翻译文件中,且页面也显示正常了
最终方案
根据现象1分析得出如果使用 Legacy 模式时可以读取 @Options 注解中的翻译内容
根据现象2分析得出如果使用 Composition 模式时可以读取 setup() 回调中的翻译内容
那么就可以兼容这两种方式使该组件以任何模式引入时都可以正常翻译了,完整代码如下:
<template>
<div class="pager">
<span class="grid-pager">
{{$t('pager.total', {total: totalRows})}}
</span>
<div class="grid-pager">
<a-pagination
size="small"
v-model:current="index"
:total="totalRows"
v-model:pageSize="size"
:pageSizeOptions="pageSizeOptions"
:showSizeChanger="showSizeChanger"
:showQuickJumper="showQuickJumper"
:simple="simple"
@change="change" />
</div>
</div>
</template>
<script lang="ts">
import { Options, Vue, setup } from 'vue-class-component';
import { Composer, useI18n } from 'vue-i18n';
import { Emit, Prop, Watch } from 'vue-property-decorator';
@Options({
name: 'pager',
// 兼容选项式API模式
i18n: {
messages: {
zh_CN: {
'pager.total': '共{total}条'
},
zh_TW: {
'pager.total': '共{total}條'
},
en_US: {
'pager.total': '{total} entries in total'
}
}
}
})
export default class Pager extends Vue {
@Prop({ type: Number, }) public totalRows: number;
@Prop({ type: Number, }) public pagerSize: number;
@Prop({ type: Number, }) public pageIndex: number;
@Prop({ type: Array, default: ['20', '50', '100']}) public pageSizeOptions: Array<number>;
@Prop({ type: Boolean, default: false }) public showSizeChanger: boolean;
@Prop({ type: Boolean, default: true }) public showQuickJumper: boolean;
@Prop({ type: Boolean, default: false }) public simple: boolean;
public index: number = 1;
public size: number = 20;
// 兼容组合式API模式
public componentSetup = setup(() => {
const i18n: Composer = useI18n({
locale: 'zh_CN',
messages: {
zh_CN: {
'pager.total': '共{total}条'
},
zh_TW: {
'pager.total': '共{total}條'
},
en_US: {
'pager.total': '{total} entries in total'
}
},
// 将当前翻译内容合并到全局翻译文件中
useScope: 'global'
});
return {
i18n
};
});
/**
* change事件
*
* @param {number} index 下标
* @param {number} size 每页条数
* @returns {{index: number, size: number}} 返回分页器当前页码与当前每页条数
*/
@Emit()
public change(index: number, size: number): {index: number, size: number} {
return {
index: index - 1,
size
};
}
}
</script>
<style lang="scss" scoped></style>