1. Overview

Exceptions in Java are used to signal that something has gone wrong in a program. In addition to throwing the exception, we can even add a message to provide additional information.

In this article, we’ll take advantage of the getLocalizedMessage method to provide exception messages in both English and French.

2. Resource Bundle

We need a way to lookup messages using a messageKey to identify the message and the Locale to identify which translation will provide the value for the messageKey. We’ll create a simple class to abstract access to our ResourceBundle for retrieving English and French message translations:

public class Messages {

    public static String getMessageForLocale(String messageKey, Locale locale) {
        return ResourceBundle.getBundle("messages", locale)
          .getString(messageKey);
    }

}

Our Messages class uses ResourceBundle to load the properties files into our bundle, which is at the root of our classpath. We have two files – one for our English messages and one for our French messages:

# messages.properties
message.exception = I am an exception.
# messages_fr.properties
message.exception = Je suis une exception.

3. Localized Exception Class

Our Exception subclass will use the default Locale to determine which translation to use for our messages. We’ll get the default Locale using Locale#getDefault.

If our application were running on a server, we would use the HTTP request headers to identify the Locale to use instead of setting the default. For this purpose, we’ll create a constructor to accept a Locale.

Let’s create our Exception subclass. For this, we could extend either RuntimeException or Exception. Let’s extend Exception and override getLocalizedMessage:

public class LocalizedException extends Exception {

    private final String messageKey;
    private final Locale locale;

    public LocalizedException(String messageKey) {
        this(messageKey, Locale.getDefault());
    }

    public LocalizedException(String messageKey, Locale locale) {
        this.messageKey = messageKey;
        this.locale = locale;
    }

    public String getLocalizedMessage() {
        return Messages.getMessageForLocale(messageKey, locale);
    }
}

4. Putting It All Together

Let’s create some unit tests to verify that everything works. We’ll create tests for English and French translations to verify passing a custom Locale to the exception during construction:

@Test
public void givenUsEnglishProvidedLocale_whenLocalizingMessage_thenMessageComesFromDefaultMessage() {
    LocalizedException localizedException = new LocalizedException("message.exception", Locale.US);
    String usEnglishLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(usEnglishLocalizedExceptionMessage).isEqualTo("I am an exception.");
}

@Test
public void givenFranceFrenchProvidedLocale_whenLocalizingMessage_thenMessageComesFromFrenchTranslationMessages() {
    LocalizedException localizedException = new LocalizedException("message.exception", Locale.FRANCE);
    String franceFrenchLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(franceFrenchLocalizedExceptionMessage).isEqualTo("Je suis une exception.");
}

Our exception can use the default Locale as well. Let’s create two more tests to verify the default Locale functionality works:

@Test
public void givenUsEnglishDefaultLocale_whenLocalizingMessage_thenMessageComesFromDefaultMessages() {
    Locale.setDefault(Locale.US);

    LocalizedException localizedException = new LocalizedException("message.exception");
    String usEnglishLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(usEnglishLocalizedExceptionMessage).isEqualTo("I am an exception.");
}

@Test
public void givenFranceFrenchDefaultLocale_whenLocalizingMessage_thenMessageComesFromFrenchTranslationMessages() {
    Locale.setDefault(Locale.FRANCE);

    LocalizedException localizedException = new LocalizedException("message.exception");
    String franceFrenchLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(franceFrenchLocalizedExceptionMessage).isEqualTo("Je suis une exception.");
}

5. Caveats

5.1. Logging Throwables

We’ll need to keep in mind the logging framework we’re using to send Exception instances to the log.

Log4J, Log4J2, and Logback use getMessage to retrieve the message to write to the log appender. If we use java.util.logging, the content comes from getLocalizedMessage.

We might want to consider overriding getMessage to invoke getLocalizedMessage so we won’t have to worry about which logging implementation is used.

5.2. Server-Side Applications

When we localize our exception messages for client applications, we only need to worry about one system’s current Locale. However, if we want to localize exception messages in a server-side application, we should keep in mind that switching the default Locale will affect all requests within our application server.

Should we decide to localize exception messages, we’ll create a constructor on our exception to accept the Locale. This will give us the ability to localize our messages without updating the default Locale.

6. Summary

Localizing exception messages is fairly straightforward. All we need to do is create a ResourceBundle for our messages, then implement getLocalizedMessage in our Exception subclasses.

As usual, the examples are available over on GitHub.