Локализация ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-16

Интернационализация (i18n) и локализация (l10n) в Java

Программа, написанная на компьютере с английскими настройками, отправляется к пользователю в России, и String.format("%.2f", 1234.5) вдруг возвращает "1234,50" вместо "1234.50". Следующий вызов Double.parseDouble на этой строке падает с NumberFormatException. Чтобы таких сюрпризов не было, приложение нужно заранее подготовить к разным языкам и регионам. Этим и занимаются интернационализация и локализация.

Интернационализация (i18n) — это проектирование приложения так, чтобы для добавления нового языка не нужно было перекомпилировать и пересобирать всё приложение. Для этого тексты интерфейса хранят отдельно от кода, в файлах свойств, которые приложение загружает во время работы. Форматы чисел, дат и валют берут из текущей локали, а не прописывают в коде.

Локализация (l10n) — это адаптация приложения к конкретному языку и региону: перевод текстов интерфейса и документации, а также настройка форматов даты и времени, валют, чисел и других региональных особенностей.

Презентация с видео на Patreon →

Что такое i18n и l10n: расшифровка

i18n — это сокращение английского слова internationalization (интернационализация): первая буква i, последняя n и 18 букв между ними. l10n так же сокращает слово localization (локализация): l, 10 букв, n. Такие сокращения называют нумеронимами. Программисты придумали их, чтобы не писать длинные слова целиком.

Сокращение Полное слово Как получилось Значение
i18n internationalization i + 18 букв + n Интернационализация: подготовка кода к разным языкам
l10n localization l + 10 букв + n Локализация: адаптация под конкретный язык и регион
g11n globalization g + 11 букв + n Глобализация: i18n и l10n вместе
L10n localization То же, что l10n Заглавную L иногда пишут, чтобы её не путали с цифрой 1

В Java обе задачи решает один и тот же набор классов, а связывает их локаль — объект java.util.Locale, то есть пара «язык + регион», например ru-RU или en-US.

i18n и l10n: в чём разница

Интернационализация (i18n) делается один раз, при разработке: программист готовит код так, чтобы его можно было перевести. Локализация (l10n) делается для каждого языка отдельно: переводчик или разработчик добавляет переводы и региональные настройки, не трогая код. Без i18n локализация превращается в переписывание программы.

Признак Интернационализация (i18n) Локализация (l10n)
Суть Сделать код независимым от языка и региона Приспособить приложение к конкретному языку и региону
Когда Один раз, на этапе проектирования и разработки Для каждого нового языка или рынка
Кто делает Разработчик Переводчик, разработчик, специалист по региону
Нужна ли перекомпиляция Да, это изменение кода Нет: добавляется файл с переводами
Пример в Java Тексты вынесены в ResourceBundle, даты форматируются через DateTimeFormatter с локалью Добавлен файл messages_de_DE.properties с немецкими строками

Что меняется от локали к локали

От локали зависит не только язык интерфейса. Меняются разделители в числах, порядок частей даты, названия месяцев, символ и место валюты, первый день недели и порядок сортировки строк. Ниже показано, как одни и те же данные выглядят в трёх локалях при стандартных форматтерах Java 11+.

Что en-US de-DE ru-RU Класс Java
Число 1234,5 1,234.5 1.234,5 1 234,5 (неразрывный пробел) NumberFormat
Сумма 1234,5 в валюте страны $1,234.50 1.234,50 € 1 234,50 ₽ NumberFormat.getCurrencyInstance
Дата 8 марта 2026 (LONG) March 8, 2026 8. März 2026 8 марта 2026 г. DateTimeFormatter
Первый день недели Воскресенье Понедельник Понедельник WeekFields.of(locale)
Приветствие в интерфейсе Welcome! Willkommen! Добро пожаловать! ResourceBundle

Почему вывод отличается от Java 8

Начиная с Java 9 данные о локалях по умолчанию берутся из Unicode CLDR (JEP 252), а не из старых таблиц JRE. Поэтому один и тот же форматтер в Java 8 и Java 11+ может выдать разные строки: другой символ валюты, другой пробел-разделитель, «г.» после года. Не сравнивайте в тестах отформатированные строки, записанные на другой версии JDK.

Инструменты Java для i18n

В стандартной библиотеке Java для интернационализации есть всё необходимое, сторонние библиотеки не нужны. Основу составляют Locale (какой язык и регион) и ResourceBundle (где лежат переводы). Остальные классы форматируют числа, даты и сообщения по правилам выбранной локали.

Класс Пакет Для чего Подробнее
Locale java.util Описывает язык и регион; передаётся во все форматтеры Урок о классе Locale
ResourceBundle java.util Загружает переводы из .properties-файлов для нужной локали Урок о классе ResourceBundle
NumberFormat java.text Числа, валюты, проценты по правилам локали Урок о классе NumberFormat
DateTimeFormatter java.time.format Даты и время: ofLocalizedDate(FormatStyle) + withLocale Пример ниже
MessageFormat java.text Шаблоны с параметрами: "Найдено {0} файлов", где порядок слов задаёт перевод —
Collator java.text Сортировка строк по алфавиту конкретного языка (ё, ä, é) —
String.format(Locale, ...) java.lang Форматирование строк с явно заданной локалью —

Фреймворки строятся на тех же идеях. Например, MessageSource в Spring Boot читает те же messages_xx.properties и выбирает язык по заголовку Accept-Language запроса.

Как сделать приложение интернационализированным: 4 шага

Интернационализация Java-приложения сводится к четырём шагам. Все строки интерфейса выносятся из кода в properties-файлы. Локаль пользователя задаётся явно. Переводы загружаются через ResourceBundle. Числа и даты выводятся только через форматтеры с этой локалью.

  1. Вынести тексты. Вместо System.out.println("Добро пожаловать!") — ключ message.welcome в файлах messages.properties, messages_ru_RU.properties и т.д. Формат файлов и правила их именования описаны в уроке о ResourceBundle.
  2. Определить локаль. Взять её из настроек пользователя, заголовка HTTP-запроса или Locale.getDefault(). Способы создать локаль описаны в уроке о Locale.
  3. Загрузить переводы. ResourceBundle.getBundle("messages", locale) сам найдёт подходящий файл. Если файла для этой локали нет, он возьмёт более общий.
  4. Форматировать с локалью. Числа, валюты и даты выводить через NumberFormat, DateTimeFormatter, MessageFormat с той же локалью, а не склеивать строки вручную.

Пример локализации даты (java localization example): одна и та же LocalDate выводится в трёх локалях, при этом код не меняется.

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class LocalizedDateExample {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2026, 3, 8);
        Locale[] locales = {Locale.US, Locale.GERMANY, Locale.forLanguageTag("ru-RU")};

        for (Locale locale : locales) {
            DateTimeFormatter formatter = DateTimeFormatter
                    .ofLocalizedDate(FormatStyle.LONG)
                    .withLocale(locale);
            System.out.println(locale.toLanguageTag() + ": " + date.format(formatter));
        }
    }
}
// Вывод (Java 11+):
// en-US: March 8, 2026
// de-DE: 8. März 2026
// ru-RU: 8 марта 2026 г.

Чтобы добавить, например, французский, достаточно поставить в массив Locale.FRANCE. Ни строки формата, ни названия месяцев в коде не появляются: их берёт JDK. Это и есть результат интернационализации.

Что изменилось в современных версиях Java

С Java 9 ResourceBundle читает properties-файлы в кодировке UTF-8 (JEP 226), поэтому кириллицу больше не нужно превращать в До... утилитой native2ascii. Конструкторы new Locale("ru", "RU") помечены устаревшими с Java 19, вместо них используйте Locale.of("ru", "RU") или Locale.forLanguageTag("ru-RU").

Где локаль по умолчанию ломает код

Если локаль не указана явно, многие методы Java берут Locale.getDefault(), то есть настройки операционной системы или JVM. Поэтому один и тот же код по-разному работает на машине разработчика и на сервере. Внутренние данные (ключи, идентификаторы, форматы файлов) нужно обрабатывать с Locale.ROOT, а локаль пользователя применять только к тексту, который он увидит.

Ошибка 1: число, которое не читается обратно. Это пример из начала урока.

Locale.setDefault(Locale.forLanguageTag("ru-RU"));

String s = String.format("%.2f", 1234.5);
System.out.println(s);                       // Вывод: 1234,50
double d = Double.parseDouble(s);            // NumberFormatException: For input string: "1234,50"

// Исправление: для машинного формата явно указать Locale.ROOT
String fixed = String.format(Locale.ROOT, "%.2f", 1234.5);
System.out.println(fixed);                   // Вывод: 1234.50
System.out.println(Double.parseDouble(fixed)); // Вывод: 1234.5

Ошибка 2: что выведет этот код?

Locale tr = Locale.forLanguageTag("tr-TR");

System.out.println("title".toUpperCase(tr));          // Вывод: TİTLE
System.out.println("TITLE".toLowerCase(tr));          // Вывод: tıtle
System.out.println("title".toUpperCase(Locale.ROOT)); // Вывод: TITLE

В турецком языке у буквы i заглавная пара с точкой (İ), а у I строчная пара без точки (ı). Если приложение запущено с турецкой локалью по умолчанию, то "title".toUpperCase() без аргументов вернёт "TİTLE". Тогда сравнение equals("TITLE"), поиск по ключу в Map или разбор команды тихо перестанут работать. Для ключей, имён enum и протоколов используйте toUpperCase(Locale.ROOT) или equalsIgnoreCase.

Часто задаваемые вопросы

Что делают сначала: i18n или l10n?

Сначала i18n. Код один раз готовят к переводу: выносят тексты, форматируют данные с локалью. Потом l10n повторяют для каждого языка, добавляя только файлы переводов. Если начать с локализации в неподготовленном коде, придётся искать и заменять строки по всему проекту. Сравнение по пунктам — в разделе «i18n и l10n: в чём разница».

Что значат a11y и g11n, и как они связаны с i18n?

Это нумеронимы, построенные так же, как i18n. g11n (globalization) — это i18n и l10n вместе, то есть подготовка продукта к мировому рынку. a11y (accessibility, 11 букв между a и y) — доступность для людей с ограниченными возможностями. Это отдельная задача, но её часто решают вместе с i18n: например, экранные дикторы тоже опираются на язык интерфейса.

Какую локаль использует Java, если её не указать?

Локаль по умолчанию, Locale.getDefault(). JVM определяет её при старте по настройкам операционной системы. Переопределить её можно параметрами запуска -Duser.language=en -Duser.country=US или вызовом Locale.setDefault(...). С Java 7 локаль по умолчанию задаётся отдельно для отображения (Locale.Category.DISPLAY) и для форматирования (Locale.Category.FORMAT). Подробнее — в уроке о Locale.

Нужно ли перекомпилировать Java-приложение, чтобы добавить новый язык?

Если i18n сделана правильно, не нужно. Достаточно положить в classpath новый файл, например messages_fr_FR.properties, с теми же ключами, и ResourceBundle.getBundle подхватит его для французской локали. Форматы чисел и дат JDK уже знает. Перекомпиляция нужна, только если тексты зашиты в код. Правила поиска файлов — в уроке о ResourceBundle.

Video Explanation

Prefer video format? Watch this lesson with examples and explanations.

Comments

Please log in or register to have a possibility to add comment.