Monday, June 17, 2013

Look Ma, no ResourceBundle :) Step 2: Dealing with message arguments

Java
In the previous post an approach to internationalization of Java applications was suggested that does not need a ResourceBundle to do the job. Collections of localized messages may be arranged to be referenced as static class members rather than string aliases for properties in a ResourceBundle. The main benefit is gained from getting references to messages compiler verifiable, with programmer's IDE autocompletion aids available for localized messages as a bonus .

This approach was described and implemented previously at initial level, being still more of a concept than of a tool for real life applications. The main feature missing was an ability to supply arguments to localized messages, similar to MessageFormat.

Changes


Getting message arguments into the game resulted in certain refactoring, particularly in substantial separation of code from data and coining a concept of I18nHandler. Another change in concept was due to the fact that it is not possible to get a string value of an object via its toString() method if you are to supply formatting arguments. Hence, only methods like s(TArg1 arg1, TArg2 arg2) are available in these cases. Some less important change is that I18nItem no more extends HashMap but rather has more Map instances as members . And the former p(String language, String message) method for instantiating multi-language messages was refactored to lm(String localeTag, String message) in a separate class. One more notable change is that differentiation of messages by language turned into differentiation by locale language tag. And finally, the package was renamed to usn.i18n.nobundle.

New design


Data

LocalizedMessage is the basic container for localized messages. Internally it is a tuple of a locale language tag and a corresponding localized message. In case you do not care about locale-specific formatting of dates and numbers, feel free to reduce the full language tag to just the language subtag like "en". The localized message may be a mere string or a formatting pattern as per MessageFormat convention. The standard way to construct a LocalizedMessage instance is via its static factory method lm(String localeTag, String message). Normally there is no need to construct an isolated LocalizedMessage instance unless as a component of an I18nItem.

I18nItem is the basic class to represent an internationalized message. Internally is has a collection of LocalizedMessage instances for a necessary set of locales. I18nItem is not intended for direct use by applications but rather serves as the base class for an hierarchy of more specific subclasses.

Special care was taken to reduce the risk of wrong use of arguments for messages, e.g. providing arguments of wrong type or in wrong number. With this in mind the following I18nItem subclasses are offered for use:
  • I18nItem0 – the class to use for messages without arguments;
  • I18nItem1<TArg1> – a generic class to use for messages with exactly one argument;
  • I18nItem2<TArg1,TArg2> – a generic class to use for messages with exactly two arguments;
  • I18nItem2<TArg1,TArg2,TArg3> – a generic class to use for messages with exactly three arguments;
  • I18nItemAny – a class to use for messages with arbitrary number of arguments, as the last resort for cases that did not fit into previous subclasses;
A snippet for declaring some set of internationalized messages might look like this:
...
import usn.i18n.nobundle.I18nItem;

// get 'lm()' available as shorthand method without being prefixed by
// 'LocalizedMessage'
import static usn.i18n.nobundle.LocalizedMessage.lm;

...

static I18nItem0 MESSAGE_1 = new I18nItem0
    (lm ("en", "That's cool!"),
     lm ("fr", "C'est le pied!"),
     lm ("ru", "Круто!"));
static I18nItem1<Integer> MESSAGE_2 = new I18nItem1<Integer>
    (lm ("en", "I know {0,number,integer} guys seeking something" +
               " like this..."),
     lm ("fr", "Je sais que {0,number,integer} gars à la recherche" +
               " de quelque chose comme ceci..."),
     lm ("ru", "Я знаю ещё {0,number,integer} ребятишек, которые" +
               " хотели чего-то подобного..."));

Handlers

The next important topic to take care of is an approach to obtaining user language / locale preferences during run time. In anticipation of vast variety of application architectures, this topic is resolved in the most general way with the help of the I18nHandler class. This class has absorbed almost all the code that is responsible for handling of methods like s(TArg1 arg1, TArg2 arg2), and meanwhile it allows applications to achieve necessary behavior by its subclassing and overriding getDefaultLocaleTag() and most importantly getUserLocaleTags() methods. The only thing an application needs to do is:
1) subclass I18nHandler or select from its existing subclasses as necessary;
2) create an instance of the selected I18nHandler subclass; this instance will establish itself as a singleton and take care of all the rest.

I18nHandlerForSingleUser is an almost ready to use I18nHandler subclass that is intended for use by applications like desktop ones, with single user per application instance, when it is possible to define all user preferences upon application startup. You may only still wish to override the getDefaultLocaleTag() method.

Context based internationalization

There are also cases to be taken care of specially, when user preferences are conveniently available from some execution context rather than from the application directly. A standard example is a web application, where some primitive user preferences may be retrieved from a ServletRequest instance. Or maybe even better from an associated HttpSession instance. These cases are taken care of by I18nHandlerInContext<TContext> and I18nHandlerForServletRequest subclasses, to be subclassed further as necessary.

And  I18nHandlerInContext<TContext> subclasses require a special family of I18nItem subclasses, that take a TContext instance as the first argument for all their methods:

Internationalization for logging

Finally, to bring the renovated package closer to real world application needs, two more classes are added that get this internationalization technology available for logging: I15dLogger and I15dLoggerFactory. These two bridge I18nItem with SLF4J in hope that is going to be sufficient these days .

Behind the scenes


So what happens when an I18nItem instance's s() or similar method is called?

  1. The I18nHandler singleton is located.
  2. Its getUserLocaleTags() method is called. This method is expected to be overridden by a subclass to do something meaningful . An array of user preferred locale language tags is the result.
  3. A negotiation between user preferences and locales available for given I18nItem instance takes place in the handler's findBestLocaleTag(I18nItem,String[]) method. For every user's preferred locale two attempts are made: for exact match and for approximate match via language subtag. If no luck, then the system default locale is attempted. If again no luck, then plain English is attempted. And if not successful again, the first locale available for given I18nItem instance is selected.
  4. The localized message (message pattern) is retrieved from given I18nItem instance for the locale language tag selected on the previous step.
  5. Message formatting arguments are applied if required.

Downloads


Both jar and source downloads are available. And the project is now hosted at Github. Please feel free to use and modify. The license is BSD.


Usage – HOW-TO


  1. Download the .jar file .
  2. Add it to your application's class path.
  3. Make a decision on the I18nHandler subclass you need: whether it should depend on some context for obtaining user's locale preferences or should it get these preferences from the application directly; hence select an existing I18nHandler subclass or make a subclass of your own.
  4. Create an instance of the chosen I18nHandler subclass in your application; no need to care about its further fate; feel free to mark it with @SuppressWarnings ("unused").
  5. Create as many of I18nItem sublasses' instances as you need for all the messages that require internationalization.
  6. Use your messages via corresponding s(TArg1 arg1) and similar methods.
  7. Refer to the modest example on the package summary javadocs page and to API docs in general when necessary.
  8. Enjoy .

Package name


I personally feel not very comfortable having a negative word like "nobundle" in the package name. But got no better idea so far. So positive ideas on naming are welcome!

Comments welcome


Though the library is already tested in a real life application, it is probably undercooked and may contain bugs. It may also ask for improvement here and there. So any comments are welcome too!..