ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Towards Bug-Free Code
Pages: 1, 2, 3, 4

I18N using Type-Safe Resource Bundles

A way of preventing such mistakes from happening is by using a ListResourceBundle where the locale-specific messages are written as Java classes instead of as property files. The ListResourceBundle can be enhanced to accept keys belonging to an enum type. If the keys are wrong, then the code will fail the compilation step and it can be corrected immediately. The task becomes easier if you use a good Java editor that has an auto-completion feature.


public abstract class
                AbstractTypeSafeResourceBundle
                <K extends Enum<K>>
                extends ListResourceBundle
{
    private final EnumMap<K, Object> catalog;
    private final Object[][] cachedCatalog;

    public AbstractTypeSafeResourceBundle(
                Class<K> clazz)
    {
        catalog = new EnumMap<K, Object>(clazz);

        loadCatalogMap();

        //---------------------------------------

        cachedCatalog =
                new Object[catalog.size()][2];

        int i = 0;
        for (K key : catalog.keySet())
        {
            cachedCatalog[i][0] = key.name();
            cachedCatalog[i][1] = catalog.get(key);
            i++;
        }
    }

    public EnumMap<K, Object> getCatalogCopy()
    {
        return catalog.clone();
    }

    protected final void load(K key, Object obj)
    {
        catalog.put(key, obj);
    }

    protected abstract void loadCatalogMap();

    //--- ListResourceBundle methods ---

    protected final Object[][] getContents()
    {
        return cachedCatalog;
    }
}
        

Each module or project should create an enum, which will serve as the set of keys to the bundle:


public enum ResourceKey
{
    info_Hello,
    info_GoodMorning;
}
        

The locale-specific bundles will look like this:


//English version.
public class ProjectXResBundle extends
AbstractTypeSafeResourceBundle<ResourceKey>
{
    public ProjectXResBundle()
    {
        super(ResourceKey.class);
    }

    protected void loadCatalogMap()
    {
        load(ResourceKey.info_GoodMorning,
                "Good morning");

        load(ResourceKey.info_Hello, "Hello!");
    }
}

//German version.
public class ProjectXResBundle_de_DE extends
AbstractTypeSafeResourceBundle<ResourceKey>
{
    public ProjectXResBundle_de_DE()
    {
        super(ResourceKey.class);
    }

    protected void loadCatalogMap()
    {
        load(ResourceKey.info_GoodMorning,
                "Guten Morgen");

        load(ResourceKey.info_Hello, "Hallo!");
    }
}
        

A direct way of using the bundles would be as shown below:


final ResourceBundle englishResourceBundle =
                ResourceBundle.getBundle(
                ProjectXResBundle.class.getName());

System.out.println(
                englishResourceBundle.getString(
                ResourceKey.info_Hello.name()));


final ResourceBundle germanResourceBundle =
                ResourceBundle.getBundle(
                ProjectXResBundle.class.getName(),
                new Locale("de", "DE"));

System.out.println(germanResourceBundle.getString(
                ResourceKey.info_Hello.name()));
        

Or you can define a helper class that takes care of substituting the parameters to construct the localized message by using the varargs feature in 1.5:


public class Helper
{
    public static String format(ResourceBundle
                resourceBundle,
                Enum key,
                Object... paramsForI18NMsg)
    {
        final Object l10nObj =
                resourceBundle.getObject(key.name());

        final String messagePattern =
                l10nObj.toString();

        if (paramsForI18NMsg.length > 0)
        {
            final MessageFormat formatter =
                new MessageFormat(messagePattern,
                resourceBundle.getLocale());

            return formatter.format(paramsForI18NMsg);
        }

        return messagePattern;
    }
}
        

If the message takes time as a parameter to format according to locale, it will look like this (standard ResourceBundle conventions):


public class ProjectXResBundle_de_DE extends
AbstractTypeSafeResourceBundle<ResourceKey>
{
    public ProjectXResBundle_de_DE()
    {
        super(ResourceKey.class);
    }

    protected void loadCatalogMap()
    {
        load(ResourceKey.info_GoodMorning,
                "Guten Morgen {0,time,short} - {1}");

        load(ResourceKey.info_Hello, "Hallo!");
    }
}
        

With the substitution parameters forming the varargs, the code looks a lot simpler and cleaner:


//No parameters, yet no unsightly "new Object[]{}"
//that had to be used until 1.5.
System.out.println(Helper.format(
                germanResourceBundle,
                ResourceKey.info_Hello));

System.out.println(Helper.format(
                germanResourceBundle,
                ResourceKey.info_GoodMorning,
                new Date(), "Ashwin"));
        

The last message prints "Guten Morgen 17:58 - Ashwin" with the time in the 24-hour format as it is used in Germany.

The source code contains a package called common, which takes the Registry a step further by allowing ComponentFactories to be added to the Registry instead of just Component implementations (if required) and a generic logging framework that allows enums to be used for internationalized messages in conjunction with AbstractTypeSafeResourceBundle. The projectx package has examples that use these.

Resources

Ashwin Jayaprakash is a software engineer at BEA Systems R & D Centre, Bangalore, using Java/J2EE to develop WebLogic Integration products.


Return to ONJava.com.