12.3 Internationalizing Your Struts Applications

The internationalization support provided by the Struts framework focuses almost exclusively on the presentation of text and images for the application. Functionality such as accepting input from nontraditional languages is not covered within the Struts framework.

As you've already seen, depending on your Struts configuration settings, the framework can determine the preferred locale for a user and store it into the user's session. Once the user's locale has been determined, Struts can use this locale to look up text and other resources from the resource bundles. The resource bundles are essential components in the Struts framework.

12.3.1 The Struts Resource Bundle

As you saw in Chapter 4, each of your application modules can be configured with one or more resource bundles. The information within each bundle is available to actions, action forms, JSP pages, and custom tags alike.

12.3.1.1 Creating a Struts resource bundle

Resource bundles that you create must follow the conventions of the PropertyResourceBundle class from the Java core library. That is, you need to create a text file for your resource bundle that has a .properties extension and that adheres to the guidelines discussed in the JavaDoc for the java.util.Properties class. The most important of these guidelines is that the format of the messages within this file is:

key=value

Example 12-3 displays a properties file called StorefrontMessageResources.properties that can be loaded by the Struts framework.

Example 12-3. A simple Struts resource bundle
global.title=Virtual Shopping with Struts
global.error.invalidlogin=The login was unsuccessful! Please try again.
global.required={0} is a required value.
 
label.featuredproducts=This Weeks Featured Products
label.email=Email Address
label.password=Password
label.returning
label.firstName=First Name
label.lastName=Last Name
label.address=Address
label.city=City
label.state=State
label.postalCode=Postal Code
label.zip=Zip
label.country=Country
label.phone=Phone
 
button.add=Add Me
button.delete=Delete
button.checkout=Checkout
button.saveorder=Save Order
 
errors.required={0} is required.
errors.minlength={0} can not be less than {1} characters.
errors.maxlength={0} can not be greater than {1} characters.
errors.invalid={0} is invalid.   
errors.byte={0} must be a byte.
errors.short={0} must be a short.
errors.integer={0} must be an integer.
errors.long={0} must be a long.
errors.float={0} must be a float.
errors.double={0} must be a double.   
errors.date={0} is not a date.
errors.range={0} is not in the range {1} through {2}.
errors.creditcard={0} is not a valid credit card number.
errors.email={0} is not a valid e-mail address.

You must be sure to name the message resource file with the extension .properties, or the Struts framework will not be able to load it.

Notice that the keys used in Example 12-3 are separated by a period (.). We could have used other characters in the keys, or we could have used a single-word key like labelPhone=Phone. Using namespacing in your keys is a great way to organize the localized text to make maintenance easier and to prevent name collisions. This is similar to how Java classes use package names.

You have to be careful, however, when using characters other than the period as a separator. The colon (:), for example, can be used instead of the equals sign (=) to separate the key and the value, and it will cause problems if you use it within your keys. If you don't want to use the period character in your keys, you can safely use the underscore or the hyphen character. Spaces are not a good choice, as they will also cause problems.

12.3.1.2 Resource bundle naming guidelines

The naming of the resource bundle is critical to it working properly. All resource bundles have a base name that you select. In Example 12-3, the name StorefrontMessageResources was used as a base name. If you needed to provide an additional localized resource bundle for the French language and the country Canada, you would create a properties file called StorefrontResouces_fr_CA.properties with the appropriate localized resources.

When the Struts framework searches for a message from one of the bundles, it looks for a bundle that is the closest match, based on the base name and the locale. If no locale is provided, it will use the default locale. Only when it fails to find a resource bundle with a specific language and country code as part of the name will it default to the base resource bundle. The base resource bundle is the one without any language or country code in its name.

You should always provide a base resource bundle. If a user with a locale that you don't support visits your site, the application will select the base bundle for that user.

12.3.1.3 The resource bundle and the classpath

The resource bundle needs to be placed in a location where it can be found and loaded. This means that the same class loader that loads the web application must also be able to locate and load the resource bundle. For web applications, the appropriate location is the WEB-INF/classes directory.

If you provide a package name for the resource bundle, it must reside in the correct package as well. For example, if you name your resource bundle com.oreilly.struts.StorefrontResources.properties, you must place it into the WEB-INF/classes/com/oreilly/struts directory.

12.3.2 Accessing the Resource Bundle

The resource bundles for a Struts application are loaded at startup, and each bundle is represented in memory by an instance of the org.apache.struts.util.MessageResources class (actually by its concrete subclass, PropertyMessageResources).

Each MessageResources instance is stored in the ServletContext when the application is initialized and can be accessed from just about any component within the servlet container. However, you'll more typically use a combination of the custom tags and the ActionMessage or ActionError classes to access the resources, rather than calling methods on the MessageResources class directly. In fact, if you use a combination of the declarative exception handling discussed in Chapter 10 and the Validator framework from Chapter 11, it's possible that you won't have to create ActionMessage or ActionError instances at all; the framework will do it for you automatically.

If you need to create an ActionMessage or ActionError manually, however, you can. From the validate( ) method of an ActionForm, it would look something like this:

public ActionErrors validate(ActionMapping mapping, HttpServletRequest request){
 
  ActionErrors errors = new ActionErrors(  );
 
  if(getEmail() == null || getEmail().length(  ) < 1) {
    errors.add(ActionErrors.GLOBAL_ERROR,
               new ActionError("security.error.email.required"));
  }
 
  if(getPassword() == null || getPassword().length(  ) < 1) {
    errors.add(ActionErrors.GLOBAL_ERROR,
               new ActionError("security.error.password.required"));
  }
  return errors;
}

The String argument passed into the constructor of the ActionError class must be a key in the resource bundle. The ActionMessage and ActionError classes have several constructors, most of which allow you to pass in substitution arguments, for when your messages are compound messages, as explained earlier in this chapter. You can also create ActionMessages and ActionErrors from an Action class. The following is a small fragment from the Storefront LoginAction class:

// Log in through the security service
IStorefrontService serviceImpl = getStorefrontService(  );
 
String errorMsg = null;
UserView userView = null;
try{
  // Attempt to authenticate the user
  userView = serviceImpl.authenticate(email, password);
}catch( InvalidLoginException ex ){
  ActionErrors errors = new ActionErrors(  );
  ActionError newError = new ActionError("error.security.invalidlogin");
  errors.add( ActionErrors.GLOBAL_ERROR, newError );
  saveErrors( request, errors );
  return mapping.findForward( IConstants.FAILURE_KEY );
}catch( ExpiredPasswordException ex ){
  ActionErrors errors = new ActionErrors(  );
  ActionError newError = new ActionError("error.security.passwordexpired");
  errors.add( ActionErrors.GLOBAL_ERROR, newError );
  saveErrors( request, errors );
  return mapping.findForward( IConstants.FAILURE_KEY );
}catch (AccountLockedException ex){
  ActionErrors errors = new ActionErrors(  );
  ActionError newError = new ActionError("error.security.accountlocked" );
  errors.add( ActionErrors.GLOBAL_ERROR, newError );
  saveErrors( request, errors );
  return mapping.findForward( IConstants.FAILURE_KEY );
}

This fragment is another reason why the declarative exception-handling feature of Struts is so attractive. Look at the amount of work that we did here. With declarative exception handling, all of this would be defined within the Struts configuration file.

12.3.2.1 The Bean tag library's MessageTag class

The Struts framework contains several custom tags that can be used in conjunction with the MessageResources for an application. One of the most important, however, is the Message tag that is part of the Bean tag library.

This custom tag retrieves a message string from one of the bundles for an application. It supports optional parametric replacement if the JSP page requires it. All you need to do is provide the key from the bundle and specify which application bundle to use, and the tag will write out the information to the JSP page. For example, the following JSP fragment uses the MessageTag to write out the title of the HTML page:

<head>
  <title><bean:message key="global.title"/></title>
</head>

This is one tag that you will find yourself using quite often within your Struts applications.

12.3.3 Setting the Character Set

Supporting character sets other than the typical U.S. default ISO-8859-1 is a little tricky. There are several steps that you must perform before your environment will be prepared to support them. First, you need to configure the application server and/or servlet container to support the character-encoding scheme that you want to use. For example, for Unicode you would tell the container to interpret input as UTF-8. Check with the vendor's documentation, as each one will be configured differently. You can also set up a servlet filter to do this, but this requires a container that supports the 2.3 Servlet API.

Next, the contentType property within the controller element in the Struts configuration file needs to be set correctly. Set it to text/html;charset=UTF-8 for HTML Unicode support. You can also specify this in the JSP pages by putting the following line at the top of each page:

<%@ page contentType="text/html; charset=utf-8" %>

and this line in the HTML head section:

<head>
 <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
</head>

Another option is to set the content type and encoding scheme in the response object, like this:

response.setContentType("text/html; charset=UTF-8");

However, you might have to do this in several different places, making maintenance and customization more difficult.

You may also have to tell the browser to always send URLs as UTF-8. There's usually a checkbox or option to do this—in IE 5.5, it's in the advanced options section.

There are two final steps to ensure that your application can fully support Unicode. The first is to make sure your database is configured for Unicode. This is usually the default, but you should verify that this is the case. Second, if you're using the JRE rather than the SDK, use the I18N version and not the U.S. version.