«1. Обзор

Исключения в Java используются, чтобы сигнализировать о том, что в программе что-то пошло не так. Помимо создания исключения, мы можем даже добавить сообщение для предоставления дополнительной информации.

В этой статье мы воспользуемся преимуществами метода getLocalizedMessage для предоставления сообщений об исключениях на английском и французском языках.

2. Пакет ресурсов

Нам нужен способ поиска сообщений с использованием messageKey для идентификации сообщения и Locale для определения того, какой перевод предоставит значение для messageKey. Мы создадим простой класс для абстрактного доступа к нашему ResourceBundle для получения переводов сообщений на английский и французский языки:

public class Messages {

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

}

Наш класс Messages использует ResourceBundle для загрузки файлов свойств в наш пакет, который находится в корне пути к классам. У нас есть два файла — один для наших английских сообщений и один для наших французских сообщений:

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

3. Localized Exception Class

Наш подкласс Exception будет использовать Locale по умолчанию, чтобы определить, какой перевод использовать. для наших сообщений. Мы получим локаль по умолчанию, используя Locale#getDefault.

Если бы наше приложение работало на сервере, мы бы использовали заголовки HTTP-запроса для определения используемой локали вместо установки значения по умолчанию. Для этой цели мы создадим конструктор, который будет принимать Locale.

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);
    }
}

Давайте создадим наш подкласс Exception. Для этого мы могли бы расширить либо RuntimeException, либо Exception. Давайте расширим Exception и переопределим getLocalizedMessage:

4. Собираем все вместе

@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.");
}

Давайте создадим несколько модульных тестов, чтобы убедиться, что все работает. Мы создадим тесты для переводов на английский и французский языки, чтобы проверить передачу пользовательской локали в исключение во время создания:

@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.");
}

Наше исключение также может использовать локаль по умолчанию. Давайте создадим еще два теста для проверки работы функциональности Locale по умолчанию:

5. Предостережения

5.1. Ведение журнала Throwables

Нам нужно помнить о структуре ведения журнала, которую мы используем для отправки экземпляров Exception в журнал.

Log4J, Log4J2 и Logback используют getMessage для получения сообщения для записи в приложение журнала. Если мы используем java.util.logging, содержимое поступает из getLocalizedMessage.

Мы могли бы рассмотреть возможность переопределения getMessage для вызова getLocalizedMessage, чтобы нам не пришлось беспокоиться о том, какая реализация ведения журнала используется.

5.2. Серверные приложения

Когда мы локализуем наши сообщения об исключениях для клиентских приложений, нам нужно беспокоиться только о текущей локали одной системы. Однако, если мы хотим локализовать сообщения об исключениях в приложении на стороне сервера, мы должны помнить, что переключение локали по умолчанию повлияет на все запросы на нашем сервере приложений.

Если мы решим локализовать сообщения об исключениях, мы создадим конструктор для нашего исключения, чтобы принять Locale. Это даст нам возможность локализовать наши сообщения без обновления локали по умолчанию.

6. Резюме

Локализация сообщений об исключениях довольно проста. Все, что нам нужно сделать, это создать ResourceBundle для наших сообщений, а затем реализовать getLocalizedMessage в наших подклассах Exception.