I18N Messages and Logging

John Mazzitelli

December 22, 2006


Summary

The purpose of this project is to provide an easy to use API that allows you to incorporate internationalized (I18N) messages into your Java applications.

This project provides an API that allows developers to:

It is recommended that you review the full API documentation since all classes are fully documented with Javadoc. You should be able to come up to speed fairly quickly on how to use this API by reading this page and the API docs.

Download

You can download the software from the i18nlog project download page.


Details

Msg

The core class in the i18nlog project is Msg. It is the object that accesses the appropriate resource bundle properties file for the appropriate locale and it will replace the message's placeholders (e.g. {0}, {1}) with the values passed into its variable argument parameters. Although you no longer have to work directly with the following core Java classes, you might want to read the Javadocs on ResourceBundle and MessageFormat to get a feel for how i18nlog does what it does.

The Msg class provides a set of constructors to build your I18N messages, or you can use its set of createMsg static methods to create them. Once created, you simply call getLastMessage() or toString() which both do the same thing - return the I18N message that was last retrieved from the locale's resource bundle.

You can switch the locale used by the Msg by calling setLocale() - calling getLastMessage() after doing that will return the message in that locale's translation. Similarly, if you serialize a Msg object that relies on the VM's default locale and send it over the wire where the new VM's default locale is different, that deserialized Msg object will re-translate the message in the new locale and return that new localized message (of course, you will need to ensure that new VM has that locale's resource bundle in the appropriate classloader for the new language's message to be found).

The typical usage of the Msg class is as follows:

   System.out.println( Msg.createMsg( "my-bundle-key", name, 5 ) );

where "my-bundle-key" refers to a message in a bundle properties file. "name" and "5" is just an example of passing an arbitrary arguments list - each object represents the value to replace the placeholder in its respective position. For example, if "my-bundle-key" refers to a message in my bundle file named "messages_en.properties" where that message is "Hello {0} - you have {1} emails waiting", and "name" has the value "John", then this will print out the string "Hello John - you have 5 emails waiting".

There are additional things you can do that won't be explained in detail here; again, go to the Javadoc and review the methods available to get a sense of the features that are available. Suffice it to say, in calls to Msg methods, you can explicitly specify what locale you want the messages to be in and what resource bundle to read by explicitly specifying the base bundle name. In the example above, it uses the default settings (e.g. the VM's default locale is used).

Also, you will probably want to use the @I18N-annotations in conjunction with the Msg API (and the Logger/LocalizedException API which is explained below). This would enable you to have compile time checks for that "my-bundle-key" parameter argument (through the use of Java constants) and it would also allow you to automatically generate your resource bundles without you having to manually manage/maintain them. More on this later.

Logger

i18nlog provides a means by which you can log I18N messages using any logging framework you want. The logging frameworks i18nlog directly supports today is Apache Log4J (log4j), Apache Commons Logging (commons-logging) and JDK logging, but commons-logging itself supports pluggable logging frameworks - therefore, i18nlog picks up that capability "for free". That said, i18nlog was written in such a way that, with a little more work, it can be made extensible to work with your own logging framework directly (for those that do not want to use or have a dependency on log4j or commons-logging or use JDK logging).

The main class of the logging subsystem is Logger. It provides the typical set of trace, debug, info, warn, error and fatal methods. However, instead of taking a string consisting of the message itself, you pass in the resource bundle key and a variable arguments list for the placeholder values that are to be replaced within the message. It uses the Msg class under the covers to get the actual localized message.

You obtain Logger objects by using the factory class LoggerFactory. For those that have used other logging frameworks like log4j or commons-logging, the programming model is basically the same when creating your Logger objects via the factory:

   public static final Logger LOG = LoggerFactory.getLogger(MyClass.class);

By default, LoggerFactory will use JDK logging unless Apache Log4J or Apache Commons Logging is found in your classpath. If Apache Log4J is found, it will be used; otherwise, if Apache Commons Logging is found, it will be used (note that Log4J takes precedence over Commons). You can explicitly define which logging framework to use by setting the system property i18nlog.logger-type to jdk, log4j or commons. There is also a public API, LoggerFactory.resetLoggerType(), to allow you to programmatically set the logging framework to be used.

You can explicitly tell the factory what resource bundle the Logger should use and what Locale the messages should be in (see the full LoggerFactory API for the methods you'd need to use to do this). Note that it is possible to define a different "log locale" default that the loggers will use as compared to the default locale Msg instances will use. The LoggerLocale class is used for this feature. This is to facilitate the use-case where I want to log messages in a language my support group can read, but my user-interface is in a language that my users can read (which may be different). For example, my users may be German-speaking, but the software is supported by a group that works in France and is only French-speaking. In this case, when my German users have a problem, they will normally send the logs to the support group in France. Having the log messages in German isn't very helpful, so the software could, by default, set its log locale to Locale.FRENCH while allowing Msg to default to the user's default locale of Locale.GERMAN. On the other hand, if my German users want to try to debug a problem themselves or just wish to read the logs to see what the software is doing, having the log files contain messages in French isn't helpful to them. In this case, my German users will be able to "flip a setting" and have the log files dump messages in German. This magical setting is the locale of the LoggerLocale - refer to its API for more information on how to switch the log locale). Of course, this is all possible only if I take the time to translate my resource bundle messages in both French and German and I ship those two language resource bundles with the software (but this is what I18N is all about so nothing should be a surprise here).

A typical I18N logging usage is as follows:

   public static final Logger LOG = LoggerFactory.getLogger(MyClass.class);
   ...
   LOG.debug("my-bundle-key", name, count);
   ...
   try
   {
      ...
   }
   catch (Exception e)
   {
      LOG.warn(e, "my-error-key", name);
   }

The above shows two distinct logging features. The first is the fact that you log an I18N message specified by the bundle key, as opposed to the actual message itself. This works the same way as the Msg API. The second feature is the logging of a message that is associated with a particular exception. Notice that the exception must come as the argument before the message key string (this is due to the nature of how varargs are parsed - putting the exception last in the argument list, as is the case with most other logging frameworks, would cause only the exception message to be logged and not its full stack trace. This can be considered a feature in and of itself; if you know you have an exception that you would never want to have its stack trace dumped in the log, put it in the variable arguments list so it is treated like any other object, that is, its toString() value will be the only thing logged).

There are additional features that i18nlog adds to its logging framework. The first is the ability to tell Logger not to dump stack traces of exceptions even if the exception was logged using the exception log methods (there is one for each log level). This is useful if you wish to run your app in a slightly quieter mode - you might not care to see all the exception stack traces during a particular run. Note that this feature cannot turn off stack dumps for exceptions logged at the FATAL level - fatal exceptions logged with that method always have their stack traces dumped. Read the API docs on the Logger class to learn more about how to enable and disable this feature.

The second additional feature in the logging framework is the ability to log a message's associated resource bundle key along with the message itself. The resource bundle key is the same across all locales - so no matter what language the log messages are in, the keys will always be the same. This is useful if a user has set his log locale to a language that you cannot read and thus the log messages themselves are not useful. With the resource bundle key logged along with the message itself, you can use that key as the code to identify the message (you could then look up that code in your software documentation or in a resource bundle that contains the messages in your language). Read the API docs on the Logger class to learn more about how to enable and disable this feature.

And lastly, you can get localized messages directly from a Logger instance via its getMsg() and getMsgString() methods. Since Logger instances can be using a different locale than Msg instances, these APIs allow you to ask a Logger for a message in its log locale without knowing what that locale is (and thus without having to tell Msg what to use). The typical use case for this is to obtain log messages that are to be passed to an exception's constructor where the exception to be instantiated is not derived from i18nlog's LocalizedException or LocalizedRuntimeException exception classes.

Localized Exceptions

i18nlog provides two base exception classes (for both checked and unchecked exceptions) that can be used to create your own localized exceptions. See LocalizedException and LocalizedRuntimeException. These have constructors whose signatures are very similar to the Msg class. They simply allow you to specify your exception message via a resource bundle key and a variable argument list of placeholder values, with the ability to optionally specify the base bundle name and locale. This allows your exception messages to be localized to different languages, just as Msg can retrieve localized messages.

Obtaining I18N Messages Within Ant Scripts

You can obtain I18N messages within your Ant scripts by using the I18N message ant task (named the <i18n-msg> task). This allows you to output localized messages within your Ant scripts. The <i18n-msg> task extends Ant's core <echo> task - so all attributes that the echo task accepts are also accepted by the <i18n-msg> task. The only difference is the message attribute isn't the actual message; instead it is the resource bundle message key. This new Ant task can also accept a bundle attribute if you need to define the base bundle name where your localized message is located (the default is "messages"). The locale attribute is rarely needed, but if it is set, it defines the actual locale in which your message should be displayed. The default is your VM's default locale - this is normally what you want. Finally, you can define a property attribute. If you define this attribute, then this task will not echo the message; instead, the given property will be set with the localized message string. If your message has argument placeholders (e.g. {0}), you need to define child <arg> elements to define the values for those placeholders. Here's a few examples on how to use this Ant task:

   <taskdef name="i18n-msg"
            classpathref="i18nlog-jar-and-bundles.classpath"
            classname="mazz.i18n.ant.I18NMessageAntTask" />

   <i18n-msg message="Example.simple-message" bundle="example-messages" />
   
   <i18n-msg message="Example.simple-message" bundle="example-messages" property="test.i18n.property" />
   <echo message="${test.i18n.property}" />
   
   <i18n-msg message="Test.arg-message">
     <arg value="100" />  
     <arg value="2" />  
   <i18n-msg>

I18N Annotations and Resource Bundle Auto-Generation

So far, we've assumed that when ever you need to specify a particular message, you would provide the message's bundle key by hardcoding its value in the API calls; e.g.: Msg.createMsg("my-bundle-key"). Because this does not provide a way for nice compile-time checks (to ensure that key actually refers to an actual bundle message), the developer must ensure that the key does not have a typo and the developer must remember to add the actual message associated with that key to the resource bundle properties file. This is not an easy thing to do and is ripe for problems that will not manifest itself until you run your app and notice that messages are missing. This isn't even easy to unit test. What we need is a way to get the compiler to check these things for us. It would also be nice to have a tool that automatically generates our resource bundle properties files so the developer isn't responsible for manually adding new messages to them and manually cleaning up old, obsolete messages that are no longer used.

Fortunately, i18nlog provides mechanisms to do those things. First, there are several I18N annotations that allow you to annotate your Java classes to facilitate the automatic generation of resource bundles via a custom Ant task so you don't have to manually create and edit your resource bundle .properties files. The message annotations are placed on constants that you use in place of your resource bundle key strings, so this inherently forces compile time checks. This means that typos introduced in the code (i.e. misspelling a constant name) and usage of obsolete/deleted messages are detected at compile time.

The annotations that are available are:

The @I18NResourceBundle annotation defines what resource bundle properties file you want to put your localized messages in. This can annotate an entire class or interface, or it can annotate a specific field. If you annotate a class or interface, all @I18NMessage annotations found in that class or instance will be stored, by default, in that resource bundle. If the annotation is on a particular constant field, it will be the default bundle for that constant only.

The @I18NMessage can only annotate a field, and more specifically, should only annotate a static final field that is of type java.lang.String (in fact, an error will be generated by the Ant task if this is not the case - more on the Ant task below). This annotation defines the actual localized message translation for a particular locale. You can define multiple message translations for a single field using the @I18NMessages annotation (which simply wraps multiple @I18NMessage annotations) The constant String value of the field that you are annotating is the resource bundle key string. It is these constants that you pass into the Msg, Logger and LocalizedException methods/constructors when you want to refer to a specific resource bundle key. This avoids having to hardcode string literals into your method calls and thus avoids the possibility of introducing a typo in the string literal and avoids the possibility that you are using a message key that no longer exists (since deleting a constant would cause all uses of that old constant to be flagged as an error by the compiler).

This is probably confusing, so an example is in order here. Let's look at the ExampleAnnotatedClass that is in the i18nlog project as an illustrative example on how to annotate your classes (this class also has a couple of incorrectly used annotations; those incorrect usages are documented in the comments within the class so as not to confuse the reader).

First, notice that the class declaration has a @I18NResourceBundle annotation.

   @I18NResourceBundle( baseName = "example-messages" )
   public class ExampleAnnotatedClass

This tells us the default file where this class's I18N messages will be written. In this example, all I18N messages found within the scope of this class definition will be stored, by default, in the resource bundle properties file named "example-messages_en_US.properties" assuming my VM's default locale is "en_US". The @I18NResourceBundle annotation defines what the base bundle name is via its baseName attribute (if you do not specify the baseName attribute, the default will be "messages"). It also indicates which locale the messages are written in via the defaultLocale attribute (if this attribute is not specified, the default will be that of the defaultlocale attribute of the i18n ANT task or, if that is not defined, the VM's default locale). Two additional things to note here: first, both class and interface declarations can be annotated with @I18NResourceBundle - since both can contain static final String constants (this is useful if you want to put all of your I18N message fields in a single interface - thus keeping all I18N information in a single location within your code base). Secondly, this top-level annotation can be overridden by placing a @I18NResourceBundle annotation on a particular field that wants to put its messages in a resource bundle that is different than that specified by the top-level annotation. In the ExampleAnnotatedClass example, you can see this on the field MESSAGE_THREE_KEY:

   @I18NMessage( "This is my en_CA version of the third message that should go in a en_CA bundle" )
   @I18NResourceBundle( baseName = "example-messages-for-third",
                        defaultLocale = "en_CA" )
   public static final String MESSAGE_THREE_KEY = "Example.message3-key";

Now that you have defined where you want to store your I18N messages via the @I18NResourceBundle annotation, you then begin to define the messages themselves. That's where the @I18NMessage and @I18NMessages annotations come in. You need to define a static final String constant whose value is the bundle key of the localized message. This bundle key is the same across all locales - it identifies your message regardless of what language the message is displayed as. Once you define the constant, you need to annotate it to denote it as an I18N message. If you are multi-lingual, you can use I18NMessages to provide multiple translations for your message; the typically use-case, however, is that you provide a single translation in a @I18NMessage annotation. So, back to our example class, you can see these annotations at work here:

   @I18NMessages( {
                    @I18NMessage( "This is my English message: {0}" ),
                    @I18NMessage( value = "This is my UK-English message: {0}",
                                  locale = "en_UK"),
                    @I18NMessage( value = "Dieses ist meine deutsche Anzeige: {0}",
                                  locale = "de")
                 })
   public static final String MESSAGE_KEY = "Example.message-key";

   @I18NMessage( "This is my English version of the second message" )
   public static final String MESSAGE_TWO_KEY = "Example.message2-key";

You can see that for MESSAGE_KEY, I decided to provide three translations - one for the default locale (which is defined by my @I18NResourceBundle annotation), one for the "en_UK" locale and one for the "de" (aka German) locale. For the MESSAGE_TWO_KEY bundle key constant, I only defined a single translation in my default locale.

All that said, in your typical use-case, you will have a single @I18NResourceBundle annotation on a top-level interface and each I18N constant field will have a single @I18NMessage that uses the default locale as defined by the @I18NResourceBundle annotation:

   @I18NResourceBundle( baseName="my-messages" defaultLocale="en" )
   public interface MyMessageKeys
   {
      @I18NMessage( "This is an English message" )
      public static final String I18N_FIRST_MESSAGE = "Example.first-message";

      @I18NMessage( "Hello {0}.  You visited this website {1} times" )
      public static final String I18N_WELCOME = "Example.welcome";

      // ... and any more you want to define
   }

Now you use these in your code rather than hardcoding the string literals:

   LOG.debug( MyMessageKeys.I18N_FIRST_MESSAGE );
   System.out.println( Msg.createMsg( MyMessageKeys.I18N_WELCOME, "John", counter ) );

At this point, you are still responsible for creating and maintaining your own resource bundle files. In fact, as of right now, I wouldn't even need the I18N annotations - simply creating constants that define my bundle keys is enough to enforce compile time checks on my usage of these bundle keys. However, in the above example, I would have to manually create a file called "my-messages_en.properties" and edit it such that the following messages were added to it:

   Example.first-message=This is an English message
   Example.welcome=Hello {0}.  You visited this website {1} times

That's where the custom I18N ant task comes in. This custom Ant task ships with the i18nlog distribution. Its job is to scan your classes looking for these I18N annotations and, based on them, will automatically create your resource bundles for you. This means that as you add more I18N message constant fields, they will automatically be added to your resource bundles. If you delete an I18N message constant, that message will be removed from the resulting resource bundle that the Ant task generates. Your only responsibility is to maintain the accuracy of the Java constant definitions and their annotations. To run the Ant task, you need to have something like the following in your Ant build script:

   <taskdef name="i18n"
            classpathref="i18nlog-jar.classpath"
            classname="mazz.i18n.ant.I18NAntTask" />

   <i18n outputdir="${classes.dir}"
         defaultlocale="en"
         verbose="true"
         verify="true">
      <classpath refid="my.classpath" />
      <classfileset dir="${classes.dir}"/>
   </i18n>

If you are building your projects using Maven2, you can embed this Ant task in your Maven pom.xml using something like this:

  <build>    
    <plugins>
      <plugin>
        <artifactId>maven-antrun-plugin</artifactId>
        <executions>
          <execution>
            <phase>process-classes</phase>
            <configuration>
              <tasks>
                <!-- generate the I18N resource bundles -->
                <taskdef name="i18n"
                         classpathref="maven.runtime.classpath"
                         classname="mazz.i18n.ant.I18NAntTask" />

                <i18n outputdir="${project.build.outputDirectory}"
                      defaultlocale="en"
                      verbose="false"
                      verify="true">
                   <classpath refid="maven.runtime.classpath" />
                   <classfileset dir="${project.build.outputDirectory}">
                      <include name="**/*.class"/>
                   </classfileset>
                </i18n>
              </tasks>
            </configuration>
            <goals>
              <goal>run</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>  
  </build>

The only things you really have to worry about here is that 1) you must give the Ant task a classpath that can find your I18N annotated classes and their dependencies (<classpath>) and 2) you have to give a set of class files to the Ant task which is the list of files that are to be scanned for I18N annotations (<classfileset>). It is recommended that you use the verbose mode the first time you use the Ant task so you can see what its doing. Once you get the build the way you want it, you can turn off verbose mode.

Generating Help Documentation

One optional feature you can use with this ANT task is the ability to generate help documentation which consists of a reference of all your resource bundle key names with their messages along with some additional description of what the message means. There is an optional attribute you can specify in your @I18NMessage annotations - the help attribute. Its value can be any string that further describes the message. Think of this as documentation that further describes what situation occurred that the message is trying to convey. The auto-generated help documents can, therefore, provide a cross-reference between the message keys, the messages themselves and more helpful descriptions of the messages.

Many times, you can use this as a "message code" or "error code" listing, where each of your resource bundle keys can be considered a "message code" or "error code". To generate help documentation, you need to use the <helpdoc> inner tag inside of the <i18n> task:

   <i18n outputdir="${classes.dir}">
      <classpath refid="my.classpath" />
      <classfileset dir="${classes.dir}"/>
      <helpdoc outputdir="${doc.dir}/help"/>
   </i18n>

For every resource bundle generated, you will get an additional help document output in the given directory specified in <helpdoc>. There are additional attributes you can specify in the <helpdoc> tag:

More needs to be said about the templateitem attribute. When specified, this must point to a file that contains a string that will be copied for each message. The contents in the template file is a template for each message's help item. The template can contain one or more replacement strings:

For each message, the template item string will be written to the associated help document - with each replacement string in the template item string replaced according to the message. For example, if the message belongs to bundle "mymessages_en" and has a key of "my.bundle.key" with a value of "This is my message {0}", the template item string will be written to the help document "mymessages_en.html" with all @@@I18NBUNDLE@@@ replacement strings replaced with "mymessages_en", all @@@I18NKEY@@@ replacement strings replaced with "my.bundle.key" and all @@@I18NMESSAGE@@@ replacement strings replaced with "This is my message {0}". If the I18NMessage had a help attribute specified, its value will replace the @@@I18NHELP@@@ string. If no template item file is specified, a default template string will be used that will generate a simple HTML table.

Note that these replacement strings are only meaningful within the contents of the template item file with the exception of @@@I18NBUNDLE@@@ which is also replaced within the contents of the template header and footer files.

The idea behind this help documentation generation is that you will end up with a document (or documents) containing a list of all your message key codes and their messages. You can then edit those help documents by providing additional documentation on what those messages mean.


Building From Source

First, grab the code from the CVS repository as explained here. The module name is "i18nlog":

   cvs -z3 -d:pserver:anonymous@i18nlog.cvs.sourceforge.net:/cvsroot/i18nlog co -P i18nlog

Now, all you need to do is run the Ant build script to build the distribution (note that this requires a JDK that is at version 1.5 or higher):

   ant package-dist

If you wish to see the custom Ant task generate an example set of resource bundles, execute the sample test script like this:

   ant -f i18n-test-build.xml

You can then compare the ExampleAnnotatedTestClass and its annotations with the resource bundles that were generated and stored in the build/test-ant-output directory.

Copyright © 2006 John J. Mazzitelli All Rights Reserved.