skip to content

In a Spring MVC web app, how is the Locale for a request determined, and what is the role of LocaleResolver?

level: seniorimportance: should knowfreq 40%

answer

  1. localeResolver bean → DispatcherServlet → LocaleContextHolder
  2. AcceptHeader = default, read-only (Accept-Language)
  3. Session / Cookie = mutable, persist choice
  4. LocaleChangeInterceptor + ?locale=fr
  5. AcceptHeader + interceptor = throws

basics

~10 s

A LocaleResolver decides which Locale a request uses. Spring MVC's DispatcherServlet calls it, then MessageSource lookups and LocaleContextHolder use that Locale. The default is AcceptHeaderLocaleResolver, which reads the browser's Accept-Language header.

solid answer

~40 s

Spring MVC delegates locale determination to a LocaleResolver bean (named 'localeResolver'). On each request the DispatcherServlet resolves a Locale, binds it to LocaleContextHolder, and exposes it via RequestContextUtils; controllers and views then use it, and MessageSource.getMessage is typically called with LocaleContextHolder.getLocale(). Implementations: AcceptHeaderLocaleResolver (default) reads the Accept-Language header and is read-only (setLocale throws); SessionLocaleResolver stores the chosen locale in the HttpSession; CookieLocaleResolver persists it in a cookie; FixedLocaleResolver always returns a configured locale. To let users switch languages, register a LocaleChangeInterceptor that reads a request parameter (e.g. ?lang=fr) and calls resolver.setLocale — which only works with a mutable resolver (Session/Cookie), not AcceptHeader. Spring Boot auto-configures AcceptHeaderLocaleResolver but backs off if you define your own.

code

java · 22 lines
java
@Configuration
public class WebI18nConfig implements WebMvcConfigurer {

    @Bean
    public LocaleResolver localeResolver() {
        var resolver = new CookieLocaleResolver("APP_LOCALE");
        resolver.setDefaultLocale(Locale.ENGLISH);
        return resolver; // mutable → works with LocaleChangeInterceptor
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        var interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("locale"); // ?locale=fr switches language
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}

go deeper

for a junior

Knows locale often comes from the Accept-Language header.

for a middle

Names the resolver implementations and how LocaleChangeInterceptor switches language.

for a senior

Explains DispatcherServlet→LocaleContextHolder binding, read-only vs mutable resolvers, and Boot back-off.

for a principal

Designs locale strategy (header vs cookie vs user-preference store), thread-local propagation, and WebFlux differences.

**The problem.** MessageSource needs a `Locale`, but where does the *request's* locale come from? In Spring MVC that decision is abstracted behind `org.springframework.web.servlet.LocaleResolver`. **LocaleResolver interface.** Two methods: `Locale resolveLocale(HttpServletRequest)` and `setLocale(HttpServletRequest, HttpServletResponse, Locale)`. Not all implementations support `setLocale`. **How DispatcherServlet uses it.** On every request, `DispatcherServlet` looks up the bean named **`localeResolver`**, calls `resolveLocale`, and binds the result into a `LocaleContext` on `LocaleContextHolder` (a thread-local). Downstream: `RequestContextUtils.getLocale(request)`, JSP/Thymeleaf views, `@RequestMapping` methods that accept a `Locale` parameter, and `MessageSource` calls all use that locale. Because it's thread-bound, even non-web code invoked on that thread can call `LocaleContextHolder.getLocale()`. **Built-in implementations:** - **AcceptHeaderLocaleResolver** — reads the HTTP `Accept-Language` header. It's the **default** (and Spring Boot's). It is **read-only**: `setLocale` throws `UnsupportedOperationException`, because you can't change the browser's header. Supports `supportedLocales` / `defaultLocale`. - **SessionLocaleResolver** — stores the locale as an `HttpSession` attribute; survives across requests in the session; supports `setLocale`. - **CookieLocaleResolver** — persists the locale in a cookie; survives across sessions; supports `setLocale`. - **FixedLocaleResolver** — always returns one configured locale (setLocale unsupported); useful for single-language apps or testing. **Switching language at runtime.** Register a `LocaleChangeInterceptor` (a `HandlerInterceptor`) whose `paramName` (default `locale`) is read from the query string; when present it calls `localeResolver.setLocale(...)`. This requires a **mutable** resolver — pairing `LocaleChangeInterceptor` with `AcceptHeaderLocaleResolver` fails because that resolver can't be set. Typical setup: `CookieLocaleResolver` or `SessionLocaleResolver` + `LocaleChangeInterceptor` so `?locale=fr` switches and persists the language. **Spring Boot notes.** `WebMvcAutoConfiguration` provides an `AcceptHeaderLocaleResolver` unless you define your own `localeResolver` bean (or set `spring.mvc.locale` / `spring.web.locale` and `locale-resolver=fixed`). Defining your own bean backs off the default. **Gotchas.** (1) The bean must be named `localeResolver`. (2) `LocaleContextHolder` is thread-bound — spawned threads / async work don't inherit it unless propagated. (3) In WebFlux the abstraction differs (`LocaleContextResolver`).

  • Why does LocaleChangeInterceptor fail with AcceptHeaderLocaleResolver?
    AcceptHeaderLocaleResolver is read-only — its setLocale throws UnsupportedOperationException because the locale comes from the immutable Accept-Language header. The interceptor calls setLocale, so you must use a mutable resolver like Session or Cookie.
  • How does a MessageSource call get the current request's locale without you passing it explicitly?
    DispatcherServlet binds the resolved locale to LocaleContextHolder (a thread-local), so LocaleContextHolder.getLocale() returns it anywhere on that thread. Controllers can also declare a Locale parameter, which Spring injects from the same source.

saying these in an interview costs you the question

  • Saying AcceptHeaderLocaleResolver supports setLocale
  • Thinking the resolver bean name is arbitrary (must be 'localeResolver')
  • Assuming LocaleContextHolder is inherited by async/child threads automatically

context