Internationalizing Your Application with Spring Framework i18n - Adding Spring Framework Into the Mix - PROFESSIONAL JAVA FOR WEB APPLICATIONS (2014)

PROFESSIONAL JAVA FOR WEB APPLICATIONS (2014)

Part II Adding Spring Framework Into the Mix

Chapter 15 Internationalizing Your Application with Spring Framework i18n

IN THIS CHAPTER

· The importance of Spring Framework i18n

· Using Spring’s internationalization and localization APIs

· Configuring Spring’s internationalization support

· How to internationalize your code

· Internationalizing the Customer Support application

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

You can find the wrox.com code downloads for this chapter at http://www.wrox.com/go/projavaforwebapps on the Download Code tab. The code for this chapter is divided into the following major examples:

· Localized-Application Project

· Customer-Support-v12 Project

NEW MAVEN DEPENDENCIES FOR THIS CHAPTER

There are no new Maven dependencies for this chapter. Continue to use the Maven dependencies introduced in all previous chapters.

WHY DO YOU NEED SPRING FRAMEWORK I18N?

In Chapter 7 you learned about internationalization (i18n) and localization (L10n) using the JSTL Internationalization and Formatting tag library (fmt). If you have not read Chapter 7, you do not need to go back and read it now; however, if you don’t understand internationalization, localization, or the formatting tag library, you should go back and read the “Using the Internationalization and Formatting tag library” section of Chapter 7. That section gives you a basic overview of the principals of internationalization and localization and introduces you to using the formatting tag library to achieve these objectives. More important, it covers language codes, region and country codes, variants, Locale, and TimeZone, which you must understand to effectively use Spring Framework’s internationalization support. This chapter uses many of these concepts and technologies but does not re-cover these topics.

In this chapter you explore Spring Framework’s internationalization and localization facilities and learn how using them is much simpler than using the container’s facilities directly. You will come to understand message sources and more Spring JSP tags, and you will finally internationalize and localize the Customer Support application.

Making Internationalization Easier

One of the things you probably decided about the Internationalization and Formatting tag library is that it isn’t exactly the easiest to use. First, you must configure your resource bundles in your deployment descriptor, or in a ServletContainerInitializer orServletContextListener, using the javax.servlet.jsp.jstl.fmt.localizationContext context parameter. The bundles must be classes or files present on the classpath (in /WEB-INF/classes), despite your possibly wanting to obtain them from somewhere else (such as a database, or even just a different file).

Also, you have to implement a way to detect the locale the user wants to use. HttpServletRequest does include getLocale and getLocales methods, which derive the wanted locales from the Accept-Language HTTP request header and return the system default Locale in the absence of that header, but this mechanism works only in limited circumstances. The user must use a computer configured with his preferred language (not always the case, especially on public computers) and the browser must support the Accept-Language header (typical these days, but not guaranteed). After you determine the wanted locale, you must then configure the tag library to use that locale using the javax.servlet.jsp.jstl.core.Config class and the Config.FMT_LOCALE constant. Oh, and don’t forget about remembering to manually change locale settings from request to request, which also isn’t supported automatically.

Spring provides simplifications for all these tasks, enabling you to do less work to support your international users. In addition to this, you can use Spring’s i18n support throughout your code rather than just inside JSPs. Spring’s i18n support leverages, and in some cases wraps, the i18n support built into the Java SE and Java EE platforms and the JSTL. In this chapter, you learn about all these features and more. You also learn about the internationalization and localization concepts they empower.

Localizing Error Messages Directly

One of the drawbacks of Java SE and EE internationalization is the strict reliance on strings as localization keys. To be sure, you ultimately have to look up error codes, and the easiest way to do this is with strings. However, it’s easier if things such as Throwables and validation error objects can pass directly to localization APIs without always making a call to determine the error code before looking up the localized message. Using Spring Framework’s MessageSourceResolvable you can do just that. You can pass any object that implements this interface to any Spring i18n API and resolve it automatically. In the section “Internationalizing Your Code,” you use this to adopt a reliable pattern for handling, logging, and propagating exceptions. In Chapter 16, you use this feature even more for bean validation errors.

USING THE BASIC INTERNATIONALIZATION AND LOCALIZATION APIS

Before you dive into internationalizing your applications, you should be familiar with some basic classes and APIs. Some of these are platform classes and APIs, so they may be familiar to you already. The rest are Spring Framework classes and APIs, and you must understand how these work together.

Understanding Resource Bundles and Message Formats

Just like the standard tag library, Spring Framework i18n uses resource bundles and message formats. It also uses an abstraction above resource bundles called message sources to support an easier API for obtaining localized messages. In practice, a resource bundle is an implementation of java.util.ResourceBundle. A ResourceBundle is a collection (not a Collection) of message keys that are mapped to localized message formats. The important point to notice here is that the keys are message formats, not messages themselves.

Of course, message formats (java.text.MessageFormat) look a lot like localized string messages when stored in a database or properties file. But these message formats can actually contain a variety of placeholder templates that are replaced at run time with supplied argument values. If the types of the values are specified as number, date, or time types, they are automatically formatted properly for the given locale. For example, the following are all U.S. English-localized message formats:

The road is long and windy.

There are {0} cats on the farm.

There are {0,number,integer} cats on the farm.

With a {0,number,percentage} discount, the final price is {1,number,currency}.

The value of Pi to seven significant digits is {0,number,#.######}.

My birthdate: {0,date,short}. Today is {1,date,long} at {1,time,long}.

My birth day: {0,date,MMMMMM-d}. Today is {1,date,MMM d, YYYY} at {1,time,hh:mma).

There {0,choice,0#are no cats|1#is one cat|1<are {0,number,integer} cats}.

Importantly, placeholders are numbered, and when using the message codes, you specify the arguments in the same order as the placeholder numbers, not in the order the placeholders appear in the message. This is because the placeholders might appear in a different order in other languages. Placeholders always follow one of the following syntaxes, where # is the placeholder number and italic text represents user-supplied values:

{#}

{#,number}

{#,number,integer}

{#,number,percent}

{#,number,currency}

{#,number,custom format as specified in java.text.DecimalFormat}

{#,date}

{#,date,short}

{#,date,medium}

{#,date,long}

{#,date,full}

{#,date,custom format as specified in java.text.SimpleDateFormat}

{#,time}

{#,time,short}

{#,time,medium}

{#,time,long}

{#,time,full}

{#,time,custom format as specified in java.text.SimpleDateFormat}

{#,choice,choice format as specified in java.text.ChoiceFormat}

The number, date, and time placeholders follow the same formatting rules as established in the <fmt:formatNumber> and <fmt:dateFormat> tags. This means the date and time placeholders do not currently support the Java 8 Date and Time API. Unfortunately, support for these types is not scheduled until the Java 9 SE release.

When you specify resource bundles using the context parameter javax.servlet.jsp.jstl.fmt.localizationContext, its value is one or more (comma-separated) strings representing the basenames for resource bundles. The JSTL then knows to use these basenames to locate resource bundles when internationalization tags are used. When the JSTL needs to localize a message, it calls one of the getBundle methods on the ResourceBundle class and specifies the basename and Locale. ResourceBundle then constructs a list of possible matching resource bundle names of the following formats:

[baseName]_[language]_[script]_[region]_[variant]

[baseName]_[language]_[script]_[region]

[baseName]_[language]_[script]

[baseName]_[language]_[region]_[variant]

[baseName]_[language]_[region]

[baseName]_[language]

If the Locale does not contain a variant, the first and fourth names are omitted from the list. If it does not contain a region, the second and fifth names are omitted, and the first and fourth names simply contain the script and variant or language and variant, respectively, separated by two underscores (for example, baseName_en__JAVA). If the Locale does not contain a script, the first three names are all omitted. The resulting list is then checked for existing resource bundles with those names.

For each bundle name in the list, ordered with the precedence of the previous list of bundle name formats, ResourceBundle first attempts to load and instantiate a class extending ResourceBundle with the specified bundle name and then returns that class. If no class is found, ResourceBundle then replaces any periods (.) in the name with forward slashes (/), appends .properties to the name, and then looks for a file on the classpath with that name, returning a PropertyResourceBundle for that file if it exists. If, after searching all bundle names, ResourceBundle does not find a matching bundle, it uses the fallback Locale to generate a new list of possible bundle names and searches again. If it still does not find a matching bundle, it looks for a class and then a file matching just the basename with no other qualifiers, and then throws an exception if no bundle is found.

When a ResourceBundle is found and returned, it can then be used to resolve message codes to message format strings. The bundle file consists of standard Java properties-style messages, with keys using message codes and values using MessageFormat strings. You can construct MessageFormat instances from these value strings.

If the basenames you specify in the javax.servlet.jsp.jstl.fmt.localizationContext context parameter are files, you can see how easy this might be to manage. For example, you might have basenames labels and errors with the following files on your classpath:

labels_en.properties

labels_en_US.properties

labels_en_GB.properties

labels_fr_FR.properties

errors_en.properties

errors_en_US.properties

errors_en_GB.properties

errors_fr_FR.properties

Each of these results in its own ResourceBundle over time. However, what if you want to store your messages in a database? You either need a different class for each locale supported, with most classes performing essentially the same logic (selecting values from the database), or you have to implement your own system for resolving ResourceBundle instances. (And even then you still need a separate instance for each supported locale.) This is because a given ResourceBundle instance supports only one locale at a time. Checking the API documentation for ResourceBundle confirms that, indeed, the methods for resolving messages contain no Locale parameters. You should quickly realize that this pattern is unsustainable.

Message Sources to the Rescue

Spring message sources provide an abstraction of and wrapper around resource bundles. Message sources, which implement the org.springframework.context.MessageSource interface, provide three simple methods for resolving a String message using aMessageSourceResolvable object and Locale, or a String message code, object array arguments list, default message, and Locale. The fact that these methods accept Locales means you need only a single MessageSource instance to obtain a localized message for any locale. Furthermore, because they return messages that have already been formatted instead of message formats, MessageSources eliminate one more step (formatting the message) from the task of localizing messages.

Out-of-the-box, Spring Framework provides two implementations of MessageSource:

· org.springframework.context.support.ResourceBundleMessageSource

· org.springframework.context.support.ReloadableResourceBundleMessageSource

ResourceBundleMessageSource actually has a collection of ResourceBundles backing it. It uses the getBundle method of ResourceBundle to locate its bundles, so it essentially uses the exact same strategy (meaning bundle properties files must be on the classpath in /WEB-INF/classes).

One downside of ResourceBundles detected with getBundle is that they are cached forever (until the JVM shuts down, that is), and sometimes that isn’t desirable. The ReloadableResourceBundleMessageSource is, as the name implies, reloadable. It is not backed with aResourceBundle (despite its name), but it follows similar bundle detection rules. Using basenames, it locates bundle files (only files, not classes) using the same algorithm as ResourceBundle. However, these files can be either on the classpath (if the basename starts withclasspath:) or on the file system relative to the context root. Because files loaded on the classpath are typically cached forever, using classpath resources makes the ReloadableResourceBundleMessageSource unreloadable, so this is usually avoided. A typical place to put bundle files for this message source is in /WEB-INF/i18n. Using the MessageSource API is simple:

@Inject MessageSource messageSource;

...

this.messageSource.getMessage("foo.message.key", new Object[] {

argument1, argument2, argument3

}, user.getLocale());

this.messageSource.getMessage("foo.message.key", new Object[] {

argument1, argument2, argument3

}, "This is the default message. Args: {0}, {1}, {2}.", user.getLocale());

Undoubtedly, you should see how much easier MessageSource implementations are to use within Java code. Your Java code no longer requires knowing the right basename, locating a ResourceBundle for the basename and Locale, resolving a message format from the bundle, and then formatting the message. Instead, it just needs to call a single method on an injected MessageSource implementation. This would be infinitely useful in a desktop application. But, in a well-designed web application, how often do you really localize within the Java code?

There are certainly uses cases for using the MessageSource API directly in a web application. For example, when you send e-mails or other notifications you need to localize the contents of those notifications. Also, some web services localize returned error messages if the Accept-Language request header is specified. However, most of your localization takes place in your JSPs, and what good does a MessageSource do you there? The JSTL clearly expects ResourceBundles, not MessageSources.

Using Message Sources to Internationalize JSPs

Spring supports this need by providing the org.springframework.context.support.MessageSourceResourceBundle. (Notice the similarity to ResourceBundleMessageSource; be sure not to confuse these.) MessageSourceResourceBundle extends ResourceBundle and exposes an underlying MessageSource for a particular Locale, delegating calls on the ResourceBundle methods to the underlying MessageSource. Whenever you access a JSP using JstlView, Spring MVC automatically sets up the MessageSourceResourceBundle for the user-specified or default Locale using the javax.servlet.jsp.jstl.fmt.LocalizationContext so that <fmt:message> works properly.

Of course, this works only for JSPs you access when using JstlView (either directly or with a view resolver) from a Spring MVC controller. There are other JSPs that you might access without Spring, such as error pages or simple pages that don’t require a controller. Because Spring is not involved in the request life cycle for these types of JSPs, it cannot set up the MessageSourceResourceBundle automatically.

There are two different tactics you can use to get a MessageSourceResourceBundle for internationalizing these JSPs that aren’t controlled by Spring Framework:

· The easiest approach is to simply use the <spring:message> tag from the Spring tag library instead of the <fmt:message> tag. The <spring:message> tag, which you learn more about later in this chapter, has several advantages over the <fmt:message> tag. One of those advantages is the ability to use a MessageSource directly.

· If you do not want to or cannot use the <spring:message> tag for some reason, the other approach is to create a Filter that applies to all the JSP requests not handled by Spring Framework. This filter, which you need to wire with Spring, would then use theorg.springframework.web.servlet.support.JstlUtils class to mimic the behavior of the JstlView and set up the LocalizationContext. Of course, if you use a technique other than Accept-Language to set the user locale, you need to make sure the user locale is discovered and set before this filter executes on the filter chain.

The following hypothetical filter accomplishes this second approach.

public class JstlLocalizationContextFilter implements Filter

{

private ServletContext servletContext;

@Inject MessageSource messageSource;

@Override

public void doFilter(ServletRequest request, ServletResponse response,

FilterChain chain) throws IOException, ServletException

{

JstlUtils.exposeLocalizationContext(

(HttpServletRequest)request, this.messageSource

);

chain.doFilter(request, response);

}

@Override

public void init(FilterConfig config) throws ServletException

{

this.servletContext = config.getServletContext();

WebApplicationContext context =

WebApplicationContextUtils.getRequiredWebApplicationContext(

this.servletContext);

AutowireCapableBeanFactory factory =

context.getAutowireCapableBeanFactory();

factory.autowireBeanProperties(this,

AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);

factory.initializeBean(this, "jstlLocalizationContextFilter");

this.messageSource = JstlUtils.getJstlAwareMessageSource(

this.servletContext, this.messageSource

);

}

@Override

public void destroy() { }

}

CONFIGURING INTERNATIONALIZATION IN SPRING FRAMEWORK

Now that you understand how message sources and resource bundles work, you are probably eager to learn how to configure them. Configuring a message source in Spring is easy and requires only a few lines of code. However, that’s not all that it takes to get internationalization working properly in Spring.

Most sites provide some way for users to change their locale, and it’s likely that you also want to provide this capability. In addition to temporarily changing their locale, many users will want to permanently set their locale using some sort of user profile setting. These are all things that you must consider. This section discusses the different options, and shows you how to configure internationalization in Spring Framework. You use the Localized-Application project, available on the wrox.com code download site, during this section and the next. It contains the Bootstrap, RootContextConfiguration, and ServletContextConfiguration classes that pick up from the previous chapter.

Creating a Message Source

Creating a message source in Spring Framework is a simple task. All you have to do is create a @Bean method in the RootContextConfiguration class and return the MessageSource implementation of your choice. The bean must be named messageSource.

...

private static final Logger schedulingLogger =

LogManager.getLogger(log.getName() + ".[scheduling]");

@Bean

public MessageSource messageSource()

{

ReloadableResourceBundleMessageSource messageSource =

new ReloadableResourceBundleMessageSource();

messageSource.setCacheSeconds(-1);

messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());

messageSource.setBasenames(

"/WEB-INF/i18n/messages", "/WEB-INF/i18n/errors"

);

return messageSource;

}

@Bean

public ObjectMapper objectMapper()

...

In this case you use the ReloadableResourceBundleMessageSource. You probably immediately noticed that the cache time in seconds was set to -1. This disables reloading and makes the message source cache messages forever (until the JVM restarts).

Why, you may ask, don’t you just use the ResourceBundleMessageSource instead? The ReloadableResourceBundleMessageSource isn’t backed with actual ResourceBundles like the ResourceBundleMessageSource, so it performs better than ResourceBundleMessageSource — but only if you disable reloading. With reloading enabled (cacheSeconds > 0), it takes about twice as long to resolve messages as does ResourceBundleMessageSource. Setting the cache time to -1 is the best-performing configuration you can use in a production environment. In a development environment, you might want to set the cache time to a positive number so that you can change localized messages without restarting Tomcat. This is a perfect candidate for Spring’s Bean Definition Profiles, which you learned about in Chapter 12.

Another thing you probably noticed about the message source configuration is that the default encoding has been set to UTF-8. Spring must know what encoding your properties files are in so that it can read them properly. There’s actually another property,fileEncodings, which you can use to set the encodings of individual files. The defaultEncoding property sets the encoding for only those files not found in the fileEncodings property. Because UTF-8 can encode any character from any known language with as little space as possible, in most cases you just want to set the default encoding to UTF-8 and ensure that all your properties files are encoded in UTF-8. This is vastly simpler than trying to manage different encodings for each file depending on the language it contains.

Finally, the message source is configured with the basenames /WEB-INF/i18n/messages and /WEB-INF/i18n/errors. This means that the message source will look for filenames like /WEB-INF/i18n/messages_en_US.properties, /WEB-INF/i18n/errors_fr_FR.properties, and so on.

Of course, this is just one option of infinite possibilities. Spring comes with only two message sources, both of which use files to load messages, but you may implement MessageSource in any way you need and return that implementation instead. For example, some types of applications host multiple customers, each with many employees or members, and those customers may want to customize the localization for their accounts. This is much easier to manage in a database of some sort, rather than a collection of properties files. Perhaps the perfect solution is a key-value NoSQL database, such as Redis, RavenDB, or MongoDB (which is actually a document database but works great for key-value storage as well). Using a NoSQL repository (perhaps with Spring Data Redis or Spring Data MongoDB), you can easily create a MessageSource that retrieves messages from the database.

Understanding Locale Resolvers

In concept, locale resolvers are similar to view resolvers. Spring uses a locale resolver as a strategy for determining the locale for the current request so that it can determine how to localize messages (and so that it can tell the JSTL how to localize messages). Locale resolvers provide a way to obtain the user’s locale without relying solely on the Accept-Language header. (Though the default implementation, org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver, does just that.) Because you don’t want to rely solely on theAccept-Language header, and you want to provide a way for users to change their locale to something other than their browser’s locale, you don’t want to use the default LocaleResolver implementation. A common alternative to the default isorg.springframework.web.servlet.i18n.SessionLocaleResolver. This resolver uses the following strategy:

· SessionLocaleResolver looks on the current session for the session attribute whose name is equal to the SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME constant. If the attribute exists its value is returned.

· SessionLocaleResolver next checks whether its defaultLocale property is set and returns it if it is.

· Finally, SessionLocaleResolver returns the value of getLocale on the HttpServletRequest (which comes from the Accept-Language header).

Setting up the locale resolver is as simple as creating a new @Bean in your configuration. The DispatcherServlet detects the resolver and automatically uses it for all locale-fetching actions. For example, your request handler methods may have a parameter of typeLocale, and Spring automatically uses the value provided by the LocaleResolver to supply that argument.

JstlUtils also uses this resolver to determine the user’s locale. Because HttpServletRequest automatically returns the server default encoding if no Accept-Language header exists, that is sufficient for a fallback, and you do not need to set a default locale on theSessionLocaleResolver in most cases. (In fact, setting the default locale prevents the resolver from using the Accept-Language header.) When configuring the LocaleResolver @Bean, you should place it in the ServletContextConfiguration. Using the RootContextConfigurationwould cause all DispatcherServlets to use the same LocaleResolver, which is not desirable. The bean must be named localeResolver.

...

}

@Bean

public LocaleResolver localeResolver()

{

return new SessionLocaleResolver();

}

@Bean

public ViewResolver viewResolver()

...

The DispatcherServlet is responsible for setting a LocaleResolver request attribute on each incoming request using the resolver that you configure. This makes the LocaleResolver available to any code executed by the DispatcherServlet or any code that has access to the request object after the DispatcherServlet has set the attribute. It should be clear, then, that error pages and other non-view JSPs do not have access to the LocaleResolver. In the previous section you used a custom JstlLocalizationContextFilter to configure the message source for these pages. You can tweak it slightly to also set the LocaleResolver on the request.

...

private ServletContext servletContext;

private LocaleResolver = new SessionLocaleResolver();

@Inject MessageSource messageSource;

@Override

public void doFilter(ServletRequest request, ServletResponse response,

FilterChain chain) throws IOException, ServletException

{

request.setAttribute(

DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver

);

JstlUtils.exposeLocalizationContext(

(HttpServletRequest)request, this.messageSource

);

...

This code does not use an @Injected LocaleResolver because the filter is wired using the root application context, but the localeResolver bean exists on the child DispatcherServlet application context. The LocaleResolver implementations are all very lightweight objects, so it’s okay to have a duplicate resolver here.

Using a Handler Interceptor to Change Locales

Now your application can determine the user’s desired locale, but how do you set that session attribute if the user wants a different locale? For this you need a handler interceptor. The org.springframework.web.servlet.HandlerInterceptor interface determines how to intercept requests handled in the DispatcherServlet, similar to a Filter. Its preHandle method is executed after the DispatcherServlet receives the request but before it executes the handler method on the controller. The postHandle method is executed after the handler method returns but before the view is rendered. The afterCompletion method executes after the view renders and right before DispatcherServlet returns control to the container.

If you have filter-like behavior you need to implement, you want to do it with a Spring-managed bean, and the behavior needs to apply only to requests served by the DispatcherServlet, using a HandlerInterceptor is a great way to do it.

The org.springframework.web.servlet.i18n.LocaleChangeInterceptor is a HandlerInterceptor for changing the locale when requested. On each request to the DispatcherServlet, it looks for a request parameter, which defaults to locale but can be customized. If this request parameter exists, the interceptor converts the String parameter to a Locale and then uses the LocaleResolver’s setLocale method to set the locale. This way, the LocaleResolver is responsible for determining both how to retrieve the locale and how to set the locale.

To set up the LocaleChangeInterceptor or any other interceptors, you override the addInterceptors method of WebMvcConfigurerAdapter in your ServletContextConfiguration class. If you want to customize the request parameter that the interceptor checks for, you could instantiate the interceptor, call the setParamName method, and then add it to the registry.

...

}

@Override

public void addInterceptors(InterceptorRegistry registry)

{

super.addInterceptors(registry);

registry.addInterceptor(new LocaleChangeInterceptor());

}

@Bean

public LocaleResolver localeResolver()

...

Now, on any page you can add a link to change locales and simply submit it to the current page. This not only changes the locale for the current page, it also changes the locale for all subsequent pages the user visits until his session times out or he closes his browser.

Providing a User Profile Locale Setting

If your application is one that users can sign up for and log in to, chances are they’re going to want to set their locale once and have that locale used automatically every time they come back to the site. Speaking in the abstract, you can provide a profile settings page somewhere for users to change various settings like their name, e-mail address, password, time zone, and locale, among others. But how do you use this setting, and how do you make changes to it immediately visible?

You have a couple of options at your disposal to utilize user profile locale settings. First, you can simply use an @Injected LocaleResolver on your login controller and your profile controller. When users authenticate or update their profile, you simply call setLocale on the resolver to update their current locale.

One disadvantage of this technique — a problem that also exists with the SessionLocaleResolver in general — is that the application forgets a user’s locale after he logs out and closes his browser, or his session times out. When he returns, the application may display in a different language. In these cases, you may want to create a custom LocaleResolver that prefers the logged-in user’s locale and uses a cookie value as a backup. Because the org.springframework.web.servlet.i18n.CookieLocaleResolver already takes care of much of that for you, you could just extend that resolver.

public class UserCookieHeaderLocaleResolver extends CookieLocaleResolver

{

@Override

public Locale resolveLocale(HttpServletRequest request)

{

Locale locale = null;

Principal user = request.getUserPrincipal();

if(user != null && user instanceof FooPrincipal)

locale = ((FooPrincipal)user).getLocale();

if(locale == null)

locale = super.resolveLocale(request);

return locale;

}

}

Because you have several options on how to achieve this task and those options largely depend on your authentication mechanism and user API, this example is not demonstrated in the Localized-Application project.

Including Time Zone Support

When internationalizing your application, locale is not the only topic that you should consider. In addition to language and region, time zones are a major issue for web application users. The majority of users want to see times displayed on a page in their time zone, not the server time zone, especially if that server is on the other side of the world. Often it can be difficult to even know what day it is in different parts of the world! Spring Framework 4.0 now includes first-class support for time zones, including thejava.util.TimeZone and java.time.ZoneId classes. Spring includes PropertyEditors for these types, so you can specify TimeZone and ZoneId method parameters in your controller methods and Spring can convert request parameters, path variables, and header values for these method parameters.

Spring can also resolve the user’s time zone and provide it to your controller methods, similar to how it resolves and provides the user’s locale to your controller methods. However, this mechanism functions differently than locale resolution. There are no TimeZone orZoneId resolvers, and there are no change interceptors. Time zones are handled differently than locales, and they are not normally changed on the fly like locales are. Since the earliest days of Spring, you have been able to manually set the current Locale using theorg.springframework.context.i18n.LocaleContextHolder. This tool serves as both a replacement for and supplement to the various LocaleResolvers, ensuring that you can always manipulate the Locale whenever needed. The Locale is stored in a ThreadLocal variable, following the request throughout the rest of its entire lifecycle.

As of Spring 4.0, LocaleContextHolder also supports setting and retrieving the current TimeZone. You can use the static methods in this class for setting the user’s TimeZone, and Spring automatically sets the JSTL TimeZone property and provides access to TimeZone andZoneId controller method parameters. This makes the task of managing user TimeZones in your applications much simpler. All you have to do is determine which TimeZone a user wants to use and set that TimeZone on the LocaleContextHolder.

Understanding How Themes Can Improve Internationalization

Spring Framework has a concept of themes that is very similar to internationalization support. Themes are collections of Cascading Style Sheets, JavaScript files, images, and other resources necessary to style your site. The Theme interface represents a theme, and the available ThemeResolvers (including a SessionThemeResolver and a CookieThemeResolver) can resolve the appropriate theme for a user. Not surprisingly, the ThemeChangeInterceptor uses a configurable request parameter that defaults to theme to update the user’s selected theme. The themes feature even provides a ResourceBundleThemeSource, nearly identical to the ResourceBundleMessageSource, which loads key-to-resource-path instructions from properties files. Finally, when creating your views you use the <spring:theme> tag, again nearly identical to the <spring:message> tag, to output the appropriate resource URLs for a particular theme.

At this point you’re probably wondering what this has to do with internationalization and localization, other than the API being so similar. Remember that, when internationalizing an application, language content isn’t the only thing you must account for. Different languages around the world also print in different directions.

· English and other western languages read left-to-right and then top-to-bottom.

· Middle Eastern languages such as Arabic and Hebrew typically read right-to-left then top-to-bottom.

· Even more difficult, Japanese, Chinese, and Korean read top-to-bottom and then right-to-left.

· Mongolian reads top-to-bottom and then left-to-right.

If you thought simply translating your application was hard, wait until you try to account for all four language directions!

Spring Framework themes can actually be a big help here. Instead of using a standard ThemeResolver you can create a custom ThemeResolver that sets the theme based on the current locale. Using java.awt.ComponentOrientation’s getOrientation(Locale) method, you can detect the appropriate text direction based on the Locale and then return the correct theme for that text direction. You also want a custom LocaleResolver and LocaleChangeInterceptor that prevent the user from selecting a locale that you don’t support (such as Mongolian). Because the Theme is always based on the Locale, you don’t need a ThemeChangeInterceptor. After this is configured, you can change the text direction of your views using nothing but CSS, greatly reducing the amount of work you have to do over other solutions (such as having custom views for each text direction).

NOTE This topic is obviously advanced, and the complexities of, and issues that arise from, supporting multiple text directions are numerous and outside the scope of this book. For this reason, this is the only mention you will see about text direction in this book. Hopefully, it gives you ideas for how to better support your international users.

INTERNATIONALIZING YOUR CODE

In Chapter 7, you experimented with internationalizing your JSPs using <fmt:message>, <fmt:formatDate>, and <fmt:formatNumber>. This section does not rehash the details of the Formatting and Internationalization Tag Library, but you can use it to internationalize your JSPs. The Localize-Application project contains a HomeController with one mapping in it. The simple handler method adds just a few items to the model and returns a view name.

@Controller

public class HomeController

{

@RequestMapping(value = "/", method = RequestMethod.GET)

public String index(Map<String, Object> model)

{

model.put("date", Instant.now());

model.put("alerts", 12);

model.put("numCritical", 0);

model.put("numImportant", 11);

model.put("numTrivial", 1);

return "home/index";

}

}

The /WEB-INF/i18n/messages_en_US.properties file contains messages localized for U.S. English.

title.alerts=Server Alerts Page

alerts.current.date=Current Date and Time:

number.alerts=There {0,choice,0#are no alerts|1#is one alert|1<are \

{0,number,integer} alerts} in the log.

alert.details={0,choice,0#No alerts are|1#One alert is|1<{0,number,integer} \

alerts are} critical. {1,choice,0#No alerts are|1#One alert is|1<{1,number,\

integer} alerts are} important. {2,choice,0#No alerts are|1#One alert \

is|1<{2,number,integer} alerts are} trivial.

Finally, the /WEB-INF/i18n/messages_es_MX.properties file contains messages localized for Mexican Spanish.

title.alerts=Server Alertas Página

alerts.current.date=Fecha y hora actual:

number.alerts={0,choice,0#No hay alertas|1#Hay una alerta|1<Hay \

{0,number,integer} alertas} en el registro.

alert.details={0,choice,0#No hay alertas son críticos|1#Una alerta es \

crítica|1<{0,number,integer} alertas son críticos}. \

{1,choice,0#No hay alertas son importantes|1#Una alerta es importante\

|1<{1,number,integer} alertas son importantes}. \

{2,choice,0#No hay alertas son triviales|1#Una alerta es trivial\

|1<{2,number,integer} alertas son triviales}.

Using the <spring:message> Tag

If you are familiar with the <fmt:message> tag, the <spring:message> tag should come naturally because it is very similar but ultimately better. The code attribute is the equivalent of the key attribute for <fmt:message> and specifies the message code. Both tags have varand scope attributes responsible for exporting the localized value to an EL variable instead of printing it on the page inline. <spring:message> does not have an equivalent for the bundle attribute because <spring:message> uses a MessageSource instead of a ResourceBundle.

The javaScriptEscape attribute is especially useful because, if set to true, it causes the characters " and ' in the final, formatted message to be replaced with \" and \', respectively, so that it is safe for use in JavaScript strings. By default, this attribute is false, and it has no equivalent in <fmt:message>. The htmlEscape attribute, also unique to <spring:message>, escapes special characters <, >, &, ", and ' in the final, formatted message with their equivalent entity escape sequences if its value is true. By default, its value is false.

If most or all the <spring:message> tags on a page should be HTML escaped, you can use the tag <spring:htmlEscape defaultHtmlEscape="true" /> in your JSP to affect all <spring:message> tags that follow it. If most or all the <spring:message> tags in your entire application should be HTML escaped, you can set the context init parameter defaultHtmlEscape to true in the deployment descriptor or programmatically, and this will affect all <spring:message> tags in your application. For purposes of precedence, the htmlEscape attribute of<spring:message>, if explicitly set, always overrides the <spring:htmlEscape> tag and the context init parameter, and the <spring:htmlEscape> tag, if explicitly used, always overrides the context init parameter.

<context-param>

<param-name>defaultHtmlEscape</param-name>

<param-value>true</param-value>

</context-param>

The final difference between <fmt:message> and <spring:message> comes in how you specify the message to localize. When you use <fmt:message>, you can specify the message using only the key attribute or tag body as the message code and, if necessary, nested<fmt:param> tags for format parameter arguments. <spring:message> is much more flexible, enabling you to use any of the following three strategies. These are all mutually exclusive; you cannot use more than one of these per use of the <spring:message> tag. You can specify the message:

· Traditionally using the code attribute or tag body as the message code and, if necessary, nested <spring:argument> tags for format parameter arguments. The <spring:argument> tag, added in Spring 4.0, works just like the <fmt:param> tag. You can also optionally specify a default message format using the text attribute, and that value is used if the message code does not resolve. You should not use this strategy with the arguments, argumentSeparator, or message attributes.

· Using the code attribute or tag body as the message code and, if necessary, provide a delimited list of arguments in the arguments attribute. By default, the delimiter is a single comma, but you can customize the delimiter using theargumentSeparator attribute. You can also optionally specify a default message format using the text attribute, and that value is used if the message code does not resolve. You should not use this strategy with the message attribute or <spring:argument> nested tag.

· Using an instance of MessageSourceResolvable for the message attribute using an EL expression. Because a MessageSourceResolvable provides its own codes, arguments, and default messages, you should not use this with the code, arguments, argumentSeparator, or text attributes, a tag body, or nested <spring:argument> tags.

Use of the <spring:message> and <fmt:message> tags together is demonstrated in the /WEB-INF/jsp/view/home/index.jsp file of the Localized-Application project. You’ll notice that this file contains absolutely no string literals, but instead uses message internationalization for all text output. This is how it should be done.

<%--@elvariable id="date" type="java.util.Date"--%>

<%--@elvariable id="alerts" type="int"--%>

<%--@elvariable id="numCritical" type="int"--%>

<%--@elvariable id="numImportant" type="int"--%>

<%--@elvariable id="numTrivial" type="int"--%>

<!DOCTYPE html>

<html>

<head>

<title><spring:message code="title.alerts" /></title>

</head>

<body>

<h2><spring:message code="title.alerts" /></h2>

<i><fmt:message key="alerts.current.date">

<fmt:param value="${date}" />

</fmt:message></i><br /><br />

<fmt:message key="number.alerts">

<fmt:param value="${alerts}" />

</fmt:message><c:if test="${alerts > 0}">

<spring:message code="alert.details">

<spring:argument value="${numCritical}" />

<spring:argument value="${numImportant}" />

<spring:argument value="${numTrivial}" />

</spring:message>

</c:if>

</body>

</html>

Handling Application Errors Cleanly

As you well know by now, application errors happen. You can’t prevent them completely. Eventually, something goes wrong and your application does not function properly. Usually, this causes a thrown exception. In Chapter 11, you learned about how logging can help you handle these errors cleanly. However, hiding all errors from users is not an acceptable alternative to displaying all error stack traces to users. When something goes wrong, users need to know. You should log technical details, but you should display a useful error message for users to help them understand what went wrong in the least technical terms possible. This error message must also be localized. You do not want this internationalization to affect the messages written to your logs, just what displays to the user.

You have many different ways to approach this, and it is outside the scope of this book to cover all the possibilities. Instead, this book presents a pattern for your consideration and demonstrates how it can greatly simplify your application development.

When an exception of some expected (but not wanted) type occurs, such as a SQLException when executing SQL statements using JDBC, the natural tendency is to catch the exception and log it. This is okay, but you still need to report that error message to the user somehow. You could rethrow the exception, but then how does a higher layer in the application know that it has already been logged? Also if you simply rethrow the exception, how does a useful error message get presented to the user? A view catching an exception thrown three layers down, after all, has no idea in what context the exception was thrown, and therefore has no ability to create a useful error message for it.

To tackle the first problem, you can create your own custom exception and throw it instead of rethrowing the original exception. You might name it LoggedException and then institute a policy that LoggedExceptions should never be logged and should be rethrown if caught. All the LoggedException constructors require that you include the underlying exception as the cause. This certainly solves the first problem, but it doesn’t solve the second.

A good way to tackle the second problem is to make LoggedException implement MessageSourceResolvable. Then, it contains its own error code, default message, and arguments that you can use to internationalize display of the exception. However, if you think about this for a minute, you will quickly realize that some circumstances require you to throw internationalized exceptions without first catching an underlying exception. So, what you really need is an InternationalizedException that just implementsMessageSourceResolvable, and a LoggedException that extends it. Listing 15-1 shows the InternationalizedException from the Localized-Application project.

LISTING 15-1: InternationalizedException.java

public class InternationalizedException extends RuntimeException

implements MessageSourceResolvable {

private static final long serialVersionUID = 1L;

private static final Locale DEFAULT_LOCALE = Locale.US;

private final String errorCode;

private final String[] codes;

private final Object[] arguments;

public InternationalizedException(String errorCode, Object... arguments) {

this(null, errorCode, null, arguments);

}

public InternationalizedException(Throwable cause, String errorCode,

Object... arguments) {

this(cause, errorCode, null, arguments);

}

public InternationalizedException(String errorCode, String defaultMessage,

Object... arguments) {

this(null, errorCode, defaultMessage, arguments);

}

public InternationalizedException(Throwable cause, String errorCode,

String defaultMessage,Object... arguments) {

super(defaultMessage == null ? errorCode : defaultMessage, cause);

this.errorCode = errorCode;

this.codes = new String[] { errorCode };

this.arguments = arguments;

}

@Override

public String getLocalizedMessage() {

return this.errorCode;

}

public String getLocalizedMessage(MessageSource messageSource) {

return this.getLocalizedMessage(messageSource, this.getLocale());

}

public String getLocalizedMessage(MessageSource messageSource,Locale locale) {

return messageSource.getMessage(this, locale);

}

@Override

public String[] getCodes() {

return this.codes;

}

@Override

public Object[] getArguments() {

return this.arguments;

}

@Override

public String getDefaultMessage() {

return this.getMessage();

}

protected final Locale getLocale() {

Locale locale = LocaleContextHolder.getLocale();

return locale == null ? InternationalizedException.DEFAULT_LOCALE:locale;

}

}

The getLocale method and getLocalizedMessage methods aren’t strictly required but do make it easier to use the exception from within Java code. This exception can be thrown anywhere in your code with or without a causing exception. Extending it is theLoggedException, as shown in Listing 15-2. This exception doesn’t even have any methods or fields; it just restricts the possible constructors to require the user to supply a causing exception.

LISTING 15-2: LoggedException.java

public class LoggedException extends InternationalizedException {

private static final long serialVersionUID = 1L;

public LoggedException(Throwable cause, String errorCode,

Object... arguments) {

this(cause, errorCode, null, arguments);

}

public LoggedException(Throwable cause, String errorCode,

String defaultMessage, Object... arguments) {

super(cause, errorCode, defaultMessage, arguments);

}

}

You already realize the advantage of logging an exception and throwing the LoggedException in its place, which keeps higher layers from catching the exception and relogging it. To demonstrate how easy it is to localize the exception, follow these steps:

1. Add the following to the handler method of your HomeController.

2. model.put("exception", new InternationalizedException(

3. "bad.food.exception", "You ate bad food."

));

4. Add a translation for the message in /WEB-INF/i18n/errors_en_US.properties:

bad.food.exception=You ate bad food.

5. In /WEB-INF/i18n/errors_es_MX.properties, use:

bad.food.exception=Comiste comida en mal estado.

6. Update /WEB-INF/jsp/view/home/index.jsp to display the exception using <spring:message>.

7. ...

8. </c:if>

9. <c:if test="${exception != null}"><br /><br />

10. <spring:message message="${exception}" />

11. </c:if>

12. </body>

</html>

To test out the Localized-Application project, compile it, start Tomcat from your IDE, and go to http://localhost:8080/i18n/ in your favorite browser. You should see the page, nicely displayed in English. Go to http://localhost:8080/i18n/?locale=es_MX and the page should change to Spanish. More important, you can now go back to http://localhost:8080/i18n/ without the locale parameter as many times as you like, and the page still displays in Spanish. Only when you go to http://localhost:8080/i18n/?locale=en_USdoes the page revert to English and stay that way even without the locale parameter.

NOTE If you visit the application on a computer whose locale is set to Spanish, the page should actually display in Spanish first, not English, assuming your browser sends the Accept-Language header. If that’s the case, reverse the instructions and tryen_US as the first locale value, instead.

Updating the Customer Support Application

The Customer-Support-v12 project, which you can get from the wrox.com code download site, is now internationalized using virtually the same configuration as the Localized-Application project. The only difference is that the Customer Support application has an additional resource bundle basename in the message source configuration.

messageSource.setBasenames(

"/WEB-INF/i18n/titles", "/WEB-INF/i18n/messages",

"/WEB-INF/i18n/errors"

);

The i18n files /WEB-INF/i18n/errors_en_US.properties, messages_en_US.properties, and titles_en_US.properties contain dozens of messages between them. There is no localization for any other language. Most of the internationalization is routine use of <spring:message>, but the /WEB-INF/jsp/view/chat/chat.jsp file is an interesting example. This file is full of JavaScript string literals that need to be localized. <spring:message> with javaScriptEscape set to true is handy for this, but that’s not the only dilemma. For example, take a look at this message in messages_en_US.properties, which ultimately will be placed in a JavaScript string:

message.chat.joined=You are now chatting with {0}.

This message is parameterized and requires you to complete arguments. So how does this work when the parameter is not available at time of view rendering, but instead must be populated by JavaScript? Well, quite simply, the formatter just ignores extra replacement templates when arguments don’t exist for them, so this message is written to the JavaScript code as 'You are now chatting with {0}.' at render time. Using that value, then, just requires calling the replace method on the JavaScript string to replace the parameter with the appropriate argument.

infoMessage('<spring:message code="message.chat.joined" javaScriptEscape="true"/>'

.replace('{0}', message.user));

The final challenge when internationalizing the Customer Support application is dealing with chat messages. Unlike all other localized messages, which resolve when the view renders, the ChatService and ChatController output messages over the WebSocket connection must also be localized. The <spring:message> and <fmt:message> tags do you no good here.

Using the Message Source Directly

To solve this problem, the ChatMessage needs to consist of a message code and arguments and be localized programmatically. The ChatController uses the MessageSource directly to accomplish this. The first step to take is to refactor the ChatMessage class.

public class ChatMessage implements Cloneable

{

private Instant timestamp;

private Type type;

private String user;

private String contentCode;

private Object[] contentArguments;

private String localizedContent;

private String userContent;

// mutators and accessors

// enum

// clone

static abstract class MixInForLogWrite

{

@JsonIgnore public abstract String getLocalizedContent();

@JsonIgnore public abstract void setLocalizedContent(String l);

}

static abstract class MixInForWebSocket

{

@JsonIgnore public abstract String getContentCode();

@JsonIgnore public abstract void setContentCode(String c);

@JsonIgnore public abstract Object[] getContentArguments();

@JsonIgnore public abstract void setContentArguments(Object[] c);

}

}

The new MixInForLogWrite and MixInForWebSocket inner classes are special classes to support the Mix-In Annotations feature of Jackson Data Processor. The localizedContent should not be written to the chat log file because it is localized for a particular user. Likewise, the contentCode and contentArguments don’t need to be transmitted over the WebSocket connection because the message is localized already. On the other hand, the user supplies the userContent property, and thus you cannot localize it. You must transmit and write this property to the log as-is. To use these Mix-In Annotations, first add a @PostConstruct method to the DefaultChatService.

@PostConstruct

public void initialize()

{

this.objectMapper.addMixInAnnotations(ChatMessage.class,

ChatMessage.MixInForLogWrite.class);

}

You can now add a different Mix-In class to the ObjectMapper in the ChatMessageDecoderCodec using the static initializer. Because this is a different ObjectMapper instance, it won’t interfere with the Mix-In you just added in the DefaultChatService.

MAPPER.addMixInAnnotations(ChatMessage.class,

ChatMessage.MixInForWebSocket.class);

Next, you need to refactor the DefaultChatService to use message codes and arguments instead of static messages.

public CreateResult createSession(String user)

{

...

message.setContentCode("message.chat.started.session");

message.setContentArguments(user);

...

}

public JoinResult joinSession(long id, String user)

{

...

message.setContentCode("message.chat.joined.session");

message.setContentArguments(user);

...

}

public ChatMessage leaveSession(ChatSession session, String user,

ReasonForLeaving reason)

{

...

if(reason == ReasonForLeaving.ERROR)

message.setType(ChatMessage.Type.ERROR);

message.setType(ChatMessage.Type.LEFT);

if(reason == ReasonForLeaving.ERROR)

message.setContentCode("message.chat.left.chat.error");

else if(reason == ReasonForLeaving.LOGGED_OUT)

message.setContentCode("message.chat.logged.out");

else

message.setContentCode("message.chat.left.chat.normal");

message.setContentArguments(user);

...

}

At this point everything compiles, but the endpoint is not yet localizing messages. It first needs a MessageSource and a Locale:

private Locale locale;

private Locale otherLocale;

...

@Inject MessageSource messageSource;

The locales can’t be injected, so the modifyHandshake method of the ChatEndpoint.EndpointConfigurator class gets the Locale from Spring and adds it to the WebSocket Session.

config.getUserProperties().put(LOCALE_KEY,

LocaleContextHolder.getLocale());

The onOpen method of the ChatEndpoint assigns the locale.

this.locale = EndpointConfigurator.getExposedLocale(session);

...

this.otherWsSession = this.chatSession.getCustomer();

this.otherLocale = EndpointConfigurator

.getExposedLocale(this.otherWsSession);

The ChatEndpoint also needs an internal helper method to make localization easier. Calling this method, you can clone and localize a ChatMessage in one line of code.

private ChatMessage cloneAndLocalize(ChatMessage message, Locale locale)

{

message = message.clone();

message.setLocalizedContent(this.messageSource.getMessage(

message.getContentCode(), message.getContentArguments(), locale

));

return message;

}

You can localize all the places where internally generated ChatMessages are sent. Remember that you cannot localize user-generated ChatMessages because they contain user content, not message codes.

...

session.getBasicRemote().sendObject(this.cloneAndLocalize(

result.getCreateMessage(), this.locale

));

...

session.getBasicRemote().sendObject(this.cloneAndLocalize(

this.chatSession.getCreationMessage(), this.locale

));

session.getBasicRemote().sendObject(this.cloneAndLocalize(

result.getJoinMessage(), this.locale

));

this.otherWsSession.getBasicRemote()

.sendObject(this.cloneAndLocalize(

result.getJoinMessage(), this.otherLocale

));

...

this.wsSession.getBasicRemote()

.sendObject(this.cloneAndLocalize(

message, this.locale

));

this.wsSession.close(closeReason);

...

this.otherWsSession.getBasicRemote()

.sendObject(this.cloneAndLocalize(

message, this.otherLocale

));

this.otherWsSession.close(closeReason);

Now that the internationalization of the Customer Support application is complete, compile it, start Tomcat from your IDE, and go to http://localhost:8080/support/. Log in and browse around. Create, list, and view tickets, and view the list of sessions. Log in from another browser and engage in a chat session. Internationalization and localization clearly take a great deal of effort, but the tools provided by Spring Framework make the task much easier.

SUMMARY

In this chapter, you have learned a great deal about internationalization (i18n) and localization (L10n) in both concept and practice. You have witnessed how difficult these tasks are and experimented with all the Spring Framework tools provided to make it easier. You internationalized JSP views, using <spring:message> and <fmt:message>, and Java strings, using Spring’s MessageSource. You also learned about all your options when configuring Spring’s internationalization and localization support and were introduced to a Logged and Internationalized Exception pattern. Finally, you got a look at support for user time zones and a brief introduction on the complexities of text direction in non-western locales.

In the next chapter, you learn about JSR 303/JSR 349 automatic bean validation and Hibernate Validator. The chapter relates closely to this one, and you will find that the skills and tools you discovered in this chapter are indispensable as you familiarize yourself with bean validation.