3.2 JSF的国际化支持
程序国际化是商业系统的一个基本要求,因为今天的软件系统不再是简单的单机程序,往往都是一个开放系统,需要面对来自全世界各个地方的浏览者。因此,国际化是商业系统中不可或缺的一部分。
JSF提供了非常优秀的国际化支持,它提供了3种方式的国际化支持:
静态内容国际化:包括页面中提示消息、文字和按钮文本等。
错误消息国际化:包括类型转换失败、输入校验失败等错误消息的国际化。
动态数据国际化:主要是对服务器端对象所加载数据的国际化。
与其他MVC框架完全相同的是,JSF程序国际化同样是建立在Java国际化的基础之上,一样也是通过提供不同国家/语言环境的消息资源,然后通过ResourceBundle加载指定Locale对应的资源文件,再取得该资源文件中指定key对应的消息——整个过程与Java程序国际化完全相同,只是JSF框架对Java程序国际化进行了进一步封装,从而简化了应用程序的国际化。
提示
关于Java程序国际化的相关知识,请读者自行参考笔者所著的《疯狂Java讲义》一书,在该书的9.6节有关于Java程序国际化的详细介绍。
3.2.1 加载国际化资源文件
与Java程序国际化完全相似的是,JSF同样允许使用两种国际化资源文件:
使用*.properties国际化资源文件。
使用*.class类文件作为国际化资源文件。
因为使用*.properties文件作为国际化资源文件更加简单、方便,因此本节示例应用都会选择使用*.properties属性文件作为国际化资源文件。至于使用*.class类文件作为国际化资源文件的示例,则可以参考《疯狂Java讲义》中关于国际化的介绍。
程序国际化所需资源文件的内容是多个key-value对,不同国家、语言环境对应的资源文件中的key是不变的,但value则随不同国家、语言而改变。
当为程序编写好国际化资源文件之后,接下来需要在JSF应用中加载国际化资源文件。JSF提供了两种加载国际化资源文件的方式:
在faces-config.xml文件的<application…/>元素中用<resource-bundle…/>子元素注册。
在JSF页面中使用<f:loadBundle…/>标签来加载国际化资源文件。
对于上面两种方式而言,可以认为第一种方式加载的是全局国际化资源文件,所有JSF页面都可使用它;第二种方式只对当前页面有效,因此可认为是加载局部国际化资源文件。
JSF配置文件中<application…/>元素的功能比较多,它不仅可用于加载国际化消息资源,还有其他一些关于应用配置的信息,都在该元素内进行管理。<application…/>元素的内部结构如图3.9所示。
图3.9 <application…/>元素的内部结构
不管使用哪种方式来加载国际化资源文件,总需要指定两个内容:
资源文件的base name:JSF应用将根据该base name来加载国际化资源文件。
为资源文件定义一个“别名”:应用将会使用该“别名”来访问资源文件内的国际化消息。首先为应用提供一个全局国际化资源文件,该文件内容如下:
程序清单:codes\03\3.2\i18n\WEB-INF\src\global.properties
title=猜图书 greeting=恭喜!猜对了!
提供上面全局国际化资源文件之后,通过faces-config.xml文件来加载该文件。加载国际化资源文件的代码片段如下:
程序清单:codes\03\3.2\i18n\WEB-INF\faces-config.xml
<application> <resource-bundle> <!-- 指定国际化资源文件的base name是global --> <base-name>global</base-name> <!-- 程序可通过global变量来访问国际化资源文件 --> <var>global</var> </resource-bundle> </application>
接下来再提供一个局部国际化资源文件,该文件内容如下:
程序清单:codes\03\3.2\i18n\WEB-INF\src\local.properties
name=书名: price=价格: process=处理
局部国际化资源文件通过<f:loadResource…/>标签来加载,任何JSF页面使用该标签之后即可访问该资源文件中的国际化消息。例如,如下代码即可加载国际化资源。
<!-- 加载国际化资源文件,加载后可通过local变量访问它 -->
<f:loadBundle basename="local" var="local"/>
注意
上面两份国际化资源文件中都包含了非西欧字符,因此不要忘记了使用native2ascii命令来处理这份国际化资源文件。上面两份国际化资源文件经过该命令处理后文件名分别为global_zh_CN.properties、local_zh_CN.properties,其中_zh_CN后缀表明它们是简体中文环境下的消息资源文件。
3.2.2 使用国际化消息
JSF使用国际化消息非常简单,通过值表达式即可访问资源文件中的key——当我们加载国际化资源文件时总会通过var子元素或属性为资源文件指定“别名”,以后要访问国际化资源文件时通过如下格式即可:
#{varName.key}
下面页面中即可通过该语法来使用国际化消息,页面代码如下:
程序清单:codes\03\3.2\i18n\welcome.jsp
<!-- 加载国际化资源文件,加载后可通过local变量访问它 --> <f:loadBundle basename="local" var="local"/> <!-- 使用全局资源文件中的消息 --> <h1><h:outputText value="#{global.title}"/></h1> <b>${sessionScope.tip}</b> <h:form> <!-- 使用局部资源文件中的消息 --> <h:outputText value="#{local.name}"/> <h:inputText value="#{bookBean.name}"/><br/> <!-- 使用局部资源文件中的消息 --> <h:outputText value="#{local.price}"/> <h:inputText value="#{bookBean.price}"/><br/> <h:commandButton value="#{local.process}" action="#{bookBean.process}"/><br/> </h:form>
如果在简体中文环境下访问该页面,将看到如图3.10所示的页面。
图3.10 简体中文环境下看到的页面
对于上面应用,如果要让它支持美式英语环境,则还需要为应用增加如下两份国际化资源:
程序清单:codes\03\3.2\i18n\WEB-INF\src\global_en_US.properties
title=Guess Book greeting=Congratulation! You are Right!
程序清单:codes\03\3.2\i18n\WEB-INF\src\local_en_US.properties
name=book name: price=price: process=processing
接下来我们通过Windows控制面板中的“区域和语言”来设置本机环境为“英语(美国)”,如图3.11所示。
图3.11 设置操作系统的“区域和语言”
完成如图3.11所示的设置之后,接下来使用浏览器来访问该应用将看到,如图3.12所示的页面。
图3.12 美式英语环境下看到的页面
需要指出的是,即使我们使用另一台机器(简体中文环境的设置)来访问该页面,依然可以看到如图3.12所示的界面。这表明在默认情况下,JSF将根据服务器运行环境来确定呈现Locale,这显然不是我们希望看到的结果,我们希望JSF应用会根据客户端浏览器来确定Locale——如果客户端浏览器采用简体中文环境,则呈现如图3.10所示的页面;如果客户端浏览器采用美式英语环境,则呈现如图3.12所示的页面。
为此我们还需要对JSF应用增加配置,JSF与其他MVC框架有所不同的地方是:JSF除了需要为国际化支持提供资源文件之外,还需要通过faces-config.xml文件来配置该应用支持哪些国家、语言Locale。
在faces-config.xml文件中配置Locale通过<locale-config…/>元素完成,该元素可以接受两个子元素:
<default-locale…/>:该元素指定该应用的默认Locale。
<supported-locale…/>:该元素可以出现多次,用于列出该JSF应用所有支持的Locale。
如果开发JSF应用时没有在faces-config.xml文件中配置<locale-config…/>元素,JSF将会根据服务器所在的语言、国家环境来确定Locale。为了指定该JSF应用支持简体中文、美式英语两种运行环境,则应该在faces-config.xml文件的<application…/>元素中增加如下配置片段:
程序清单:codes\03\3.2\i18n\WEB-INF\faces-config.xml
<locale-config> <!-- 指定该应用默认使用的Locale --> <default-locale>zh_CN</default-locale> <!-- 下面列出该应用所支持的全部Locale --> <supported-locale>en_US</supported-locale> </locale-config>
上面<default-locale…/>、<supported-locale…/>元素的值就是我们提供的国际化资源文件。上面配置表明该JSF应用默认的Locale是简体中文环境,除此之外,该应用还支持美式英语Locale。
增加上面配置之后,接下来我们无须通过控制面板将Windows设为美式英语环境,只要将Firefox浏览器设为美式英语环境,即可看到如图3.12所示的页面。
为Firefox设置美式英语环境需要单击Firefox主菜单中的“工具 -> 选项”,然后在其“内容”Tab页中单击如图3.13所示的“选择”按钮,Firefox弹出如图3.14所示的设置语言、国家环境对话框。
图3.13 设置语言、国家Locale
除此之外,JSF还允许通过<f:view…/>指定使用何种Locale来呈现该页面,<f:view…/>标签(其他JSF标签都位于该标签之内)可通过locale属性来指定呈现该页面所使用的Locale。如果我们为一个页面中<f:view…/>元素指定locale属性,那么不管客户端浏览器所在国家、语言环境如何,该页面总会以<f:view…/>元素中指定的Locale来呈现该页面。
图3.14 为Firefox设置美式英语环境
注意
在默认情况下,JSF应用将会根据客户端浏览器来确定呈现某页面的Locale;但如果某个页面中<f:view…/>标签指定了locale属性,则该应用将会根据该locale属性指定的Locale来呈现该页面。
3.2.3 动态数据国际化
动态数据国际化主要是指对服务器端对象所加载数据的国际化,最常见的情形就是让托管Bean加载国际化资源文件。如果需要实现动态数据国际化,此时可以借助于Java提供的ResourceBundle类,可通过它来加载国际化资源文件,然后再通过key来访问国际化消息。
需要指出的是,ResourceBundle加载国际化资源文件时需要指定一个Locale参数,通常我们通过FacesContext来获取JSF应用的当前Locale作为参数传入即可。
例如,下面托管Bean需要设置一个错误提示信息,这个错误提示信息也必须支持国际化,因此托管Bean通过ResourceBundle来实现错误提示消息的国际化。程序代码如下:
程序清单:codes\03\3.2\beani18n\WEB-INF\src\org\crazyit\jsf\BookBean.java
public class BookBean { private String name; private Double price; //无参数的构造器 public BookBean() { } //初始化全部属性的构造器 public BookBean(String name , Double price) { this.name = name; this.price = price; } //省略name属性的setter和getter方法 … //省略price属性的setter和getter方法 … //编写处理导航的方法 public String process() { FacesContext fc = FacesContext.getCurrentInstance(); //使用ResourceBundle来加载国际化消息资源 ResourceBundle rb = ResourceBundle.getBundle( "global" , fc.getViewRoot().getLocale()); //下面几行代码用于测试ExternalContext的方法 ExternalContext ec = fc.getExternalContext(); if (name.equals("疯狂Java讲义") && price == 99) { //将数据存入session范围 ec.getSessionMap().put("website" , "crazyit.org"); return "success"; } else { ec.getSessionMap().put("tip" , rb.getString("wrong")); return "failure"; } } }
上面程序中第一行粗体字代码就是使用ResourceBundle加载国际化消息资源的代码,第二行粗体字代码就是通过ResourceBundle根据key读取国际化消息,这就实现了动态数据国际化。
由于上面程序是当用户“猜错”时的提示消息,因此访问该应用时故意猜错将看到如图3.15所示的界面。
图3.15 动态数据国际化
从上面介绍可以看出,其实动态数据国际化主要还是借助了Java本身的国际化支持,如果开发者对Java国际化本身就很熟悉,那么在JSF中执行动态数据国际化应该也很简单。
除此之外,JSF还可以通过使用<message-bundle…/>元素来加载定义自定义消息资源包,如果程序需要访问通过该元素加载的国际化消息资源包中的消息,那么就需要先获得该消息资源包的baseName——可通过JSF提供的Application对象的getMessageBundle()方法来获取。
例如,我们把上面国际化消息放入baseName为crazyit_mess的消息资源包中(也就是如果应用想支持简体中文、美式英语两种环境,则应该提供crazyit_mess_zh_CN.properties、crazyit_mess_en_US. properties两个文件)。提供了上面国际化消息资源文件之后,还应该在faces-config.xml文件的<application…/>元素内增加如下配置:
<!-- 指定自定义国际化消息资源 --> <message-bundle>crazyit_mess</message-bundle>
增加上面配置之后,该应用将会加载该消息资源包,通过如下代码即可访问到该消息资源包的baseName。
//通过Application对象获取JSF应用里消息资源包的baseName getApplication().getMessageBundle()
由于应用可能经常需要访问自定义消息包中的国际化消息,为此我们定义如下工厂类:
程序清单:codes\03\3.2\messi18n\WEB-INF\src\org\crazyit\jsf\util\MessageFactory.java
public class MessageFactory { //构造器私有,该类仅作为静态工厂类使用 private MessageFactory() { } //该方法用于为带占位符的字符串填充参数 public static String substituteParams(Locale locale , String msgtext, Object[] params) { String localizedStr = null; if ((params == null) || (msgtext == null)) { return msgtext; } StringBuffer b = new StringBuffer(100); MessageFormat mf = new MessageFormat(msgtext); if (locale != null) { mf.setLocale(locale); b.append(mf.format(params)); localizedStr = b.toString(); } return localizedStr; } public static FacesMessage getMessage(Locale locale , String key, Object[] params) { FacesMessage result = null; String summary = null; String detail = null; String bundleName = null; ResourceBundle bundle = null; //判断用户是否提供了消息资源包 if ((bundleName = getApplication() .getMessageBundle()) != null) { if ((bundle = ResourceBundle.getBundle( bundleName, locale, getCurrentLoader(bundleName))) != null) { try { summary = bundle.getString(key); } catch (MissingResourceException e) { } } } if (summary == null) { bundle = ResourceBundle.getBundle( FacesMessage.FACES_MESSAGES , locale , getCurrentLoader(bundleName)); if (bundle == null) { throw new NullPointerException(); } try { summary = bundle.getString(key); } catch (MissingResourceException e) { } } if (summary == null) { return null; } if ((summary == null) || (bundle == null)) { throw new NullPointerException(); } //为国际化消息填充参数 summary = substituteParams(locale, summary, params); try { detail = substituteParams(locale , bundle.getString(key + "_detail") , params); } catch (MissingResourceException e) { } return new FacesMessage(summary , detail); } //----------下面是本工厂类提供的静态工厂方法---------- //获取国际化消息,以数组形式为占位符 public static FacesMessage getMessage( FacesContext context , String key, Object... params) { if ((context == null) || (key == null)) { throw new NullPointerException( "One or more parameters could be null"); } Locale locale = null; if ((context != null) && (context.getViewRoot() != null)) { locale = context.getViewRoot() .getLocale(); } else { locale = Locale.getDefault(); } if (locale == null) { throw new NullPointerException(); } FacesMessage message = getMessage(locale, key, params); if (message != null) { return message; } locale = Locale.getDefault(); return getMessage(locale, key, params); } //直接使用当前Locale来获取国际化消息(0个占位符) public static FacesMessage getMessage(String key) { return getMessage(getCurrentLocale(), key, null); } //直接使用当前Locale来获取国际化消息(多个占位符) public static FacesMessage getMessage(String key , Object... params) { return getMessage(getCurrentLocale(), key, params); } //----------下面是为本工厂类提供服务的protected方法---------- //获取应用相关的Application对象 protected static Application getApplication() { return (FacesContext.getCurrentInstance().getApplication()); } //获取当前的ClassLoader对象 protected static ClassLoader getCurrentLoader(Object fallbackClass) { ClassLoader loader = Thread.currentThread() .getContextClassLoader(); if (loader == null) { loader = fallbackClass.getClass() .getClassLoader(); } return loader; } //获取当前的Locale对象 protected static Locale getCurrentLocale() { FacesContext context = FacesContext.getCurrentInstance(); Locale locale = null; //如果应用Context已经初始化且当前Context的ViewRoot不为null if ((context != null) && (context.getViewRoot() != null)) { locale = context.getViewRoot().getLocale(); if (locale == null) { locale = Locale.getDefault(); } } else { locale = Locale.getDefault(); } return locale; } }
上面粗体字代码就是通过Application获取国际化消息baseName的关键代码。上面静态工厂类提供了大量静态工厂方法来获取国际化消息。
提供了MessageFactory静态工厂类之后,接下来就可在托管Bean中通过该工厂类来获取国际化消息了。代码如下所示:
程序清单:codes\03\3.2\messi18n\WEB-INF\src\org\crazyit\jsf\BookBean.java
//编写处理导航的方法
public String process()
{
FacesContext fc = FacesContext.getCurrentInstance();
ExternalContext ec = fc.getExternalContext();
if (name.equals("疯狂Java讲义")
&& price == 99)
{
//将数据存入session范围
ec.getSessionMap().put("website"
, "crazyit.org");
return "success";
}
else
{
ec.getSessionMap().put("tip"
, MessageFactory.getMessage("wrong"));
return "failure";
}
}
正如上面程序中粗体字代码所看到的,通过MessageFactory工厂类获取国际化消息非常方便,如果国际化消息中包含占位符,则使用该工厂类提供的getMessage(String key, Object... params)方法即可。该工厂类适用性非常广泛,读者可以直接将它复制到自己的应用中使用。
3.2.4 让用户选择语言
前面提到JSF应用将会优先根据<f:view…/>标签中的locale属性来确定客户端的Locale,这样我们可以在页面上放置一个下拉菜单,当用户通过下拉菜单改变语言时,我们只要改变页面上<f:view…/>标签的locale属性就可实现让用户选择语言的效果。
首先创建如下页面。
程序清单:codes\03\3.2\customi18n\welcome.jsp
<f:view locale="#{localeBean.locale}"> <html> <head> <title>JSP Page</title> </head> <body> <!-- 加载国际化资源文件,加载后可通过local变量访问它 --> <f:loadBundle basename="local" var="local"/> <h:form> <!-- 通过下拉菜单来选择语言/国家环境 --> <h:selectOneMenu value="#{localeBean.locale}" valueChangeListener="#{localeBean.choose}" onchange="this.form.submit();" immediate="true"> <f:selectItem itemValue="en_US" itemLabel="#{global.en_USText}"/> <f:selectItem itemValue="zh_CN" itemLabel="#{global.zh_CNText}"/> </h:selectOneMenu> </h:form> <!-- 使用全局资源文件中的消息 --> <h1><h:outputText value="#{global.title}"/></h1> <b>${sessionScope.tip}</b> <h:form> <!-- 使用局部资源文件中的消息 --> <h:outputText value="#{local.name}"/> <h:inputText value="#{bookBean.name}"/><br/> <!-- 使用局部资源文件中的消息 --> <h:outputText value="#{local.price}"/> <h:inputText value="#{bookBean.price}"/><br/> <h:commandButton value="#{local.process}" action="#{bookBean.process}"/><br/> </h:form> </body> </html> </f:view>
上面页面代码中第一行粗体字代码的<f:view…/>标签指定了locale="#{localeBean.locale}",这表明该页面将会根据localeBean的locale属性来确定呈现页面的Locale。页面上还使用<h:selectOneMenu…/>标签创建了一个下拉菜单,并指定当下拉菜单的值发生改变时会提交它所在的表单,提交到localeBean的choose方法。下面是localeBean的源代码。
程序清单:codes\03\3.2\customi18n\WEB-INF\src\org\crazyit\jsf\LocaleBean.java
public class LocaleBean { private String locale; //无参数的构造器 public LocaleBean() { } //初始化全部属性的构造器 public LocaleBean(String locale) { this.locale = locale; } //省略locale属性的setter和getter方法 … public void choose(ValueChangeEvent vce) { //将用户选择的值作为当前locale this.locale = (String)vce.getNewValue(); } }
实现上面LocaleBean之后,每当用户希望选择不同的语言时,只要通过页面上的下拉菜单进行选择即可。