jquery mobile 的国际化

随着移动技术的普及,基于 jQuery 并且针对移动平台的 JavaScript 框架 jQuery Mobile 应运而生。jQuery Mobile 不仅承袭了 jQuery 的诸多优点,更为移动平台定制了许多皮肤和开发部件,大大减轻了开发人员的工作量。随着 HTML5 技术的日渐完善,加上 JavaScript 技术本身具有的跨平台特性,jQuery Mobile 或者类似的框架必然拥有更加广泛的市场,这从 Adobe 放弃移动 Flash 和 Microsoft 边缘化 SilverLight 的决定中可见一斑。

虽然 jQuery Mobile 已经显得非常完善与强大,但很让人意外的是它竟然没有提供一套标准的方法来支持多语言的处理。多语言支持对于一个大型网站,尤其是企业级的面向全球的网站是必须具备的功能, 很多其他的框架比如 Dojo 就内置了对该功能的支持。有鉴于此,本文将讨论提供一种简单有效的方法来实现针对 jQuery Mobile 的多语言支持。该方法参考了传统意义上在 server 端实现的多语言支持方法和 Dojo 对多语言支持的方法,并且已在多个企业网站上部署实践。

本文假设读者对 jQuery 和 jQuery Mobile 已经有一定的认识和使用经验,对 HTML5 和 CSS3 也有相当的了解,因此对于上述技术的特定 API 不再赘述。如有疑问,请自行查阅相关文档或者与笔者联系。本文最后会提供本文所使用的程序源代码。

基本思路

简单来说,多语言支持就是为所有在网站上能找到的文字做一张映射表,根据用户的语言选择来显示同个关键字所对应的不同的值。多语言支持是在网站的开发初期就需要考虑的问题,因为该映射表将包含整个网站的文字内容,如果在开发过程中才决定创建和使用这样的映射表,就会耗费大量的时间去修改已经完成的网页,这还不包括重新测试的时间。

我们的基本思路可以概括为以下几个步骤:

  1. 创建映射表

    前文已经提到,映射表将包含所有该网站会用到的文字内容,在任何一个页面被创建之前,该映射表就应该已经存在,以便为页面提供需要的文字。由于该映射表需要在客户端被 jQuery 读取,我们建议使用 XML 格式或者 JSON 格式来创建该映射表。

  2. 为 jQuery Mobile 的 HTML 页面插入新的标签

    jQuery Mobile 本身不支持多语言,所以我们需要一个新的标签来提示该 HTML 的内容需要被映射表中的内容所替换,以及该内容所对应的关键字是什么。该标签可以是任意非 HTML 标准的标签值,比如 langID。

  3. 在 jQuery Mobile 页面的创建过程中插入映射表的文字内容

    jQuery Mobile 和 jQuery 不同,它的页面创建过程包含多个不同的阶段,比如 pagebeforecreate、pagecreate、pageinit 等。通常情况下我们会在 pageinit 中修改 HTML 的内容或者加入事件处理的方法。但是对于映射表内容的替换,必须在 pageinit 之前完成。因为在 pageinit 中 jQuery Mobile 已经完成创建各个部件,这个时候再去直接修改 HTML 内容会导致已经被创建的部件无法正常显示。

  4. 处理 jQuery Mobile 的动态内容

    很多 jQuery Mobile 的部件包含了特殊的文字,比如一个输入框的提示信息,往往不是在 HTML 中直接体现的,或者不能简单的加以替换,这个时候就需要使用 JavaScript 的程序来修改这样的内容。

具体步骤

接下来我们用 jQueryMobile 制作一个登陆界面,对前文提到的四个步骤进行具体解释。每个步骤还会包含示例的效果截屏图。

  1. 创建映射表

    前文提到映射表可以是 XML 格式或者 JSON 格式,以方便 jQuery 读取。在我们的例子中,我们将推荐使用 JSON 格式,这样不仅方便读取,而且方便在 JavaScript 程序中作为对象直接使用。

    在我们的例子中,我们将同时支持中文和英文语言。所以我们会创建两份映射表,分别对应中文和英文。事实上你也可以把所有语言放入一个映射表,但是出于管理的方便性和防止映射表过于庞大,我们推荐使用不同的映射表文件对应不同的语言。

    在登录界面,我们需要显示以下的中英文文字内容:

    这样的话我们的映射表将相应显示以下的内容:

    清单 1.中文映射表 text_cn.json
    {
    "Login" : {
    "Title" : "登录",
     "Description" : "请输入您的用户名和密码",
     "Lable_User_Name" : "用户名",
     "Label_Password" : "密码",
     "Label_Login_button" : "登录",
     "Tip_User_Name" : "手机号或者电子邮箱地址"
    }
    }
    清单 2.英文映射表 text_en.json
    {
    "Login" : {
    "Title" : "Login",
    "Description" : "Please provide your username and password",
     "Lable_User_Name" : "User Name",
     "Label_Password" : "Password",
     "Label_Login_button" : "Login",
     "Tip_User_Name" : "Mobile number or email address"
    }
    }

    注意两份映射表的 key 必须保持一致。

    • 登陆界面的标题,比如:登录 / Login

    • 登录界面的介绍,比如:请输入您的用户名和密码/ Please provide your username and password

    • 用户名标签,比如:用户名 / User Name

    • 密码标签,比如:密码 / Password

    • 登录按钮的标签,比如:登录 / Login

  2. 插入新的标签

    首先我们将使用 jQuery Mobile 的创建一个简单的登录界面,主要由 2 个输入框部件和 1 个按钮部件组成,其中还包括了页眉和介绍内容。HTML 的内容如下:

    页眉

    <div data-role="header">
    <h1>Title</h1>
    </div>

    介绍内容

    <div>
    <p>Description</p>
    </div>

    用户名和密码输入框

    <div>User Name Label</div>
    <div data-role="fieldcontain" class="ui-hide-label">
    <input type="text" name="name" id="txt_userID"
    data-clear-btn="true" value=""/>
    </div>
    <div>Password Label</div>
    <div data-role="fieldcontain" class="ui-hide-label">
    <input type="password" name="password" id="txt_password" 
    data-clear-btn="true" value=""/>
    </div>

    登录按钮

    <a href="#" id="btn_login" data-role="button" data-icon="arrow-r" 
    data-iconpos="right" data-inset="true" data-inline="true">
    Login Button
    </a>

    我们希望这个登录界面以弹出框的方式显示,所以在 JavaScript 中还要做以下工作:

    $(document).on("pageinit", "#pop_login", function(event) {
    $("#pop_login").dialog();
    });

    最后显示的界面如下

    图 1.登陆界面 1

    登陆界面 1

    现在我们需要给所有出现文字的地方插入一个标签,表示该 HTML 的内容需要被映射表中的真实内容所替换,我们将使用langID作为标签,你可以自行选择喜欢的标签名。

    比如替换标题为:

    <div>
    <p langID="Login.Description">Description</p>
    </div>

    我们所使用的标签内容为 Login.Title, 表示这个 HTML 的内容为映射表中 Login 这个对象下面的 Title 对象的内容。

    给其他的 HTML 插入对应的标签,这个步骤就算完成了。

  3. 插入映射表的文字内容

    前面我们已经创建了映射表,并且用标签的方式把映射表和 HTML 对应了起来。接下来这个步骤就要从映射表中读取内容并且替换对应 HTML 中的内容。前文中已经提到,替换的工作必须在 pageinit 之前完成,否则会导致 jQuery 的某些部件无法正常显示。有兴趣的读者可以自己尝试一下,如果在 pageinit 中替换,登录的按钮会无法正常显示。这里限于篇幅,我们不再进行尝试,而是直接在 pagebeforecreate 中进行替换。

    首先我们要读取映射表文件,假设我们要显示中文:

    $(document).on("pagebeforecreate", "#pop_login", function(event) {
    $.ajax({
    beforeSend	: function() { $.mobile.loading("show"); },
     type: "GET",
     url : "language/text_cn.json",
     dataType : "json",
     contentType : "application/json; charset=utf-8",
     async : false,
     cache : false,
     success : function(json) {
         $.mobile.loading("hide");
        language = json;
        replaceHTML();
    }
    });
    });

    这里要注意几个点:

    function replaceHTML(){
    $("*").each(function(index, domNode){
    var currLanguage = language;
     var langID = $(domNode).attr("langID");
     if(langID){
         var langIDSlipt = langID.split(".");
         for(var i=0; i<langIDSlipt.length; i++){
             currLanguage = currLanguage[langIDSlipt[i]];
         }
         $(domNode).html(currLanguage);
     }
    });
    }

    到目前为止,所有的静态 HTML 内容都已经被替换,我们的界面会变成这样:

    图 2.登陆界面 2

    登陆界面 2

    • async 必须设置成 false,因为我们希望在映射文件读取成功之后再进行下一步。如果 async 是 true,在映射文件读取完成之前 jQueryMobile 就开始创建各个部件,同样会导致无法正常显示。

    • 读取成功之后我们把映射表里面的内容赋值给一个全局变量:language,因为在 pageinit 的里面我们也许还要修改一些动态的内容,但是不希望再次去读取一遍映射表。

    • replaceHTM()这个函数会负责把 HTML 里面所有带有 langID 标签项的内容替换成映射表里的内容。具体做法如下:

  4. 处理动态内容

    经过前面 3 个步骤,看起来似乎我们的界面已经完成了,只要修改一下读取的映射表文件,我们就能正确显示所需要的语言。但是事实上前面 3 步只能显示静态内容,对于动态的内容仍然无法显示。

    接下来我们就要给这个界面加入动态内容。我们会给用户名这个输入框部件加入提示信息。

    <div data-role="fieldcontain" class="ui-hide-label">
    <input type="text" name="name" id="txt_userID" 
    placeholder="Placeholder" 
    data-clear-btn="true" value=""/>
    </div>

    由于提示信息是属于输入框部件的一个标签,而不是内容,我们无法通过前面所用的方法来替换这个提示信息。这就需要我们在 JavaScript 的程序中进行手动替换。

    $("#txt_userID").attr("placeholder", language.Login.Tip_User_Name);

    这样这个提示信息被替换成了正确的内容,我们的最终登录界面显示如下:

    图 3.登陆界面 3

    登陆界面 3

优缺点分析及提高

本方案的优点是:

  • 步骤简单明了,容易上手。

  • 扩展性好,方便加入新的支持语言。

  • 运行流畅,用户不会发觉画面替换过程。

  • 翻译人员几乎无须对程序本身有所了解,可直接翻译映射表文件。

本方案也有以下缺点:

  • 由于需要下载映射表文件,当映射表内容较多且网络状况不大好时用户也许需要等待较长时间才能看到画面。

  • 由于缓存的原因,修改映射表可能导致程序无法正常运行。比如 JavaScript 试图读取一个新的映射表关键字,而缓存的映射表不包含这个关键字,就会出现 JavaScript 错误。

  • 映射表里只能存放固定的文字内容。动态内容比如“用户 XXX 不存在”,xxx 为用户输入的动态用户名,需要做额外处理才能显示。日期格式等内容同样需要额外处理。

为了解决以上的几个缺陷,我们可以做如下的几点调整:

  • 第一次下载映射表之后就把映射表存入浏览器的本地缓存。以后每次都从缓存中读取,以节省读取的时间。

  • 为了对应映射表可能的修改,我们需要给缓存里的映射表一个版本号。当服务器修改了映射表,JavaScript 程序需要把新的版本号发送给客户端。一旦发现版本号不符合,则下载新的映射表。

  • 对于动态的文字或者日期,可以在读取映射表内容之后采用正则表达式进行进一步处理。

  • 目前显示的语言需要在服务器端设置。我们可以在客户端读取浏览器的语言设置,自动读取跟浏览器语言一致的映射表,让用户的使用更加方便。

注意:代码不包括 jQuery 和 jQuery Mobile 的 library,请在官网自行下载。




(二)


最近写了一个jQuery mobile国际化插件,用于解决其国际化的问题。

我们知道jQuery mobile采取的是DOM增强,想要达到国际化的效果,我们有如下两种思路:

  • 服务端国际化,类似于struts等国际化,页面完全由后端生成

  • 前端国际化,即前端检测浏览器语言然后自动下载国际化信息并执行

第一种思路在Web应用上使用较多,但是如果页面被打到PhoneGap等容器里面,就无效了。所以我们主要思考第二种方案。由于jQuery mobile是DOM增强,所以我们必须在其增强之前替换国际化信息,否则设置页面DOM一变化,我们就没有机会设置国际化信息了。

在jQuery mobile中,是以Page为单位来渲染的,所以我们可以在每一个Page渲染前进行国际化处理。Page渲染增强前会触发pagebeforecreate事件,我们只需要监听该事件,然后进行国际化即可。如下面的代码所示:

复制代码

(function( $, window, undefined ) {        
    var language =  window.navigator.language.replace("-","_"),

        defaultFolder = "i18n",

        mobile = $.mobile,

        version = mobile.version;
    
        
    mobile.i18n = {        
        /**
         * 获得国际化信息
         * @param {String} key 国际化信息字符串,如"user.name"
         * @param {Object} context 国际化信息上下文,默认为window.i18n
         * @method getText         */
        getText: function( key, context ){            if( !key || typeof( key ) !== "string" ) return;            var parts = key.split("."),
                obj = context || window.i18n || window;            if ( typeof( obj ) !== "object" ) return;            for( var i=0, p ; p = parts[i]; i++){
                obj =  ( p in obj ) ? obj[p] : undefined;                if ( obj === undefined ) return;
            }    
            return obj;
        },        
        
        /**
         * 处理某个DOM元素中的国际化标签
         * @param {DOM} 
         * @return {jQuery} 封装后的jQuery对象         */
        applyI18n: function( ele ){            var $eles = $( ele ),
                getText = this.getText;            if( $eles.length ===0 ) return;            var applyContext = function() {                var inputs = "input,textarea,select",
                    $this = $( this ),
                    key = $this.attr( "data-i18n" ),
                    value = getText( key ),
                    reg = new RegExp( "\\b" + this.nodeName.toLowerCase() + "\\b" );
                inputs.match( reg ) ? $this.val( value ) : $this.text( value );
            };            
            var apply2Ele = function( $ele ) {
                $ele.children().length === 0 ? 
                    $ele.each( applyContext ) : 
                    $ele.find( "[data-i18n]" ).each( applyContext );
            };            return $eles.each( function(){                var $ele = $(this),
                    isScriptEle = $ele[0].tagName.toLowerCase() === "script",
                    scriptType  = $ele[0].type,
                    $div = $("<div></div>");                if( scriptType === "" || scriptType === "text/javascript" ) return;
                apply2Ele( isScriptEle ? $div.html( $ele.html() ) : $ele );
                isScriptEle && $ele.html( $div.html() );
            });
        },        
        /**
         * 获得当前浏览器的语言
         * @return {String} 返回当前语言类型,如"zh-CN"、"en-US"等         */
        getLanguage: function() {            return language.replace("_","-");
        }
        
    };    
    
    var loadJSON = function( folder ) { 
        var url = folder + "/" + language + ".json?_=" + new Date().getTime();
        $.ajax({
            url: url,
            async: false,
            dataType: "json",
            success: function( msg ){
                window.i18n = msg;
            },
            error: function(){
                console.error("error: Could not find file " +  url )
            }
        });
        
    }, i18n = mobile.i18n;        
    
    // 检测是否开启国际化,用于可以在$(document).bind("mobileinit",function(){})中进行设置
    if( mobile.i18nEnabled ) {        
        var path = $("script").filter(function(){ return this.src.match(/jquery\.mobile/)})[0].src,
            reg = new RegExp("(.*?)\/\\w+\/jquery\.mobile-" + version +"(?:\.min)?\.js$");
        
        path = path.match(reg)[1];
        
        loadJSON( path + "/" + ( mobile.i18nFolder || defaultFolder ) ) ;        
        // 页面渲染前进行国际化信息替换
        $(document).bind("pagebeforecreate", function( evt ) { 
            var $page = $( evt.target );
            i18n.applyI18n( $().add( $page ).add( $page.find( "script" ) ) );
        });
   }        

})( jQuery, this );

复制代码

在代码的最后,我们可以看到监听了pagebeforecreate事件,当Page切换时我们会获得其DOM内容,然后查找是否存在"data-i18n"属性,有的话就会从国际化文件中获取内容。注意上面的代码会自动下载国际化文件,如"zh_CN.json"、"en_US.json"等,这些文件一般位于名为i18n的文件夹,并且该文件夹和jquery.mobile-X.X.X.js的父目录同级。使用样例如下所示:

 

复制代码

<!DOCTYPE html><html >
    <head >
        <meta name="viewport"  content="width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no" />
        <meta charset='utf-8' />
        <meta name="apple-mobile-web-app-capable" content="yes" />
        <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
        <link rel="stylesheet" href="libs/jquery.mobile-1.3.1.min.css" />
    </head>
    <body  style='visibility:hidden;'>
        <div data-role="page" id='main' data-theme="b">
            <div data-role="header" data-theme="b">
                <h1 data-i18n="main.header"></h1>
            </div>
            <div data-role="content">
                <p><a href="#next" data-transition="slide" data-i18n="main.next"></a></p>
                <input data-i18n="main.header"  />
                <textarea  data-i18n="main.header" ></textarea>
                <select data-i18n="main.value" data-enhance = "false">
                    <option value="1">One</option>
                    <option value="2">Two</option>
                </select>
                <a data-role="button" data-i18n="main.btn" id="btn"></a>
            </div>
            
            <div data-role='footer'>
                <p style='margin-left:10px' data-i18n="main.footer"></p>
            </div>
            
            <script type="text/tpl" id="tpl" >
                <p data-i18n="main.next"></p>
            </script>
        </div>
        
        <div data-role="page" id='next' style="height:100%"  >
            <div data-role="header" >
                <a href="#" data-role="button" data-back="true" data-rel="back" data-i18n="page2.next.back"></a>
                <h1 data-i18n="page2.next.header"></h1>
            </div>
            <div data-role="content">
                <p data-i18n="page2.next.content"></p>
                <p><a data-rel="back" href="#"  data-i18n="page2.next.prev"></a></p>
            </div>
            
        </div>
        <script src="libs/jquery-2.0.2.min.js"></script>
        <script>
            $(document).bind("mobileinit", function(){                // enable i18n                $.mobile.i18nEnabled = true;                //$.mobile.i18nFolder = "i18n"                $.mobile.ignoreContentEnabled = true;

                $(document).delegate("#btn", "vclick", function(){

                    alert( $.trim( $("#tpl").html() ) );
                });
            });

            $(function(){
                $(document.body).css( "visibility", "visible") ;
            });        
        </script>
        <script src="libs/jquery.mobile-1.3.1.min.js"></script>
        <script src="i18n.js"></script>
    </body></html>

复制代码

zh_CN.json等国际化文件内容如下所示:

复制代码

{     "title": "国际化测试",     "main": {        "header": "滑动效果展现",        "next": "下一页",        "footer": "我是Footer",        "value": "2",        "btn": "获得javascript模板内容"
     },     
     "page2": {     
         "next": {         
            "back": "返回",            "header": "内容展现",            "content": "我是内容",            "prev": "上一页"
         }
     }

}



标签:<a href="/?tag=jquery mobile">jquery mobile</a>