Spring Boot 3.5: Internationalization (i18n) with messages.properties

Introduction
Internationalization (i18n) is the process of designing your application to be adapted to various languages, regional
peculiarities, and technical requirements of a target market.
Internationalization is a crucial step in the process of localizing your product. In this tutorial, we will show you how
to use Spring Boot to create a simple internationalized application,
how to get translated messages from messages_xx.properties
files, render translated HTML from HTML template
using Thymeleaf and how to use LocaleResolver
to change the language of the application.
Used technologies
- Java 21+ (LTS recommended)
- Spring Boot 3.5.4
- Thymeleaf 3.1.3
- Maven

Configuration
1. Create messages_xx.properties
files
The messages are stored in the messages_XX.properties
files. The XX
is the language code. For
example, messages_pl_PL.properties
is the Polish version of the messages.
footerText=© 2025 SimpleLocalize. Wszelkie prawa zastrzeżone.
linkText=Utwórz konto SimpleLocalize
message=Dziękujemy za wypróbowanie naszego demo SimpleLocalize dla Spring Boot!
title=Hej {0}!
With SimpleLocalize, managing translations is effortless. Upload your source language from *.properties
, and get translations in other
languages automatically using machine translation, AI, or by collaborating with your own translators. You can automate uploading and downloading messages_xxx.properties
files via SimpleLocalize CLI.
2. Configure messages_xx.properties
location
The default location for messages is src/main/resources/messages
.
You can change this by setting the spring.messages.basename
property in your application.properties
file
or by providing your ResourceBundleMessageSource
bean.
Application properties configuration:
# application.properties - Spring Boot 3.5
spring.messages.basename=i18n/messages
spring.messages.use-code-as-default-message=true
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=false
spring.messages.cache-duration=3600
Java Configuration using ResourceBundleMessageSource
:
@Bean
public ResourceBundleMessageSource messageSource() {
var resourceBundleMessageSource = new ResourceBundleMessageSource();
resourceBundleMessageSource.setBasenames("i18n/messages"); // directory with messages_XX.properties
resourceBundleMessageSource.setUseCodeAsDefaultMessage(true); // if message not found, use the key as default message
resourceBundleMessageSource.setDefaultLocale(Locale.of("en")); // default locale
resourceBundleMessageSource.setDefaultEncoding("UTF-8"); // encoding of the messages files
return resourceBundleMessageSource;
}
Please note that not every setting is available in application.properties
file.

3. Configure resolving locale from requests
The default locale resolver is AcceptHeaderLocaleResolver
which resolves the locale from the Accept-Language
header.
You can change this by setting the spring.mvc.locale-resolver
property in your application.properties
file or by
providing your LocaleResolver
bean,
creating LocaleChangeInterceptor
and registering it via addInterceptors
method (see WebMvcConfigurer
class).
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
sessionLocaleResolver.setDefaultLocale(Locale.of("en"));
return sessionLocaleResolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
return localeChangeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
4. Create HTML template with Thymeleaf
One of the most common use cases for internationalization in Spring Boot is to render HTML pages (or emails) with translated messages. In this case, we will use Thymeleaf as it's one of the most popular template engines in the Java ecosystem and it works great with Spring Boot, and it supports internationalization out of the box.
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" th:attr="lang=${lang}">
<head>
<title>Spring Boot Email Example</title>
</head>
<body>
<header>
<h1 th:utext="#{title(${userName})}"></h1>
</header>
<article>
<p th:text="#{message}"></p>
<a th:href="${url}" th:text="#{linkText}"></a>
</article>
<footer>
<p th:text="#{footerText}"></p>
</footer>
</body>
</html>
You might want to learn about what is 'hreflang' and how to use it if you are creating multilingual app or website.
Quick Thymeleaf guide:
th:attr="lang=${lang}"
- sets the language of the documentth:text="#{message}"
- gets a message withmessage
key frommessages_xx.properties
th:utext="#{title(${userName})}"
- same asth:text
, but allows you to use variables in the message, and it does not escape HTML charactersth:href="${url}"
- inserts a value of theurl
variable
Tip: If you are creating email templates, you may want to check mjml.io that allows you to create responsive email templates using a simple markup language.
Get translated messages
Use MessageSource
to get translated messages. This is the default way to get translated messages in Spring Boot.
import org.springframework.context.MessageSource;
@Autowired
private MessageSource messageSource;
@Test
void shouldGetTranslatedTextFromLocalFileAndLocale() {
//given
Locale locale = Locale.of("pl","PL");
//when
String titleTextWithArgument=messageSource.getMessage("title",new Object[]{"Foo Bar"},locale);
//then
assert titleTextWithArgument.equals("Hej Foo Bar!");
}
Translate API exceptions
You can return translated API exceptions by using standard Spring Boot @ControllerAdvice
.
For these purposes, we will create a custom ErrorController
that handles exceptions and returns translated messages.
@ExceptionHandler(Exception.class)
- catches all exceptions not caught by other methods@ExceptionHandler(IllegalArgumentException.class)
- catchesIllegalArgumentException
exceptions
Both methods gets Locale
from LocaleContextHolder
and returns translated message from messages_xx.properties
file
using MessageSource
bean.
@RestControllerAdvice
public class ErrorController
{
@Autowired
private MessageSource messageSource;
@ExceptionHandler(Exception.class)
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<ApiError> exception()
{
String message = getLocalizedMessage("exception.internalServerError");
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return ResponseEntity
.status(status)
.body(new ApiError(message, status));
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> badRequest()
{
String message = getLocalizedMessage("exception.badRequest");
HttpStatus status = HttpStatus.BAD_REQUEST;
return ResponseEntity
.status(status)
.body(new ApiError(message, status));
}
private String getLocalizedMessage(String translationKey)
{
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(translationKey, null, locale);
}
}
By default, Spring Boot uses Accept-Language
header to resolve user locale,
but in this tutorial we changed this behavior by registering custom LocaleChangeInterceptor
bean.
To change the language of exception, you need to use lang
parameter, e.g. /api/my-health?lang=pl_PL
.
The number of ways to return translated messages is endless, but the solution above might be one of the best ones as we keep all the logic and translation keys for exceptions in one place.
Translate @Validation messages
Spring Boot provides support for localizing validation error messages using the Bean Validation API (@Validated
, @Valid
) with custom message properties.
Create a LocalValidatorFactoryBean
bean to configure the validation messages:
@Configuration
public class Configuration {
@Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource()); // Use the message source bean to resolve validation messages
return bean;
}
}
Create a request DTO with validation annotations and localized messages:
public record CreateUserRequest(
@NotBlank(message = "{error.user.name.notblank}")
String name,
@Email(message = "{error.user.email.invalid}")
@NotBlank(message = "{error.user.email.invalid}")
String email
) {}
Make sure to create the corresponding messages in your messages_xx.properties
files:
# messages_pl_PL.properties
error.user.name.notblank=Imię nie może być puste.
error.user.email.invalid=Adres e-mail jest nieprawidłowy.
error.user.email.notblank=Adres e-mail nie może być pusty.
Create a sample controller to handle user creation requests with validation:
@RestController
@Validated
public class UserController {
@PostMapping("/api/users")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {
return ResponseEntity.ok("User created successfully");
}
}
Create a global exception handler to handle validation errors and return localized messages:
@RestControllerAdvice
public class ErrorController
{
@ExceptionHandler(BindException.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> validationFailed(BindException exception)
{
List<String> errors = exception.getFieldErrors()
.stream()
.map(fieldError -> {
String field = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
return field + ": " + defaultMessage;
})
.toList();
ApiError apiError = new ApiError(errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiError);
}
}
Translate web pages
You can also return translated web pages (HTML) by using standard Spring Boot @Controller
.
Spring Boot will automatically resolve user locale and render HTML from my-html-template.html
template with translated
messages.
This is the default way to return translated web pages in Spring Boot.
@Controller
public class WelcomeController
{
@GetMapping("/welcome")
public String renderHtmlFromTemplate(Model model)
{
model.addAttribute("userName", "Jakub");
return "my-html-template";
}
}
Run the application and open http://localhost:8080/welcome
in your browser.
You can change the language by adding ?lang=pl_PL
to the URL.
Translate any HTML content
Use ThymeleafEngine
bean to render HTML with translated messages. This is probably the most
popular way to render HTML with translated messages in Spring Boot.
@Autowired
private TemplateEngine templateEngine;
public String renderHtmlFromTemplate(Locale locale, String userName)
{
Context context = new Context();
context.setLocale(locale);
context.setVariable("userName",userName);
context.setVariable("lang",locale.getLanguage());
context.setVariable("url","https://simplelocalize.io");
return templateEngine.process("my-html-template",context);
}

Handling Accept-Language Header
Default Behavior with AcceptHeaderLocaleResolver:
Spring Boot uses AcceptHeaderLocaleResolver
by default, which automatically parses this header:
@RestController
public class LocaleTestController {
@GetMapping("/api/locale-info")
public Map<String, Object> getLocaleInfo(HttpServletRequest request) {
// Get the resolved locale
Locale resolvedLocale = LocaleContextHolder.getLocale();
// Get the raw Accept-Language header
String acceptLanguageHeader = request.getHeader("Accept-Language");
return Map.of(
"resolvedLocale", resolvedLocale.toString(),
"acceptLanguageHeader", acceptLanguageHeader,
"supportedLocales", List.of("en", "pl", "de", "fr")
);
}
}
Custom Accept-Language Processing:
For more control over Accept-Language processing, you can create a custom resolver:
@Component
public class CustomAcceptHeaderLocaleResolver extends AcceptHeaderLocaleResolver {
private final List<Locale> supportedLocales = List.of(
Locale.of("en"),
Locale.of("pl"),
Locale.of("de"),
Locale.of("fr")
);
@Override
public Locale resolveLocale(HttpServletRequest request) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage == null || acceptLanguage.isEmpty()) {
return getDefaultLocale();
}
// Parse Accept-Language header manually for custom logic
List<Locale.LanguageRange> languageRanges = Locale.LanguageRange.parse(acceptLanguage);
Locale bestMatch = Locale.lookup(languageRanges, supportedLocales);
return bestMatch != null ? bestMatch : getDefaultLocale();
}
@Override
public Locale getDefaultLocale() {
return Locale.of("en");
}
}
Testing Accept-Language Header:
You can test different language preferences using curl or browser developer tools:
# Test with Polish preference
curl -H "Accept-Language: pl,en;q=0.9" http://localhost:8080/api/locale-info
# Test with German preference
curl -H "Accept-Language: de-DE,de;q=0.9,en;q=0.8" http://localhost:8080/api/locale-info
# Test with unsupported language (fallback to default)
curl -H "Accept-Language: ja,ko;q=0.9" http://localhost:8080/api/locale-info
When using LocaleChangeInterceptor
with query parameters (like ?lang=pl
), it takes precedence over the Accept-Language header.
Sanitization of user input
When rendering user input in HTML templates or email, you watchout for XSS (Cross-Site Scripting) attacks. For example, if you render user input directly in the HTML template without sanitization, it can lead to XSS vulnerabilities.
Thymeleaf automatically escapes HTML characters in text attributes, like th:text
:
<p th:text="${userInput}"></p>
but if you use th:utext
to render unescaped HTML, you must ensure that the content is safe or sanitize it before rendering.
For example, if you render user input directly in the HTML template using th:utext
, it can lead to XSS vulnerabilities:
<p th:utext="${userInput}"></p>
utext
stands for "unescaped text" and it allows you to render HTML content without escaping it.
To prevent XSS vulnerabilities, you can use a library like OWASP Java HTML Sanitizer to sanitize the user input before rendering it in the HTML template:
@Component
public class SecureMessageResolver {
private final MessageSource messageSource;
private final HtmlSanitizer htmlSanitizer;
public String getSecureMessage(String key, Locale locale, Object... args) {
String message = messageSource.getMessage(key, args, locale);
// Sanitize HTML content to prevent XSS
return htmlSanitizer.sanitize(message);
}
@Cacheable(value = "translations", key = "#key + '_' + #locale.language")
public String getCachedMessage(String key, Locale locale) {
return getSecureMessage(key, locale);
}
}