Локализация и интернационализация в Java. Practical Tasks
Положили в messages_ru.properties строку title=Отчёт, запустили программу — а на консоли вместо «Отчёт» набор символов вида «Ð...». До Java 9 класс ResourceBundle читал properties-файлы в кодировке ISO-8859-1, и кириллица из файла в UTF-8 превращалась в мусор. Начиная с Java 9 (JEP 226) такие файлы по умолчанию читаются в UTF-8, но это лишь один из нюансов, на которых спотыкаются при первой локализации.
На этой странице — 3 задачи по локализации и интернационализации в Java: properties-файлы с текстами на русском и английском, отчет, заголовок которого меняется вместе с локалью, и цены в валюте текущей локали для учебного интернет-магазина. Для решения понадобятся классы Locale, ResourceBundle и NumberFormat; общая теория — в уроке «Локализация и интернационализация в Java».
Темы Java на русском и английском (ResourceBundle)
- Создать properties-файлы на русском и английском языках, содержащие названия изученных тем по Java.
- Создать метод, который получает в качестве параметра локаль и распечатывает всю информацию из properties-файла.
Подсказка
Перебрать все ключи поможет метод ResourceBundle.keySet(), значение по ключу возвращает getString(key). Файлы положите в classpath, например src/main/resources/topics_ru.properties и topics_en.properties, и загружайте через ResourceBundle.getBundle("topics", locale).
Класс FullReport: отчет с заголовком на языке локали
Отчёт о сотрудниках · part 2 of 4 · ← part 1 · part 3 →
- Создать класс
FullReport, который выводит ту же информацию, что и классReport(часть 1 на странице заданий по строкам), но заголовок отчета должен изменяться в зависимости от выбранной локали (используем классResourceBundle). - Для форматирования чисел использовать класс
NumberFormat.
С чего начать
Вынесите заголовки отчета в report_ru.properties и report_en.properties, а числа форматируйте через NumberFormat.getNumberInstance(locale) или getCurrencyInstance(locale). Локаль передавайте параметром, а не берите Locale.getDefault() внутри метода: так отчет легко проверить для обеих локалей в одном запуске.
Чек с ценами в валюте текущей локали
Интернет-магазин · part 4 of 11 · ← part 3 · part 5 →
- При покупке вывести на консоль отчет о купленных продуктах. Денежные единицы должны соответствовать текущей локали (используем класс NumberFormat). Например, для локали
uk_UA:Продукты Цена -------------------------------------- Джинсы 1 500,78 ₴ Юбка 1 000,56 ₴ Галстук 500,78 ₴ -------------------------------------- Итого: 3 002,12 ₴ - Отчет должен быть на языке текущей локали — используем ResourceBundle.
Нюанс с деньгами
Храните цены в BigDecimal, а не в double: иначе итог может разойтись с суммой строк на копейку. Разделитель тысяч в выводе NumberFormat — неразрывный пробел, поэтому не сравнивайте результат со строкой, набранной вручную.
На чём чаще всего ошибаются
В задачах на локализацию программа чаще всего ломается не из-за логики, а из-за окружения: properties-файл не попал в classpath, в одном из файлов не хватает ключа, а отформатированная сумма содержит неразрывный пробел вместо обычного. В таблице — симптом, причина и исправление.
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Файл лежит не в classpath или базовое имя указано без пакета | MissingResourceException: Can't find bundle for base name messages, locale ru_RU | Положить файлы в src/main/resources (Maven/Gradle); для файла в папке i18n вызывать getBundle("i18n.messages") |
Ключ есть в messages_en.properties, но забыт в messages_ru.properties | Значение молча берётся из базового файла messages.properties; если ключа нет и там — MissingResourceException: Can't find resource for bundle ..., key ... | Держать одинаковый набор ключей во всех файлах, базовый файл без суффикса использовать как запасной |
Результат format() сравнивают со строкой, набранной вручную | "1 500,78 ₴".equals(...) возвращает false: разделитель тысяч — неразрывный пробел (U+00A0 или U+202F, зависит от локали и версии JDK) | Сравнивать с результатом того же NumberFormat или отключить группировку: setGroupingUsed(false) |
Цены хранятся в double | 0.1 + 0.2 даёт 0.30000000000000004, итог чека расходится с суммой строк на копейку | Хранить суммы в BigDecimal; NumberFormat.format() принимает его как Number |
Локаль создают через new Locale("ru", "RU") | С Java 19 конструкторы Locale помечены @Deprecated, компилятор выдаёт предупреждение | Locale.of("ru", "RU") (Java 19+) или Locale.forLanguageTag("ru-RU") |
Что выведет этот код: запасная локаль
Если для запрошенной локали нет своего properties-файла, ResourceBundle.getBundle() сначала ищет файл для локали по умолчанию (Locale.getDefault()) и только потом берёт базовый файл без суффикса. Поэтому на компьютере с русской локалью «немецкий» отчет внезапно выводится по-русски, а не на языке базового файла.
// src/main/resources/messages.properties: title=Report
// src/main/resources/messages_ru.properties: title=Отчёт
Locale.setDefault(Locale.of("ru", "RU")); // до Java 19: new Locale("ru", "RU")
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.GERMANY);
System.out.println(bundle.getString("title"));
System.out.println(bundle.getLocale());
// Вывод:
// Отчёт
// ru Файла messages_de_DE.properties нет, поэтому поиск переходит к локали по умолчанию ru_RU и находит messages_ru.properties. Чтобы при отсутствии перевода всегда получать базовый файл, отключите этот шаг через ResourceBundle.Control:
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.GERMANY,
ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT));
System.out.println(bundle.getString("title"));
System.out.println(bundle.getLocale().equals(Locale.ROOT));
// Вывод:
// Report
// true Вариант с Control работает в обычном (немодульном) проекте; если вызвать его из именованного модуля Java 9+, будет выброшено UnsupportedOperationException.
Проверь свой код
Перед тем как сверяться с решением, пройдитесь по рекомендациям по оформлению кода: имена классов и методов, отсутствие «магических» строк вместо ключей из properties-файла, локаль, переданная параметром, а не зашитая в метод.
Часто задаваемые вопросы
Нужно ли экранировать кириллицу в properties-файлах через \uXXXX?
Для ResourceBundle в Java 9+ нет: файлы читаются в UTF-8, а если файл не декодируется как UTF-8, он перечитывается как ISO-8859-1. Экранирование нужно на Java 8 и при загрузке через Properties.load(InputStream), который по-прежнему использует ISO-8859-1. Во втором случае передайте Reader: properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)).
Чем ResourceBundle отличается от Properties?
Properties — просто словарь «ключ → значение» из одного файла. ResourceBundle сам выбирает файл по локали, строит цепочку родителей (messages_ru_RU → messages_ru → messages) и кеширует загруженные бандлы. Для текстов интерфейса на нескольких языках нужен именно ResourceBundle.
Как вывести цену в гривнах, если на компьютере английская локаль?
Передайте локаль явно: NumberFormat.getCurrencyInstance(Locale.of("uk", "UA")). Вызов setCurrency(Currency.getInstance("UAH")) у форматтера английской локали поменяет только символ или код валюты, а разделители и порядок останутся английскими. Частые ошибки с форматированием сумм — в разделе «На чём чаще всего ошибаются».
Как проверить программу для другой локали, не меняя настройки ОС?
Узнать текущую локаль можно через Locale.getDefault(). Поменять её — параметрами JVM -Duser.language=en -Duser.country=US или вызовом Locale.setDefault(...) в начале main. Надёжнее всего передавать локаль в методы параметром: тогда отчет проверяется для обеих локалей в одном запуске.
Comments