Previous Section  < Day Day Up >  Next Section

11.1 Localizing Application Output

Remember the pages for setting user preferences from Chapter 9? One of the pages allows the user to select her preferred language; let's add what's needed for actually using the selected language. Figure 11-1 shows one of the preferences pages when Swedish is selected as the preferred language.

Figure 11-1. User information page with Swedish as the selected language
figs/Jsf_1101.gif

Java was designed with internationalization in mind and includes a number of classes to make the process as painless as possible. In i18n terminology, a locale represents a specific geographical region. In Java, a locale is represented by an instance of the java.util.Locale class. Java includes other classes for formatting dates and numbers according to the rules defined for a locale, and classes to help you include localized strings and other objects in an application.

You create a Locale instance using a constructor that takes a country code and language code as arguments:

java.util.Locale usLocale = new Locale("en", "US");

Here, a Locale for U.S. English is created. George Bernard Shaw (a famous Irish playwright) once observed, "England and America are two countries divided by a common language," so it's no surprise that both a language code and a country code are needed to describe some locales completely. The language code, a lowercase two-letter combination, is defined by the ISO 639 standard available at http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt. The country code, an uppercase two-letter combination, is defined by the ISO 3166 standard, available at http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html. Table 11-1 and Table 11-2 show some of these codes.

Table 11-1. ISO-639 language codes

Language code

Language

af

Afrikaans

da

Danish

de

German

el

Greek

en

English

es

Spanish

fr

French

ja

Japanese

pl

Polish

ru

Russian

sv

Swedish

zh

Chinese

Table 11-2. ISO-3166 country codes

Country code

Country

DK

Denmark

DE

Germany

GR

Greece

MX

Mexico

NZ

New Zealand

ZA

South Africa

GB

United Kingdom

US

United States

The country code is optional because it's redundant for some languages (like Swedish). Another optional item supported by the Locale class is called a variant. A variant can specify a locale that applies to a specific platform, in addition to a language and a country. One example is if you use a locale to select help texts, you may want to provide one set of descriptions for Internet Explorer and another for Netscape browsers.

In JSF, a view is associated with a specific locale through a UIViewRoot property named locale of the type Locale. If no locale is specified for a view, JSF uses information encoded in the request data to try to determine which locale the user prefers. Browsers may send this information in a request header called Accept-Language. The value of this header contains one or more codes for the user's preferred locales based on how the browser is configured. If you use a Netscape 6 or Mozilla browser, you can specify your preferred locales in the Edit Preferences dialog, under the Navigator Languages tab. In Internet Explorer 5, you find the same thing in Tools Internet Options when you click the Languages button under the General tab. If you specify more than one language, they are included in the header as a comma-separated list:

Accept-Language: en-US, en, sv

The languages are listed in order of preference, with each language represented by just the language code or the language code and country code separated by a dash (-). This example header specifies the first choice as U.S. English, followed by any type of English, and finally Swedish. The HTTP specification also allows an alternative to listing the codes in order of preference, namely, adding a so-called q-value to each code. The q-value is a value between 0.0 and 1.0, indicating the relative preference between the codes.

If the locale property for a view isn't set, e.g., because it's the first time the view is requested, JSF compares the locales specified by the Accept-Language header to the list of locales supported by the application, declared like this in the faces-config.xml file:

<faces-config>

  <application>

    <locale-config>

      <default-locale>en<default-locale>

      <supported-locale>sv</supported-locale>

      ...

    </locale-config>

    ...

  </application>

  ...

</faces-config>

Within the <locale-config> element, one <default-locale> element declares the default locale and zero or more <supported-locales> declare additional supported locales. The default locale is included automatically in the set of supported locales, so you shouldn't declare it twice.

For each locale from the Accept-Language header (in priority order), JSF first tries to find a supported locale that matches all parts of the preferred locale: language, country, and variant. If it doesn't find a perfect match, it drops the variant and tries again. If it still can't find a match, it drops the country. As soon as it finds a supported locale using this algorithm, it selects it and ignores the other locales. This means that with English, German, and Swedish as the available locales and an Accept-Language header containing the value "sv, en-US", the Swedish locale is selected (it's listed first, so it has higher priority). With an Accept-Language header such as "fr, en-US", the English locale is selected, since the highest priority locale (fr) is not available, and the closest match for the en-US locale is the en locale. If the application supports both the en and the en-US locale, the en-US locale is used because it's an exact match for the user's preferences. If the browser doesn't send an Accept-Language header or if none of its locales matches a supported locale, the application's default locale is selected. If no default locale is declared, the Java runtime's default locale is used instead.

Letting JSF pick a locale based on the Accept-Language header helps selecting the best initial locale for a user, but for a localized application you should always provide means for explicit locale selection. Many users aren't aware of the browser locale selection feature. Besides, providing a list of locales to pick from makes it clear to the user exactly which locales are supported. The sample application defaults to English but allows the user to pick one of the other supported locales through the preferences screen.

Example 11-1 shows the JSP page where the user selects the preferred language

Example 11-1. Internationalized language selection page (expense/stage2/prefLang.jsp)
<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>



<f:view locale="#{userProfile.locale}">

  <f:loadBundle basename="labels" var="labels" />

  <html>

    <body>

      <h2><h:outputText value="#{labels.prefLangHeader}" /></h2>

      <h:form>

        <h:panelGrid columns="2">

          <h:outputText value="#{labels.langSelectLabel}" />

          <h:selectOneRadio value="#{userProfile.locale}">

            <f:selectItem itemValue="sv" itemLabel="#{labels.svChoice}"/>

            <f:selectItem itemValue="en" itemLabel="#{labels.enChoice}"/>

          </h:selectOneRadio>

        </h:panelGrid>

        <h:commandButton value="#{labels.prevButtonLabel}" 

          immediate="true" action="previous" />

        <h:commandButton value="#{labels.nextButtonLabel}" 

          action="#{userHandler.updateProfile}" />

        <h:commandButton value="#{labels.cancelButtonLabel}" 

          immediate="true" action="cancel" />

      </h:form>

    </body>

  </html>

</f:view>

The user selects the preferred language by clicking one of two radio buttons, represented by the <h:selectOneRadio> action element. The value attribute of this element binds it to the locale property of a UserProfile bean in the session scope. So far, it's the same as the version of this page that we looked at in Chapter 9.

What's different is that the selected locale is now actually used to pick the corresponding text for everything in the page. Note how I use a locale attribute for the <f:view> action, bound to the UserProfile bean's locale property. The first time a user requests the page, the UserProfile bean is created and added to the session scope under the name userProfile (when the value binding is evaluated, based on the managed bean declaration in the faces-config.xml file). The bean's locale property has the value "en" as default, so this is the value passed to the <f:view> action and used subsequently to set the UIViewRoot locale property value. If the user selects another locale and submits the form, the new locale is picked up the next time the JSP page is processed. All the preferences pages linked from the language selection page shown in Example 11-1 are internationalized in the same way, so you can click the Next button after changing the language selection to verify that it works.

The UIViewRoot locale property can also be set programmatically, for example, by an action method or an event listener, but you must be careful when you do so. The same locale that was used to render the response must also be used to process the input (if any) sent with the next request. The locale property must therefore be changed only after all input values have been processed (typically at the end of the Process Validations phase at the earliest) and before the first component is rendered (typically at the very beginning of the Render Response phase at the latest).

11.1.1 Selecting Localized Text

The next JSF action in Example 11-1 is the <f:loadBundle> action. This action loads the resource bundle specified by the basename attribute that corresponds to the locale identified by the locale property value in UIViewRoot. A resource bundle holds localized resources (such as text, images, and sounds) and is represented by an instance of the java.util.ResourceBundle class. This class is actually the abstract superclass for two subclasses that do the real work, ListResourceBundle and PropertyResourceBundle, but it provides methods that let you get an appropriate subclass instance, hiding the details about which subclass actually provides the resources. Details about the difference between these two subclasses are beyond the scope of this book. Suffice it to say that the <f:loadBundle> action can use resources provided through either one of them, or even custom subclasses.

For most web applications, an instance of the PropertyResourceBundle is used. A PropertyResourceBundle instance is associated with a named set of localized text resources. Keys identifying resources and their corresponding text values are stored in a regular text file, known as a resource bundle file:

prefLangHeader=Language

langSelectLabel=Language:

enChoice=English

svChoice=Swedish

This is the same kind of file we used in Chapter 7 when we defined error messages. This example shows four keys: prefLangHeader, langSelectLabel, enChoice, and svChoice. The key is a string, without space or other special characters, and the value is any text. If the value spans more than one line, the linebreak must be escaped with a backslash character (\):

multi_line_msg=This text value\

continues on the next line.

A resource bundle file must use a .properties file name extension, but there can be more than one file per bundle, with a locale code included in the filename. To localize an application, create a separate resource bundle file for each locale, all with the same main name (the base name) but with unique suffixes to identify the locale. For instance, a file named lables_en_US.properties, where en is the language code for English and US is the country code for U.S.A., can contain text for the US English locale. All resource bundle files must be located in the classpath used by the Java Virtual Machine (JVM). In the case of web applications, I suggest that you store the file in the application's WEB-INF/classes directory, because this directory is always included in the classpath. The sample application contains two resource bundle files, one for each supported locale (English and Swedish): labels_en.properties and labels_sv.properties.

In addition to loading a ResourceBundle instance that represents the specified base name and the current locale, the <f:loadBundle> wraps it in a java.util.Map and saves the Map in the request scope with the name specified by the var attribute. The Map implementation of the get() method calls through to the wrapped ResourceBundle instance's getObject() method. If the specified key matches a resource, its value is returned. Otherwise, the key embedded in questions marks, e.g., "???myKey???," is returned to make it easier to detect common problems, such as key name typos.

With localized resources exposed as a Map, you can use standard JSF EL expressions to access the localized values. For instance, this is how the main title of the language selection page is handled in Example 11-1:

<f:view locale="#{userProfile.locale}">

  <f:loadBundle basename="labels" var="labels" />

  <html>

    <body>

      <h2><h:outputText value="#{labels.prefLangHeader}" /></h2>

The <f:view> action sets the locale for the view to the currently selected locale held by the userProfile bean's locale property, and the <f:loadBundle> action exposes the labels resource bundle for this locale as a Map named labels in the request scope. The key for the localized page title is prefLangHeader, so picking up a localized value for this key is as easy as using an <h:outputText> action with the value attribute set to a JSF EL expression that receives the prefLangHeader property from the labels Map variable. If you look at all other JSF components in Example 11-1, you'll see that they all receive their value from the labels variable in the same way.

It's fairly common for resource bundle keys to be composed from multiple words separated by dots: for instance, "choice.en". A dot is a special character in the JSF EL, so trying to access a key from a bundle with an expression like #{labels.choice.en} always fails. If your resource bundle key names contain dots, you must use the array element operator instead, i.e., #{labels['choice.en']}.


11.1.2 Formatting Dates and Numbers

One thing the inhabitants of this planet have a hard time agreeing on is how to write dates and numbers. The order of the month, the day, and the year; if the numeric value or the name should be used for the month; what character to use to separate the fractional part of a number; all of these details differ between countries, even between countries that speak the same language. And even though these details may seem picky, using an unfamiliar format can cause a great deal of confusion. For instance, if you ask for something to be done by 5/2, an American thinks you mean May 2 while a Swede believes that it's due by February 5.

Java provides two classes, named java.text.NumberFormat and java.text.DateFormat, for formatting numbers and dates appropriately for a specific locale, and the standard JSF converters for date/time and numeric values use these classes.

Example 11-2 shows a localized version of the menu area page with an added feature: it displays the current date on the right.

Example 11-2. Internationalized menu area page (expense/stage2/menuArea.jsp)
<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>



<jsp:useBean id="now" scope="request" class="java.util.Date" />

<f:view locale="#{userProfile.locale}">

  <f:loadBundle basename="labels" var="labels" />

  <h:form>

    <table cellpadding="0" cellspacing="0" width="100%">

      <tr>

        <td>

          <h:commandButton value="#{labels.newButtonLabel}" 

            disabled="#{reportHandler.newDisabled}" />

          <h:commandButton value="#{labels.deleteButtonLabel}" 

            disabled="#{reportHandler.deleteDisabled}" />

          <h:commandButton value="#{labels.submitButtonLabel}" 

            disabled="#{reportHandler.submitDisabled}" />

          <h:commandButton value="#{labels.acceptButtonLabel}" 

            rendered="#{reportHandler.acceptRendered}" 

            disabled="#{reportHandler.acceptDisabled}" />

          <h:commandButton value="#{labels.rejectButtonLabel}" 

            rendered="#{reportHandler.rejectRendered}"

            disabled="#{reportHandler.rejectDisabled}" />

        </td>

        <td align="right">

          <h:outputText value="#{labels.loggedInAs}" />

          "${pageContext.request.remoteUser}"

          [<h:outputLink value="../../logout.jsp">

             <h:outputText value="#{labels.logoutLinkLabel}" />

           </h:outputLink>]

          [<h:outputLink value="prefUser.faces">

             <h:outputText value="#{labels.prefLinkLabel}" />

           </h:outputLink>]

          <h:outputText value="#{now}">

            <f:convertDateTime datestyle="long" />

          </h:outputText>

        </td>

      </tr>

    </table>

  </h:form>

</f:view>

The menu area page is internationalized in the same way as the language selection page we looked at earlier: the <f:view> action sets the locale and all text is picked up from the bundle loaded by the <f:loadBundle> action. The only new thing is the <h:outputText> element at the end, showing the value of the now variable created by the <jsp:useBean> standard JSP action at the top of the page. The now variable holds the current date and time and, because the output component is configured with a standard date/time converter, its value is formatted according to the current locale. Figure 11-2 shows this version of the menu area page with the date formatted according to the Swedish locale.

Figure 11-2. The menu area with a date formatted according to the Swedish locale
figs/Jsf_1102.gif

As I mentioned before, the format of a date or a time value can be controlled through various attributes supported by the <f:convertDateTime> action. In Example 11-2, I use only the datestyle attribute, but Appendix A describes all available options. The standard number converter also formats the value of the component it's attached to according to the rules for the current locale, and it can be configured with similar attributes.

11.1.3 Localizing Messages

If your application supports more than one locale, you must be concerned also with the messages generated by the validators, the converters, and possibly your application code. The good news is that JSF is already internationalized with regards to how it handles messages—and if you follow the code samples for application-generated messages from Chapter 7, so is your application. This piece of code from the custom validator we developed in Chapter 7 shows what I mean:

package com.mycompany.jsf.validator;



import java.text.MessageFormat;

import java.util.Date;

import java.util.Locale;

import java.util.ResourceBundle;

import javax.faces.application.Application;

import javax.faces.application.FacesMessage;

import javax.faces.component.StateHolder;

import javax.faces.component.UIComponent;

import javax.faces.context.FacesContext;

import javax.faces.validator.Validator;

import javax.faces.validator.ValidatorException;

...

public class LaterThanValidator implements Validator, StateHolder {

    ...

    public void validate(FacesContext context, UIComponent component,

        Object value) throws ValidatorException {



        Application application = context.getApplication( );



        String messageBundleName = application.getMessageBundle( );

        Locale locale = context.getViewRoot( ).getLocale( );

        ResourceBundle rb = 

            ResourceBundle.getBundle(messageBundleName, locale);



        UIComponent peerComponent = component.findComponent(peerId);

        if (peerComponent == null) {

            String msg = rb.getString("peer_not_found");

            FacesMessage facesMsg = 

                new FacesMessage(FacesMessage.SEVERITY_FATAL, msg, msg);

            throw new ValidatorException(facesMsg);

        }

        ...

    }

}

The validate() method obtains the current locale from the UIViewRoot and uses it to get a ResourceBundle for this locale. It then gets the localized message string from this bundle. This means that you just need to add message resource bundle files with localized error messages for all the locales you support. Here's what the custom messages file for the Swedish locale (custMessages_sv.properties) looks like:

javax.faces.validator.DoubleRangeValidator.MINIMUM=Ange ett värde större an {0}.



not_later=Ogiltigt datum

not_later_detail=Ange ett datum senare än {0}

There are a couple of important things to notice. The first is that all JSF validators and converters, and all other types of classes that generate messages, also use a locale-specific ResourceBundle instance to get the message text, so you can localize all standard messages as well. Here I also define a Swedish version of a DoubleRangeValidator message.

Second, note that I don't specify a value for the peer_not_found key used by the LaterThanValidator code. This illustrates a neat feature of the ResourceBundle class, namely, that an instance for a specific bundle—say, "en_US"—is linked to a parent instance for a less precise locale—say, "en"—all the way up to the default bundle (a bundle without a locale code appended to the base name). For the sample application, I have a default resource bundle file named custMessages.properties containing all messages in English and a custMessages_sv.properties file containing messages in Swedish, so when I ask the ResourceBundle for the custMessages_sv.properties file to give me the value for a key that's not defined there, it returns the value defined in custMessages.properties instead. I use this feature in the sample application to avoid translating all the messages that can only occur due to design errors, as opposed to user errors. It can also be a real time-saver when you deal with a language where only a few messages need to be customized for a specific country, while most of them are the same for all countries speaking the same language.

    Previous Section  < Day Day Up >  Next Section