Saturday, February 23, 2013

Look Ma, no ResourceBundle :) Or yet another approach to i18n in Java

Java
Everyone knows the traditional way to make a Java application international: one should give every text message a symbolic name, place all the strings in various languages in a set of special resource files, and then get message strings used via their symbolic names in the prescribed manner. This is not all of course, and one furthermore has to face different locales, plurals, genders, etc. ... And ResourceBundle has always been an important part of these internationalization facilities.

This approach is well developed, and lots of tools are available to help using it, and still there is an issue about it. Java code is linked to string resources via symbolic names that are mere strings, and it is pretty easy to get them out of sync in the course of changes. And as these links are not checked by compiler, a programmer is almost certain to feel happy until a surprise at run time...

A colleague of mine has once mentioned that it would be more robust and convenient to have text resources linked to Java code via class member names. Then these links would be checked by the compiler, and a programmer could also benefit from autocompletion facilities provided by modern IDE's.

It took some time to get the idea crystallized, and here is the result. It is all about a class named I18nItem that extends HashMap (that might be any other Map) and has an overridden toString() method. Every instance of such a class would wrap one text message in different languages, indexed by language. One more task was to implement a convenient way to construct such collections as members of some class, so to allow easy referring to them from other parts of code.

The new approach in action


Let's have a look at what a standard use case might look like.

First an application would typically have I18nItem subclassed:
...
import usn.util.i18n.I18nItem;

public class AppSpecificI18nItem extends I18nItem
  {
    private static final long serialVersionUID = 1L;

    // define a simple subclass constructor to be used
    AppSpecificI18nItem (I18nItem.LanguagePair... data)
      {
        super (data);
      }

    // let's assume the default application language is going to be
    // French, so override the appropriate method
    @Override
    protected String getDefaultLanguage ()
      {
        return "fr";
      }

    // then the application needs to define the way to obtain current
    // user's language preferences as string array, most preferred
    // ones coming first
    @Override
    protected String [] getUserLanguages ()
      {
        // normally we are expected to do something more interesting
        // here...
        return super.getUserLanguages ();
      }
  } // class AppSpecificI18nItem

Next we need to construct our international string resources without using ResourceBundle – in any Java class you may find appropriate:
...
import usn.util.i18n.I18nItem;

// get 'p()' available as shorthand method without being prefixed by
// 'I18nItem'
import static usn.util.i18n.I18nItem.p;

public class MyMessages
  {
    public static I18nItem MESSAGE_1 = new AppSpecificI18nItem
        (p ("en", "That's cool!"),
         p ("fr", "C'est le pied!"),
         p ("ru", "Круто!")); 
    public static I18nItem MESSAGE_2 = new AppSpecificI18nItem
        (p ("en", "Just what we need!"),
         p ("fr", "Exactement ce qu'il nous faut!"),
         p ("ru", "То, что нужно!"));
    // ...
  } // class MyMessages

And finally we are going to use the messages we have defined:
...

// in some cases implicit conversion to String is available, and the
// overridden 'getUserLanguages()' method combined with
// 'I18nItem#toString()' takes care of everything...
System.out.println (MyMessages.MESSAGE_1);

// ... and in other cases the 's()' shorthand method is at our
// disposal...
logger.info (MyMessages.MESSAGE_2.s ());

And in every case like these your IDE is likely to assist you with right selection of the message to use...

Downloads


Both jar-with-source and demo downloads are available. Please feel free to use and modify. The license is BSD.

Acknowledgements


Special thanks to my colleague Sergey Petrov for the idea that access to a resource via some class member might be preferable against string alias .



Continued: Step 2: Dealing with message arguments

added on 2013-06-21