Localization
Localization is something more and more web sites are having to deal with. Localization is never easy, but Stripes does what it can to make it as painless as possible. In this document we'll cover:
Determining the Locale to use
The first challenge you're likely to face when building a localized application is to figure out what Locale to use for a given request. Chances are you will support a limited number of languages and locales. When a user submits a request, the request can contain a ordered list of locales that the user prefers. Somehow you have to decide, based upon your supported locales, and the user's preferred locales, what locale to serve them with. This is all compounded by the fact that locales can be one, two or three segments long, denoting the language, locale and variant.
Stripes uses a LocalePicker to determine the locale to use for a request. The LocalePicker
is executed from the Stripes Filter, so it will execute even for direct-to-JSP navigation (read: you don't have to go through an ActionBean to take advantage of it). Once the LocalePicker
has determined the locale to use, Stripes uses a HttpServletRequestWrapper
to make calls to request.getLocale()
and request.getLocales()
to return only the chosen locale. This means that not only will Stripes use the correct locale without having to re-determine it, but that any other localization tool that relies on request.getLocale[s]()
should also default to the correct locale. This includes the JSTL fmt:*
tags - cool huh?
Stripes uses the DefaultLocalePicker by default. The DefaultLocalePicker
uses a configured list of locales to determine the locales that the system supports. If no list is supplied it will be defaulted to a single locale equal to the system locale, i.e. the one obtained by calling java.util.Locale.getDefault()
. An example list for a site that supports English (of the American variety) and Japanese might look like this:
<init-param> <param-name>LocalePicker.Locales</param-name> <param-value>en_US,ja</param-value> </init-param>
At request time the DefaultLocalePicker
runs through the user's preferred locale list, and tries to match it against the locales available in the system. The picking algorithm is such that it prefers more matching segments to less, and for the same number of matching segments, prefers locales higher up the user's preference list.
While it is clear that en_US_foo matches en_US_foo with three segments, it is also the case that en matched against en would be treated as having all three segments match. This is because in the second case, the locale and variant segments are null in both strings, and so are treated as equal.
Stripes performs locale picking on every request. This means that if a user changes their browser settings within a session, an application may serve up pages using different locales to the same user in the same session. If you want to ensure that the same locale is used for the duration of a user's session, all you need to do is extend the DefaultLocalePicker
and override the pickLocale()
method to place the picked locale in session, and check for it there on subsequent invocations.
Similarly if you want to employ a different algorithm to pick locales, all you need to do is implement your own LocalePicker and then configure Stripes to use it.
Determining the Character Encoding to use
Determining the correct character encoding to use can also be extremely tricky. Most browsers do not indicate a character encoding when submitting form data, and as a result the server will, by default, use the system character encoding. This can be quite problematic, especially when you handle multiple character encodings!!!
To assist in this process, Stripes allows you to designate a character encoding per Locale; simply follow the Locale with a colon and then the name of the character encoding. For example:
<init-param> <param-name>LocalePicker.Locales</param-name> <param-value>en_US:UTF-8,ja:Shift_JIS</param-value> </init-param>
If a character encoding is specified then Stripes will ensure that all character data in the HttpServletRequest is converted to characters using the specified encoding, and that all character data sent back to the client will be encoded in the same encoding. If no character encoding is specified for locale, then Stripes does not intervene and whatever encoding the Servlet container picks will be used.
Finding localized resources
Stripes recognizes two types of localizable information that it uses. The first type is error messages. The second is field names. Each type of information is looked up using a resource bundle. If you are writing a localized application it is worth spending some time understanding resource bundles in depth - I'm not going to explain everything about them here. Suffice to say that probably the most common type of resource bundle is the PropertyResourceBundle which is simply a set of properties files with similar names.
By default Stripes uses a single resource bundle for both error messages and field names. The bundle name is StripesResources
. This means that Stripes looks for properties files (in the classpath) with names like StripesResources.properties
, StripesResources_en_US.properties
etc. Resource bundles are semi-intelligent, so that if an exact match is not found, it will always return a bundle and will try to use the best match available.
There are two ways you can change the bundles that Stripes uses. The easiest way is to configure the DefaultLocalizationBundleFactory
with different name for the bundles. You can use different bundles for error messages and field names, or use the same bundle - it's up to you.
Bundle names should always consist of the prefix (e.g. StripesResources) and never the file name (e.g. StripesResources.properties). There are multiple types of bundles, so if the properties file bundle does not meet your needs, check out the JDK documentation for other strategies.
Localized validation
There's actually not a huge deal to be said about localized validation. The built in number and date type converters use the localization capabilities of the java.text
package to perform localized type conversion. So, if a user is being served pages localized into French, it is expected that the user is inputting numbers and dates appropriate to the French locale.
The BooleanTypeConverter and the EnumeratedTypeConverter do not accept localized input. Booleans are not normally displayed to the user as text or hand entered, and since the JDK doesn't provide support for localizing Booleans, neither does Stripes. If you need to do this, it shouldn't be hard to write your own(use a dictionary to lookup the words true and false in a bunch of languages etc. etc.). EnumeratedTypes by definition have a limited number of values, and these are determined at compile time, so it does not make sense to localize them.
Localized error messages
All the validation errors produced by Stripes' built in validation and type converters are instances of ScopedLocalizableError. This ensures that the error message displayed to the user can be localized if and when necessary. Take a look at the Validation Reference to see the names of the validation error messages used.
It's fairly common to use field names in validation messages, and there are a number of ways to do this. Your first option is to just hard-code the field name into the message. Since the messages themselves can be localized, that'll work, but since you might use the same error message again and again, for different fields, you'll have to copy/paste/edit that error many times.
A better strategy is to use the replacement parameters provided to insert the field name into a message template. Thus you can define a global message - {0} is a required field
- instead of writing hundreds of messages with field names embedded. The {0} is replaced at runtime with the name of the field... but the field name has to come from somewhere!
If you don't define field names, Stripes will attempt to provide something vaguely user-friendly by decomposing the form field name. But this will never be localized. The better alternative is to provide localized values for your field names. This is done in the field name bundle (by default StripesResources). The syntax is:
actionPath.fieldName=Field Name # or just... fieldName=Field Name
This allows you to be specific about field names within a form, but to also define field names that are used in many places just once. For example, if you have a form with action="/security/login.action"
, and a field called user.password
, Stripes will first look for a resource declaration like:
/security/login.action.user.password=Secret Magic Word
If it cannot find the above resource, it will then look for a resource declaration like:
user.password=Secret Magic Word
At first blush the property names with slashes look a little odd, but you'll get used to it!
Localizing errors in your own code
There are (at least) two cases that may arise where you will need to manufacture errors in an application and if your application is localized, these should be too.
Firstly, if you write your own TypeConverter classes you may need to provide validation errors when conversion fails. You should probably use ScopedLocalizableError in this case, since it will allow you to provide default error messages and then override them in specific places.
Secondly, if any of your ActionBeans implement Validatable
you will need to produce your own error messages for custom validation failures. Since these errors are likely to be specific to the ActionBean, it is suggested that you use the LocalizableError class. The name of the error message is entirely up to you, but it is suggested that it should start with the action path in order to be consistent with other error message keys.
Localized buttons and labels
Localized buttons and labels are really just a special case of localized field names as described in the 'Localized Error Messages' section above. Stripes provides the following tags for generating form buttons: <stripes:button.../>, <stripes:submit.../> and <stripes:reset.../>
, and a single tag for generating form field labels: <stripes:label.../>
. While each of these tags allow you to determine the value displayed to the user directly on the page, they also support localized values using field name lookups. At the risk of repeating what was said above, if you have a form like:
<stripes:form action="/security/login.action"> <stripes:label for="username"/>: <stripes:text name="username"/> <stripes:submit name="login"/> <stripes:reset name="reset"/> </stripes:form>
then you will probably want to define the following resources in your field name bundle:
# Like this... /security/login.action.username=Username /security/login.action.login=Log In /security/login.action.reset=Clear Form # Or Maybe just like this... username=Username login=Log In reset=Clear Form
If neither of the possible localization resources are available for a given button or label, the tag will then examine both the body of the tag and the value
attribute. If the body is non-null and non-empty that will be used, otherwise the value
attribute will be used. This means that if you need to use a localization bundle or property names that do not line up with the way Stripes expects them, you can do something like this:
<stripes:submit name="save"><fmt:message bundle="MyBundle" key="button.save"/></stripes:submit>
Pass localized parameters to a layout
If you are using the Layout features of stripes you may want to pass localized values to the layout. See the section Passing additional information to the layout for more information about how to pass arbitrary formatted (including localized) content to a layout.
Localizing other things
Stripes doesn't provide tools to localize your entire application for several reasons:
- That's a lot of work
- People like to localize in different ways (e.g. one localizable JSP vs. one JSP per locale)
- The JSTL formatting tags integrate very nicely with Stripes as a default option
Using stripes with the JSTL formatting tags provides a neat and more complete localization solution. There are a couple of things to take into account.
Firstly, there is absolutely nothing wrong with using the same resource bundle(s) to provide Stripes with localized resources and to provide localized resources through the JSTL tags. There are no issues with doing this, so for example, if you need field names displayed in other places on the page, there's no reason not to access the StripesResources bundle (or whatever you are using). Configure the JSTL localization by specifying the name of the stripes resource bundle in your web.xml:
<context-param> <param-name> javax.servlet.jsp.jstl.fmt.localizationContext </param-name> <param-value>StripesResources</param-value> </context-param>
Secondly, you should rarely have a need to use the <fmt:setLocale.../>
tag. Because of Stripes' strategy of using a filter and a request wrapper, all components in your application, including the JSTL tags, will always get the right locale when they call request.getLocale().
One exception is that <fmt:message>
does not call request.getLocale()
(1). In the case that a request sends no accept-language headers, you will need to include a call to <fmt:setLocale value="${pageContext.request.locale}"/>
before <fmt:message>
.