Overview

Compiler aware internationalization (I18N) with Java ResourceBundle

3 Comments

Like in almost every project we had to implement a internationalization and localization mechanism. We started to use Java’s standard ResourceBundle-concept. But after some weeks we had property-files which didn’t really fit to the used localization-keys in our application. This is a common problem which is due to refactoring.

Inspired by the internationalization-feature in Google’s Web Toolkit we wanted to create a solution, which can be tracked by the compiler. GWT is using its own compiler to create the client-javascript. And there is a separate compilation for every Locale. So it is very easy for GWT to create javascript-code to get the localized messages. The used javascript-code is selected by GWT on the client side based on the user’s Locale. All you have to do is to implement the Messages-interface and use it in your application. The solution is very convenient. For example, you can use the java-reference-search in your IDE and the GWT-Compiler even fails, if you miss to declare a translation in your property-files for a method in the Messages-Interface.

Our Goal: Instead of

Messages.getString("example");

we want to use

Messages.get().example();


A simple Java Proxy and some small JUnit-Tests are all that we need. Not that hard…

We assume that you have a ResourceBundle with some messages. Probably you are storing the user-Locale into a ThreadLocal-variable. This is a common approach of handling the Locale-information. We are using a simple ServletFilter to set the user-Locale into the LocaleContextHolder of Spring. This is used by Spring MVC or Spring Security and fits perfectly. In case you are not using Spring, you can easily implement your own ThreadLocal-Variable.

If you are doing something like this, your solution to access the messages may look like that.

   public final class Messages {
      ...
      public static String getString(String key) {
         ResourceBundle.getBundle(BUNDLE_NAME, LocaleContextHolder.getLocale()).getString(key);
      }
      ...
}

What we want to do, is to get some kind of compile-time error-checking. First we create an interface with a method definition for each message

public interface OurProjectMessages() {
   String example();
}

and in our Messages-Class we are returning this interface – implemented by a java proxy. And we change the modifier of the unsafe getString(String key) to private.

public final class Messages {
   ...
   private static OurProjectMessages messages = (OurProjectMessages) Proxy.newProxyInstance(//
      OurProjectMessages.class.getClassLoader(),//
      new Class[] { OurProjectMessages.class }, //
      new MessageResolver());
 
   private static class MessageResolver implements InvocationHandler {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) {
         return Messages.getString(method.getName());
      }
   }
 
   public static OurProjectMessages get() {
      return messages;
   }
 
   private static String getString(String key) {
      ResourceBundle.getBundle(BUNDLE_NAME, LocaleContextHolder.getLocale()).getString(key);
   }
   ...
}

Finish – Now we can use the code from the first example above to access our messages (Messages.get().example();). That’s nice and helps you to keep the overview of your used messages. But it is only half the job. You can still miss to declare translation in your property-files or your property-files can be polluted with old unused translations.

The solution is to implement a JUnit-Test. The test is included in our continuous integration and colors our build red, if someone has missed to keep attention to the messages. There are test in both directions – for example:

   @Test
   public void shouldHaveInterfaceMethodForAllMessages() {
      ...
   }
   @Test
   public void shouldHaveMessagesForAllInterafaceMethods() {
      ...
   }
   ...

The Test provides some nice error-messages – for example:
...AssertionError: No translations for [messages_en.properties#example]
or
...AssertionError: No interface method for : [messages_en.properties#exampleNotExisting]

You can find the implementation details of the Unit-Test in the demo-project.

This is just the easiest example – If you are interested please check the demo-project. You will find some more implementation details, including the handling of arguments for parameterized messages Message.get().example("2","2011-31-01"); or getting display-texts of Enums Message.getEnumText(SomeEnum.EXAMPLE); Please note, that the goal of the demo-project was to keep it as small as possible. That’s the reason, why some stuff is handcoded instead of using a framework for it.

Download Demo-Project

Kommentare

Comment

Your email address will not be published. Required fields are marked *