Exceptional practices, Part 3

Use message catalogs for easy localization

In Parts 1 and 2 of this series, I explored several ways in which exceptions could be used more effectively to deliver error-recovery information to the parties that need it -- other Java classes, developers, and users. In Part 3, I look at the oft-ignored problem of internationalization and a technique for simplifying internationalization: the use of message catalogs for storing message text.

Read the whole series on exception handling:

Part 1 emphasized that if two different exceptions could potentially have different error-recovery procedures, then they should be of different classes -- although perhaps derived from the same base class. You never want to be in the situation where you will be tempted to use the message text to differentiate between two different exceptions. The message text associated with exceptions is explanatory only and exclusively for the consumption of people, not code.

Don't build error messages by hand

While most developers agree that it's a bad idea to hard-code text strings inside program code, most of us do exactly this all the time. The following code fragment illustrates a common but undesirable technique of throwing an exception:

Listing 1. Build error messages by hand

  if (!file.exists()) {
    throw new ResourceException("Cannot find file " + file.getName());
  }

Despite the fact that you probably see this construct every day, this is a bad way to build error messages. The explanatory error text simply does not belong in the Java code; it should be retrieved from an external source. What happens when you want to localize this code for a foreign market? Someone will have to comb through all the source code, identifying all text strings that will be used to build exceptions. Then a translator will have to translate them all, and then you have to figure out how to maintain different versions of the same code for multiple markets. And you have to repeat this process for every release.

Even if you never plan to localize, there are other good reasons for removing error message text from code: Error messages can easily get out of sync with the documentation -- a developer might change an error message and forget to inform the documentation writer. Similarly, you might want to change the wording of all error messages of a certain type to make them more consistent, but since you must comb through all the sources to find all such error messages, you could easily miss one, resulting in inconsistent error messages.

Clearly, the source code is not the place for explanatory messages, which really are more closely related to documentation than to code. In fact, developers probably shouldn't even be writing error messages any more than they should be writing documentation. How many times have you seen error messages that are utterly incomprehensible except to the person who wrote the code? As long as the error messages live inside the source code, they will be created and owned by developers, often to the detriment of usability.

Use MessageFormat to build error messages

Java provides us with several tools to make internationalizing message text much simpler. The java.text.MessageFormat class allows us to parameterize textual error messages. MessageFormat resembles the printf function in C, but is even more flexible, as it allows for parameter reordering in the format string. The following version of Listing 1 is slightly better because it doesn't hide the error message text in the middle of the code:

Listing 2. Build error messages with MessageFormat

  public static final String MSG_FILE_NOT_FOUND = "Cannot find file {1}";
  ...
  public static String formatMessage(String messageString, Object arg0) {
    MessageFormat mf = new MessageFormat(messageString);
    Object[] args = new Object[1];
    args[0] = arg0;
    return mf.format(args);
  }
  ...
  if (!file.exists()) {
    throw new ResourceException(formatMessage(MSG_FILE_NOT_FOUND, 
                                              file.getName()));
  }

This version is better than the first in several ways: The error message is separated (somewhat) from the code, so it is much easier to find when localizing the class. Separating the error message from the code reduces the work required for localization and reduces the chance that an error message will be missed. It also encourages developers to reuse the same message if multiple methods throw the same error so the program will be more likely to have a consistent set of error messages.

However, this technique, while a big improvement, still puts the error message text inside the source code, which causes several problems. Localizers must have access to the source code, which might be a political problem in some organizations. Also, multiple codeline branches must be managed and merged to produce localized versions when the classes are updated. A much better approach would take the error messages completely out of the Java classes that throw them and migrate them into some sort of resource database, sometimes called a message catalog.

The ResourceBundle class

Message catalogs are nothing new -- the original MacOS (1984) provided a sophisticated resource manager for storing user interface items separately from the program code. Even before that, the VMS (Virtual Memory System, circa 1978) operating system had a mechanism for storing message strings separately from the program for simple localization. While it's easy to build your own message catalog facility, the Java class libraries already provide a useful mechanism for managing localized resources, the ResourceBundle class. A ResourceBundle stores a set of key-value pairs; the key names are static and specific to the program, but the values might change from locale to locale. The Java class libraries provide several helper classes for simplifying the resource bundle process, ListResourceBundle and PropertyResourceBundle.

You construct a resource bundle by creating one or more classes that implement ResourceBundle and use a special naming convention to associate the concrete implementation class with a locale. For example, suppose you want to create a resource bundle called com.foo.FooResources and provide implementations for English, French, and German. To do that, you would create a default implementation class called com.foo.FooResources and then additional implementations for other locales named com.foo.FooResources_en (which might be the same as the default), com.foo.FooResources_fr, and com.foo.FooResources_de. You could create additional localized versions later. The ResourceBundle class will use the current locale to find the most appropriate version of the resource bundle; if the class can't find a localized version, it will use the default.

In order to prevent runtime errors caused by typos in key names, the key names used by an application should be given symbolic names; these names should be used instead of the actual key names. That way, if you mistype a message string's name, you'll find out at compile time. (The Javadoc pages for ResourceBundle make the error of embedding strings in the text as keys, but you should strive to do better in your programs.) Here is a simple example of how the above code fragments would be written using resource bundles:

Listing 3. Build error messages with resource bundles

public interface FooResourcesKeys {
  public static String MSG_FILE_NOT_FOUND = "MSG_FILE_NOT_FOUND";
  public static String MSG_CANT_OPEN_FILE = "MSG_CANT_OPEN_FILE";
}
public class FooResources extends ListResourceBundle
implements FooResourcesKeys {
  public Object[][] getContents() {
    return contents;
  }
  
  static final Object[][] contents = {
    // Localize from here
    {MSG_FILE_NOT_FOUND, "Cannot find file {1}"},
    {MSG_CANT_OPEN_FILE, "Cannot open file {1}"},
    // Localize to here
  };
}
public class MessageUtil {
  private static ResourceBundle myResources =
    ResourceBundle.getBundle("com.foo.FooResources");
  private static String getMessageString(String messageKey) {
    return myResources.getString(messageKey);
  }
  public static String formatMessage(String messageKey) {
    MessageFormat mf = new MessageFormat(getMessageString(messageKey));
    return mf.format(new Object[0]);
  }
  public static String formatMessage(String messageKey, 
                                     Object arg0) {
    MessageFormat mf = new MessageFormat(getMessageString(messageKey));
    Object[] args = new Object[1];
    args[0] = arg0;
    return mf.format(args);
  }
  public static String formatMessage(String messageKey, 
                                     Object arg0,
                                     Object arg1) {
    MessageFormat mf = new MessageFormat(getMessageString(messageKey));
    Object[] args = new Object[2];
    args[0] = arg0;
    args[1] = arg1;
    return mf.format(args);
  }
  // Include implementations of formatMessage() for as many arguments
  // as you need
}
public class SomeClass implements FooResourcesKeys {
  ...
  if (!file.exists()) {
    throw new ResourceException(
      MessageUtil.formatMessage(MSG_FILE_NOT_FOUND, 
                                file.getName()));
  }
}

In the above example, for each error message you will use in the application, you need to place one entry in FooResourcesKeys, and a corresponding entry in each resource bundle implementation (one for each locale) for that key. If you've named your resource bundles correctly, ResourceBundle will be able to chain lookups so that if an entry is not in the localized bundle, it will search the default bundle too. For each point in your code where you will throw an exception, you will need to create the message string through MessageUtil as shown in Listing 3's SomeClass.

In order to facilitate code reuse, you can extend this technique to support multiple resource bundles, with one bundle per component or even one per package. In this case, the MessageUtil class would be slightly more complicated, specifying an additional argument for identifying what bundle a given error message comes from.

Resource bundles can also be used for other localizeable resources, such as text used in Swing components like buttons and labels, or even for nontextual resources like audio files or images.

Hidden dividends from message catalogs

Message catalogs not only simplify the localization process, they also greatly simplify the maintenance of a product's multiple localized versions. With each release, localizers need only compare the resource bundle's previous version with the current one to know exactly what incremental localization work is required.

Using message catalogs to store exception message strings offers another hidden benefit. Once you've placed all the exception messages in a message catalog, you now have a comprehensive list of all the exception messages your application might throw. This provides an easy starting point for the documentation writer to use when creating the manual's "Troubleshooting" or "Error Messages" section. Message catalogs also make it easy for documentation writers to track changes in the resource bundles and ensure the documentation stays in sync with the software. And since error messages are arguably more a function of documentation than engineering, using resource bundles renders it more practical for the documentation writers to own the resource bundle source files, which is probably good for everyone involved -- developers and users alike.

Create useful error messages

Maintaining error message strings using message catalogs requires a bit more work up front than the obvious way of building error messages, but it pays off in a number of ways. The ResourceBundle classes in the Java class libraries allow you to easily build message catalogs into your application. Not only does the message catalog technique enforce a clean separation between the UI and the program logic, it also makes it easier to localize the software and maintain localized versions, and to produce documentation that accurately reflects what the program does. As a further bonus, because ResourceBundle classes make it simpler for people other than the developer to be involved in error message creation, they make it more likely that the error messages will be useful to the user.

Brian Goetz is a professional software developer with more than 15 years of experience. He is a principal consultant at Quiotix, a software development and consulting firm located in Los Altos, Calif.
1 2 Page 1