Context Sensitive Resource Bundle entries in JavaServer Faces applications – going beyond plain language, region & variant locales

 

We were faced by an interesting challenge: our JSF application should display boilerplate text – titles, button labels, prompt, error messages, tool tips etc, – in a context sensitive way. Not just by language, region and variant – the well known dimensions along which the standard JSF and Java mechanism works with Resource Bundles. Beyond that simple ‘locale’ sensitivity – which we also needed – we need a more specialized context dependency. Along several dimensions.

For example when a user of younger age category approaches the web application, the text presented should be (or at least could be) different from whatever we show our senior users. Also when the application is accessed in the context of a certain brand or company the text may need to be different from other brand or company contexts. And the marketing department came up with the ability to presents some text tailored to the time of year – Winter or Summer, Holiday Season or no Christmas in sight – or the day of the week – working day or weekend. Good old marketing department – if they were to rule the world….

And so we got started. How can we cater for these various context dependencies, along various mutually orthogonal dimensions.

We started with a close look at the default mechanism in JSF 1.2. And then took it from there.

Default ResourceBundle facilities in JavaServer Faces 1.2

The default facilities are pretty simple:

– ResourceBundles are configured, either per page or for the application as a whole (as of JSF 1.2). The latter is usually to be preferred, as specifying a resource bundle for every page is quite a task.

The ResourceBundle configuration in the faces-config.xml looks like this:

<faces -config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee">
  <application>
    <resource -bundle>
      <base -name>nl.amis.appBundle</base>
      <var>msg</var>
    </resource>
  </application>

Here the base-name refers to a properties file on the file system. Note that instead of a properties file we can also use a class – which can give some control over the encoding and special characters in messages and/or where to retrieve the messages from (for example a database).

The properties file in this case is extremely simple:

title=The Interesting World of Internationalization
formSubmit=Apply Changes if you like
ageCategorySelector=Select Age Category

Just three keys with corresponding messages. At this point no locale specific versions of the bundle – so no appBundle_en_us.,properties or appBundle_nl.properties files.

With the configuration in faces-config.xml and the properties file with the keys and messages in place, we can create a JSF page that uses messages from the bundle. Note that JSF takes care of applying the proper locale – read from the ViewRoot (FacesContext.getCurrentInstance().getViewRoot().getLocale()).

The very simple JSF page looks like this:

 ...
  <f:view>
    <html>
      ...
      <body>
        <h:form>
        <h:panelgrid columns="1" >
          <h:outputtext value="#{msg.title}"/>
          <h:commandbutton value="#{msg.formSubmit}"/>
          </h:panelgrid>
        </h:form>
      </body>
    </html>
  </f:view>

Note the references to i18n-ed messages using EL expressions of the format #{msg[‘key’]} or the equivalent #{msg.key} . The resource bundle was registered with a variable called msg that we can refer to in these expressions.

When we run the page, it will display an extremely ugly page that takes its boiler plate text from the properties file.

Context Sensitive Resource Bundle entries in JavaServer Faces applications - going beyond plain language, region & variant locales jsfi18n01

Setting the stage for context sensitive resource processing – Intercepting the ResourceBundle requests

So far nothing special. But we have to start preparing for the influence exerted by the context. We have to ask ourselves: how can we let the context influence the way in which messages are retrieved from the resource bundle. Surely the default JSF and Java resource bundle mechanism has no knowledge beyond plain old Locale. It is up to us to preprocess a message-request to the resource bundle in order to handle the context sensitivity. So we have to intercept the request from the application before the resource bundle mechanism is invoked.

Well, that is easy enough. The resource bundle calls are specified through EL Expressions like #{msg.key}. If we make sure that msg is no longer the resource bundle itself but our very own managed bean that can implements the Map interface – in order to handle the .key request – we have achieved the interception.

Our bean should subsequently at some point call upon the real Resource Bundle to handle the request. But now we can fiddle with the request. In two ways: we can manipulate the key and we can decide to call another than the default resource bundle.

Let’s start with the plain interception, no fiddling at all.

The configuration of our managed beans – note there are two beans, one to implement Map and intercept the message request (MessageProvider) – the other one to implement the interception and apply context sensitivity logic:

<managed -bean>
    </managed><managed -bean-name>msgMgr</managed>
    <managed -bean-class>nl.amis.MessageManager</managed>
    <managed -bean-scope>session</managed>

  
  <managed -bean>
    </managed><managed -bean-name>msg</managed>
    <managed -bean-class>nl.amis.MessageProvider</managed>
    <managed -bean-scope>request</managed>
    <managed -property>
      <property -name>msgMgr</property>
      <value>#{msgMgr}</value>
    </managed>
  
  

Now that we have hijacked the msg name for our MessageProvider, we should register the Resource Bundle itself under a different name.

 <application>
    <resource -bundle>
      <base -name>nl.amis.appBundle</base>
      <var>msgbundle</var>    
    </resource>
  </application>

The implementation of  the MessageProvider class is very simple – it pass the request onwards to the MessageManager:

package nl.amis;

import java.util.HashMap;

public class MessageProvider extends HashMap{

    private MessageManager msgMgr;
    public MessageProvider() {
    }

    @Override
    public Object get(Object key) {
        return msgMgr.getMessage((String)key);
    }

    public void setMsgMgr(MessageManager msgMgr) {
        this.msgMgr = msgMgr;
    }

    public MessageManager getMsgMgr() {
        return msgMgr;
    }
}

The references in the JSF pages can all stay the same: it makes no difference whether the EL expression in the page references the Resource Bundle registered with JSF directly or a managed bean like we do here.

Now for the MessageManager. The initial implementation that proves our interception mechanism works but does no manipulation looks like this:

public class MessageManager {

    public String getMessage(String key) {

        // use standard JSF Resource Bundle mechanism
        return getMessageFromJSFBundle(key);

        // use the default Java ResourceBund;e mechanism
        // return getMessageFromResourceBundle(key);
    }

    private String getMessageFromResourceBundle(String key) {
        ResourceBundle bundle = null;
        String bundleName =
            "nl.amis.appBundle";
        String message = "";
        Locale locale =
            FacesContext.getCurrentInstance().getViewRoot().getLocale();
        try {
            bundle =
                    ResourceBundle.getBundle(bundleName, locale, getCurrentLoader(bundleName));
        } catch (MissingResourceException e) {
            // bundle with this name not found;
        }
        if (bundle == null)
            return null;
        try {
            message = bundle.getString(key);
        } catch (Exception e) {
        }
        return message;

    }

    private String getMessageFromJSFBundle(String key) {
        return (String)resolveExpression("#{msgbundle['" + key + "']}");
    }

    public static ClassLoader getCurrentLoader(Object fallbackClass) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (loader == null)
            loader = fallbackClass.getClass().getClassLoader();
        return loader;
    }

    // from JSFUtils in Oracle ADF 11g Storefront Demo
    public static Object resolveExpression(String expression) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Application app = facesContext.getApplication();
        ExpressionFactory elFactory = app.getExpressionFactory();
        ELContext elContext = facesContext.getELContext();
        ValueExpression valueExp =
            elFactory.createValueExpression(elContext, expression,
                                            Object.class);
        return valueExp.getValue(elContext);
    }
}

We can choose between two ways to access the Resource Bundle.

One is using the JSF mechanism directly – which will only work for Resource Bundles that are explicitly registered with JSF in faces-config.xml files. See method getMessageFromJSFBundle.

The other one goes around whatever facilities JSF offers and uses the standard Java ResourceBundle library directly. With this approach, we have to find out the Locale ourselves. We can use this approach with any resource bundle file on the classpath, without them having been explicitly registered in faces-config.xml. This approach is used in getMessageFromResourceBundle().

Enter the Context that should influence the Resource Bundle results

At this point the extra context enters the picture. For simplicity’s sake we will assume the context to be indicated by a property on a session scope managed bean. The context can be set using a List control in the user interface (typically this would be handled in a more subtle way).

The MessageManager bean is extended with the ageCategory property and a getter and setter method.

The JSF page is extended with the list control – that for some weird reason does not get its labels from the resource bundle:

           <h:selectonelistbox value="#{msgMgr.ageCategory}"
                                label="#{msg.ageCategorySelector}">
              <f:selectitem itemLabel="Junior" itemValue="junior"
                            itemDescription="Age category Under 35"/>
              <f:selectitem itemLabel="Senior" itemValue="senior"
                            itemDescription="Age category 35+"/>
            </h:selectonelistbox>

Now we can set the context. Question is: what difference does it make? Or better yet: how can we have it make a difference? When the user toggles from Senior to Junior age category, what should be the effect on the text in the page?

Basically there are two ways to handle this. One is to add additional keys to the resource bundle; these keys are composed of the original (base) key and an identification of the context in which the key applies. For example:

title=The Interesting World of Internationalization
formSubmit=Apply Changes if you like
ageCategorySelector=Select Age Category
title_senior=The never ending wonders of the World of Internationalization
formSubmit_senior=Notify the application of your desires by pressing this button
title_junior=Speaking your own language
ageCategorySelector_junior=Pick your own age peer group

Here we have added the keys to the resource bundle properties file we were already using. Note that we do not need all keys to be included for every context we want to support: only when the message should be context specific for a certain context do we need to provide an extra entry.

The change required in the code of the MessageManager is minimal. We should first try to find the message for the key composed of the base key (passed in from the page) and the current context. When no message was found, we try again, this time with only the base key.

    public String getMessage(String key) {

        // add the current context to the key and dive into the large resource bundle with all keys, simple and composed with context

        // use the JSF bundle mechanism, that requires <application><resource -bundle> elements in the faces-config.xml
        //        String msg = getMessageFromJSFBundle(key+"_"+ageCategory);
        //        if (msg==null || msg.startsWith("???"))
        //            msg = getMessageFromJSFBundle(key);

        // use the default Java ResourceBund;e mechanism
                String msg = getMessageFromResourceBundle(key+"_"+ageCategory);
                if (msg==null || msg.startsWith("???"))
                    msg = getMessageFromResourceBundle(key);

        return msg;
    }

Again, two approaches are possible, one going through JSF and the other going straight at the ResourceBundles.

Note: the complex keys could have some fancy hierarchical context scheme is you like: junior_christmas_male_brandX_page1Title, christmas_male_brandX_page1Title, male_brandX_page1Title, brandX_page1Title, pageTitle1.

Run the page; select Senior and press the button:

Context Sensitive Resource Bundle entries in JavaServer Faces applications - going beyond plain language, region & variant locales jsfi18n02

Then select Junior and press the button:

Context Sensitive Resource Bundle entries in JavaServer Faces applications - going beyond plain language, region & variant locales jsfi18n03

Resource Bundle per context value

Instead of adding all context specific keys and messages to the same big old resource bundle (file) – we can also create specific files for each context that we want to support.

If we do this, we give the context precedence of the locale: we first check the context specific resource bundles for the specific locale, the less specific locale and the default locale and only when we exhausted the context specific bundles for all applicable locales will we check the not context specific bundle, for all the locales. So we end up with a message that is specific to the context but not to the language. The approach with  a single resource bundle and composite keys gives precedence to the locale over the context.

In our example, we can create files called appBundleSenior.properties and appBundleJunior.properties for example. The latter contains:

title=Speaking your own language
formSubmit=Make it happen!
ageCategorySelector=Pick your own age peer group

So only the entries for the context value of Junior – with the base keys, no fancy composite keys this time.

In order to access the contents of this file, we have to make small changes in the MessageManager. Changes that differ for the JSF Bundle approach or the plain Java ResourceBundle approach.

To discuss the latter case first: we can simply access all ResourceBundles on the classpath. If we just pass in another name, that’s alright with the mechanism. We extend the getMessageFromResourceBundle method with a second parameter: the bundlePostFix. This postfix indicates the context value. The postfix is simply added to the bundlename and the method tries to find a message:

 private String getMessageFromResourceBundle(String key,
                                                String bundlePostfix) {
        ResourceBundle bundle = null;
        String bundleName =
            "nl.amis.appBundle" + (bundlePostfix == null ? "" : bundlePostfix);
        String message = "";
        Locale locale =
            FacesContext.getCurrentInstance().getViewRoot().getLocale();
        try {
            bundle =
                    ResourceBundle.getBundle(bundleName, locale, getCurrentLoader(bundleName));
        } catch (MissingResourceException e) {
            // bundle with this name not found;
        }
        if (bundle == null)
            return null;
        try {
            message = bundle.getString(key);
        } catch (Exception e) {
        }
        return message;
    }

So a small change is also required in the getMessage() method:

    public String getMessage(String key) {
        String msg = getMessageFromResourceBundle(key, (String)ageCategory);
        if (msg == null || msg.startsWith("???"))
            msg = getMessageFromResourceBundle(key, "");
        return msg;
    }

We pass the postfix – and the unadorned key – to the method and when no message is returned, we try again in the base bundle.

Using the JSF bundle mechanism, the change is one step more complex: we need to register the context specific resource bundle with JSF in the faces-config.xml file:

  <application>
    <resource -bundle>
      <base -name>nl.amis.appBundle</base>
      <var>msgbundle</var>
    </resource>
 <resource -bundle> 
    <base -name>nl.amis.appBundleJunior</base> 
    <var>juniormsgbundle</var> 
 </resource>
</application>

Now we can rewrite the getMessageFromPrefixedBundle() method

private String getMessageFromPrefixedBundle(String key,
                                                String bundlePrefix) {
        return (String)resolveExpression("#{" + bundlePrefix + "msgbundle['" +
                                         key + "']}");
    }

Here we have assumed that the context sensitive identifier is applied as prefix to the name of the Resource Bundler var.

The change in the getMessage() method:

    public String getMessage(String key) {
        // for the JSF ResourceBundle mechanism, assume a second <application><resource -bundle> entry that specifies a variable called <context identifier>msgbundle
                String msg = getMessageFromPrefixedBundle(key, (String)ageCategory);
                if (msg==null || msg.startsWith("???"))
                    msg = getMessageFromJSFBundle(key);
        return msg;
    }

Summary

You have seen how the standard Resource Bundle mechanism in JSF can be used. Then we have suggested a way to intercept i18n message request in a managed bean that can manipulate the request, taking the current context – other than the standard Locale – into account. What we have not shown is how the Resource Bundle can be implemented by a class that either specifies the key/message pairs hard codedly or gets them from an external source – such as a database.

Resources

Download the JDeveloper 11g Application with the code for this article: jsfresourcebundletest.zip.

 

3 Comments

  1. ceyesuma March 10, 2009
  2. ceyesuma March 9, 2009
  3. Ed Burns November 17, 2008