Localised attribute values in JSF and Seam
There are various features for /2009/03/31/jsf-seam-localization[Language localisation in JSF and Seam]. However, these features do not provide any way to localise text in an attribute value that contains a placeholder in a Facelets view, without writing extra code. This article shows how.
Localised attribute values containing placeholders
Suppose that you want to localise a hyperlink in your application whose text is 'Notify Peter', with the view mark-up:
<h:commandLink action="#{action.notify}" value="Notify Peter"/>
You need to use a placeholder for this text, because the word order is different in some languages:
# English - {0} is the user name
notify.user = Notify {0}
# Dutch
notify.user = {0} attenderen
The usual way to render this in a Facelets view is:
<h:outputFormat value="#{messages['notify.user']}">
<f:param value="#{user.name}"/>
</h:outputFormat>
Apart from being somewhat verbose, this is a problem, because you cannot
put this XML mark-up inside the h:commandLink
attribute value.
Instead, we need a way to render the localised text in an Expression
Language expression:
<h:commandLink action="#{action.notify}" value="#{localisedText}"/>
JSTL localisation function
One way to do this is to write a JSTL message
function that takes a
message key and parameters for placeholders:
<h:commandLink action="#{action.notify}"
value="#{eg:message('user.notify', user.name)}"/>
To do this, you need a eg.taglib.xml
Facelets tag library descriptor:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE facelet-taglib PUBLIC
"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
"http://java.sun.com/dtd/facelet-taglib_1_0.dtd">
<facelet-taglib>
<namespace>http://example.com/taglib</namespace>
<function>
<function-name>message</function-name>
<function-class>com.example.jsf.Functions</function-class>
<!-- Params: String message key, Object[] varargs parameters -->
<function-signature>
java.lang.String
getLocalisedMessage(java.lang.String, java.lang.Object[])
</function-signature>
</function>
</facelet-taglib>
-
and a Java function implementation:
package com.example.jsf;
import java.text.MessageFormat;
import java.util.MissingResourceException;
import org.jboss.seam.core.ResourceBundle;
public class Functions {
/**
* Formats a localised message from the resource bundle for
* the Seam locale.
*
* @param messageKey the key to the formatted message
* @param params replacement parameters for placeholders
* @return localised formatted message, or the messageKey
*/
public static String getLocalisedMessage(
final String messageKey, final Object... params) {
try {
// Get the message from the Seam resource bundle,
// using the current Seam locale.
final String message =
ResourceBundle.instance().getString(messageKey);
return MessageFormat.format(message, params);
}
catch (final MissingResourceException e) {
return messageKey;
}
}
}
This now means that we can use our message function in expressions everywhere in our views, for messages with or without placeholders, rather than using the clunky XML syntax.
<h1>#{eg:message('salutations.hello', scopes.world)}</h1>
This gets us further, but it turns out that we are still not done, because sooner or later you need to do a locale-specific format, such as a date.
Locale-specific parameter formatting
MessageFormat
supports locale-dependent parameter formatting using format types. For
example, you can format dates using the date
format type, and various
format styles:
comment.added1 = Comment posted {0,date}
comment.added2 = Comment posted {0,date,short}
comment.added3 = Comment posted {0,date,dd-MM-yyyy}
This is only going to work if the MessageFormat
has the correct
locale. We can get the current Seam locale like this:
/**
* Gets the locale from Seam, falls-back to Locale.ENGLISH
*/
public static Locale getSeamLocale() {
final org.jboss.seam.Component localeComponent =
(org.jboss.seam.Component) Contexts
.lookupInStatefulContexts("org.jboss.seam.core.locale.component");
if (localeComponent != null) {
final org.jboss.seam.core.Locale seamLocale =
(org.jboss.seam.core.Locale) localeComponent.newInstance();
if (seamLocale != null) {
return seamLocale.getLocale();
}
}
return Locale.ENGLISH;
}
-
and use it like this:
final MessageFormat formatter =
new MessageFormat(message, getSeamLocale());
return formatter.format(params);
Again, more progress but we are still not done.
JSF converters
Using MessageFormat
date formatting is not what you want if you are
already using a JSF-Facelets
custom date converter that formats dates as 'today' and 'yesterday',
for example. The problem here is that MessageFormat
is not integrated
with JSF and Seam. This is where things might get less pretty.
For example, we could directly format date parameters to the message function:
public static String getLocalisedMessage(
final String messageKey, final Object... params) {
// Nasty hack: format any parameters that turn out to be dates.
for (int i = 0; i < params.length; i++) {
if (params[i] instanceof Date) {
params[i] = DateUtil.formatRelativeDate((Date) params[i], true);
}
}
// ...
However, this is no good if you have more than one date formatter for a
single parameter - ours has a parameter that determines whether the time
is shown as well, or just a date. This is why we ended up with a custom
version of MessageFormat
that allows us to register our own format
types for our JSF converters, for use like:
comment.added = Comment posted {0,relativedate}
The code for this is left as an exercise for the reader.