Skip to main content

Spring Boot i18n : บทเรียนการตั้งค่าการรองรับหลายภาษา

กำหนดค่า MessageSource สร้างไฟล์ properties เฉพาะภาษา ค้นหาภาษา และเรนเดอร์เทมเพลต Thymeleaf หลายภาษา จากนั้นทำให้การแปลเป็นอัตโนมัติด้วย AI

1

เพิ่มการพึ่งพา

Spring Boot Starter Web มีการกำหนดค่า MessageSource อัตโนมัติให้ทันที เพิ่ม Thymeleaf สำหรับเทมเพลต i18n ที่เรนเดอร์บนเซิร์ฟเวอร์ และเพิ่ม validation starter สำหรับข้อความแสดงข้อผิดพลาดฉบับโลคัลไลซ์

Spring Boot กำหนดค่า bean MessageSource ที่อ่าน messages.properties จาก classpath โดยอัตโนมัติ คุณต้องกำหนดค่าอย่างชัดเจนเฉพาะเมื่อต้องปรับ basename การเข้ารหัส หรือพฤติกรรมแคช
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 ตามข้อกำหนด basename ได้แก่ 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;
    }
}
หากคำแปลคืนชื่อคีย์แทนข้อความที่แปลแล้ว สาเหตุที่พบบ่อยที่สุดคือ basename ผิด ค่าเริ่มต้นคือ 'messages' ซึ่งจับคู่กับ messages.properties ใน classpath หากชื่อไฟล์ต่างออกไปหรืออยู่ในไดเรกทอรีย่อย ให้ตั้ง 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 ซึ่งอิงคุกกี้ หากให้บริการทั้งสองแบบจากแอปเดียว ควรใช้ LocaleResolver แบบกำหนดเองที่ตรวจคุกกี้ก่อน แล้วถอยไปใช้ส่วนหัว 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 ไม่เพียงพอ ให้ใช้ MessageFormat ของ ICU4J แทน

ทำให้การแปลเป็นอัตโนมัติ

เมื่อตั้งค่า i18n เสร็จแล้ว ให้แปลไฟล์ .properties ด้วย AI โดยบอกผู้ช่วย AI ใน 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 จับคีย์ที่หายไปและตัวยึดตำแหน่งเสียหายก่อนส่งขึ้นใช้งาน แล้วทดสอบ UI ด้วยคำแปลจำลองผ่าน i18n-pseudo ก่อนคำแปลจริงจะมาถึง

ไม่ต้องกำหนดค่าด้วย spring-locale-chain

spring-locale-chain เป็น starter โอเพนซอร์สของ 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 หรือใช้ลำดับหลีก 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 เพื่อเห็นการเปลี่ยนแปลงโดยไม่ต้องเริ่มใหม่ ในระบบจริง ให้กำหนดระยะเวลาแคชที่เหมาะสม เช่น 3,600 วินาที เพื่อสมดุลประสิทธิภาพกับความเร็วในการอัปเดต

ถอยไปใช้ภาษา 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 →

คำถามที่พบบ่อย