当前位置: 首页 > article >正文

按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等管理系统,不太适合:

  1. 不精准。不该翻译的地方翻译了,比如用户输入的内容:产品编码、产品型号,Grid的cell内容...除非能用“translate='no'”把不翻译的dom标注得很完整。
  2. 不专业。翻译的字典是通用的,可能因为行业不同,牛头不对马嘴。插件能支持用我自己的字典?  因此,ERP这种管理系统,一般要有自己的多语言方案。

要做一个翻译方案,要考虑以下问题:

  1. 字典。字典格式(json,csv,xml)、字典的读取、解析、编辑。
  2. 国家语言标识。语言代码( Language Code)、国家代码( Country Code)、Windows Language Code Identifier (LCID),一般不再使用旧的Code Page(简体中文936,繁体中文950)。
  3. 语言切换。LCID存哪里。是否重加载页面。
  4. 翻译函数。高效的把一个文本字串,查找字典,翻译成对照文本。
  5. 翻译的时机。在何时调用翻译函数。

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']);
    }
  }
}

这个方案的几个特点:

  1. 外挂式。写页面的程序员一般情况下感觉它不存在。特殊的地方还是用显式的使用t函数,或标记translate='no'。
  2. 相对准确。相比浏览器的自动翻译,不会“误伤”。如DataGrid,会翻译column,footer的title,不会翻译到cell。相对于传统i18n机制显式的使用t函数,没那么精准,如:SelectBox下拉框,当选择的文本用于代码比较时是不能翻译的,还是需要显式的标记translate='no'或挂载beforeTranslate事件告知不翻译。
  3. 切换语言时能做到不重加载页面就刷新。传统i18n机制可能也能做到,只要t函数是响应式的,能响应locale的变化。但是,某些第3方组件包,其内置的一些文本字串,切换语言后,即使重加载了其对应的内置字典,也不会自动刷新。如devextreme-vue的DataGrid的noDataText。  

文无第一,武无第二,各种方案没有绝对的好坏,就像react与vue,暂时无法谁碾压谁,适合不同的场景和不同的团队。

这个方案的几个注意点:

  1. 如何从vue组件实例找到其类别。
  2. 注册类别还是注册类别名称。
  3. 何时调用翻译函数。
  4. 不同厂家的vue组件属性修改机制的差别。
  5. 切换语言时如何不重加载页面而刷新。
  6. 人为设定不翻译某些组件的机制。
  7. 切换语言时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);

但是,直接用类别,需要把组件加载到内存中,有几个坏处:

  1. 组件包的组件全部加载了。实际项目可能没有用到那么多组件,比如devextreme中的甘特图、枢纽分析Grid组件;
  2. 组件加载机制可能与外部不同。你可能是用一个包加载,外面可能是用分文件加载,这会导致相同组件在内存中有两份,可能导致组件内部逻辑错误。  
//加载方式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

http://www.kler.cn/a/419495.html

相关文章:

  • 手撸了一个文件传输工具
  • 数据结构 (16)特殊矩阵的压缩存储
  • [Linux] 进程间通信——匿名管道命名管道
  • 算法刷题Day5: BM52 数组中只出现一次的两个数字
  • C++STL之vector(超详细)
  • 【IMF靶场渗透】
  • 美畅物联丨如何通过 FFmpeg 解码视频
  • STM32 HAL库开发学习5. 系统滴答定时器
  • Linux HTTP代理Squid 基本变更配置及目标白名单方式限制转发
  • Flutter 之 InheritedWidget
  • 【大模型】ChatGPT 提示词优化进阶操作实战详解
  • 新能源,半导体,医药生物等高科技企业如何高效、安全、智能管理文件数据
  • 汽车IVI中控开发入门及进阶(三十五):架构QML App Architecture Best Practices
  • Docker Buildx 与 CNB 多平台构建实践
  • 无人机地面遥控遥测技术与算法概述!
  • 文库 | 从嬴图的技术文档聊起
  • 网络性能四大关键指标:带宽、时延、抖动和丢包率
  • vue项目中单独文件的js不存在this.$store?.state怎么办
  • 在RockyLinux9.4上安装Microk8s
  • Neo4j启动时指定JDK版本
  • Linux-异步IO和存储映射IO
  • C++《set与map》
  • websocket企业开发中常见问题及解决方案
  • 汽车软件单元测试的重要性
  • 基于Java Springboot房屋租赁App且微信小程序
  • 【LLM】Langchain+RAG大模型学习笔记