Skip to main content

Spring Boot i18n: Hướng dẫn thiết lập quốc tế hóa

Cấu hình MessageSource, tạo tệp properties theo locale, phân giải locale và kết xuất mẫu Thymeleaf đa ngôn ngữ, sau đó tự động dịch bằng AI.

1

Thêm phần phụ thuộc

Spring Boot Starter Web tích hợp sẵn cấu hình tự động cho MessageSource. Thêm Thymeleaf để kết xuất mẫu i18n phía máy chủ và validation starter để bản địa hóa thông báo lỗi.

Spring Boot tự động cấu hình bean MessageSource đọc messages.properties trên classpath. Bạn chỉ cần cấu hình rõ ràng khi muốn tùy chỉnh basename, bảng mã hoặc cách lưu vào bộ nhớ đệm.
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

Cấu hình MessageSource và LocaleResolver

MessageSource của Spring tải bản dịch từ tệp .properties theo quy ước basename: messages.properties (mặc định), messages_de.properties (tiếng Đức), messages_ja.properties (tiếng Nhật). Cấu hình LocaleResolver để xác định locale cho từng yêu cầu.

Tệp bản dịch

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.

Cấu hình 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;
    }
}
Nếu kết quả dịch trả về tên khóa thay vì văn bản đã dịch, nguyên nhân phổ biến nhất là basename sai. Giá trị mặc định là 'messages', tương ứng với messages.properties trên classpath. Nếu tệp có tên khác hoặc nằm trong thư mục con, hãy đặt rõ spring.messages.basename.

Phân giải locale

Cấu hình cách Spring xác định locale đang hoạt động cho từng yêu cầu. CookieLocaleResolver duy trì lựa chọn của người dùng qua các phiên. LocaleChangeInterceptor cho phép người dùng chuyển locale bằng tham số truy vấn như ?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

Dùng bản dịch trong mã

Truy cập thông báo đã dịch trong controller qua cơ chế tiêm MessageSource, trong mẫu Thymeleaf bằng cú pháp #{...} và trong API REST bằng tham số Locale được tự động phân giải.

Controller dùng 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";
    }
}

Mẫu Thymeleaf

Biểu thức #{...} của Thymeleaf tự động phân giải khóa thông báo từ tệp .properties. Truyền tham số bằng cú pháp #{key(arg0, arg1)}. Mẫu dùng locale do LocaleResolver phân giải.

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>
Biểu thức Thymeleaf như #{greeting('World')} truyền đối số vào MessageFormat. Văn bản tĩnh trong thẻ HTML đóng vai trò dự phòng khi xem mẫu không qua Spring, hữu ích cho nhà thiết kế làm việc trực tiếp trên mẫu.

Bản địa hóa API REST

Với API REST, Spring tự động phân giải Locale từ tiêu đề Accept-Language. Tiêm locale dưới dạng tham số phương thức và truyền vào MessageSource. Máy khách chuyển ngôn ngữ bằng cách gửi các tiêu đề Accept-Language khác nhau.

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!"}
}
API REST thường dùng AcceptHeaderLocaleResolver (dựa trên tiêu đề), còn ứng dụng web dùng CookieLocaleResolver (dựa trên cookie). Nếu cùng một ứng dụng phục vụ cả hai, hãy cân nhắc LocaleResolver tùy chỉnh kiểm tra cookie trước rồi dự phòng bằng tiêu đề Accept-Language.

Thông báo xác thực bean

Spring tự động phân giải thông báo ràng buộc xác thực từ MessageSource. Dùng chữ giữ chỗ ngoặc nhọn như {validation.name.required} trong chú thích ràng buộc và xác định bản dịch trong tệp .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

Xử lý dạng số nhiều và biến

Spring dùng java.text.MessageFormat để nội suy và xử lý dạng số nhiều. Mẫu ChoiceFormat xử lý quy tắc số nhiều cơ bản; để hỗ trợ đầy đủ dạng số nhiều ICU (6 dạng của tiếng Ả Rập, 3 dạng của tiếng Nga), hãy thêm thư viện 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 không giống quy tắc số nhiều ICU. Nó dùng khoảng số (0#, 1#, 1<) thay cho danh mục CLDR (zero, one, two, few, many, other). ChoiceFormat không đủ cho các ngôn ngữ có quy tắc số nhiều phức tạp như tiếng Ả Rập, Ba Lan hoặc Nga; hãy dùng MessageFormat của ICU4J.

Tự động dịch

Sau khi hoàn tất thiết lập i18n, hãy dùng AI để dịch tệp .properties. Trong IDE, yêu cầu trợ lý AI dịch tệp nguồn hoặc dùng CLI i18n Agent trong quy trình 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
Dịch tăng dần: khi thêm khóa mới vào messages.properties, chỉ dịch khóa mới thay vì tạo lại mọi tệp locale. Cách này giữ nguyên các bản dịch đã được con người rà soát.

Tự động kiểm soát chất lượng bản dịch

Dùng i18n-validate để phát hiện khóa thiếu và chữ giữ chỗ hỏng trước khi phát hành. Thử nghiệm UI bằng bản dịch giả lập qua i18n-pseudo trước khi có bản dịch thật.

Không cần cấu hình với spring-locale-chain

spring-locale-chain là Spring Boot starter nguồn mở, tự động cấu hình LocaleResolver, LocaleChangeInterceptor và xác thực locale được hỗ trợ chỉ bằng một phần phụ thuộc. Xác định các locale hỗ trợ trong application.yml, thư viện sẽ xử lý phần còn lại.

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>

Cấu trúc tệp đề xuất

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

Lỗi thường gặp

Ký tự ngoài ASCII hiển thị sai

Tệp .properties của Java mặc định dùng bảng mã ISO-8859-1, không phải UTF-8. Các ký tự như chữ có dấu umlaut (ü) hoặc ký tự CJK sẽ hiển thị sai. Cách khắc phục: đặt spring.messages.encoding=UTF-8 trong application.yml hoặc dùng chuỗi thoát Unicode như \u00FC trong tệp .properties. ReloadableResourceBundleMessageSource của Spring Boot mặc định dùng UTF-8, còn ResourceBundleMessageSource thì không.

ChoiceFormat lỗi với dạng số nhiều ngoài tiếng Anh

ChoiceFormat của Java ({0,choice,0#|1#|1<}) chỉ hỗ trợ khoảng số, không thể biểu diễn danh mục số nhiều CLDR như 'few' hoặc 'many'. Các ngôn ngữ như tiếng Ả Rập (6 dạng), Ba Lan (3 dạng) và Nga (3 dạng) cần ICU4J để xử lý đúng. Đừng cho rằng ChoiceFormat hỗ trợ mọi ngôn ngữ.

Thay đổi bản dịch không xuất hiện

Theo mặc định, ResourceBundleMessageSource lưu gói tài nguyên vào bộ nhớ đệm vô thời hạn. Khi phát triển, dùng ReloadableResourceBundleMessageSource với cacheSeconds=0 để thấy thay đổi mà không cần khởi động lại. Trên môi trường thực tế, đặt thời lượng bộ nhớ đệm hợp lý (ví dụ 3.600 giây) để cân bằng hiệu năng và tốc độ cập nhật.

Bất ngờ chuyển về locale JVM

Theo mặc định, Spring chuyển về locale mặc định của JVM (Locale.getDefault()), không phải tệp messages.properties. Đặt spring.messages.fallback-to-system-locale=false trong application.yml để luôn dùng bundle mặc định. Nếu không, máy chủ có locale JVM là 'fr' sẽ hiển thị tiếng Pháp thay vì tiếng Anh khi locale được yêu cầu thiếu khóa.

Dùng thử i18n Agent ngay

Thả tệp bản dịch của bạn vào đây

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

hoặc nhấp để duyệt

Ngôn ngữ đích

Không cần đăng kýBáo giá tức thì

Dự phòng locale với spring-locale-chain

Khi thiếu khóa dịch trong một locale khu vực như pt-BR, Spring Boot chuyển thẳng về locale mặc định thay vì kiểm tra locale cha pt trước.

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

Xem Hướng dẫn dự phòng locale để biết danh sách đầy đủ các framework được hỗ trợ và 75 chuỗi tích hợp sẵn. Learn more →

Câu hỏi thường gặp