15. Internationalization with Wicket
In chapter 12.2 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.
15.1. Localization
As we have seen in paragraph 12.2, 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
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 . |
15.1.1. 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>]]]
For example a bundle with MyBundle as base name and localized for Mandarin Chinese (language code zh, country code CH, variant cmn) will have MyBundle_zh_CH_cmn as full name. A base name can be a fully qualified class name, meaning that it can include a package name before the actual base name. The specified package will be the container of the given bundle. For example if we use org.foo.MyBundle as base name, the bundle named MyBundle will be searched inside package org.foo. The actual base name (MyBundle in our example) will be used to build the full name of the bundle following the same rules seen above. ResourceBundle is an abstract factory class, hence it exposes a number of factory methods named getBundle to load a concrete bundle. Without going into too much details we can say that a bundle corresponds to a file in the classpath. To find a file for a given bundle, getBundle needs first to generate an ordered list of candidate bundle names. These names are the set of all possible full names for a given bundle. For example if we have org.foo.MyBundle as base name and the current locale is the one seen before for Mandarin Chinese, the candidate names will be:
org.foo.MyBundle_zh_CH_cmn
org.foo.MyBundle_zh_CH
org.foo.MyBundle_zh
org.foo.MyBundle
The list of these candidate names is generated starting from the most specific one and subtracting an optional parameter at each step. The last name of the list corresponds to the default resource bundle which is the most general name and is equal to the base name. Once that getBundle has generated the list of candidate names, it will iterate over them to find the first one for which is possible to load a class or a properties file. The class must be a subclass of ResourceBundle having as class name the full name used in the current iteration. If such a class is not found, getBundle will try to locate a properties file having a file name equals to the current full name (Java will automatically append extension .properties to the full name). For example given the resource bundle of the previous example, Java will search first for class org.foo.MyBundle_zh_CH_cmn and then for file MyBundle_zh_CH_cmn.properties inside package org.foo. If no file is found for any of the candidate names, a MissingResourceException will be thrown. Bundles contains local-dependent string resources identified by a key that is unique in the given bundle. So once we have obtained a valid bundle we can access these objects with method getString (String key).
As we have seen before working with feedback messages, in Wicket most of the times we will work with properties files rather than with bundle classes. In paragraph 12.2 we used a properties file having as base name the class name of the application class and without any information about the locale. This file is the default resource bundle for a Wicket application. In paragraph 15.3 we will explore the algorithm used in Wicket to locate the available bundles for a given component. Once we have learnt how to leverage this algorithm, we will be able to split our bundles into more files organized in a logical hierarchy.
15.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)
15.2.1. 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();
If set, style’s value contributes to the final full name of the bundle and it is placed between the base name and the locale’s informations:
<base name>[_style][_<language code>[_<COUNTRY_CODE>[_<variant code>]]]
Wicket gives the priority to candidate names containing the style information (if available). The other parameter we can use for localization is variation. Just like style also variation is a string value, but it is defined at component-level. The value of variation is returned by Component’s method getVariation(). By default this method returns the variation of the parent component or a null value if a component hasn’t a parent (i.e. it’s a page). If we want to customize this parameter we must overwrite method getVariation and make it return the desired value.
Variation’s value contributes to the final full name of the bundle and is placed before style parameter:
<base name>[_variation][_style][_<language code>[_<COUNTRY_CODE>[_<variant code>]]]
15.2.2. Using UTF-8 for 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 \u7F51\u7AD9. For this reason ISO 8859-11 is being replaced with another Unicode-compliant character encoding called UTF-8. Text files created with this encoding can contain Unicode symbols in plain format. Wicket provides a useful convention to use properties file encoded with UTF-8. We just have to add prefix .utf8. to file extension (i.e. .utf8.properties).
If you want to use UTF-8 with your text files, make sure that your editor/IDE is actually using this character encoding. Some OS like Windows use a different encoding by default. |
15.2.3. Using XML files as resource bundles
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>
To use XML bundles in Wicket we don’t need to put in place any additional configuration. The only rule we have to respect with these files is to use properties.xml as extension while their base name follows the same rules seen so far for bundle names.
15.2.4. 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 15.3. 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");
15.2.5. Localization of bundles in Wicket
In paragraph 12.2 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!
Along with this file we can also find a bundle for German (WicketApplication_de.properties) and another one in XML format for simplified Chinese (WicketApplication_zh.properties.xml). The example project consists of a single page (HomePage.java) displaying the greeting message. The current locale can be changed with a drop-down list and the possible options are English (the default one), German and simplified Chinese:
The label displaying the greeting message has a custom read-only model which returns the message with method getString. The initialization code for this label is this:
IModel<String> model = () -> getString("greetingMessage");
add(new Label("greetingMessage", model));
The rest of the code of the home page builds the stateless form and the drop-down menu used to change the locale.
List<Locale> locales = Arrays.asList(Locale.ENGLISH, Locale.CHINESE, Locale.GERMAN);
final DropDownChoice<Locale> changeLocale =
new DropDownChoice<Locale>("changeLocale", new Model<Locale>(), locales);
StatelessForm<Void> form = new StatelessForm<Void>("form"){
@Override
protected void onSubmit() {
Session.get().setLocale(changeLocale.getModelObject());
}
};
setStatelessHint(true);
add(form.add(changeLocale))
15.2.6. 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.
15.2.7. Reading bundles with tag
String resources can be also retrieved directly from markup code using tag
<wicket:message key="greetingMessage">message goes here</wicket:message>
By default the resource value is not escaped for HTML entities. To do that use the escape attribute:
<wicket:message key="greetingMessage" escape="true">message goes here</wicket:message>
wicket:message can be adopted also to localize the attributes of a tag. The name of the attribute and the resource key are expressed as a colon-separated value. In the following markup the content of attribute value will be replaced with the localized resource having ‘key4value’ as key:
<input type="submit" value="Preview value" wicket:message="value:key4value"/>
If we want to specify multiple attributes at once, we can separate them with a comma:
<input type="submit" value="Preview value" wicket:message="value:key4value, title:key4title"/>
Finally, we can work with more complex text templates nesting components within a wicket:message element. For example:
<wicket:message key="myKey">
This text will be replaced with text from the properties file.
<span wicket:id="amount">[amount]</span>.
<a wicket:id="link">
<wicket:message key="linkText"/>
</a>
</wicket:message>
myKey=Your balance is ${amount}. Click ${link} to view the details.
linkText=here
and
add(new Label("amount",new Model("$5.00")));
add(new BookmarkablePageLink("link",DetailsPage.class));
Results in:
Your balance is $5.00. Click <a href="...">here</a> to view the details.
15.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.
15.3.1. 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).
15.3.2. 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")));
Page bundle:
labelValue=Default value
anotherLabel.labelValue=Value for anotherLabel
Label with id anotherLabel will display the value ‘Value for anotherLabel’ while label label will display ‘Default value’. In a similar fashion, parent containers can specify a resource for a nested child component prepending also its relative path (the path is dot-separated):
Page code:
Form<Void> form = new Form<>("form");
form.add(new Label("anotherLabel",new ResourceModel("labelValue")));
add(form);
Page bundle:
labelValue=Default value
anotherLabel.labelValue=Value for anotherLabel
form.anotherLabel.labelValue=Value for anotherLabel inside form
With the code and the bundle above, the label inside the form will display the value ‘Value for anotherLabel inside form’.
15.3.3. 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 wicket-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.
15.3.4. 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.
15.3.5. Extending the default lookup algorithm
Wicket implements the default lookup algorithm using the strategy pattern. The concrete strategies are abstracted with the interface org.apache.wicket.resource.loader.IStringResourceLoader. 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 18.3.
NestedStringResourceLoader: allows to replace nested Strings and can be chained up with other resource loader
Developer can customize lookup algorithm removing default resource loaders or adding custom implementations to the list of the resource loaders in use. This task can be accomplished using method getStringResourceLoaders of setting class org.apache.wicket.settings.ResourceSettings:
@Override
public void init()
{
super.init();
//retrieve ResourceSettings and then the list of resource loaders
List<IStringResourceLoader> resourceLoaders = getResourceSettings().
getStringResourceLoaders();
//customize the list...
15.4. Localization of component’s choices
Components that inherit from AbstractChoice (such as DropDownChoice, CheckBoxMultipleChoice 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<Void> form = new StatelessForm<Void>("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));
}
Default bundle (English):
selectColor.null=Select a color
green=Green
red=Red
blue=Blue
yellow=Yellow
German bundle:
selectColor.null=Wählen Sie eine Farbe
green=Grün
red=Rot
blue=Blau
yellow=Gelb
Italian bundle:
selectColor.null=Scegli un colore
green=Verde
red=Rosso
blue=Blu
yellow=Giallo
Along with the localized versions of colors names, in the bundles above we can also find a custom value for the placeholder text (“Select a color ”) used for null value. The resource key for this resource is ‘null’ or ‘
15.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.
15.5.1. ResourceModel
Model org.apache.wicket.model.ResourceModel acts just like the read-only model we have implemented in paragraph 15.3. It simply retrieves a string resource corresponding to a given key:
//build a ResourceModel for key 'greetingMessage'
new ResourceModel("greetingMessage");
We can also specify a default value to use if the requested resource is not found:
//build a ResourceModel with a default value
new ResourceModel("notExistingResource", "Resource not found.");
15.5.2. 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
}
Enum:
public enum ORDER_STATUS {
PAYMENT_ACCEPTED(0),
IN_PROGRESS(1),
SHIPPING(2),
DELIVERED(3);
private int code;
//Getters and setters for private fields
}
Now what we want to do in this page is to print a simple label which displays the status of an order and the date on which the order has been submitted. All the informations about the order will be passed to a StringResourceModel with a model containing the bean Order. The bundle in use contains the following key/value pairs:
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.
The values above contain a property expression (${orderDate}) that will be evaluated on the data object of the model. The same technique can be applied to the resource key in order to load the right resource according to the state of the order:
Order order = new Order(new Date(), ORDER_STATUS.IN_PROGRESS);
add(new Label("orderStatus", new StringResourceModel("orderStatus.${status.code}", Model.of(order))));
As we can see in the code above also the key contains a property expression (${status.code}) which makes its value dynamic. In this way the state of an object (an Order in our example) can determinate which resource will be loaded by StringResourceModel. If we don’t use properties expressions we can provide a null value as model and in this case StringResourceModel will behave exactly as a ResourceModel. StringResourceModel supports also the same parameter substitution used by standard class java.text.MessageFormat.
Parameters can be generic objects but if we use a model as parameter, StringResourceModel will use the data object inside it as actual value (it will call getObject on the model). Parameters are passed as a vararg argument with method setParameters(Object… parameters). Here is an example of usage of parameter substitution:
Java code:
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").setParameters(propertyModel, 3);
Bundle:
orderStatus.delay=Your order submitted on ${0} has been delayed by {1} days.
One further parameter we can specify when we build a StringResourceModel is the component that must be used by the lookup algorithm. Normally this parameter is not relevant, but if we need to use a particular bundle owned by a component not considered by the algorithm, we can specify this component as second parameter. If we pass all possible parameters to StringResourceModel’s constructor we obtain something like this:
new StringResourceModel("myKey", myComponent, myModel);
Default value is supported as well, both as string model or as string value:
new StringResourceModel("myKey", myComponent, myModel).setDefaultValue("default");
15.6. Summary
Internationalization is a mandatory step if we want to take our applications (and our business!) abroad. Choosing the right strategy to manage our localized resources is fundamental to avoid to make a mess of them. In this chapter we have explored the built-in support for localization provided by Wicket, and we have learnt which solutions it offers to manage resource bundles. In the final part of the chapter we have seen how to localize the options displayed by a component (such as DropDownChoice or RadioChoice) and we also introduced two new models specifically designed to localize our components without introducing in their code any detail about internationalization.