按vue组件实例类型实现非侵入式国际化多语言翻译
#vue3##国际化##本地化##international#
web界面国际化,I18N(Internationalization,国际化),I11L(International,英特纳雄耐尔),L10N(Localization,本地化),显示文字多语言化是其主要内容。
浏览器的js提供了Intl全局对象,html提供了translate属性。
console.log(Intl,window.Intl);
<p translate="no">Don't translate this!</p>
某些浏览器插件支持一键“翻译网页”。其实现很简单,递归遍历找到<span><text>等文字标签,按通用字典对照翻译,并监听dom的变化,对弹出的内容或更新的内容也能即时翻译。这种一网打尽式的翻译,对某些wiki、doc、博客、新闻类网站比较适合,但是对一些ERP等管理系统,不太适合:
- 不精准。不该翻译的地方翻译了,比如用户输入的内容:产品编码、产品型号,Grid的cell内容...除非能用“translate='no'”把不翻译的dom标注得很完整。
- 不专业。翻译的字典是通用的,可能因为行业不同,牛头不对马嘴。插件能支持用我自己的字典? 因此,ERP这种管理系统,一般要有自己的多语言方案。
要做一个翻译方案,要考虑以下问题:
- 字典。字典格式(json,csv,xml)、字典的读取、解析、编辑。
- 国家语言标识。语言代码( Language Code)、国家代码( Country Code)、Windows Language Code Identifier (LCID),一般不再使用旧的Code Page(简体中文936,繁体中文950)。
- 语言切换。LCID存哪里。是否重加载页面。
- 翻译函数。高效的把一个文本字串,查找字典,翻译成对照文本。
- 翻译的时机。在何时调用翻译函数。
i18n(http://npmmirror.com/package/i18n,http://npmmirror.com/package/i18next)组件提供了一套简单的机制。 对vue框架,i18n对照有个vue-i18n(http://npmmirror.com/package/vue-i18n)。 i18n的实现了多语言化的基本机制,需要程序员配合,在合适的地方调用翻译函数。
我这里要讲的是一种非侵入式的,外挂式的多语言转换机制,比较适合已经完成的系统,突然要加多语言机制。 我的这篇老文章里(https://nodejs.blog.csdn.net/article/details/248218)谈到了这种方法,那是在delphi、c#开发的winform程序中,在web开发中,因为有了react、vue这样的框架实现了web组件化,因此也可以使用类似的方案。这个方案的主要思想是:为每个vue类注册一个翻译函数,在必要时把vue组件实例按其类的翻译函数来逐个翻译。
acroMLClassMethod.register('DataGrid',t_DataGrid);
acroMLClassMethod.register('GridColumn',t_GridColumn);
acroMLClassMethod.register('GridFilterButton',t_GridFilterButton);
acroMLClassMethod.register('Menu',t_Menu);
acroMLClassMethod.register('MenuItem',t_MenuItem);
翻译函数,如:v3-easyui组件的翻译函数,像这个样子:
function t_GridColumn(t,instWrap){
let inst=instWrap.$;
acroMLClassMethod.translateProps(t,inst.props,['title']);
}
function t_GridFilterButton(t,instWrap){
let inst=instWrap.$;
let items=instWrap.$data.items;
if (items){
for(let i=0;i<items.length;i++){
let item=items[i];
acroMLClassMethod.translateProps(t,item,['text']);
}
}
}
这个方案的几个特点:
- 外挂式。写页面的程序员一般情况下感觉它不存在。特殊的地方还是用显式的使用t函数,或标记translate='no'。
- 相对准确。相比浏览器的自动翻译,不会“误伤”。如DataGrid,会翻译column,footer的title,不会翻译到cell。相对于传统i18n机制显式的使用t函数,没那么精准,如:SelectBox下拉框,当选择的文本用于代码比较时是不能翻译的,还是需要显式的标记translate='no'或挂载beforeTranslate事件告知不翻译。
- 切换语言时能做到不重加载页面就刷新。传统i18n机制可能也能做到,只要t函数是响应式的,能响应locale的变化。但是,某些第3方组件包,其内置的一些文本字串,切换语言后,即使重加载了其对应的内置字典,也不会自动刷新。如devextreme-vue的DataGrid的noDataText。
文无第一,武无第二,各种方案没有绝对的好坏,就像react与vue,暂时无法谁碾压谁,适合不同的场景和不同的团队。
这个方案的几个注意点:
- 如何从vue组件实例找到其类别。
- 注册类别还是注册类别名称。
- 何时调用翻译函数。
- 不同厂家的vue组件属性修改机制的差别。
- 切换语言时如何不重加载页面而刷新。
- 人为设定不翻译某些组件的机制。
- 切换语言时DDKey从哪里找。
1、如何从vue组件实例找到其类别
- instWrap.$options.name
- instWrap.$.constructor.name
- instWrap.$.type.name
translateCom(t,instWrap,isTranslateChildren=true){
//console.log(instWrap);
let inst=instWrap.$;
let typeName=instWrap.$options.name;
if (!typeName){
if (inst.constructor && (inst.constructor.name!='Object')) typeName=inst.constructor.name;
}
if (!typeName) typeName=inst.type.name;
if (!typeName){
for(let i=0;i<classNameGetters.length;i++){
typeName=classNameGetters[i](instWrap);
if (typeName) break;
}
}
//console.log(typeName,instWrap);
let m;
if (typeName){
m=classMethods[typeName];
}
else{
let {classID,method}=getClassMethod(inst.type);
m=method;
}
if (m){
if (instWrap.$attrs.translate!='no'){
m(t,instWrap);
}
}
if (isTranslateChildren){
acroMLClassMethod.translateVNode(t,inst.subTree,isTranslateChildren);
}
},
如果从已知渠道拿不到类别名称,那再实现一个机制,让组件厂商告诉你如何获取。如:devextreme-vue组件,上面3个渠道是得不到组件类别名称的。只有特别处理:
acroMLClassMethod.registerClassNameGetter(function(instWrap){
let className;
if (instWrap.$_instance) className=instWrap.$_instance.NAME;
if (!className) className=instWrap.widget;
//if (!className) className=instWrap.$options.$_optionName;
return className;
});
2、注册类别还是注册类别名称
前面看到注册翻译函数时,类别都是使用的类别名称,如果使用类别,也是可以实现的,即:
acroMLClassMethod.register(DataGrid,t_DataGrid);
acroMLClassMethod.register(Menu,t_Menu);
但是,直接用类别,需要把组件加载到内存中,有几个坏处:
- 组件包的组件全部加载了。实际项目可能没有用到那么多组件,比如devextreme中的甘特图、枢纽分析Grid组件;
- 组件加载机制可能与外部不同。你可能是用一个包加载,外面可能是用分文件加载,这会导致相同组件在内存中有两份,可能导致组件内部逻辑错误。
//加载方式1:
import DX from 'devextreme-vue';
//加载方式2:
import {DxDataGrid,DxColumn,DxPager,DxPaging,DxGroupPanel,DxSearchPanel,DxSelection,DxFilterRow,DxScrolling} from 'devextreme-vue/data-grid';
import {DxTextBox,DxButton as DxTextBoxButton} from 'devextreme-vue/text-box';
import DxButton from 'devextreme-vue/button';
import DxCheckBox from 'devextreme-vue/check-box';
import DxColorBox from 'devextreme-vue/color-box';
//加载方式3:
import DxButton from 'devextreme-vue/button.mjs';
import DxCheckBox from 'devextreme-vue/check-box.mjs';
3、何时调用翻译函数
谢天谢地,vue3还保留了app.mixin。可以混入mounted、updated、unmounted函数到全部的vue组件中,这是监测vue组件生命周期的好地方。
import acroMLClassMethod from 'acroml/vue/acroML.ClassMethod.mjs';
import {YJEvent} from 'foil/util.yjEvent.mjs';
/**
* 用insts来维护vue组件实例列表。
* 因为当多语言切换时,从$root不知道如何遍历到DxDropDownBox模版中的DxDataGrid
*/
let insts=[];
let isTranslating=false;
class YJLocaleTranslator extends YJEvent{
init(app){
let self=this;
app.mixin({
mounted(){
/**
* app.mixin混入到任何组件的生命周期中。只翻译当前组件。
* 注意:这里的this是被混入的vue组件实例,不是YJVueTranslator实例
*/
//let inst=Vue.getCurrentInstance();
//console.log('DOM mounted:',this.$options.name,this);
let instWrap=this;
setTimeout(function(){
/**
* 混入的函数是先于组件的函数执行。
* 用setTimeout造成异步效果,等待让DxDataGrid实例的$_instance有值。
* 如果不用setTimeout,混入到mounted函数中,那时DxDataGrid实例的$_instance也不会有值。
*/
if (self.translateInst(instWrap)){
insts.push(instWrap);
}
}, 0);
},
unmounted(){
let index=insts.indexOf(this);
if (index>=0) insts.splice(index,1);
},
updated(){
/**
* 翻译可能导致组件再更新,进入死循环。
* update可能有很多原因,大多与显示翻译无关。
* 组件更新时,可能把父组件文本属性给子组件,必须再次翻译。
*/
if (isTranslating) return;
let instWrap=this;
//console.log('beforeUpdate',instWrap);
setTimeout(function(){
isTranslating=true;
try{
self.translateInst(instWrap);
}
finally{
/**
* 用setTimeout造成异步效果,让“因为翻译引起的组件更新”不再进入翻译
*/
setTimeout(function(){
isTranslating=false;
},0);
}
},0);
}
});
}
translate(){
let self=this;
let t1=new Date();
for(let i=0;i<insts.length;i++){
let inst=insts[i];
self.translateInst(inst);
}
let t2=new Date();
console.log(`acroml translated ${insts.length} instances used ${t2.getTime()-t1.getTime()} ms.`);
}
translateInst(instWrap){
let self=this;
let ops={isTranslate:true};
self.emit('beforeTranslate',instWrap,ops);
if (!ops.isTranslate) return false;
acroMLClassMethod.translateCom(t,instWrap,false);
//acroMLClassMethod.translateVNode(t,inst.vnode,false);
self.emit('afterTranslate',instWrap);
return true;
}
}
let yjLocaleTranslator=new YJLocaleTranslator();
export default yjLocaleTranslator;
export {yjLocaleTranslator}
4、不同厂家的vue组件属性修改机制的差别
一般的组件,修改instWrap.$.props就可以,而且能做到不重新加载页面就刷新。如:ant-design-vue的Table:
function t_Table(t,instWrap){
//console.log('t_Table',instWrap);
let inst=instWrap.$;
if (inst.props.columns){
for(let i=0;i<inst.props.columns.length;i++){
let column=inst.props.columns[i];
acroMLClassMethod.translateProp(t,column,'title');
}
}
acroMLClassMethod.translateProps(t,inst.props.locale);
inst.props.locale={...inst.props.locale};
}
如:element-plus的ElTable:
function t_ElTable(t, instWrap){
let inst=instWrap.$;
//console.log(instWrap);
acroMLClassMethod.translateProps(t,inst.props,['emptyText','confirmFilter','resetFilter','clearFilter','sumText']);
let columns=instWrap.columns;
if (columns){
for(let i=0;i<columns.length;i++){
acroMLClassMethod.translateProp(t,columns[i],'label');
}
}
}
但是,devextreme-vue比较特殊,这样修改instWrap.$.props无效或不会刷新(问了devexrpess厂家,说官方不支持切换语言后不重加载页面刷新)。因为它的核心组件包devextreme,也用于react和angular,而且对于DataGrid的内部组件,有prop和slot两种定义方式:
<DxDataGrid
:show-borders="true"
:groupPanel="{visible:true,emptyPanelText:'File'}"
:searchPanel="{visible:true,placeholder:'Edit'}">
</DxDataGrid>
<DxDataGrid :show-borders="true">
<DxSearchPanel :visible="true" placeholder='Edit' />
<DxGroupPanel :visible="true" emptyPanelText='File' />
</DxDataGrid>
因此,它定义了一套API机制来更新属性。
instWrap.$_instance.option();
instWrap.$_instance.state();
instWrap.$_instance._getDefaultOptions();
因此,我们使用这些API来翻译组件:
function wrap_transDevextremeCom(proc){
return function(t,instWrap){
let inst=instWrap.$_instance;
if (!inst){
return;
}
let state;
if (inst.state) state=inst.state();
let defaultValues=inst._getDefaultOptions();
let tag=switchLocale();
switchLocale('en');
let defaultDDKeys=inst._getDefaultOptions();
switchLocale(tag);
options=inst.option();
//if (inst.NAME=='dxDataGrid') console.log(2,options);
/**
* inst.option()返回的类型是object,不知为何修改placeholder后用inst.option(options)放回去不生效。
* 但是inst.option({'placeholder':'上海'});或inst.option('placeholder','上海');会刷新。
* 必须在修改前用JSON.parse(JSON.stringify(options))或options={...options}处理一下。
* 有一个警告:error:35 W0001 - dxPopup - 'closeOnOutsideClick' option is deprecated in 22.1. Use the 'hideOnOutsideClick' option instead.
*/
//options=JSON.parse(JSON.stringify(options));
options={...options};
inst.beginUpdate();
try{
proc(t,instWrap,options,defaultDDKeys,defaultValues);
inst.option(options);
/**设置预设参数可能丢掉了状态,恢复 */
if (state) inst.state(state);
}
finally{
inst.endUpdate();
}
}
}
DxDataGrid的翻译就这样写了,也能做到切换语言时不重加载页面而刷新:
function t_DxDataGrid(t,instWrap,options,defaultDDKeys,defaultValues){
//console.log('t_DxDataGrid',instWrap);
/**
* 不要企图通过instWrap.$.props去修改参数,因为DxDataGrid的某些参数有2种写法:
* (1)<DxDataGrid searchPanel="{visible:true,placeholder:'search...'}"><DxDataGrid>
* (2)<DxDataGrid><DxSearchPanel visible='true' placeholder='search...' /><DxDataGrid>
* 用instWrap.$.props时,第2种写法的参数是找不到的。
* 不能通过coumns属性更新column的caption,因为<DxDataGrid><DxColumn dataField='ID />这种写法,columns属性为空
* 必须通过method:columnOption来更新column的caption
*/
acroMLClassMethod.translateProps(t,options,
['hint','noDataText'],defaultDDKeys,defaultValues);
acroMLClassMethod.translateNodeProps(t,options,
['loadPanel'],
['text']
);
acroMLClassMethod.translateNodeProps(t,options,
['columnChooser'],
['emptyPanelText','title']
);
acroMLClassMethod.translateNodeProps(t,options,
['columnChooser','search','editorOptions'],
['placeholder']
);
acroMLClassMethod.translateNodeProps(t,options,
['columnFixing','texts'],
['fix','leftPosition','rightPosition','unfix']
);
acroMLClassMethod.translateNodeProps(t,options,
['editing','texts'],
['addRow','cancelAllChanges','cancelRowChanges','confirmDeleteMessage',
'confirmDeleteTitle','deleteRow','editRow','saveAllChanges',
'saveRowChanges','undeleteRow','validationCancelChanges'
]
);
acroMLClassMethod.translateNodeProps(t,options,
['export','texts'],
['exportAll','exportSelectedRows','exportTo']
);
acroMLClassMethod.translateNodeProps(t,options,
['filterPanel','texts'],
['clearFilter','createFilter','filterEnabledHint']
);
acroMLClassMethod.translateNodeProps(t,options,
['filterRow'],
['applyFilterText','betweenEndText','betweenStartText','resetOperationText',
'showAllText']
);
acroMLClassMethod.translateNodeProps(t,options,
['groupPanel'],
['emptyPanelText'],defaultDDKeys,defaultValues
);
acroMLClassMethod.translateNodeProps(t,options,
['grouping','texts'],
['groupByThisColumn','groupContinuedMessage','groupContinuesMessage',
'ungroup','ungroupAll']
);
acroMLClassMethod.translateNodeProps(t,options,
['headerFilter','texts'],
['cancel','emptyValue','ok']
);
acroMLClassMethod.translateNodeProps(t,options,
['pager'],
['inforText']
);
acroMLClassMethod.translateNodeProps(t,options,
['searchPanel'],
['placeholder'],defaultDDKeys,defaultValues
);
acroMLClassMethod.translateNodeProps(t,options,
['sorting'],
['ascendingText','clearText','descendingText']
);
acroMLClassMethod.translateNodeProps(t,options,
['summary','texts'],
['avg','avgOtherColumn','count','max','maxOtherColumn','min','minOtherColumn',
'sum','sumOtherColumn']
);
let columns=options['columns'];
if (columns){
for (let i=0;i<columns.length;i++){
let column=columns[i];
acroMLClassMethod.translateProp(t,column,'caption',column.dataField);
acroMLClassMethod.translateProps(t,column,['trueText','falseText']);
}
}
}
5、切换语言时如何不重加载页面而刷新
切换语言后,简单粗暴的方式是window.reload()重加载页面。但是全部状态都丢失。一般组件的内置文本,如devextreme-vue的DataGrid的noDataText,groupPanel的emptyPanelText,它是组件创建时按当前内置字典获取的,不会在字典重加载后自动刷新。但是本方案,因为切换语言时,会重翻译全部vue组件实例,所以可以刷新。
6、人为设定不翻译某些组件的机制
如果不翻译某个组件,可以设置translate属性为no,也可以实现一个beforeTranslate事件机制,如:
yjLocaleTranslator.on('beforeTranslate',function(instWrap,ops){
//console.log(instWrap,ops);
/**
* 不翻译某个组件实例的方法:
* (1)给组件设置一个attr:translate='no'
* (2)挂载beforeTranslate事件,阻止某些组件翻译。
*/
if (instWrap===self.$refs.dropdownbox) ops.isTranslate=false;
});
7、切换语言时DDKey从哪里找
切换语言前,vue组件的显示字串已经翻译成当前语言了,如:"OK"翻译成了简体“确认”,再切换语言到繁体时,哪里去找OK的词?很简单,翻译前把OK作为DDKey保存起来,直接保存在vue组件实例上。
translateProp(t,obj,propName,propDefaultDDKey,propDefaultValue){
if (!obj) return;
/**
* 没有属性值,没必要处理?
* 不行,element-plus的ElTableColumn是不给label就不显示。
* 但是devextreme-vue的DxColumn的caption可能没设置,但要按data-field来翻译显示。
* DxDataGrid的groupPanel的emptyPanelText如果没有设置,使用官方的字典翻译。
*/
//if (!obj[propName]) return;
if (!obj._acroml_DDKeys) obj._acroml_DDKeys={};
let DDKey=obj._acroml_DDKeys[propName];
if (!DDKey){
DDKey=obj[propName];
/**保存原始的DDKey */
if (!DDKey) DDKey=propDefaultDDKey;
else if (DDKey===propDefaultValue) DDKey=propDefaultDDKey;
}
/**如果DDKey是undefine就让obj[propsName]保持undefined(这样组件会使用其当前字典),如果赋值''就显示空白了 */
if (DDKey){
let newValue=t(DDKey);
if (newValue===DDKey){
if (propDefaultValue) newValue=propDefaultValue;
}
if (obj[propName]!=newValue) obj[propName]=newValue;
if (newValue!=DDKey) obj._acroml_DDKeys[propName]=DDKey;
}
else if (propDefaultValue && obj[propName]!=propDefaultValue) obj[propName]=propDefaultValue;
},
注:写文章时,涉及到vue组件版本:
- vue,3.5.12
- devextreme-vue,23.2.8
- v3-easyui,3.0.14
- ant-design-vue,4.2.3
- element-plus,2.8.1