I18n With StringTemplate

Developers accustomed to internationalizing Java [web] applications are very familiar with ResourceBundle, MessageFormat and property files. The articles Localization and Internationalization with StringTemplate and Web Application Internationalization and Localization in Action give a good introduction to how StringTemplate can be used to do i18n but they don’t explain how to get the full set of benefits we derive from ResourceBundle, and MessageFormat. In this article I describe how StringTemplate can leverage ResourceBundle and remove the need for MessageFormat in the presentation layer.

The article Localization and Internationalization with StringTemplate shows an example using the java.util.Properties class. This is sufficient for showing the benefits of using StringTemplate but there are some nice things that ResourceBundle does for us that the Properties class doesn’t. ResourceBundle will automatically locate the property files on the classpath and form a hierarchy of properties from most specific to most general.

If you have these property files:

Messages.properties
Messages_en.properties
Messages_en_US.properties

then

ResourceBundle bundle =
    ResourceBundle.getBundle("Messages", new Locale("en", "US");

will load them from the class path and chain them together so that if a string is missing from Messages_en_US.properties it will attempt to find it in Messages_en.properties and if it is still not found it will look in Messages.properties. This is useful so that the country file (_en_US) only needs to contain country specific translations.

Note: You may have other ways to get the locale such as request.getLocale(); in a web application.

The above code is much simpler than loading Properties objects and chaining them together yourself. The problem with ResourceBundle is that it is not readily usable by StringTemplate. Wrapping ResourceBundle in a class that implements the Map interface can solve this. I’ll show this class later but here is how it would be used with StringTemplate:

StringTemplate st = templates.getInstanceOf("templates/Test");
st.setAttribute("bundle", new STResourceBundleMapWrapper(bundle));

In templates/Test you can use bundle like so:

…<h1>$bundle.Title$</h1>…

Where the Messages* files contain:

Title=This is the Test Title

With the benefits of ResourceBundle and property files now available to StringTemplate its time to look at MessageFormat.

MessageFormat is very important for i18n because it allows translators to move substitutable text values around in a phrase or sentence to accommodate the structural differences between languages. MessageFormat has a few capabilities in its pattern syntax. Most of the time it is used in its simplest form for example:

MessageFormat.format(
	"On {0} server {1} crashed because {2}"…).

In the Web Application Internationalization and Localization in Action article there is a short example showing how StringTemplate can do what the MessageFormat {} patterns can. The example it gives is:

"title=Welcome to $username$’s test page"

At first glance it would seem that you can simply replace { and } with $ and $ in your properties files and you are done. The article then goes on to explain that this only works if the format string (Welcome to $username$’s test page) is a StringTemplate object and not a String. This can be done without having to create a named template and add it to a StringTemplateGroup. StringTemplate is smart enough to recursively process an attribute value when it’s type is StringTemplate.

There is, however, one other big difference between how MessageFormat is used and the StringTemplate example above. The title message has knowledge of another template attribute named “username”. In some cases this may be appropriate but generally:

  1. the person doing the localization should not have any knowledge of the set of attribute names (the attribute names form the contract between the model and the view) and
  2. a single formatted message may need to be used in multiple contexts where different attributes are substituted.

This problem is easy to fix by simply changing $username$ to $p0$ and then passing in the value for p0 in the template. For example you could do the following to define a formatted message:

StringTemplate st = templates.getInstanceOf("sometemplate");
StringTemplate message = 
    new StringTemplate("On $p0$ server $p1$ crashed because $p2$);
HashMap<String, Object> bundle = new HashMap<String, Object>();
bundle.put("message", message);
st.setAttribute("bundle", bundle)
...

and then use the message from some template like so:


Error: $date, serverName, errorReason:{p0, p1, p2 | $bundle.message$}$
	

where date, serverName, and errorReason are other attributes available to the template.

The trouble is that the message should come from the message property file; it should not be hard coded. This is where the STResourceBundleMapWrapper comes in handy again. The get method can simply look at the resource bundle value and see if it has a $ in it. If it does then it must be a template so return the string wrapped in a StringTemplate, otherwise simply return the value as is.

Now this changes the rules for the translator a little. They need to be trained on the new syntax $pn$ rather than {n}. Also $ characters anywhere else in the property file will need to be escaped as “\\$”. Thankfully the complex single quote rules of MessageFormat can be forgotten.

What about the other capabilities of the message format patterns? A pattern can include format type and format style. For example {0,number, percent}. I think that the StringTemplate rendering functionality is better than what MessageFormat provides so type and style are not needed. For the most part StringTemplate rendering means that the translator has one less thing to be concerned about because the rendering is gong to do the right thing automatically.

That leaves format type choice. I don’t think this is used much but I believe StringTemplate can handle it in a similar way. The example from the SDK javadoc is:

There {0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}.

To do this with StringTemplate create a properties file that contains:

DiskInfo0=There are no files.
DiskInfo1=There is one file.
DiskInfo2=There are $p0$ files.

Make it available to the template as described above through an attribute called bundle. The controller code sets attributes as follows according to how many files there are:

int numFiles = 500; // TODO get a real value
String numFilesMessage = null;
if (numFiles == 0)
    numFilesMessage = "DiskInfo0";
else if (numFiles == 1)
    numFilesMessage = "DiskInfo1";
else
    numFilesMessage = "DiskInfo2";
st.setAttribute("numFiles", numFiles);
st.setAttribute("numFilesMessage", numFilesMessage);

The template would look like this:

$numFiles:{p0 | $bundle.(numFilesMessage)$ }$

As promised, here is a sketch of the STResourceBundleMapWrapper class. This defines the constructor that takes the ResourceBundle to wrap and property methods for the same.


public class STResourceBundleMapWrapper implements Map
{
    private ResourceBundle m_wrappedBundle = null;

    public STResourceBundleMapWrapper(ResourceBundle bundle)
    {
        m_wrappedBundle = bundle;
    }
    public ResourceBundle getWrappedResourceBundle()
    {
        return m_wrappedBundle;
    }
    public void setWrappedResourceBundle(ResourceBundle bundle)
    {
       m_wrappedBundle = bundle;
    }
    …

All the optional methods (clear, put, putAll, remove) throw an exception as is shown for clear.


    public void clear()
    {
        throw new UnsupportedOperationException();
    }
    …

Because it is not intended for the map to be iterated over a trivial implementation for most of the methods will suffice. For example:

public boolean isEmpty()
    {
       //not intended to iterate over
       return false;
    }
    public Set keySet()
    {
       //not intended to iterate over
       return null;
    }
    …
	

Actually StringTemplate does use keySet when the “keys” pseudo property is referenced but it shouldn’t be needed for a resource bundle attribute. ResourceBundle does have a getKeys method but it returns an enumeration which would have to be turned into a Set. I didn’t bother.

The interesting stuff is in these two methods.


    public boolean containsKey(Object key)
    {
        if (key != null && key instanceof String)
        {
            try
            {
                m_wrappedBundle.getObject((String)key);
            }
            catch (MissingResourceException mre)
            {
                return false;
            }
            return true;
        }
        return false;
    }

    public Object get(Object key)
    {
        if (key != null && key instanceof String)
        {
            try
            {
                Object o = m_wrappedBundle.getObject((String)key);
                if (o instanceof String)
                {
                    // check for a potential template
                    String s = (String)o;
                    if (s.contains("$"))
                    {
                        // TODO cache this?
                        return new StringTemplate(s);
                    }
                }
                return o;
            }
            catch (MissingResourceException mre)
            {
                // fall through
            }
        }
        return null;
    }

If the key is not found in the ResourceBundle StringTemplate will render nothing because it first checks if the map contains the key and only looks up the value if it does. If you wanted to you could lie in the containsKey implementation and say all keys are present so that in the get method you can return the key itself if it is missing. This can make it obvious when a typo is made in key names.

Once I created STResourceBundleMapWrapper and figured out the StringTemplate syntax for passing the arguments to $pn$ attributes, internationalizing my web app was much easier.

One thought on “I18n With StringTemplate

Comments are closed.