13 Internationalization with Wicket - Reference Documentation
Authors: Andrea Del Bene, Carsten Hufe, Christian Kroemer, Daniel Bartl
Version: 1.0.0.BUILD-SNAPSHOT
Table of Contents
13 Internationalization with Wicket
In chapter 10 we have seen how the topic of localization is involved in the generation of feedback messages and we had a first contact with resource bundles. In this chapter we will continue to explore the localization support provided by Wicket and we will learn how to build pages and components ready to be localized in different languages.13.1 Localization
As we have seen in chapter 10, the infrastructure of feedback messages is built on top of Java internationalization (i18n) support, so it should not be surprising that the same infrastructure is used also for localization purpose. However, while so far we have used only the <ApplicationClassName>.properties file to store our custom messages, in this chapter we will see that also pages, components, validators and even Java packages can have their own resource bundles. This allows us to split bundles into multiple files keeping them close to where they are used. But before diving into the details of internationalization with Wicket, it's worthwhile to quickly review how i18n works under Java, see what classes are involved and how they are integrated into Wicket.Providing a full description of Java support for i18n is clearly out of the scope of this document. If you need more informations about this topic you can find them in the JavaDocs and in the official i18n tutorial .
Class Locale and ResourceBundle
Class java.util.Locale represents a specific country or language of the world and is used in Java to retrieve other locale-dependent informations like numeric and date formats, the currency in use in a country and so on. Such kind of informations are accessed through special entities called resource bundles which are implemented by class java.util.ResourceBundle. Every resource bundle is identified by a full name which is built using four parameters: a base name (which is required), a language code, a country code and a variant (which are all optional). These three optional parameters are provided by an instance of Locale with its three corresponding getter methods: getLanguage(), getCountry() and getVariant(). Parameter language code is a lowercase ISO 639 2-letter code (like zh for Chinese, de for German and so on) while country code is an uppercase ISO 3166 2-letter code (like CN for China, DE for Germany and so on). The final full name will have the following structure (NOTE: tokens inside squared brackets are optional):<base name>[ <language code>[ <COUNTRY_CODE>[_<variant code>]]]
- org.foo.MyBundle_zh_CH_cmn
- org.foo.MyBundle_zh_CH
- org.foo.MyBundle_zh
- org.foo.MyBundle
13.2 Localization in Wicket
A component can get the current locale in use calling its method getLocale(). By default this method will be recursively called on component's parent containers until one of them returns a valid locale. If no one of them returns a locale, this method will get the one associated with the current user session. This locale is automatically generated by Wicket in accordance with the language settings of the browser.Developers can change the locale of the current session with Session's method setLocale (Locale locale):Session.get().setLocale(locale)
Style and variation parameters for bundles
In addition to locale's informations, Wicket supports two further parameters to identify a resource bundle: style and variation. Parameter style is a string value and is defined at session-level. To set/get the style for the current session we can use the corresponding setter and getter of class Session:Session.get().setStyle("myStyle");
Session.get().getStyle();
<base name>[ style][ <language code>[ <COUNTRY_CODE>[ <variant code>]]]
<base name>[ variation][_style][ <language code>[ <COUNTRY_CODE>[ <variant code>]]]
Using XML files as resource bundles
Java uses the standard character set ISO 8859-11 to encode text files like properties files. Unfortunately ISO 8859-1 does not support most of the extra-European languages like Chinese or Japanese. The only way to use properties files with such languages is to use escaped Unicode characters, but this leads to not human-readable files. For example if we wanted to write the word 'website' in simplified Chinese (the ideograms are 网站) we should write the Unicode characters u7F51u7AD9. That's why starting from version 1.5, Java introduced the support for XML files as resource bundles. XML files are generally encoded with character sets UTF-8 or UTF-16 which support every symbol of the Unicode standard. In order to be a valid resource bundle the XML file must conform to the DTD available at http://java.sun.com/dtd/properties.dtd .Here is an example of XML resource bundle taken from project LocalizedGreetings (file WicketApplication_zh.properties.xml) containing the translation in simplified Chinese of the greeting message “Welcome to the website!”:<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <entry key="greetingMessage">欢迎光临本网站!</entry> </properties>
Reading bundles from code
Class Component makes reading bundles very easy with method getString(String key). This method searches for a resource with the given key looking into the resource bundles visited by the lookup algorithm illustrated in paragraph 12.4. For example if we have a greeting message with key greetingMessage in our application's resource bundle, we can read it from our component code with this instruction:getString("greetingMessage");
Localization of bundles in Wicket
In chapter 10 we have used as resource bundle the properties file placed next to our application class. This file is the default resource bundle for the entire application and it is used by the lookup algorithm if it doesn't find any better match for a given component and locale. If we want to provide localized versions of this file we must simply follow the rules of Java i18n and put our translated resources into another properties file with a name corresponding to the desired locale. For example project LocalizedGreetings comes with the default application's properties file ( WicketApplication.properties) containing a greeting message:greetingMessage=Welcome to the site!
AbstractReadOnlyModel<String> model = new AbstractReadOnlyModel<String>() { @Override public String getObject() { return getString("greetingMessage"); } };add(new Label("greetingMessage", model));
List<Locale> locales = Arrays.asList(Locale.ENGLISH, Locale.CHINESE, Locale.GERMAN); final DropDownChoice<Locale> changeLocale = new DropDownChoice<Locale>("changeLocale", new Model<Locale>(), locales);StatelessForm form = new StatelessForm("form"){ @Override protected void onSubmit() { Session.get().setLocale(changeLocale.getModelObject()); } };setStatelessHint(true); add(form.add(changeLocale))
Localization of markup files
Although resource bundles exist to extract local-dependent elements from our code and from UI components, in Wicket we can decide to provide different markup files for different locale settings. Just like standard markup files, by default localized markup files must be placed next to component's class and their file name must contain the locale's informations. In the following picture, CustomPanel comes with a standard (or default) markup file and with another one localized for German:When the current locale corresponds to German country (language code de), markup file CustomPanel_de.html will be used in place of the default one.Reading bundles with tag <wicket:message>
String resources can be also retrieved directly from markup code using tag <wicket:message>. The key of the desired resource is specified with attribute key:<wicket:message key="greetingMessage">message goes here</wicket:message>
<input type="submit" value="Preview value" wicket:message="value:key4value"/>
<input type="submit" value="Preview value" wicket:message="value:key4value,title:key4title"/>
13.3 Bundles lookup algorithm
As we hinted at the beginning of this chapter, by default Wicket provides a very flexible algorithm to locate the resource bundles available for a given component. In this paragraph we will learn how this default lookup algorithm works and which options it offers to manage our bundle files.Localizing pages and panels
Similarly to application class, also component classes can have their own bundle files having as base name the class name of the related component and placed in the same package. So for example if class CustomPanel is a custom panel we created, we can provide it with a default bundle file called CustomPanel.properties containing the textual resources used by this panel. This rule applies to page classes as well:One fundamental thing to keep in mind when we work with these kinds of bundles is that the lookup algorithm gives priority to the bundles of the containers of the component that is requesting a localized resource. The more a container is higher in the hierarchy, the bigger is its priority over the other components. This mechanism was made to allow containers to overwrite resources used by children components. As a consequence the values inside the resource bundle of a page will have the priority over the other values with the same key defined in the bundles of children components.To better grasp this concept let's consider the component hierarchy depicted in the following picture:If CustomPanel tries to retrieve the string resource having 'message' as key, it will get the value 'Wellcome!' and not the one defined inside its own bundle file.The default message-lookup algorithm is not limited to component hierarchy but it also includes the class hierarchy of every component visited in the search strategy described so far. This makes bundle files inheritable, just like markup files. When the hierarchy of a container component is explored, any ancestor has the priority over children components. Consider for example the hierarchy in the following picture:Similarly to the previous example, the bundle owned by CustomPanel is overwritten by the bundle of page class BasePage (which has been inherited by CustomPage).Component-specific resources
In order to make a resource specific for a given child component, we can prefix the message key with the id of the desired component. Consider for example the following code and bundle of a generic page:Page code:add(new Label("label",new ResourceModel("labelValue"))); add(new Label("anotherLabel",new ResourceModel("labelValue")));
labelValue=Default value
anotherLabel.labelValue=Value for anotherLabel
Form form = new Form("form"); form.add(new Label("anotherLabel",new ResourceModel("labelValue"))); add(form);
labelValue=Default value anotherLabel.labelValue=Value for anotherLabel form.anotherLabel.labelValue=Value for anotherLabel inside form
Package bundles
If no one of the previous steps can find a resource for the given key, the algorithm will look for package bundles. These bundles have package as base name and they can be placed in one of the package of our application:Packages are traversed starting from the one containing the component requesting for a resource and going up to the root package.Bundles for feedback messages
The algorithm described so far applies to feedback messages as well. In case of validation errors, the component that has caused the error will be considered as the component which the string resource is relative to. Furthermore, just like application class and components, validators can have their own bundles placed next to their class and having as base name their class name. This allows us to distribute validators along with the messages they use to report errors:Validator's resource bundles have the lowest priority in the lookup algorithm. They can be overwritten by resource bundles of components, packages and application class.Extending the default lookup algorithm
Wicket implements the default lookup algorithm using the strategy pattern1. The concrete strategies are abstracted with the interface org.apache.wicket.resource.loader.IStringResource Loader. By default Wicket uses the following implementations of IStringResourceLoader (sorted by execution order):- ComponentStringResourceLoader: implements most of the default algorithm. It searches for a given resource across bundles from the container hierarchy, from class hierarchy and from the given component.
- PackageStringResourceLoader: searches into package bundles.
- ClassStringResourceLoader: searches into bundles of a given class. By default the target class is the application class.
- ValidatorStringResourceLoader: searches for resources into validator's bundles. A list of validators is provided by the form component that failed validation.
- InitializerStringResourceLoader: this resource allows internationalization to interact with the initialization mechanism of the framework that will be illustrated in paragraph 15.4.
@Override public void init() { super.init(); //retrieve IResourceSettings and then the list of resource loaders List<IStringResourceLoader> resourceLoaders= getResourceSettings(). getStringResourceLoaders(); //customize the list...
13.4 Localization of component's choices
Components that inherit from AbstractChoice (such as DropDownChoice, CheckBoxMultiple Choice and RadioChoice) must override method localizeDisplayValues and make it return true to localize the values displayed for their choices. By default this method return false so values are displayed as they are. Once localization is activated we can use display values as key for our localized string resources. In project LocalizedChoicesExample we have a drop-down list that displays four colors (green, red, blue, and yellow) which are localized in three languages (English, German and Italian). The current locale can be changed with another drop-down menu (in a similar fashion to project LocalizedGreetings). The code of the home page and the relative bundles are the following:Java code:public HomePage(final PageParameters parameters) { super(parameters); List<Locale> locales = Arrays.asList(Locale.ENGLISH, Locale.ITALIAN, Locale.GERMAN); List<String> colors = Arrays.asList("green", "red", "blue", "yellow"); final DropDownChoice<Locale> changeLocale = new DropDownChoice<Locale>("changeLocale", new Model<Locale>(), locales); StatelessForm form = new StatelessForm("form"){ @Override protected void onSubmit() { Session.get().setLocale(changeLocale.getModelObject()); } }; DropDownChoice<String> selectColor = new DropDownChoice<String>("selectColor", new Model<String>(), colors){ @Override protected boolean localizeDisplayValues() { return true; } }; form.add(selectColor); add(form.add(changeLocale)); }
selectColor.null=Select a color
green=Green
red=Red
blue=Blue
yellow=Yellow
selectColor.null=Wahlen sie eine farbe
green=Grun
red=Rot
blue=Blau
yellow=Gelb
selectColor.null=Scegli un colore
green=Verde
red=Rosso
blue=Blu
yellow=Giallo
13.5 Internationalization and Models
Internationalization is another good chance to taste the power of models. Wicket provides two built-in models to better integrate our components with string resources: they are ResourceModel and StringResourceModel.ResourceModel
Model org.apache.wicket.model.ResourceModel acts just like the read-only model we have implemented in paragraph 12.3.4. It simply retrieves a string resource corresponding to a given key://build a ResourceModel for key 'greetingMessage' new ResourceModel("greetingMessage");
//build a ResourceModel with a default value new ResourceModel("notExistingResource", "Resource not found.");
StringResourceModel
Model org.apache.wicket.model.StringResourceModel allows to work with complex and dynamic string resources containing parameters and property expressions. The basic constructor of this model takes in input a resource key and another model. This further model can be used by both the key and the related resource to specify dynamic values with property expressions. For example let's say that we are working on an e-commerce site which has a page where users can see an overview of their orders. To handle the state of user's orders we will use the following bean and enum (the code is from project StringResourceModelExample):Bean:public class Order implements Serializable { private Date orderDate; private ORDER_STATUS status; public Order(Date orderDate, ORDER_STATUS status) { super(); this.orderDate = orderDate; this.status = status; } //Getters and setters for private fields }
public enum ORDER_STATUS { PAYMENT_ACCEPTED(0), IN_PROGRESS(1), SHIPPING(2), DELIVERED(3); private int code; //Getters and setters for private fields }
orderStatus.0=Your payment submitted on ${orderDate} has been accepted. orderStatus.1=Your order submitted on ${orderDate} is in progress. orderStatus.2=Your order submitted on ${orderDate} has been shipped. orderStatus.3=Your order submitted on ${orderDate} has been delivered.
Order order = new Order(new Date(), ORDER_STATUS.IN_PROGRESS); add(new Label("orderStatus", new StringResourceModel("orderStatus.${status.code}", Model.of(order))));
PropertyModel propertyModel = new PropertyModel<Order>(order, "orderDate"); //build a string model with two parameters: a property model and an integer value StringResourceModel srm = new StringResourceModel("orderStatus.delay", null, propertyModel, 3);
orderStatus.delay=Your order submitted on ${0} has been delayed by {1} days.
new StringResourceModel("myKey", myComponent, myModel, param1, param2, param3,...);