Skip to main content

i18n Spring Boot: руководство по настройке интернационализации

Настройте MessageSource, создайте файлы properties для локалей, разрешайте локали и отрисовывайте многоязычные шаблоны Thymeleaf, а затем автоматизируйте перевод с помощью ИИ.

1

Добавить зависимости

Spring Boot Starter Web сразу включает автоматическую настройку MessageSource. Добавьте Thymeleaf для серверных шаблонов i18n и стартер проверки для локализованных сообщений об ошибках.

Spring Boot автоматически настраивает компонент MessageSource, который считывает messages.properties из пути классов. Явная конфигурация нужна только для изменения базового имени, кодировки или поведения кеширования.
pom.xml
<!-- pom.xml — Spring Boot Starter Web includes MessageSource auto-config -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Thymeleaf for server-side rendered templates with i18n -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- Validation (for localized error messages) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2

Настроить MessageSource и LocaleResolver

MessageSource в Spring загружает переводы из файлов .properties по соглашению базового имени: messages.properties по умолчанию, messages_de.properties для немецкого, messages_ja.properties для японского. Настройте LocaleResolver, чтобы определять локаль для каждого запроса.

Файлы перевода

messages.properties
# src/main/resources/messages.properties (default / English)
nav.home=Home
nav.about=About
nav.settings=Settings

greeting=Hello, {0}!
cart.itemCount={0,choice,0#No items|1#1 item|1<{0,number} items}

error.notFound=Page not found
error.serverError=Something went wrong. Please try again.

Конфигурация MessageSource

I18nConfig.java
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class I18nConfig {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource source =
            new ReloadableResourceBundleMessageSource();
        source.setBasename("classpath:messages");
        source.setDefaultEncoding("UTF-8");
        source.setCacheSeconds(3600); // reload interval in dev
        return source;
    }

    // Wire MessageSource into Bean Validation
    @Bean
    public LocalValidatorFactoryBean validator(MessageSource messageSource) {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource);
        return bean;
    }
}
Если вместо переведённого текста возвращается имя ключа, наиболее вероятна ошибка в базовом имени. По умолчанию используется 'messages', соответствующее messages.properties в пути классов. Если файлы названы иначе или находятся в подкаталоге, явно задайте spring.messages.basename.

Разрешение локали

Настройте способ определения активной локали Spring для каждого запроса. CookieLocaleResolver сохраняет выбор пользователя между сеансами. LocaleChangeInterceptor позволяет переключать локали через параметр запроса, например ?lang=de.

LocaleConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

import java.util.Locale;

@Configuration
public class LocaleConfig implements WebMvcConfigurer {

    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver resolver = new CookieLocaleResolver("lang");
        resolver.setDefaultLocale(Locale.ENGLISH);
        resolver.setCookieMaxAge(3600 * 24 * 365); // 1 year
        return resolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang"); // ?lang=de switches locale
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}
3

Использовать переводы в коде

Получайте переведённые сообщения в контроллерах через внедрение MessageSource, в шаблонах Thymeleaf — через синтаксис #{...}, а в REST API — через автоматически разрешаемый параметр Locale.

Контроллер с MessageSource

HomeController.java
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Locale;

@Controller
public class HomeController {

    private final MessageSource messageSource;

    public HomeController(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @GetMapping("/")
    public String home(Model model, Locale locale) {
        // Spring injects the resolved Locale automatically
        String greeting = messageSource.getMessage(
            "greeting",
            new Object[]{"World"},
            locale
        );
        model.addAttribute("greeting", greeting);
        return "home";
    }
}

Шаблоны Thymeleaf

Выражение Thymeleaf #{...} автоматически разрешает ключи сообщений из Ваших файлов .properties. Передавайте параметры с помощью синтаксиса #{key(arg0, arg1)}. Шаблон использует локаль, разрешённую LocaleResolver.

home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="#{nav.home}">Home</title>
</head>
<body>
    <!-- Simple message lookup -->
    <h1 th:text="#{greeting('World')}">Hello, World!</h1>

    <!-- Navigation with i18n -->
    <nav>
        <a href="/" th:text="#{nav.home}">Home</a>
        <a href="/about" th:text="#{nav.about}">About</a>
        <a href="/settings" th:text="#{nav.settings}">Settings</a>
    </nav>

    <!-- Parameterized messages -->
    <p th:text="#{cart.itemCount(3)}">3 items</p>

    <!-- Language switcher -->
    <div>
        <a th:href="@{/(lang=en)}">English</a>
        <a th:href="@{/(lang=de)}">Deutsch</a>
        <a th:href="@{/(lang=ja)}">日本語</a>
    </div>

    <!-- Conditional text based on locale -->
    <p th:if="${#locale.language == 'ja'}"
       th:text="#{greeting('ユーザー')}">
        こんにちは、ユーザーさん!
    </p>
</body>
</html>
Выражения Thymeleaf, такие как #{greeting('World')}, передают аргументы в MessageFormat. Статический текст внутри тегов HTML служит резервным значением при просмотре шаблона без Spring, что удобно для дизайнеров, работающих с шаблонами напрямую.

Локализация REST API

Для REST API Spring автоматически разрешает Locale из заголовка Accept-Language. Внедрите его как параметр метода и передайте в MessageSource. Клиенты переключают языки, отправляя разные заголовки Accept-Language.

ApiController.java
import org.springframework.context.MessageSource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Locale;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class ApiController {

    private final MessageSource messageSource;

    public ApiController(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @GetMapping("/greeting/{name}")
    public ResponseEntity<Map<String, String>> greeting(
            @PathVariable String name,
            Locale locale) {  // Resolved from Accept-Language header
        String msg = messageSource.getMessage(
            "greeting", new Object[]{name}, locale
        );
        return ResponseEntity.ok(Map.of("message", msg));
    }

    // curl -H "Accept-Language: de" localhost:8080/api/greeting/Max
    // → {"message": "Hallo, Max!"}
}
REST API обычно используют AcceptHeaderLocaleResolver на основе заголовка, а веб-приложения — CookieLocaleResolver на основе cookie. Если приложение обслуживает оба варианта, создайте собственный LocaleResolver, который сначала проверяет cookie, а затем переходит на заголовок Accept-Language.

Сообщения проверки компонентов

Spring автоматически разрешает сообщения ограничений проверки из Вашего MessageSource. Используйте заполнители в фигурных скобках, такие как {validation.name.required}, в аннотациях ограничений, а переводы определите в файлах .properties.

Bean Validation i18n
import jakarta.validation.constraints.*;

public class CreateUserRequest {

    @NotBlank(message = "{validation.name.required}")
    @Size(min = 2, max = 50, message = "{validation.name.size}")
    private String name;

    @Email(message = "{validation.email.invalid}")
    private String email;
}

// In messages.properties:
// validation.name.required=Name is required
// validation.name.size=Name must be between {min} and {max} characters
// validation.email.invalid=Please enter a valid email address
//
// In messages_de.properties:
// validation.name.required=Name ist erforderlich
// validation.name.size=Name muss zwischen {min} und {max} Zeichen lang sein
// validation.email.invalid=Bitte geben Sie eine gültige E-Mail-Adresse ein
4

Обработать формы множественного числа и переменные

Spring использует java.text.MessageFormat для интерполяции и множественного числа. Шаблон ChoiceFormat обрабатывает базовые правила, но для полной поддержки ICU, включая 6 форм арабского и 3 формы русского, добавьте библиотеку ICU4J.

MessageFormat Plurals
# MessageFormat plural syntax in messages.properties
# Uses java.text.ChoiceFormat — NOT ICU plural rules
cart.itemCount={0,choice,0#No items|1#1 item|1<{0,number} items}

# For more complex plurals, use ICU4J:
# 1. Add dependency: com.ibm.icu:icu4j
# 2. Use ICUMessageSource instead of ResourceBundleMessageSource
#
# Then you can write ICU-style plurals:
# cart.items={count, plural, one {# item} other {# items}}

# Variables with MessageFormat:
welcome.message=Welcome, {0}! You have {1,number} new {1,choice,1#notification|1<notifications}.
order.total=Order total: {0,number,currency}
event.date=Event date: {0,date,long}
ChoiceFormat и правила множественного числа ICU — не одно и то же. Он использует числовые диапазоны (0#, 1#, 1<), а не категории CLDR (zero, one, two, few, many, other). Для языков со сложными правилами, таких как арабский, польский и русский, ChoiceFormat недостаточно: используйте MessageFormat из ICU4J.

Автоматизировать перевод

После завершения настройки i18n переведите файлы .properties с помощью ИИ. Попросите ИИ-помощника перевести исходный файл в своей IDE или используйте CLI i18n Agent в конвейере CI/CD.

Terminal
# Translate your .properties files with AI
# In your IDE, ask your AI assistant:
> Translate src/main/resources/messages.properties to German, Japanese, and Spanish

✓ messages_de.properties created (1.2s)
✓ messages_ja.properties created (1.5s)
✓ messages_es.properties created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate src/main/resources/messages.properties --lang de,ja,es
Переводите постепенно: добавив новые ключи в messages.properties, переведите только их, а не создавайте все файлы локалей заново. Это сохранит переводы в существующих файлах, уже проверенные людьми.

Автоматизировать контроль качества перевода

Выявляйте отсутствующие ключи и нарушенные заполнители до выпуска с помощью i18n-validate. Тестируйте интерфейс с псевдопереводами через i18n-pseudo, пока настоящие переводы ещё не готовы.

Без настройки со spring-locale-chain

spring-locale-chain — стартер Spring Boot с открытым исходным кодом, который автоматически настраивает LocaleResolver, LocaleChangeInterceptor и проверку поддерживаемых локалей с помощью одной зависимости. Определите поддерживаемые локали в application.yml, а библиотека сделает всё остальное.

pom.xml
<!-- Add spring-locale-chain for zero-config locale resolution -->
<dependency>
    <groupId>io.github.i18n-agent</groupId>
    <artifactId>spring-locale-chain</artifactId>
    <version>1.0.0</version>
</dependency>

Рекомендуемая структура файлов

Project Structure
my-spring-app/
├── src/main/
│   ├── java/com/example/
│   │   ├── config/
│   │   │   ├── I18nConfig.java          # MessageSource bean
│   │   │   └── LocaleConfig.java        # LocaleResolver + interceptor
│   │   ├── controller/
│   │   │   └── HomeController.java      # Uses MessageSource
│   │   └── MyApplication.java
│   └── resources/
│       ├── messages.properties          # Default (English)
│       ├── messages_de.properties       # German
│       ├── messages_ja.properties       # Japanese
│       ├── messages_es.properties       # Spanish
│       ├── application.yml              # Spring config
│       └── templates/
│           └── home.html                # Thymeleaf with #{...}
├── pom.xml
└── build.gradle

Распространённые ошибки

Символы не-ASCII отображаются неправильно

По умолчанию файлы Java .properties используют кодировку ISO-8859-1, а не UTF-8. Символы вроде умлаутов (ü) или CJK отображаются неправильно. Решение: задайте spring.messages.encoding=UTF-8 в application.yml или используйте escape-последовательности Unicode, например \u00FC, в файлах .properties. ReloadableResourceBundleMessageSource в Spring Boot по умолчанию использует UTF-8, а ResourceBundleMessageSource — нет.

ChoiceFormat нарушает множественное число неанглийских языков

ChoiceFormat в Java ({0,choice,0#|1#|1<}) поддерживает только числовые диапазоны и не может выразить такие категории CLDR, как 'few' или 'many'. Для правильных форм множественного числа в арабском с 6 формами, польском с 3 и русском с 3 требуется ICU4J. Не предполагайте, что ChoiceFormat обрабатывает все языки.

Изменения перевода не отображаются

По умолчанию ResourceBundleMessageSource кеширует пакеты бессрочно. Во время разработки используйте ReloadableResourceBundleMessageSource с cacheSeconds=0, чтобы видеть изменения без перезапуска. В рабочей среде задайте разумный срок кеширования, например 3600 секунд, для баланса производительности и скорости обновления.

Неожиданный переход на локаль JVM

По умолчанию Spring переходит на стандартную локаль JVM (Locale.getDefault()), а не на Ваш файл messages.properties. Задайте spring.messages.fallback-to-system-locale=false в application.yml, чтобы всегда использовать стандартный пакет. Иначе сервер с локалью JVM 'fr' при отсутствии ключа в запрошенной локали покажет французский вместо английского.

Попробовать i18n Agent

Перетащите сюда файл перевода

JSON, YAML, PO, XML, CSV, Markdown, Properties

или нажмите, чтобы выбрать

Целевые языки

Регистрация не требуетсяМгновенный расчёт

Резервные локали с spring-locale-chain

Когда в региональной локали, например pt-BR, отсутствует ключ перевода, Spring Boot сразу переходит на локаль по умолчанию, не проверяя сначала родительскую локаль pt.

Terminal
<!-- Maven -->
<dependency>
  <groupId>ai.i18nagent</groupId>
  <artifactId>spring-locale-chain</artifactId>
</dependency>
Configuration
# application.yml
locale-chain:
  fallbacks:
    pt-BR:
      - pt
      - en
    zh-Hant-HK:
      - zh-Hant
      - zh
      - en

Полный список поддерживаемых фреймворков и 75 встроенных цепочек приведён в нашем руководстве по резервным локалям. Learn more →

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