Skip to main content

Spring Boot i18n:国际化设置教程

配置 MessageSource、创建特定语言的 properties 文件、解析语言并渲染多语言 Thymeleaf 模板,再利用 AI 自动翻译。

1

添加依赖

Spring Boot Starter Web 开箱即用地包含 MessageSource 自动配置。添加 Thymeleaf 以渲染服务器端 i18n 模板,再添加 validation starter 以本地化错误消息。

Spring Boot 会自动配置一个 MessageSource bean,从 classpath 的 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

Spring 的 MessageSource 按基本名称约定从 .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',对应 classpath 中的 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 传入参数。在没有 Spring 的情况下查看模板时,HTML 标签内的静态文本会作为回退,这对直接处理模板的设计师很有用。

REST API 本地化

对于 REST API,Spring 会自动从 Accept-Language 请求头解析 Locale。将其作为方法参数注入并传给 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,而网页应用使用基于 Cookie 的 CookieLocaleResolver。如果同一应用同时提供两者,可考虑自定义 LocaleResolver,先检查 Cookie,再回退到 Accept-Language 请求头。

Bean Validation 消息

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 不够用,应改用 ICU4J 的 MessageFormat。

自动翻译

完成 i18n 设置后,使用 AI 翻译 .properties 文件。在 IDE 中让 AI 助手翻译源文件,或在 CI/CD 管线中使用 i18n Agent CLI。

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 生成伪译文来测试 UI。

通过 spring-locale-chain 实现零配置

spring-locale-chain 是一款开源 Spring Boot starter,只需一个依赖项,即可自动配置 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 字符等会显示为乱码。解决方法:在 application.yml 中设置 spring.messages.encoding=UTF-8,或在 .properties 文件中使用类似 \u00FC 的 Unicode 转义序列。Spring Boot 的 ReloadableResourceBundleMessageSource 默认使用 UTF-8,但 ResourceBundleMessageSource 并非如此。

ChoiceFormat 无法正确处理非英语复数

Java 的 ChoiceFormat({0,choice,0#|1#|1<})仅支持数字范围,无法表达 'few' 或 'many' 等 CLDR 复数类别。阿拉伯语(6 种形式)、波兰语(3 种形式)和俄语(3 种形式)等语言需要使用 ICU4J 才能正确处理复数。请勿假定 ChoiceFormat 能处理所有语言。

翻译更改未生效

ResourceBundleMessageSource 默认会无限期缓存资源包。在开发期间,使用 cacheSeconds=0 的 ReloadableResourceBundleMessageSource,无需重启即可查看更改。在生产环境中,请设置合理的缓存时长(例如 3,600 秒),以兼顾性能和更新速度。

意外回退到 JVM 区域设置

默认情况下,Spring 会回退到 JVM 的默认区域设置(Locale.getDefault()),而不是 messages.properties 文件。在 application.yml 中设置 spring.messages.fallback-to-system-locale=false,即可始终使用默认资源包。否则,当请求的区域设置缺少某个键时,如果服务器的 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 →

常见问题