Skip to content
Spring Boot sb production 3 min read

Internationalization (i18n)

Internationalization (i18n) means designing your application so it can present text in different languages without code changes. Spring Boot makes this declarative: you keep translated strings in resource bundles (messages_xx.properties), look them up through a MessageSource, and let a LocaleResolver decide which language a given request should use. The same bundles also localize Bean Validation error messages.

Message bundles

Spring Boot auto-configures a MessageSource backed by messages.properties files on the classpath. Create one file per locale under src/main/resources:

# messages.properties  (default / fallback)
greeting=Hello
cart.items=You have {0} items in your cart
# messages_es.properties  (Spanish)
greeting=Hola
cart.items=Tienes {0} artículos en tu carrito
# messages_fr.properties  (French)
greeting=Bonjour
cart.items=Vous avez {0} articles dans votre panier

Tune the base name and encoding if needed:

spring:
  messages:
    basename: messages
    encoding: UTF-8
    fallback-to-system-locale: false

Tip: Set fallback-to-system-locale: false so an unknown locale falls back to messages.properties (your chosen default) rather than the server’s OS locale, which makes behavior reproducible across environments.

Looking up messages in code

Inject the MessageSource and resolve keys against the current locale.

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.Locale;

@RestController
public class GreetingController {

    private final MessageSource messages;

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

    @GetMapping("/greeting")
    public String greeting(Locale locale) {
        return messages.getMessage("greeting", null, locale);
    }

    @GetMapping("/cart")
    public String cart(@RequestParam int count) {
        Locale locale = LocaleContextHolder.getLocale();
        return messages.getMessage("cart.items", new Object[]{count}, locale);
    }
}

The {0} placeholder in the bundle is filled from the Object[] array — these are parameterized messages backed by java.text.MessageFormat.

Output:

GET /greeting   (Accept-Language: es)   ->  Hola
GET /cart?count=3   (Accept-Language: fr)   ->  Vous avez 3 articles dans votre panier

Spring injects the resolved Locale directly as a controller method parameter, or you can read it anywhere via LocaleContextHolder.getLocale().

Choosing the locale: LocaleResolver

A LocaleResolver determines which locale applies to each request. Spring MVC’s default is AcceptHeaderLocaleResolver, which reads the browser’s Accept-Language header.

ResolverSource of localeStateless?Can change at runtime?
AcceptHeaderLocaleResolver (default)Accept-Language headerYesNo (fixed per request)
SessionLocaleResolverHTTP session attributeNoYes
CookieLocaleResolverCookieMostlyYes
FixedLocaleResolverSingle hard-coded localeYesNo

To let users pick and persist a language, switch to a SessionLocaleResolver:

import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;

@Bean
public LocaleResolver localeResolver() {
    SessionLocaleResolver resolver = new SessionLocaleResolver();
    resolver.setDefaultLocale(Locale.ENGLISH);
    return resolver;
}

Switching language with LocaleChangeInterceptor

The LocaleChangeInterceptor watches for a request parameter (e.g. ?lang=es) and updates the resolver’s stored locale. Register it on the MVC config:

import org.springframework.context.annotation.*;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }

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

Now a request to /greeting?lang=fr switches the session locale to French, and subsequent requests in that session stay French until changed again.

Note: LocaleChangeInterceptor requires a mutable resolver such as SessionLocaleResolver or CookieLocaleResolver. It cannot change the immutable AcceptHeaderLocaleResolver.

Localizing validation messages

Bean Validation messages also flow through the MessageSource. First wire the validator to use it:

import jakarta.validation.Validator;
import org.springframework.context.MessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Bean
public LocalValidatorFactoryBean getValidator(MessageSource messageSource) {
    LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
    bean.setValidationMessageSource(messageSource);
    return bean;
}

Reference a message key (in braces) from the constraint:

import jakarta.validation.constraints.*;

public record RegisterRequest(
    @NotBlank(message = "{user.name.required}") String name,
    @Email(message = "{user.email.invalid}") String email
) {}

Add the keys to each bundle:

# messages.properties
user.name.required=Name is required
user.email.invalid=Email address is not valid
# messages_es.properties
user.name.required=El nombre es obligatorio
user.email.invalid=La dirección de correo no es válida

Now a validation failure returns the message in the request’s locale. See Validation Introduction and Handling Validation Errors for how those messages surface in responses.

Best Practices

  • Always provide a complete default messages.properties as the fallback bundle.
  • Use UTF-8 encoding and fallback-to-system-locale: false for predictable resolution.
  • Prefer parameterized messages over string concatenation so word order stays correct per language.
  • Use a SessionLocaleResolver or CookieLocaleResolver plus LocaleChangeInterceptor to let users choose a language.
  • Externalize all user-facing text — including validation messages — into bundles, never inline literals.
Last updated June 13, 2026
Was this helpful?