- Spring
Spring Bootの多言語対応|messages.propertiesと切り替え
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期連続の黒字決算。