i18n in Wicket
Date: 9 February 2006<p>
Author: David Leangenhttp://www.leangen.net
Overview
In Wicket, i18n is supported right out of the box. In most cases, i18nising an application is very simple.
However, I was not satisfied with the way that links are supported when considering the i18n of an application. I therefore tinkered with Wicket to try to make it do what I want.
I am quite sure that the core Wicket developers will give me a serious slap on the wrist for my approach, as it is certainly not how Wicket was intended to be used. For me, though, it was the "lowest hanging fruit", so I picked it and it works for me.
Hopefully, this document will:
*help others who need to add serious i18n support to their applications
*act as a model with which, if I'm lucky, the Wicket team will agree with and decide to add something similar (but well-developed) in the core Wicket distribution
Be sure to visit this page again in a few weeks, as I sure others will provide excellent input to this approach.
Wicket i18n Support Out-of-the-Box
i18n of Labels and Components
We know now (right?) that to add a Label to a page, we write:
<span wicket:id="page.label">Label contents</span>
and
add( new Label( "page.label", new PropertyModel( this, "propertyName" ) );
which accesses a property from the page class. No problem.
So, what do we do if we want to i18nise this label?
Don't do this!
The instinctive approach for some people is to write:
if( getSession().getLocale h1. Locale.ENGLISH ) add( new Label( "page.label", new PropertyModel( this, "enPropertyName" ) ); else if( getSession().getLocale Locale.FRENCH ) add( new Label( "page.label", new PropertyModel( this, "frPropertyName" ) ); ...
which is a very bad idea!
Rather, use the support provided by Wicket. By setting up properties files for your pages, wicket will take care of the details for you. So, you would set up files like so:
MyPage.properties
page.label=hello
MyPage_fr.properties
page.label=salut
etc.
MyPage.properties is used for the default language for the website, MyPage_xx.properties is used for other languages, replacing xx by the language code.
You can then activate the properties files by Java code such as this:
add( new Label( "page.label", new ResourceModel( "page.label" ) );
However, Wicket provides a much quicker and simpler solution with a dedicated tag for the html, that means there is no Java code to write at all:
<wicket:message key="page.label">Default label</wicket:message>
If you want to use Wicket's i18n in other HTML elements, for example:
<input type="submit" value="Search"/>
You can't use the <wicket:message/> component, you should use the following:
<input type="submit" wicket:message="value:page.search"/>
i18n of Entire Pages
To i18nise an entire page, it's the same simple approach as for a label. We simply write entire pages with the locale name appended to the page name:
MyPage.html (default)
MyPage_en.html
MyPage_fr.html
etc.
Extending the Wicket Model
Motivations
The out-of-box support provided above is excellent! It shows that for Wicket, i18n is supported in the core of the application, and is not added as an afterthought. Great!
However, for me, it doesn't quite fit with my vision of how an i18nised website should be developed. The main problem, IMHO, is the interaction between content authors and developers when it comes to adding in links.
The Wicket approach is very clear: pure content is in the domain of the content authors (HTML files), while pure logic is in the domain of the developers (java classes). Where the two concerns meet is via the wicket:id attribute.
Some people think that links should be treated just like any other component. I tend to feel, however, that links do not fit in so well in the "component" mould and deserve special treatment. In order to be properly i18nised, links should be treated as "special" components that differ from other components in the following ways:
- two parameters are required: the id and the locale
- a link may appear in one l10n, but not in another; therefore, since a component MUST be rendered, a typical component cannot be used, as it will throw an exception if the content author does not include it
By the way, I am sure that this section will be controversial, and I will get
my wrist slapped by the Wicket developers. I understand that Wicket was not
intended to be used this way, but it was the easiest way for me to do what
I actually wanted to do.
Link Modeling
The goal is to arm the content author with a set of wicket links. To ensure (1) quality control and (2) the DRY principle, we want to centralise our links in one place, whether it be a java class, a database, or whatever (which I'll simply call the "persistence layer" in this document). Since the goal of the exercise is to i18nise the application, we also want to be able to provide different link targets for different locales.
For instance, a model for a link could be something like:
public interface Link { String getId(); URL getUrl(); LinkType getType(); String getDefaultDisplayText(); String getLocale(); }
and to retrieve this type of link (from the DB or whatever), you could use something like this:
public interface LinkService { Link getLink( final String key, final String locale ); }
Note: normally, java.util.Locale should be used in place of a String for the locale,
but I was having a few headaches and just wanted to get this done, so I used a
shortcut.
So, with the above model, we would need two pieces of information to retrieve the correct link from the persistence layer: the id and the locale.
As an example, let's say that the famous company Acme has an English website, a French website, and a Japanese website (no discrimination intended). Suppose further that Acme doesn't do any browser detection and that each locale is available at a different URL, such as:
English: www.acme.com/english/
French: www.acme.com/francais/
Japanese: www.acme.co.jp
In our model, we consider the above URLs as the same link, but localised. Depending on our application, we may want to access one target or the other, or maybe even none at all. The key is that the choice of the target should be completely left up to the content author.
The Problem with Wicket Components
Using Wicket components, we would do something like this:
In Page.html:
Take a look at <a wicket:id="links.acme">the ACME website</a>.
In Page_fr.html:
Consultez <a wicket:id="links.acme">le site ACME</a>.
And perhaps something like Page_ja.html:
<a wicket:id="links.acme">ã?â€Å"ã?¡ã‚‰</a>ã?«ã?â€?覧ã??ã? ã?•ã?„.
and in our Page.java:
add( new i18nLink( "links.acme" );
Our i18nLink component has all the information it needs, since the id is provided and it can get the locale from the session with getSession().getLocale().
Simple, right? Well, it turns out that there are few major flaws to this approach in the current version of Wicket (1.2).
First, if we reference our requirements above, depending on the locale, a page may or may not contain the link with ID "links.acme". Perhaps we want to add a German page, but ACME does not exist in German. What do we do? Or perhaps we want to simply leave out the link, since we believe that Germans won't be interested in ACME. The problem is that when we access the German l10n of the Page, Wicket will throw an exception, since we delare the component in the java but it does not appear in the markup.
Second, we may not want to use the default locale for the link. Perhaps we have a page that looks something like this:
I found a very interesting document <a wicket:id="links.document">here</a>.
(The original Salishan version can be found <a wicket:id="links.document">here</a>.
Huh? Obviously something is missing. We would need something like this:
I found a very interesting document <a wicket:id="links.document" locale="en">here</a>.
(The original Salishan version can be found <a wicket:id="links.document" locale="sal">here</a>.
But wicket does not allow us to pass parameters to our components.
The "Wicket Way", it appears, fails for i18nised links.
My Hack: Filtering the Link
If there were votes for the "worst Wicket hack", I think this one would be a serious contender. The point, though, is that is satisfied all of the requirements above (though it breaks any requirement of good software design).
All I did was add a custom filter to filter the link like so:
public final class LinkHandler extends AbstractMarkupFilter { public LinkHandler() { super( null ); } public MarkupElement nextTag() throws ParseException { // Get the next tag. If null, no more tags are available final ComponentTag tag = (ComponentTag)getParent().nextTag(); if ( null == tag || null != tag.getId() ) return tag; // Process <a> tags with "link" attribute if( null != tag.getName() && tag.getName().equals( "a" ) ) { final String linkAttr = tag.getAttributes().getString( "link" ); if ( ( null != linkAttr ) ) { final String localeAttr = tag.getAttributes().getString( "locale" ); final Link link = getLinkService().getLink( linkAttr, localeAttr ); if( null != link && null != link.getUrl() ) tag.getAttributes().put( "href", link.getUrl().toString() ); else tag.getAttributes().put( "href", "linkError" ); tag.setModified( true ); } } return tag; } private LinkService getLinkService() { // Return the service that provides the link } }
With this approach, the content author is free to include (or not) any link by simply writing:
Hey! Take a look at <a link="documentId" locale="en">this</a>!
Wicket will rewrite the tag, looking up the link target in the persistence layer.
Voila!
As a Side Note
Rather than "overloading" the <a> tag, I would have liked to have used a different namespace, and upon parsing the markup the tag would be translated to a simple <a> tag in the default namespace. However, due to the way the parser was implemented, Wicket would not allow this.
Specifically, I tried this:
<bioscene:link href="someLinkRef">Link Text</bioscene:link>
with this:
// Process all tags in the "bioscene" namespace if( null != tag.getNamespace() && tag.getNamespace().equals( "bioscene" ) ) { tag.setNamespace( null ); tag.setName( "a" ); final String attrValue = tag.getAttributes().getString( "href" ); if ( ( null != attrValue ) ) { tag.getAttributes().put( "href", processTag( attrValue ) ); tag.setModified( true ); } }
But Wicket threw this:
java.text.ParseException: Tag '<:a href="/.temp/www/main/index.html">' (line 21, column 21) has a mismatched close tag at '</bioscene:link>' (line 21, column 58)
Ah, well. I've caused enough damage already.
Misc Notes
Resource search path
(From Erik van Oosten's post to the Wicket-User list)
Wicket will search all resource files with the names equal to the
components in your component hierarchy, with your application as a last
resort.
So for a MyApplication, with a MyPage containing a MyPanel Wicket will
look in:
- MyPanel.properties (and _locale variants)
- MyPage.properties (..)
- MyApplication.properties (..)
Actually, it even goes two steps further. Wicket will also look at
property files for the base classes of MyPanel, MyPage and
MyApplication. Secondly you can have different styles and variants.