经典Java EE企业应用实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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之后,每当用户希望选择不同的语言时,只要通过页面上的下拉菜单进行选择即可。