グローバル向けのREST APIを作るとき、エラーメッセージやレスポンスを英語と日本語で出し分けたいことってありますよね。Spring Bootにはi18n(国際化)のための仕組みが組み込まれていて、MessageSourceLocaleResolver を組み合わせれば、Accept-Languageヘッダーに応じてメッセージを切り替えられます。

この記事では messages.properties の作り方からLocaleResolverの設定、@Valid バリデーションエラーメッセージのローカライズまで、REST API向けの実装を一通り解説します。Spring Boot 3.x(Jakarta EE)での動作を前提にしています。

i18n実装の全体像

Spring Bootのi18n実装は、2つのコンポーネントが中心になります。

  • MessageSource:言語別のメッセージテキストを管理する
  • LocaleResolver:リクエストからロケール(言語)を解決する

リクエストの Accept-Language: ja ヘッダーをLocaleResolverが受け取り、解決したロケールを LocaleContextHolder にセットします。あとはMessageSourceが LocaleContextHolder.getLocale() を参照して、ロケールに合ったメッセージを返してくれます。

messages.propertiesの作成

src/main/resources 配下にプロパティファイルを用意します。

src/main/resources/
├── messages.properties       # デフォルト(フォールバック)
├── messages_ja.properties    # 日本語
└── messages_en.properties    # 英語

messages.properties はロケールが解決できなかったときのフォールバックです。

# messages_ja.properties
user.name.required=ユーザー名は必須です
user.not.found=ユーザーが見つかりません
# messages_en.properties
user.name.required=User name is required
user.not.found=User not found

application.properties にファイルの場所とエンコーディングを指定します。basename は拡張子なしで指定してください(messages であって messages.properties ではない)。エンコーディングを忘れると日本語が文字化けするので必ず設定しましょう。

spring.messages.basename=messages
spring.messages.encoding=UTF-8
spring.messages.use-code-as-default-message=false

basename はカンマ区切りで複数指定もできます。バリデーション用と汎用メッセージを分けたい場合は messages,validation のように書きましょう。

MessageSourceのBean定義が必要なケース

application.properties の設定だけで自動設定は有効になります。キャッシュ時間を細かく制御したい場合は ReloadableResourceBundleMessageSource をBean定義します。

@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource =
        new ReloadableResourceBundleMessageSource();
    messageSource.setBasenames("classpath:messages");
    messageSource.setDefaultEncoding("UTF-8");
    messageSource.setCacheSeconds(0); // 開発中は即時リロード。本番は-1(永続キャッシュ)
    return messageSource;
}

setCacheSeconds(-1) はデフォルト値で 永続キャッシュ(ファイル再読み込みなし) を意味します。開発中にファイル変更を即時反映したい場合は setCacheSeconds(0) を使ってください。

@Bean で自前定義した場合、Spring Bootの自動設定(MessageSourceAutoConfiguration)が無効になります。 application.propertiesspring.messages.* 設定は参照されなくなるため、setBasenamessetDefaultEncoding はコード上で直接指定してください。

LocaleResolverの設定

REST APIにはステートレスに保てる AcceptHeaderLocaleResolver が最適です。CookieResolverやSessionResolverはロケールをクライアント・サーバー側に保持する仕組みなのでWeb MVC向きで、APIには不要な状態管理を持ち込むことになります。

@Configuration
public class WebConfig {

    @Bean
    public LocaleResolver localeResolver() {
        AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
        resolver.setDefaultLocale(Locale.JAPANESE);
        resolver.setSupportedLocales(List.of(Locale.JAPANESE, Locale.ENGLISH));
        return resolver;
    }
}

setDefaultLocale でAccept-Languageヘッダーがないリクエストへのフォールバック言語を指定します。このBean登録を忘れると Spring MVCがAccept-Languageヘッダーを正しく解決してくれないので注意してください。

MessageSourceでメッセージを取得する

Serviceクラスでメッセージを取得するには、LocaleContextHolder.getLocale() でロケールを取得して MessageSource に渡します。

@Service
@RequiredArgsConstructor
public class UserService {

    private final MessageSource messageSource;

    public User findUser(Long id) {
        return userRepository.findById(id).orElseThrow(() -> {
            String message = messageSource.getMessage(
                "user.not.found", null, LocaleContextHolder.getLocale()
            );
            return new UserNotFoundException(message);
        });
    }
}

メッセージコードが存在しない場合は NoSuchMessageException が発生します。デフォルト値を返したい場合は getMessage(code, args, defaultMessage, locale) の第4引数に文字列を渡せます。

@Validバリデーションエラーメッセージを多言語化する

spring-boot-starter-validation を使っている場合、ValidationAutoConfigurationLocalValidatorFactoryBeanMessageSource を自動注入します。そのため @Valid のバリデーションエラーメッセージを messages.properties で管理できます。アノテーションの message 属性にキーを {...} 形式で指定するだけです。

public class UserRequest {

    @NotBlank(message = "{user.name.required}")
    private String name;
}

カスタムの ValidatorFactory を自分でBean定義した場合、この自動連携が無効になるので注意が必要です。@Validated を使ったグループバリデーションについては Spring Bootの@Validated使い方とグループバリデーション も参考にしてみてください。

@RestControllerAdviceで多言語エラーレスポンスを返す

MethodArgumentNotValidException を多言語化されたエラーレスポンスに変換するには @RestControllerAdvice でハンドリングします。

@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

    private final MessageSource messageSource;

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationError(
            MethodArgumentNotValidException ex) {

        Locale locale = LocaleContextHolder.getLocale();

        List<String> messages = ex.getBindingResult().getFieldErrors().stream()
            .map(error -> messageSource.getMessage(error, locale))
            .collect(Collectors.toList());

        return ResponseEntity.badRequest()
            .body(new ErrorResponse("VALIDATION_ERROR", messages));
    }
}

messageSource.getMessage(FieldError, Locale) のオーバーロードを使うと、FieldErrorが持つメッセージコードを順番に解決してくれます。例外ハンドリングの基本については Spring BootのREST API例外処理ガイド も合わせて読んでみてください。

curlで動作確認する

# 日本語
curl -H "Accept-Language: ja" http://localhost:8080/api/users/999

# 英語
curl -H "Accept-Language: en" http://localhost:8080/api/users/999

# ヘッダーなし(デフォルトロケール=日本語が使われる)
curl http://localhost:8080/api/users/999
// Accept-Language: ja
{ "code": "NOT_FOUND", "message": "ユーザーが見つかりません" }

// Accept-Language: en
{ "code": "NOT_FOUND", "message": "User not found" }

まとめ

messages.properties の準備と AcceptHeaderLocaleResolver のBean登録、あとは MessageSource を介してメッセージを取得するだけで、REST APIの多言語対応がシンプルに完成します。既存プロジェクトへの組み込みも難しくないので、グローバル対応が必要になったときにぜひ試してみてください。