Localised attribute values in JSF and Seam

12 May 2009

by PeterHilton

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>
  1. 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;
   }
  1. 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.