Spring Boot の多言語対応は、src/main/resources/messages.properties(既定の言語)と messages_ja.properties・messages_en.properties などを置くだけで始められます。 Boot が MessageSource(メッセージを言語ごとに引く部品)を自動で作り、Thymeleaf では #{キー}、Java では messageSource.getMessage(…) で読めます。言語は既定ではブラウザの Accept-Language で決まり、?lang=en のように画面から切り替えるには LocaleResolver を差し替えます。

# src/main/resources/messages.properties(どの言語にも当たらないときの文)
greeting=Hello, {0}
# src/main/resources/messages_ja.properties(UTF-8 で保存)
greeting=こんにちは、{0}さん
<p th:text="#{greeting(${user.name})}">あいさつ</p>

つまずきやすい点は3つです。

落とし穴 何が起きるか 対処
messages.properties(言語なし)を置いていない Boot が MessageSource を作らず、すべてのキーが見つからない 言語なしのファイルを必ず置く
自分で ResourceBundleMessageSource を作り、文字コードを指定していない 日本語が ã“ã‚“ã«ã¡ã¯ のように化ける setDefaultEncoding("UTF-8")
{0} を含む文に '(シングルクォート)を書いた It's {0} が Its {0} になる ' を '' と2つ重ねる

確認環境:Spring Boot 4.1.1(Spring Framework 7.0.9、Hibernate Validator 9.1.3.Final は Boot の依存管理の版)。spring.messages.* の既定値と自動設定の条件は Boot v4.1.1 のソースで、LocaleResolver の種類と既定値は Spring Framework v7.0.9 のソースで確かめました。Spring Framework 7.0.9 の ResourceBundleMessageSource はソースからビルドし、OpenJDK 21.0.10(Linux)で文字コード・シングルクォート・ロケールの選ばれ方を動かして確かめました。MessageFormat の動きは OpenJDK 1.8.0_504・21.0.10・25.0.4.1 で同じ結果でした(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Bootの多言語対応|messages.propertiesと切り替え」

ファイルの置き方と名前

src/main/resources/
├── messages.properties        ← 既定(言語が見つからないときに使う)。必ず置く
├── messages_ja.properties     ← 日本語
├── messages_en.properties     ← 英語
└── messages_zh_TW.properties  ← 言語+国(台湾の中国語)

ファイル名は「ベース名+_言語+_国」です。ja_JP を探すときは messages_ja_JP → messages_ja → … → messages の順に探します。日本語を1つ用意するなら messages_ja.properties とすると、ja も ja_JP もこのファイルに当たります。

Boot v4.1.1 のソース(MessageSourceAutoConfiguration)では、spring.messages.basename の名前に .properties を付けたファイル(messages.properties)がクラスパスにあるときだけ、MessageSource を自動で作ります。messages_ja.properties と messages_en.properties しか置いていないと、条件に合わず作られません。

spring.messages.* の設定一覧(Boot 4.1.1 のソース)

設定 既定値 意味
spring.messages.basename messages ベース名。カンマ区切りで複数。i18n/messages のようにフォルダも書ける
spring.messages.encoding UTF-8 ファイルの文字コード
spring.messages.fallback-to-system-locale true 言語のファイルが無いとき、サーバーの OS の言語のファイルを使う
spring.messages.always-use-message-format false 引数の無い文にも MessageFormat の規則を当てる
spring.messages.use-code-as-default-message false キーが無いとき、例外のかわりにキーそのものを返す(開発中向け)
spring.messages.cache-duration 無し(ずっと持つ) ファイルを読み直す間隔。単位を書かないと秒
spring.messages.common-messages 無し 言語に関係なく共通で使うファイル
spring:
  messages:
    basename: i18n/messages, i18n/validation   # 複数のファイル
    encoding: UTF-8
    fallback-to-system-locale: false           # 下の「ロケールの選ばれ方」を参照
    cache-duration: 10s                        # 開発中だけ。本番は書かない

basename にフォルダを付けた場合も、i18n/messages.properties(言語なし)が必要です。設定の書き方全般は、関連記事「Spring Boot application.properties設定一覧」で扱っています。

fallback-to-system-locale の落とし穴

messages.properties(英語)・messages_en.properties・messages_ja.properties を置き、フランス語(fr)でメッセージを引きました。サーバーの OS の言語だけを変えています。

サーバーの言語 fallback-to-system-locale fr で引いた結果
fr_FR true(既定) Hello, Taro(言語なしのファイル)
ja_JP true(既定) こんにちは、Taroさん(OS の言語の日本語)
ja_JP false Hello, Taro(言語なしのファイル)

手元(日本語の OS)とサーバー(英語の OS やコンテナ)で、用意していない言語の利用者に出る文が変わります。 どの環境でも「言語なしのファイル」に落としたいなら、fallback-to-system-locale: false にします。

日本語の文字コード:UTF-8 が既定

Java 9 以降、.properties は UTF-8 で読めるようになり、こん… のような書き方(native2ascii)は使わなくてよくなりました。Boot の spring.messages.encoding も既定で UTF-8 です。

ただし、Spring Framework の ResourceBundleMessageSource そのものの既定は ISO-8859-1 です(v7.0.9 のソースのコンストラクタ)。Boot の自動設定を使わず、自分で Bean を作ると化けます。UTF-8 と Shift_JIS で保存した同じ内容のファイルを、文字コードの指定を変えて読みました({0} に 山田)。

setDefaultEncoding UTF-8 で保存したファイル Shift_JIS で保存したファイル
指定なし ããã«ã¡ã¯ã山田ãã ±ñÉ¿ÍA山田³ñ
UTF-8 こんにちは、山田さん ����ɂ��́A山田����
ISO-8859-1 ããã«ã¡ã¯ã山田ãã ±ñÉ¿ÍA山田³ñ
Shift_JIS 縺薙s縺ォ縺。縺ッ縲+0}縺輔s こんにちは、山田さん

化け方で原因が分かります。 ã が並ぶのは「UTF-8 のファイルを ISO-8859-1 で読んだ」、� が並ぶのは「Shift_JIS のファイルを UTF-8 で読んだ」形です。Shift_JIS で読んだ行は、{0} まで壊れて 山田 が入りませんでした。

自分で Bean を作るなら、次のように書きます。

@Bean
public MessageSource messageSource() {
    var ms = new ResourceBundleMessageSource();
    ms.setBasenames("i18n/messages");
    ms.setDefaultEncoding("UTF-8");          // これが無いと日本語が化ける
    ms.setFallbackToSystemLocale(false);
    return ms;
}

ファイルそのものを UTF-8 で保存しているかは、IDE の設定(IntelliJ IDEA の「Properties ファイルの既定のエンコーディング」など)で確かめます。

{0} とシングルクォートの落とし穴(MessageFormat)

メッセージの {0}・{1} は、JDK の java.text.MessageFormat で置き換えられます。MessageFormat では '(シングルクォート)が「ここから先は文字として扱う」という記号になります。JDK 8・21・25 で同じ結果でした。

パターン 引数 結果
It's {0} OK Its {0}(' が消え、{0} も置き換わらない)
It''s {0} OK It's OK
'{0}' is {0} x {0} is x(' で囲んだ部分はそのまま)
''{0}'' x 'x'
'{'{0}'}' x {x}(波括弧を文字として出す)
{1} と {0} A, B B と A(順番を入れ替えられる)
{0} {1} A だけ A {1}(足りない引数はそのまま残る)
{0 A IllegalArgumentException: Unmatched braces in the pattern.
{a} A IllegalArgumentException: can't parse argument number: a

数値・日付は書式が付く

パターン 引数 結果
{0} 1234567(int・long) 1,234,567(3桁区切りが入る)
{0,number,#} 1234567 1234567
{0} 3.14159 3.142(小数は3桁まで)
{0,number,#.##} 3.14159 3.14
{0} null null
{0,date,yyyy/MM/dd} java.util.Date 1970/01/01
{0} LocalDate.of(2026,10,13) 2026-10-13(toString() のまま)
{0}(ロケール de_DE) 1234567.5 1.234.567,5

ID や注文番号のような「数字だけど数ではないもの」は、3桁区切りが入ります。 ID: {0} に 1234567 を渡すと ID: 1,234,567 になりました。文字列にしてから渡すか、{0,number,#} と書きます。java.time の日付には {0,date,…} が効かないので、DateTimeFormatter で文字列にしてから渡します。

件数で文を変える:choice

files={0,choice,0#ありません|1#1件|1<{0,number,integer}件}

0 を渡すと ありません、5 を渡すと 5件 になりました。英語の単数・複数(one file/2 files)もこの書き方で切り替えられます。

Spring では「引数が無いと MessageFormat を通さない」

表の false・true は always-use-message-format の値です。Spring の MessageSource は、引数が無いときは MessageFormat を通さず、ファイルの文をそのまま返します(always-use-message-format: false が既定のため)。Spring Framework 7.0.9 の ResourceBundleMessageSource で確かめました。

ファイルの文 引数 false(既定) true
It's fine なし It's fine Its fine
It''s fine なし It''s fine It's fine
It's {0} OK Its {0} Its {0}
It''s {0} OK It's OK It's OK

同じファイルの中で、引数がある文は ''、無い文は ' と書き分けるのが既定の動きです。書き分けを間違えると、It''s fine がそのまま画面に出ます。文をあとから引数ありに変えることが多いなら、always-use-message-format: true にしてすべて '' で書くとそろいます。英語・フランス語など ' を多く使う言語で特に起きます。

言語の決め方:LocaleResolver の種類

LocaleResolver は、リクエストごとに「どの言語で出すか」を決める部品です。Boot v4.1.1 の自動設定は、spring.web.locale-resolver の値で次のどちらかを作ります。

設定 既定値 意味
spring.web.locale-resolver accept-header accept-header:ブラウザの Accept-Language で決める。fixed:いつも同じ言語
spring.web.locale 無し fixed のときの言語。accept-header ではヘッダーが無いときの言語

Spring Framework 7.0.9 にある LocaleResolver の種類です。

クラス 言語をどこに覚えるか 画面から切り替え 使いどころ
AcceptHeaderLocaleResolver 覚えない(毎回ヘッダーを見る) できない ブラウザの設定に任せる(Boot の既定)
FixedLocaleResolver 覚えない(固定) できない 1つの言語だけのアプリ
SessionLocaleResolver セッション できる ログイン中だけ覚えればよい
CookieLocaleResolver Cookie できる ログインしない画面でも次回まで覚える

AcceptHeaderLocaleResolver で言語を切り替えようとすると、ソースでは次の例外になります。

java.lang.UnsupportedOperationException: Cannot change HTTP Accept-Language header - use a different locale resolution strategy

?lang=en で切り替える完成コード

package com.example.config;

import java.time.Duration;
import java.util.List;
import java.util.Locale;
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;

@Configuration
public class I18nConfig implements WebMvcConfigurer {

    // Bean の名前は "localeResolver" にする(DispatcherServlet が名前で探す)
    @Bean
    public LocaleResolver localeResolver() {
        var resolver = new CookieLocaleResolver("APP_LANG");
        resolver.setDefaultLocale(Locale.JAPANESE);       // Cookie も無いときの言語
        resolver.setCookieMaxAge(Duration.ofDays(365));
        return resolver;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        var interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");                 // 既定は "locale"
        interceptor.setIgnoreInvalidLocale(true);         // ?lang=xx-!! で例外にしない
        registry.addInterceptor(interceptor);
    }
}
<a th:href="@{''(lang=ja)}">日本語</a> | <a th:href="@{''(lang=en)}">English</a>

Spring Framework 7.0.9 のソースで確かめた既定値です。

項目 既定値
LocaleChangeInterceptor のパラメータ名 locale
LocaleChangeInterceptor の不正な値 例外にする(ignoreInvalidLocale = false)
CookieLocaleResolver の Cookie 名(引数なしのコンストラクタ) org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE
CookieLocaleResolver の Cookie の属性 Path=/、SameSite=Lax、有効期限なし(ブラウザを閉じるまで)
CookieLocaleResolver・SessionLocaleResolver の既定の言語 setDefaultLocale が無ければ Accept-Language

localeResolver という Bean 名が大事です。 DispatcherServlet はこの名前で探すので、@Bean public LocaleResolver myResolver() のように別の名前にすると使われません。Boot の自動設定も、この名前の Bean が無いときだけ作ります。

Cookie とセッションの違いは、関連記事「Cookieとセッションの違いと仕組み|セッションIDの流れを図解」で扱っています。

3つの場所でメッセージを使う

Thymeleaf

<!-- キーだけ -->
<h1 th:text="#{title}">タイトル</h1>
<!-- 引数つき:{0} に ${user.name}、{1} に ${count} -->
<p th:text="#{welcome(${user.name}, ${count})}">ようこそ</p>
<!-- キーを変数で組み立てる -->
<span th:text="#{${'status.' + order.status}}">状態</span>
<!-- 今の言語を html の lang に入れる -->
<html th:lang="${#locale.language}">

キーが見つからないとき、Thymeleaf は ??title_ja_JP?? のように、キーと言語を ?? で囲んで表示します。画面にこの形が出たら、キーの綴りか、messages.properties(言語なし)の置き忘れを疑います。Thymeleaf の属性は、関連記事「Thymeleafの使い方|th:each・th:ifなど属性一覧と出力例」で扱っています。

Java のコード

@Service
public class MailTextService {

    private final MessageSource messages;

    public MailTextService(MessageSource messages) {
        this.messages = messages;
    }

    public String subject(String userName) {
        // 今のリクエストの言語(LocaleResolver が決めたもの)
        Locale locale = LocaleContextHolder.getLocale();
        return messages.getMessage("mail.subject", new Object[]{userName}, locale);
    }

    public String optional(String code) {
        // キーが無くても例外にしない(3つ目の引数が既定の文)
        return messages.getMessage(code, null, "(未定義)", LocaleContextHolder.getLocale());
    }
}

キーが無いときの例外の文は次のとおりです(Spring Framework 7.0.9 で確認)。

org.springframework.context.NoSuchMessageException: No message found under code 'missing' for locale 'ja'.

spring.messages.use-code-as-default-message: true にすると、例外のかわりにキー(missing.key)がそのまま返りました。翻訳漏れに気づきにくくなるので、Boot のソースの説明でも「開発中だけに勧める」とされています。

@Async や @Scheduled のように、リクエストの外で動く処理では LocaleContextHolder.getLocale() はサーバーの既定の言語を返します。メールの送信などは、利用者ごとに保存した言語を Locale で渡します。

バリデーションのメッセージ

Boot の自動設定(ValidationAutoConfiguration)は、Bean Validation のメッセージを MessageSource からも引けるようにしています。注釈の message に {キー} と書くと、messages_ja.properties の値が使われます。

public record SignupForm(
        @NotBlank(message = "{signup.name.required}")
        String name,

        @Size(min = 8, max = 64, message = "{signup.password.size}")
        String password) {
}
# messages_ja.properties
signup.name.required=名前を入力してください
signup.password.size=パスワードは{min}〜{max}文字で入力してください
# messages_en.properties
signup.name.required=Please enter your name.
signup.password.size=Password must be {min} to {max} characters.

注釈の属性({min}・{max})は名前で書きます。 {0} ではありません。message を書かない場合は、Hibernate Validator に入っている文が使われます。Hibernate Validator 9.1.3.Final の ValidationMessages_ja.properties の主なものです。

jakarta.validation.constraints.NotBlank.message = 空白は許可されていません
jakarta.validation.constraints.NotNull.message  = null は許可されていません
jakarta.validation.constraints.NotEmpty.message = 空要素は許可されていません
jakarta.validation.constraints.Size.message     = {min} から {max} の間のサイズにしてください
jakarta.validation.constraints.Email.message    = 電子メールアドレスとして正しい形式にしてください
jakarta.validation.constraints.Min.message      = {value} 以上の値にしてください
jakarta.validation.constraints.Max.message      = {value} 以下の値にしてください

既定の文を言い換えたいときは、同じキー(jakarta.validation.constraints.NotBlank.message)を messages_ja.properties に書くと、そちらが使われます。画面に出すときは th:errors="*{name}" で、今の言語の文が出ます。注釈の一覧は、関連記事「Spring Bootバリデーション一覧|@Validと@Validated」で扱っています。

REST API の多言語対応

画面を持たない REST API でも、Accept-Language で言語を決める既定の動きはそのまま使えます。エラーの文を言語ごとに返すなら、@RestControllerAdvice の中で MessageSource を使います。

@RestControllerAdvice
public class ApiErrorHandler {

    private final MessageSource messages;

    public ApiErrorHandler(MessageSource messages) {
        this.messages = messages;
    }

    @ExceptionHandler(OrderNotFoundException.class)
    public ProblemDetail notFound(OrderNotFoundException ex, Locale locale) {   // 引数で Locale を受け取れる
        var pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
        pd.setDetail(messages.getMessage("error.order.notFound", new Object[]{ex.orderNo()}, locale));
        return pd;
    }
}
curl -H "Accept-Language: en" http://localhost:8080/api/orders/999

ex.orderNo() が数値なら、上の「数値は3桁区切りになる」に気をつけます。例外処理の全体は、関連記事「Spring Boot例外処理|@ControllerAdvice完全版」で扱っています。

うまくいかないときの表

症状 原因 直し方
どの言語でも NoSuchMessageException messages.properties(言語なし)が無く、自動設定されていない 言語なしのファイルを置く
Thymeleaf に ??key_ja_JP?? キーが無い・綴りが違う・上と同じ キーと置き場所を確かめる
日本語が ã“ã‚“ と化ける UTF-8 のファイルを ISO-8859-1 で読んでいる(自前の Bean) setDefaultEncoding("UTF-8")
日本語が � で化ける ファイルが Shift_JIS で保存されている UTF-8 で保存し直す
It's の ' が消える 引数ありの文で ' を1つ書いた '' にする
It''s と2つ出る 引数なしの文で '' と書いた ' にするか、always-use-message-format: true
ID が 1,234,567 になる 数値に3桁区切りの書式が付いた {0,number,#} か文字列で渡す
用意していない言語で日本語が出る fallback-to-system-locale: true とサーバーの OS の言語 false にする
?lang=en で切り替わらない AcceptHeaderLocaleResolver のまま、または Bean 名が localeResolver でない CookieLocaleResolver などを localeResolver の名前で定義
?locale=en が効かない パラメータ名を lang に変えている 画面のリンクと setParamName をそろえる
UnsupportedOperationException: Cannot change HTTP Accept-Language header AcceptHeaderLocaleResolver に切り替えを頼んだ 切り替えできる LocaleResolver にする
ファイルを直しても画面が変わらない ずっとキャッシュされている 開発中は cache-duration を付ける

よくある質問

Q. messages.properties は何語で書きますか。 A. どの言語のファイルにも当たらなかったときに出る文なので、利用者が最も多い言語か英語にします。日本向けのアプリなら、messages.properties を日本語にして、messages_ja.properties を置かない形もあります。

Q. 言語ごとにファイルを分けず、DB で管理できますか。 A. AbstractMessageSource を継承して DB から引く MessageSource を作れます。翻訳を画面から直したい場合に使います。テーブルの設計は、関連記事「多言語対応のDB設計パターン比較|翻訳テーブルのDDLと選び方」で扱っています。

Q. フロントエンドが Angular・Vue のときはどうしますか。 A. 画面の文はフロントエンド側で翻訳し、サーバーはエラーの文やメールの文だけを MessageSource で返す分け方が多いです。関連記事「vue-i18nの使い方|useI18nで多言語対応と言語切り替え」「Angular多言語対応(i18n)の手順|$localizeと翻訳ファイル」で扱っています。

まとめ

  • messages.properties(言語なし)を必ず置く。無いと Boot が MessageSource を作らない
  • 文字コードは Boot の既定が UTF-8。自分で ResourceBundleMessageSource を作るなら setDefaultEncoding("UTF-8")(Spring 本体の既定は ISO-8859-1)
  • {0} を含む文では ' は ''。引数の無い文は MessageFormat を通らないので ' のまま
  • 数値の {0} には 3桁区切りが入る。ID は {0,number,#} か文字列で
  • fallback-to-system-locale は既定で true。サーバーの OS の言語で結果が変わる
  • 画面から切り替えるなら CookieLocaleResolver/SessionLocaleResolver を localeResolver の名前で定義し、LocaleChangeInterceptor を付ける
  • バリデーションは message = "{キー}" で messages_ja.properties から引ける

参考・出典

確認日はいずれも 2026年10月13日です。

  • Spring Boot Reference「Internationalization」:https://docs.spring.io/spring-boot/reference/features/internationalization.html
  • Spring Boot v4.1.1 ソース(MessageSourceProperties・MessageSourceAutoConfiguration・WebProperties・WebMvcAutoConfiguration・ValidationAutoConfiguration):https://github.com/spring-projects/spring-boot/tree/v4.1.1
  • Spring Framework v7.0.9 ソース(ResourceBundleMessageSource・spring-webmvc の i18n パッケージ):https://github.com/spring-projects/spring-framework/tree/v7.0.9
  • Spring Framework Reference「Locale」:https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/localeresolver.html
  • Java SE 21 API「MessageFormat」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/text/MessageFormat.html
  • Hibernate Validator 9.1.3.Final ソース(ValidationMessages_ja.properties):https://github.com/hibernate/hibernate-validator/tree/9.1.3.Final

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Bootの多言語対応|messages.propertiesと切り替え」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?