Skip to main content

Spring Boot i18n:国際化設定チュートリアル

MessageSource の設定、ロケール別 properties ファイルの作成、ロケールの解決、多言語 Thymeleaf テンプレートのレンダリング、AI による翻訳の自動化を解説します。

1

依存関係の追加

Spring Boot Starter Web には、MessageSource の自動設定が標準で含まれます。サーバー側でレンダリングする i18n テンプレートには Thymeleaf を、ローカライズされたエラーメッセージには validation starter を追加します。

Spring Boot は、クラスパス上の messages.properties を読み込む MessageSource bean を自動設定します。ベース名、エンコーディング、キャッシュ動作を変更する場合にのみ、明示的な設定が必要です。
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 は、messages.properties(デフォルト)、messages_de.properties(ドイツ語)、messages_ja.properties(日本語)というベース名規則を使用し、.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>
#{greeting('World')} のような Thymeleaf 式は、MessageFormat に引数を渡します。HTML タグ内の静的テキストは、Spring を使用せずにテンプレートを表示する際のフォールバックとなるため、デザイナーがテンプレートを直接編集する場合に便利です。

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 を使用し、Web アプリでは Cookie ベースの CookieLocaleResolver を使用します。同じアプリで両方を提供する場合は、最初に Cookie を確認し、その後 Accept-Language ヘッダーへフォールバックする独自の LocaleResolver を検討してください。

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 パターンで基本的な複数形規則を処理できますが、アラビア語の 6 形式やロシア語の 3 形式など、ICU 複数形へ完全に対応するには 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 の複数形規則とは異なります。CLDR カテゴリー(zero、one、two、few、many、other)ではなく、数値範囲(0#、1#、1<)を使用します。アラビア語、ポーランド語、ロシア語のように複雑な複数形規則を持つ言語には不十分なため、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 は、単一の依存関係で LocaleResolver、LocaleChangeInterceptor、対応ロケールの検証を自動設定するオープンソースの Spring Boot starter です。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 ファイルは、デフォルトで UTF-8 ではなく ISO-8859-1 エンコーディングを使用します。ウムラウト(ü)や 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 はデフォルトで、messages.properties ファイルではなく JVM のデフォルトロケール(Locale.getDefault())へフォールバックします。常にデフォルトバンドルを使用するには、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 →

よくある質問