OperaMasks中的扩展国际化支持

首先应该明确的是,OperaMasks是完全兼容JSF规范的,因此上面一节介绍的方法在OperaMasks中仍然可用。但OperaMasks在IoVC的支持下,允许将国际化资源引用从页面中移到LiteBean中,并因此扩展出多种支持多语言的方式。在本节的后续描述中,将统一使用以下页面为例,可以看出,现在页面完全不需要关心国际化的问题:

<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
  xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
  renderKitId="AJAX" xmlns:h="http://java.sun.com/jsf/html">
  <w:page title="Calculator">
    <w:form id="calc">
      <layout:panelGrid columns="3">
        <h:outputLabel for="first"/>
        <w:textField id="first"/>
        <h:message for="first"/>
        <h:outputLabel for="second"/>
        <w:textField id="second"/>
        <h:message for="second"/>
        <h:outputLabel for="result"/>
        <h:outputText id="result"/>
      </layout:panelGrid>
      <br/>
      <layout:panelGrid columns="4">
        <w:button id="add"/>
        <w:button id="subtract"/>
        <w:button id="multiply"/>
        <w:button id="divide"/>
      </layout:panelGrid>
    </w:form>
  </w:page>
</f:view>

在LiteBean中使用统一EL表达式

最直接的改变形式是,在LiteBean中引入资源,然后使用与页面类似的统一EL表达式。加入了国际化支持的LiteBean看上去是这个样子的:

1package demo;

...

@ManagedBean(scope = ManagedBeanScope.REQUEST)
public class CalcBean {

  /**
  * 注入资源文件
  */
  2@LocalString
  private Map<String,String> messages;

  @Bind
  3@Label("#{this.messages['first.label']}")
  private double first = 22.0;

  @Bind
  @Label("#{this.messages['second.label']}")
  private double second = 7.0;

  @Bind
  @Label("#{this.messages['result.label']}")
  private String resultLabel;

  /**
   * 用来保存计算结果
   */
  @Bind
  @Label("#{this.messages['result.label']}")
  private double result = 0;

  @Action
  @Label("#{this.messages['add.label']}")
  public void add() {
    result = first + second;
  }

  @Action
  @Label("#{this.messages['subtract.label']}")
  public void subtract() {
    result = first - second;
  }

  @Action
  @Label("#{this.messages['multiply.label']}")
  public void multiply() {
    result = first * second;
  }

  @Action
  @Label("#{this.messages['divide.label']}")
  public void divide() {
    result = first / second;
  }
}
1 注意在这个例子中,LiteBean所在的包(目录)与资源文件所在目录是一致的。
2 @LocalString标注用于引入资源,作用类似于标准JSF中的<f:loadBoundle>标签。 当LiteBean与资源文件位于同一目录下,且资源文件名前缀为LocalStrings时,无需在@LocalString标注中指定basename。
3 IoVC对在LiteBean中使用的统一EL表达式作了扩展,允许使用this标识符引用当前LiteBean实例。

由于现在所有对资源文件的引用集中到了LiteBean中,我们可以更为自然地加入一些更详细和更灵活的提示。假设我们现在有这样一种需求:当用户点击某一个操作时,result并不仅仅简单的显示一个结果, 我们还希望能够显示用户的操作是什么,譬如,当用户执行 1 + 2 操作时,中文环境下显示:"数据1加数据2等于3",英文环境下显示:"Number 1 add number 2 equals 3"。换言之,我们现在要对多语言字符串进行参数化处理。在OperaMasks中是如何实现的呢?

首先修改资源文件加入资源串:

#demo.LocalStrings_en_US.properties
...
resultLabel=Number {0} {1} Number {2} equals {3}
#demo.LocalStrings_zh_CN.properties
...
resultLabel=数值{0}{1}数值{2}等于{3}

然后对页面代码作如下修改:

<h:outputLabel for="result"/>
<h:outputText id="result"/>
改为:
<h:outputText id="resultLabel"/>

对LiteBean代码做如下修改:

@ManagedBean(scope=ManagedBeanScope.SESSION)
public class CalcBean implements Serializable {
  /**
  * 注入资源文件
  */

  @LocalString
  private Map<String,String> messages;

  @Bind
  @Label("#{this.messages['first.label']}")
  private double first = 22.0;

  @Bind
  @Label("#{this.messages['second.label']}")
  private double second = 7.0;

  /**
   * 用来保存计算结果
   */
//  @Bind
//  @Label("#{this.messages['result.label']}")
  1private double result = 0;
  
  @Bind
  2private String getResultLabel() {
      if (operatorKey != null) {
          String operator = messages.get(operatorKey);
          3return MessageFormat.format(messages.get("resultLabel"), first, operator, second, result);
      } else {
          return null;
      }
  }

  /**
   * 用来保存用户选择的操作符
   */
  private String operatorKey = null;

  @Action
  @Label("#{this.messages['add.label']}")
  public void add() {
    result = first + second;
    4operatorKey = "add.label";
  }

  @Action
  @Label("#{this.messages['subtract.label']}")
  public void subtract() {
    result = first - second;
    operatorKey = "subtract.label";
  }

  @Action
  @Label("#{this.messages['multiply.label']}")
  public void multiply() {
    result = first * second;
    operatorKey = "multiply.label";
  }

  @Action
  @Label("#{this.messages['divide.label']}")
  public void divide() {
    result = first / second;
    operatorKey = "divide.label";
  }

}
1 计算结果不再直接与页面组件绑定;
2 这里定义了一个只读属性resultLabel,根据“约定优于配置”规则与页面上id为resultLabel的<h:outputText/>绑定;
3 填充带参数的多语言字符串;
4 记录当前动作的运算符键值。

在服务器上运行页面,在中文环境下页面展现如下:

可以看出,在标准JSF如果要实现同样功能,需要在页面和LiteBean中同时关注多语言特性,了解各键值含义。通过使用IoVC,我们把页面解放出来,而只需要在LiteBean中统一进行多语言支持。

@LocalString标注

@LocalString标注用来向LiteBean中引入资源。从上面例子我们可以看出它的其中一种常用使用形式:

  1. 当不指定basename时,默认使用LiteBean所在包下前缀为LocalStrings的资源文件。

  2. 标注在类型为java.util.Map的对象属性上,注入一个对应资源文件中所有本地字符串的只读Map,可以用关键字从此Map中查找字符串。对于某一个键值(以add.label为例),查找规则如下:

    1. 首先尝试查找当前LiteBean全类名加上指定键值所得的键值,例如test.CalcBean.add.label。若找到,返回对应资源串;

    2. 若未找到,尝试查找当前LiteBean简单类名加上指定键值所得的键值,例如CalcBean.add.label。若找到,返回对应资源串;

    3. 若未找到,尝试查找指定键值,例如add.label。若找到,返回对应资源串;

    4. 若未找到,返回null。

    以上查找规则简化了多个LiteBean共用资源文件时的处理,允许我们在多个类中使用同一个资源键值。通过在资源文件中为其提供优先级更高的键值而获取到与具体类名对应的资源串。

也可以通过指定@LocalString标注的basename属性,引入任意位于/WEB-INF/classes目录下的资源文件。例如

@LocalString(basename="demo.CalcBean")

将引入/WEB-INF/classes/demo/目录中前缀为CalcBean的资源文件。

除了java.util.Map类型属性外,@LocalString标注还可以作用在以下类型的对象属性上:

  1. java.lang.String类型:以变量名为键值从本地字符串资源文件中查找字符串并注入变量,也可以在@LocalString标注中用key属性指定字符串关键字。请注意这种形式下,键值的查找规则仍然按照上文所描述的优先合并类名规则。例如:

    @LocalString(key="add.label")
    private String addLabel;
    
  2. java.util.ResourceBundle,则注入一个包含所有本地字符串的资源包。

@LocalString标注具有@Accessible特性,被其标注的属性将自动在统一EL表达式中具有公共可见性(只对统一EL表达式有效)。例如,上面在CalcBean中定义的private属性messages,由于标注为@LocalString,也允许在页面中使用统一EL表达式#{CalcBean.messages}进行访问。如果希望取消这种附加的公共可见性,可以使用@Accessible(false)进行标注。

由资源文件注入

上面介绍的方法,无论是在页面中使用<w:loadBundle>标签,或是在LiteBean中使用@LocalString标注,其本质都是由应用程序主动从资源文件中抽取资源串。OperaMasks还支持一种更为简捷的形式:由资源文件主动向LiteBean中注入多语言信息。

在此方式下,在页面和LiteBean中都无须关注多语言特性。因此除了页面仍可沿用本节开始时所描述的最简形式外,LiteBean也可以还原为没有国际化支持最简形式:

@ManagedBean(scope=ManagedBeanScope.SESSION)
public class CalcBean implements Serializable {
  @Bind
  private double first = 22.0;

  @Bind
  private double second = 7.0;

  /**
   * 用来保存计算结果
   */
  @Bind
  private double result = 0;
  
  @Action
  public void add() {
    result = first + second;
  }

  @Action
  public void subtract() {
    result = first - second;
  }

  @Action
  public void multiply() {
    result = first * second;
  }

  @Action
  public void divide() {
    result = first / second;
  }
}

现在,为了在应用中加入多语言支持,我们在CalcBean所在的包目录中加入以下LocalStrings资源文件:

LocalStrings_en_US.properties

#demo.LocalStrings_en_US.properties
CalcBean.first.label=First
CalcBean.second.label=Second
CalcBean.result.label=Result
CalcBean.add.label= +
CalcBean.subtract.label= -
CalcBean.multiply.label= *
CalcBean.divide.label = /

LocalStrings_zh_CN.properties

#demo.LocalStrings_zh_CN.properties
CalcBean.first.label=数值一
CalcBean.second.label=数值二
CalcBean.result.label=结果
CalcBean.add.label= 加
CalcBean.subtract.label= 减
CalcBean.multiply.label= 乘
CalcBean.divide.label = 除

在服务器上运行,可以看到页面展示中已加入多语言支持。

可以看出,在这种形式中,由于页面和代码中没有任何与资源文件关联的信息,对多语言的支持完全是通过以下约定规则加入的:

  1. 页面必须至少绑定一个LiteBean,且LiteBean中必须通过IoVC绑定需要提供多语言支持的页面组件(绑定到属性值或绑定到组件模型对象均可)。

  2. 资源文件必须与被绑定的LiteBean位于同一个包目录中,且文件名必须以LocalStrings开头。

  3. 资源文件中的键值使用

    <LiteBean简单类名>.<目标组件id>.<目标属性名称>

    形式进行命名。请注意这里使用的是LiteBean的简单类名,而不是向框架注册的ManagedBean引用名。

使用这种形式,我们还可以很方便地在资源文件中对页面组件属性进行初始化,从而达到更友好的国际化展现效果。例如,假如“add”这个Button,在中文环境下显示“加”,在英文环境下显示“Plus”,由于语言的差异,导致在不同语言下,button的宽度发生变化。我们希望,在中文环境下,此按钮的宽度为30,而在英文环境下,此按钮的宽度为60。为了实现这个需求,只需要在资源文件中分别加入:

LocalStrings_zh_CN.properties

#demo.LocalStrings_zh_CN.properties
CalcBean.add.minWidth=30
CalcBean.subtract.minWidth=30
CalcBean.multiply.minWidth=30
CalcBean.divide.minWidth=30

LocalStrings_en_US.properties

#demo.LocalStrings_en_US.properties
CalcBean.add.minWidth=60
CalcBean.subtract.minWidth=60
CalcBean.multiply.minWidth=60
CalcBean.divide.minWidth=60

在服务器上运行页面,可看到在中英文环境下button将以不同宽度展现。

[上一页] [下一页]