0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring MVC のロケール周りのメモ

Posted at

Spring MVC のロケール周りについて調べたのでメモ、以下のバージョンで調べました。

  • Spring Boot : 3.3.5
  • Spring Framework : 6.1.14

結論からいうと LocaleResolver がポイントとなっているようです。

Locale が必要なパターン

どういったときに ロケールを意識するかのユースケースをはじめに紹介します。

MessageSource を使う

Spring では MessageSource を使うことでメッセージを取得することができます。

Spring Bootでは messages.properties を用意しておくことでメッセージのハードコーディングを避けることができます。

以下、利用例です。

messages.properties
hello=こんにちは
@Autowired
private MessageSource messageSource;

@GetMapping("/hello")
public Hello1Response hello() {
  final String hello = messageSource.getMessage("hello", null, null);
  // 省略
}

上記のように MessageSource#getMessage メソッドを使うことで 外部ファイルに定義された値を取得することができます。

多言語対応が必要な場合

多言語対応が必要な場合は 言語ごとに messages.properties を定義します。

デフォルト

messages.properties
hello=こんにちは

英語用

messages_en.properties
hello=Hello

また、MessageSource#getMessage はいくつかオーバーロードされたものがありますが、それぞれ引数にLocale を受け取ることができます

@GetMapping("/hello")
public Hello1Response hello() {
  
  // 日本語(こんにちは)
  final String jaHello = messageSource.getMessage("hello", null, Locale.JAPANESE);
  
  // 英語(Hello)
  final String enHello = messageSource.getMessage("hello", null, Locale.ENGLISH);

  // 省略
}

上記のように Locale を渡すことで、それぞれの言語のメッセージを取得できます。

(前の例のようにnull を渡した場合は 実装クラスにもよりますが Locale.getDefault が利用されます。)

リクエストに紐づいたロケールを使いたい

先の例の方法だと至る所で言語別の分岐が必要になってしまいます。

できれば利用しているユーザーのリクエストに応じたロケールを使いたいものです。

ServletRequest#getLocale

ServletRequest には getLocale というメソッドがあり、Accept-language: リクエストヘッダーに紐づくロケールを取得することができます。

画面のない WebAPI などではこれでもよいかもしれないですが今回はSpringの仕組みの紹介をするので...

Controller のハンドラメソッドの引数で Locale が受け取れる

@GetMapping("/hello")
public Hello1Response hello(Locale locale) {
  
  final String hello = messageSource.getMessage("hello", null, locale);
}

上記のように メソッドの引数に Locale を定義することでリクエストに紐づいた ロケールを取得することができます。

ハンドラメソッドの引数は HandlerMethodArgumentResolver というインターフェースを使って解決されるのですが、Locale の場合は ServletRequestMethodArgumentResolver が利用されています。

ここでは RequestContextUtils.getLocale メソッドを呼び出しておりその先では LocaleResolver を使ってロケールの解決が行われています。

これが今回のポイントとなるインターフェースです。

上記で登場したそれぞれのメソッドは以下を参照してください。

LocaleResolver インターフェース

LocaleResolver#resolveLocale(HttpServletRequest) メソッドを使うことで現在のリクエストに紐づいたロケールを取得することができます。

このインターフェースには

  • AcceptHeaderLocaleResolver
  • CookieLocaleResolver
  • SessionLocaleResolver
  • FixedLocaleResolver

などの実装クラスがあります。 それぞれ、リクエストヘッダー、Cookie, セッション、固定値をもとにロケールを取得するようになっています。

デフォルトでは AcceptHeaderLocaleResolver が使われているようで, Spring Bootだと WebMvcAutoConfigurationConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) で定義されているので、上書き定義することで LocaleResolver を変更することができそうです。

セッションに保存すると セッションが切れたときに画面が元の言語に戻ってしまうので今回はCookieを使ったBean定義の例を示します。

@Bean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
public LocaleResolver localeResolver(WebProperties webProperties) {
  final AcceptHeaderLocaleResolver headerLocaleResolver = new AcceptHeaderLocaleResolver();
  headerLocaleResolver.setDefaultLocale(webProperties.getLocale());

  // Cookie名はデフォルトだとSpringを使っていることがわかる名前になっているので返る
  final CookieLocaleResolver localeResolver = new CookieLocaleResolver("appLocale");
  // Cookieから取得できなかったときは AcceptHeaderLocaleResolverに処理を委譲
  localeResolver.setDefaultLocaleFunction(headerLocaleResolver::resolveLocale);
  return localeResolver;
}

上記の例は CookieLocaleResolver をBean定義していますが、もしCookieから取得できなかった場合は AcceptHeaderLocaleResolver に処理を委譲してリクエストヘッダから取得するようにしています。 setDefaultLocaleFunction は無理にセットしなくてもデフォルトで ServletRequest#getLocale から取得するようになっています。

ロケールを保存する。

LocaleResolver を使うことでそれぞれの場所からLocaleを取得できますが、取得元の場所に保存しておかないと役に立ちません。

ドキュメントによると LocaleChangeInterceptor を使うとできるとありますが 実装 を見ればわかる通り ServletRequest#getParameter を利用しています。

Thymeleaf などを使った画面の場合はこれでもよいですが SPAのようにフロントエンドとJSONでやり取りする場合はこれではうまくいきません。このメソッドの実装を参考にしつつ、ロケールを設定する方法を考える必要があります。例えば以下のようにエンドポイントを用意する方法もあります。

@Autowired
private LocaleResolver localeResolver;

@PostMapping("setLocale")
public ResponseEntity<?> setLocale(@RequestBody SetLocaleRequest req, HttpServletRequest request,
    HttpServletResponse response) {

  // ロケールの設定
  // LocaleChangeInterceptor を参考
  Locale locale = StringUtils.parseLocale(req.getLang());
  localeResolver.setLocale(request, response, locale);

  return ResponseEntity.ok().build();
}

あとはログイン成功時などにもユーザーの言語設定に紐づいたロケールを保存しておくと良いかもしれないですね。

注意
AcceptHeaderLocaleResolver では setLocaleUnsupportedOperationException がスローされる実装になっているので注意してください。

おまけ

LocaleContextHolder について

LocaleContextHolderというクラスのgetLocale というstaticメソッドを使うことで LocaleResolver によって解決されたロケールを取得することができます。

LocaleContextHolder にロケールが設定されるタイミングは2か所あり

で設定されています。

RequestContextFilter では ServletRequest#getLocale で取得したロケールが設定されており、FrameworkServlet 実際は DispatcherServlet では LocaleResolver で解決されたロケールが設定されています。

つまり DispatcherServlet 以降の処理である HandlerInterceptor やハンドラメソッドなどでは LocaleResolver の値が取れますが、それより手前の処理である サーブレットフィルターではこの値をとることができません。

なので利用箇所については意識しておく必要がありそうです。

(LocaleResolver から取得した値をセットするサーブレットフィルターを作るのはどうなんだろうか?🤔)

おわりに

  • LocaleResolver の実装クラスをBean定義することでロケールの取得元を変更できること
  • ハンドラメソッドの引数に Locale を定義したり、 LocaleResolver を直接利用することで現在のリクエストに紐づくロケールを取得することができること
  • ロケールの変更には LocaleResolver#setLocale を実行することで変更できること

を紹介しました。
多言語対応する機会がないと利用しないかもしれないですが考え方だったりは覚えておくと何かの役に立つかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?