ExtJS基于Modern工具包的本地化(汉化)

新项目需要从服务器下载本地化资源,如果继续使用快速模板的本地化策略就很尴尬了,绝不可能等待本地化资源全部下载后再去加载项目,得另想办法。

在研究过了《Internationalization & Localization with Sencha Ext JS》一文后,终于有思路了。

文章的思路是通过重写Ext.Component来导入本地化数据,但前提还是要先加载好本地化资源,要解决这个问题不难,在本地化资源加载完成后,使用Ext.fireEvent出发一个本地化已准备好的事件就行了,而在Ext.Component的构造函数或初始化函数内,判断本地化是否已经准备好,如果准备好,直接执行本地化操作,如果还没准备好,就监听事件等待本地化资源加载。

思路有了就可以实现了,先完成本地化资源服务类。

新项目需要从服务器下载本地化资源,如果继续使用快速模板的本地化策略就很尴尬了,绝不可能等待本地化资源全部下载后再去加载项目,得另想办法。

在研究过了《Internationalization & Localization with Sencha Ext JS》一文后,终于有思路了。

文章的思路是通过重写Ext.Component来导入本地化数据,但前提还是要先加载好本地化资源,要解决这个问题不难,在本地化资源加载完成后,使用Ext.fireEvent出发一个本地化已准备好的事件就行了,而在Ext.Component的构造函数或初始化函数内,判断本地化是否已经准备好,如果准备好,直接执行本地化操作,如果还没准备好,就监听事件等待本地化资源加载。

思路有了就可以实现了,先完成本地化资源服务类,代码如下:

Ext.define('CommonShared.service.Localized', {
    alternateClassName: 'LocalizedService',
    singleton: true,

    config:{
        currentLanguage: null
    },

    requires:[
        'CommonShared.util.Url',
        'CommonShared.service.OAuth',
    ],
  
    isReady: false,

    constructor(config){
        const me = this;
        me.initConfig(config)
        me.initLanguages();
        me.loadResources();
        console.log(AppConfig.lang)
    },

    initLanguages(){
        const me = this;
        let current = StorageService.get('lang');
        if(current) return;
        current = AppConfig.lang === 'zh-CN' ? 'zh-Hans' 
            : AppConfig.lang === 'zh-TW' ? 'zh-Hant' : AppConfig.lang;
        me.setCurrentLanguage(current);
        StorageService.set('lang', current);
    },

    loadResources(){
        const me= this;
        me.isReady = false;
        Ext.Ajax.request({
            url: URI.get('Configuration', 'localization'),
            headers: AuthService.getAuthorizationHeader(),
            scope: me
        }).then(me.loadSuccess, me.loadFailure, null, me);
    },

    loadSuccess(response){
        const me = this,
            obj = Ext.decode(response.responseText,true);
        if(obj){
            Object.assign(me.remoteRawValue, obj);
            me.doOverride();
        }
        me.isReady = true;
        Ext.fireEvent('localizedready', me);
    },

    loadFailure(response){
        let obj  = Ext.decode(response.responseText, true),
            error = 'Unknown Error!';
        if(obj && obj.error) error = obj.error;
        Ext.Msg.alert('Error', error);
    },

    get(key, resourceName){
        const me = this,
            defaultResourceName = me.remoteRawValue.defaultResourceName,
            values = me.remoteRawValue.values;
        return resourceName && values[resourceName] && values[resourceName][key] 
            || ( values['ExtResource'] && values['ExtResource'][key] )
            || ( values[defaultResourceName] && values[defaultResourceName][key] )
            || key;
    },

    getLanguages(){
        return this.remoteRawValue.languages();
    },

    switchLanguages(value){
        const me = this;
        me.setCurrentLanguage(value);
        StorageService.set('lang', value);
        me.loadResources();
    },

    localized(cls, name,  resourceName){
        name = Ext.String.capitalize(name);
        const get = cls[`get${name}`],
            set = cls[`set${name}`];
        if(!get || !set) return;
        const value = get.apply(cls);
        if(!value) return;
        set.apply(cls, [LocalizedService.get(value,resourceName)]);
    },


    privates:{
        remoteRawValue: {},

        doOverride(){
            const me = this,
                values = me.remoteRawValue.values.ExtResource,
                newMonthNames = [],
                newDayNames = [],
                am = values['AM'] || 'AM',
                pm = values['PM'] || 'PM';
            Ext.Date.monthNames.forEach(month=>{
                newMonthNames.push(values[month] || month);

            });
            Ext.Date.monthNames = newMonthNames;
            Ext.Date.dayNames.forEach(day=>{
                newDayNames.push(values[day] || day);

            });
            Ext.Date.dayNames = newDayNames;

            //console.log(Ext.Date)
            Ext.Date.formatCodes.a = `(this.getHours() < 12 ? '${am}' : '${pm}')`;
            Ext.Date.formatCodes.A = `(this.getHours() < 12 ? '${am}' : '${pm}')`;
    
            const parseCodes = {
                g: 1,
                c: "if (/(" + am + ")/i.test(results[{0}])) {\n" +
                    "if (!h || h == 12) { h = 0; }\n" +
                    "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
                s: `(${am}|${pm})`,
                calcAtEnd: true
            };
    
            Ext.Date.parseCodes.a = Ext.Date.parseCodes.A = parseCodes;
    
    
        },
    }


})


本地化服务类是一个单类模式的类,也就是可以直接调用。在类里主要实现了在构造函数了先判断当前需要加载什么语言的资源,然后调用loadResources方法从远程加载本地化资源。加载完成后会触发·localizedready事件。在调用switchLanguages方法切换语言后,可调用loadResources方法获取新的语言资源,并再加载完成后再次触发·localizedready事件更改语言,也就是说,可以不刷新页面实现语言更新,但很遗憾,那些没有set和get方法的配置项是不能更新的,因为只要调用set方法才会刷新页面显示,如按钮调用setText方法就可更新按钮的显示文本。因而,这方法会有点小瑕疵。

经过对ExtJS包里的本地化文件的分析,已经可以实现绝大部分类的本地化了,但还是有小部分只能通过老的重写办法来更新,如Ext.Date类,这个只能在本地化类中通过doOverride方法来对它进行本地化了。

下面就是重点中的重点了,重写Ext.Component类了,代码如下:

Ext.define('CommonOverrides.shared.Component',{
    override: 'Ext.Component',

    config:{
        resourceName: null,
        localized: []
    },

    initialize(){
        const me = this;
        me.callParent(arguments);
        if(LocalizedService && LocalizedService.isReady){
            me.onLocalized();
        }else{
            Ext.on('localizedready', me.onLocalized, me);
        }
    },

    onLocalized(){
        const me = this,
            xtype = me.xtype,
            resourceName = me.getResourceName(),
            service = LocalizedService,
            localized = me.getLocalized();
        if(me.isButton || me.isMenuItem) {
            service.localized(me, 'text');
            return;
        }
        if(xtype === "loadmask"){
            service.localized(me, 'message');
            return;
        };
        if(me.isField){
            service.localized(me,'requiredMessage');
            service.localized(me,'validationMessage');
            service.localized(me,'label', resourceName);
            if(me.getPlaceholder) service.localized(me, 'placeholder', resourceName);
            if(me.getBoxLabel) service.localized(me ,'boxLabel', resourceName);
            if(xtype === 'datefield' || xtype === 'DatePicker'){
                service.localized(me,'minDateMessage');
                service.localized(me,'maxDateMessage');
            }
            if(xtype === 'numberfield'){
                me.decimalsText = LocalizedService.get(me.decimalsText);
                me.minValueText = LocalizedService.get(me.minValueText);
                me.maxValueText = LocalizedService.get(me.maxValueText);
                me.badFormatMessage = LocalizedService.get(me.badFormatMessage);
            }
            if(xtype === 'textfield'){
                me.badFormatMessage = LocalizedService.get(me.badFormatMessage);
            }
            me.setError(null);
            return;
        }
        if(me.isContainer){
            if(me.isPanelTitle || me.isPanel) {
                service.localized(me , 'title', resourceName);
                if(xtype === 'tooltip'){
                    service.localized(me , 'html', resourceName);
                }
                const collapsible = me.getCollapsible && me.getCollapsible();
                if(collapsible){
                    service.localized(collapsible, 'collapseToolText');
                    service.localized(collapsible, 'expandToolText');
                }
                me.doCustomLocalized(me, localized, resourceName);
                return;
            };
            if(me.isGridColumn){                
                service.localized(me ,'text', resourceName);
                return;
            }
            if(xtype === 'datepanel'){
                service.localized(me, 'nextText');
                service.localized(me, 'prevText');
                return;
            }
            if(me.isDataView){
                service.localized(me, 'loadingText');
                service.localized(me, 'emptyText');
                return;
            }
        }
        me.doCustomLocalized(me, localized, resourceName);
    },

    doCustomLocalized(me, localized, resourceName){
        localized.forEach(key=>{
            LocalizedService.localized(me, key, resourceName)
        });
    }


})

代码中,定义了resourceName和localized两个属性。属性resourceName的作用是指定从哪个资源中获取本地化数据,譬如有用户和产品两个资源,在对产品的字段进行本地化时,就需要从产品资源中获取本地化信息,而对应的用户的字段就从用户资源中获取,从而减少冲突。属性localized的主要作用就是为一些自定义信息进行本地化,譬如使用一个组件的html属性来显示一些需要本地化的信息,在默认的情况下是不对这些信息进行本地化的,这时候就要通过localized属性来指定了。

在onLocalized方法内,会看到一堆的判断语句,它的主要目的是对Ext JS自身组件进行全方位无遗漏的本地化。为什么要这样做呢?采用重写方式不是更好么?这里的重点还是不知道本地化资源什么时候加载完,在加载完成之前,可能有些组件已经实例化并显示了,这时候再去重写是不会改变已实例化的组件的显示的,而只有通过set方法的调用才会去刷新显示,因而只有采用这种折中的方式。

细心的读者可能会发现为什么有些需要在Ext JS包中本地化的类不在onLocalized内,这个很好理解,因为有些内部组件使用的是基础组件,如网格中的列标题的菜单,使用的就是菜单项,最终会同通过菜单项来text的属性来显示结果,只要在菜单项中对text属性进行本地化就行了,不需要单独再为这个类进行本地化。

组件的本地化工作已经完成,但这还不是全部,还要非组件类也需要本地化。

ExtJS中还有验证类和插件类这些类需要实现本地化。通过研究发现这些类都有一个父类,这就好办了,和组件类一样,在父类中添加监听就可以实现全部子类的本地化了。

下面是插件类的重写代码:

Ext.define('CommonOverrides.shared.plugin.Abstract',{
    override: 'Ext.plugin.Abstract',

    constructor: function(config) {
        const me = this;
        if (config) {
            me.cmp = config.cmp;
            me.pluginConfig = config;
            me.initConfig(config);
        }
        if(LocalizedService && LocalizedService.isReady){
            me.onLocalized();
        }else{
            Ext.on('localizedready', me.onLocalized, me);
        }
    },

    onLocalized(){
        const me = this,
            type = me.type;
        if(type === 'gridrowdragdrop'){
            me.dragText =  LocalizedService.get(me.dragText);
            return
        };
        if(type === 'listpaging'){
            LocalizedService.localized(me , 'loadMoreText');
            LocalizedService.localized(me , 'noMoreRecordsText');
        }
    },

});

插件类只有两个类需要实现本地化,但怎么找这两个类费了一点功夫,最好找到了type这个属性可以标识这个类,这就解决问题了。在gridrowdragdrop这个类中还有点小麻烦,属性dragText 性居然没有set`方法,因而这里会有个小bug。

下面是验证类的重写代码:

Ext.define('CommonOverrides.shared.data.validator.Validator',{
    override: 'Ext.data.validator.Validator',

  
    constructor: function(config) {
        const me = this;
        if (typeof config === 'function') {
            me.fnOnly = true;
            me.validate = config;
        }
        else {
            me.initConfig(config);
        }
        if(LocalizedService && LocalizedService.isReady){
            me.onLocalized();
        }else{
            Ext.on('localizedready', me.onLocalized, me);
        }
    },

    onLocalized(){
        const me = this,
            type = me.type,
             resourceName = 'ExtResource';
        if(type === 'bound' || type === 'length' || type === 'range'){
            if(type === 'bound') LocalizedService.localized(me ,'emptyMessage',resourceName);
            if(type === 'range') LocalizedService.localized(me ,'nanMessage',resourceName);
            LocalizedService.localized(me ,'minOnlyMessage', resourceName);
            LocalizedService.localized(me ,'maxOnlyMessage', resourceName);
            LocalizedService.localized(me ,'bothMessage', resourceName);
            return;
        }
        LocalizedService.localized(me ,'message', resourceName);
    },


})

验证类就比较简单了,大部分类都是通过message属性来返回错误信息的,只有三个类比较特殊。

至此,新的本地化方式就已经完成了。下面两个json文件是目前Ext JS包中全部的本地化信息:
en.json:

{
  "culture": "en",
  "texts": {
    "Welcome to login": "Welcome to login.",
    "Username/Email/Phone": "Username/Email/Phone",
    "Password": "Password",
    "Remember Me": "Remember Me",
    "Forgot password": "<a href='#ForgotPassword' class='link-forgot-password'>Forgot password</a>",
    "Login": "Login",
    "DefaultMessageTitle": "Information",
    "Sunday": "Sunday",
    "Monday": "Monday",
    "Tuesday": "Tuesday",
    "Wednesday": "Wednesday",
    "Thursday": "Thursday",
    "Friday": "Friday",
    "Saturday": "Saturday",
    "January": "January",
    "February": "February",
    "March": "March",
    "April": "April",
    "May": "May",
    "June": "June",
    "July": "July",
    "August": "August",
    "September": "September",
    "October": "October",
    "November": "November",
    "December": "December",
    "AM": "AM",
    "PM": "PM",
    "Next Month (Control+Right)": "Next Month (Control+Right)",
    "Previous Month (Control+Left)": "Previous Month (Control+Left)",
    "Sort Ascending": "Sort Ascending",
    "Sort Descending": "Sort Descending",
    "Filter": "Filter",
    "Is not a valid email address": "Is not a valid email address",
    "Must be present": "Must be present,",
    "Value must be greater than {0}": "Value must be greater than {0}",
    "Value must be less than {0}": "Value must be less than {0}",
    "Value must be between {0} and {1}": "Value must be between {0} and {1}",
    "Is not a valid CIDR block": "Is not a valid CIDR block",
    "Is not a valid currency amount": "Is not a valid currency amount",
    "Is not a valid date": "Is not a valid date",
    "Is not a valid date and time": "Is not a valid date and time",
    "Is a value that has been excluded": "Is a value that has been excluded",
    "Is in the wrong format": "Is in the wrong format",
    "Is not in the list of acceptable values": "Is not in the list of acceptable values",
    "Is not a valid IP address": "Is not a valid IP address",
    "Length must be at least {0}": "Length must be at least {0}",
    "Length must be no more than {0}": "Length must be no more than {0}",
    "Length must be between {0} and {1}": "Length must be between {0} and {1}",
    "Is not a valid number": "Is not a valid number",
    "Is not a valid phone number": "Is not a valid phone number",
    "Must be numeric": "Must be numeric",
    "Must be at least {0}": "Must be at least {0}",
    "Must be no more than than {0}": "Must be no more than than {0}",
    "Must be between {0} and {1}": "Must be between {0} and {1}",
    "Is not a valid time": "Is not a valid time",
    "Is not a valid URL": "Is not a valid URL",
    "{0} selected row{1}": "{0} selected row{1}",
    "Load More...": "Load More...",
    "No More Records": "No More Records",
    "Loading...": "Loading...",
    "No data to display": "No data to display",
    "The date in this field must be equal to or after {0}": "The date in this field must be equal to or after {0}",
    "The date in this field must be equal to or before {0}": "The date in this field must be equal to or before {0}",
    "This field is required": "This field is required",
    "Browse...": "Browse...",
    "The minimum value for this field is {0}": "The minimum value for this field is {0}",
    "The maximum value for this field is {0}": "The maximum value for this field is {0}",
    "The maximum decimal places is {0}": "The maximum decimal places is {0}",
    "Value is not a valid number": "Value is not a valid number",
    "Locked (Left)": "Locked (Left)",
    "Unlocked": "Unlocked",
    "Locked (Right)": "Locked (Right)",
    "Locked": "Locked",
    "Columns": "Columns",
    "Group by this field": "Group by this field",
    "Show in groups": "Show in groups",
    "Collapse panel": "Collapse panel",
    "Expand panel": "'Expand panel",
    "OK": "OK",
    "Abort": "Abort",
    "Retry": "Retry",
    "Ignore": "Ignore",
    "Yes": "Yes",
    "No": "No",
    "Cancel": "Cancel",
    "Apply": "Apply",
    "Save": "Save",
    "Submit": "Submit",
    "Help": "Help",
    "Close": "Close",
    "Maximize to fullscreen": "Maximize to fullscreen",
    "Restore to original size": "Restore to original size",
    "Today": "Today"
  }
}
  

zh-Hans.json:

{
  "culture": "zh-Hans",
  "texts": {
    "Welcome to login": "欢迎登录",
    "Username/Email/Phone": "用户名/邮箱/手机",
    "Password": "密码",
    "Remember Me": "记住我",
    "Forgot password": "<a href='#ForgotPassword' class='link-forgot-password'>忘记密码</a>",
    "Login": "登录",
    "DefaultMessageTitle": "信息",
    "Sunday": "日",
    "Monday": "一",
    "Tuesday": "二",
    "Wednesday": "三",
    "Thursday": "四",
    "Friday": "五",
    "Saturday": "六",
    "January": "一月",
    "February": "二月",
    "March": "三月",
    "April": "四月",
    "May": "五月",
    "June": "六月",
    "July": "七月",
    "August": "八月",
    "September": "九月",
    "October": "十月",
    "November": "十一月",
    "December": "十二月",
    "AM": "上午",
    "PM": "下午",
    "Next Month (Control+Right)": "下月 (Ctrl+→)",
    "Previous Month (Control+Left)": "上月 (Ctrl+←)",
    "Sort Ascending": "正序",
    "Sort Descending": "倒序",
    "Filter": "过滤器",
    "Is not a valid email address": "不是有效的电子邮件地址",
    "Must be present": "值必须存在",
    "Value must be greater than {0}": "值必须至少为{0}",
    "Value must be less than {0}": "值必须不超过{0}",
    "Value must be between {0} and {1}": "值必须在 {0} 和 {1} 之间",
    "Is not a valid CIDR block": "不是有效的CIDR块",
    "Is not a valid currency amount": "不是有效的货币金额",
    "Is not a valid date": "不是有效日期",
    "Is not a valid date and time": "不是有效的日期和时间",
    "Is a value that has been excluded": "是已排除的值",
    "Is in the wrong format": "格式错误",
    "Is not in the list of acceptable values": "值不在接受列表中",
    "Is not a valid IP address": "不是有效的IP地址",
    "Length must be at least {0}": "长度必须至少为{0}",
    "Length must be no more than {0}": "长度不得超过{0}",
    "Length must be between {0} and {1}": "长度必须介于{0}和{1}之间",
    "Must be numeric": "必须是数字",
    "Is not a valid number": "不是有效的数字",
    "Is not a valid phone number": "不是有效的电话号码",
    "Must be at least {0}": "必须至少为{0}",
    "Must be no more than than {0}": "必须不超过{0}",
    "Must be between {0} and {1}": "必须在 {0} 和 {1} 之间",
    "Is not a valid time": "不是有效时间",
    "Is not a valid URL": "不是有效的URL",
    "{0} selected row{1}": "选择了 {0} 行",
    "Load More...": "加载更多...",
    "No More Records": "没有更多记录",
    "Loading...": "加载中...",
    "No data to display": "没有要显示的数据",
    "The date in this field must be equal to or after {0}": "此字段中的日期必须在 {0} 之后",
    "The date in this field must be equal to or before {0}": "此字段中的日期必须为 {0}",
    "This field is required": "此字段是必填字段",
    "Browse...": "浏览中...",
    "The minimum value for this field is {0}": "该输入项的最小值是 {0}",
    "The maximum value for this field is {0}": "该输入项的最大值是 {0}",
    "The maximum decimal places is {0}": "最大十进制数 (0)",
    "Value is not a valid number": "{0} 不是有效数值",
    "Locked (Left)": "锁定(左)",
    "Unlocked": "解锁",
    "Locked (Right)": "锁定(右)",
    "Locked": "区域",
    "Columns": "列",
    "Group by this field": "以此分组",
    "Show in groups": "分组显示",
    "Collapse panel": "折叠面板",
    "Expand panel": "'展开面板",
    "OK": "确定",
    "Abort": "退出",
    "Retry": "重试",
    "Ignore": "忽略",
    "Yes": "是",
    "No": "否",
    "Cancel": "取消",
    "Apply": "应用",
    "Save": "保存",
    "Submit": "提交",
    "Help": "帮助",
    "Close": "关闭",
    "Maximize to fullscreen": "最大化到全屏",
    "Restore to original size": "恢复到原始大小",
    "Today": "今天" 
  }
}

以上两个文件可以通过本地加载的方式加载,也可以通过服务器加载,这个看项目需要。

作者:黄灯桥
原文链接:https://blog.csdn.net/tianxiaode/article/details/107736284