Not too long ago, we explored the basics of Java i18n. Now, let’s take a step into the world of web applications and see Spring boot internationalization in action.
When developing a web application, we typically use a mix of efficient and popular programming languages for both the front end and back end. But what about spoken languages? Often, knowingly or unknowingly, we rely on the built-in translation engines in our users’ browsers to manage the necessary translations, don’t we?
In today’s globalized world, it’s essential for our web applications to reach the broadest audience possible. This is where internationalization comes into play. In this article, we’ll take a closer look at how i18n works in Spring Boot.
The source code for this article is available on GitHub
Prerequisites
- Spring Boot 3.3+ (though most of the concepts here should apply to version 2 as well).
- Java SDK 17+
- Gradle 8+
I18n on Spring Boot
First, let’s create a simple Spring Boot project using Maven to get a feel for how internationalization works in Spring.
Start by creating a new Spring Boot application named
. Head over to Spring Initializr and generate a new project with the following settings:i18nboot
- Project: Gradle – Groovy
- Language: Java
- Spring Boot: 3.3.3
- Group:
com.lokalise
- Artifact:
i18nboot
- Packaging: Jar
- Java Version: 22 (feel free to use version 21 or 17)
Download the generated ZIP file and extract it to a directory of your choice. Then, open your favorite IDE and load the i18nboot
project.
Adding dependencies
Open build.gradle
file in the project root and update the dependencies
section like this:
dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' }
Run:
gradle build
This should properly install the dependencies. Nice!
Adding translation files
Get a free trial of Lokalise
Get a free trialNext, we’ll add language resources to our app. In the src/main/resources
directory, create a new folder called lang
. Inside this folder, create a simple Java properties file named messages.properties
and add the following key-value pairs for your default language (in this case, English):
hello=Hello! welcome=Welcome to my app switch-en=Switch to English switch-it=Switch to Italian
The messages
file will act as the base name for our translations, but feel free to use any other name if you prefer.
By default, messages.properties
will serve as the fallback translation file for the Spring Boot application, meaning it will be used if no specific translation match is found.
Next, add a second file named messages_it.properties
in the same lang
directory. This file will hold the translations for the Italian locale. Copy the same keys from messages.properties
and provide their corresponding Italian translations:
hello=Ciao! welcome=Benvenuti nella mia app switch-en=Passa all'Inglese switch-it=Passa all'italiano
Finally, open the src/main/resources/application.properties
file and configure the base name for your translation files by adding the following line of code:
spring.messages.basename=lang/messages
Translation files naming rules
When naming translation files, you’ll need to follow the same rules used by Java’s built-in i18n functions for naming language resource files:
- All resource files must be in the same package.
- All resource files must share a common base name.
- The default resource file should only use the base name (e.g.,
messages.properties
orres.properties
). - Additional resource files must be named following this pattern:
BASENAME_LOCALE
(e.g.,messages_it.properties
for Italian). - Regional suffixes are supported as well, using this pattern:
BASENAME_LOCALE_REGIONAL
(e.g.,messages_en_US.properties
for US English).
Testing it out
Now, let’s see how we can manage translations using ResourceBundleMessageSource
. Rather than configuring it directly in the main
method, a better practice is to define it as a Spring-managed bean. This allows Spring Boot to handle the lifecycle and makes it easier to use throughout your application.
Defining MessageSource as a bean
In Spring Boot, we typically configure MessageSource
as a bean. Open the src/main/java/com/lokalise/i18nboot/I18nBootApplication.java
file and update it as follows:
package com.lokalise.i18nboot; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.util.Locale; @SpringBootApplication public class I18nbootApplication { public static void main(String[] args) { SpringApplication.run(I18nbootApplication.class, args); } // Defining MessageSource as a bean for Spring to manage @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasenames("lang/messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } }
Fetching messages
To test whether the translations are working, let’s create a new file src/main/java/com/lokalise/i18nboot/MessageSourceTest.java
:
package com.lokalise.i18nboot; import org.springframework.boot.CommandLineRunner; import org.springframework.context.MessageSource; import org.springframework.stereotype.Component; import java.util.Locale; @Component public class MessageSourceTest implements CommandLineRunner { private final MessageSource messageSource; public MessageSourceTest(MessageSource messageSource) { this.messageSource = messageSource; } @Override public void run(String... args) throws Exception { // Fetch and print the message for "hello" in Italian locale String message = messageSource.getMessage("hello", null, Locale.ITALIAN); System.out.println(message); // Should print "Ciao!" } }
Now, you can start the application by running the following command:
gradle bootRun
When the application starts, you should see the following output in your terminal: “Ciao!”. This confirms that the Italian translation is working as expected! You might also see some warning messages in the terminal, but don’t worry—we’ll address those later.
Spring Boot web application
Thanks to the magic of Spring Boot, we’ve already built the foundation of our internationalization (i18n) example project. Now it’s time to add i18n functionality to our web application.
Meet LocaleResolver
The LocaleResolver
interface handles locale resolution when localizing web applications. Spring provides several LocaleResolver
implementations that are useful in different scenarios:
- FixedLocaleResolver: Always resolves to a fixed locale specified in the project properties. This is mostly used for debugging purposes.
- AcceptHeaderLocaleResolver: Resolves the locale using the
Accept-Language
HTTP header from incoming requests. - SessionLocaleResolver: Stores the resolved locale in the user’s
HttpSession
. The locale persists as long as the session remains active. - CookieLocaleResolver: Stores the resolved locale in a cookie on the user’s machine. This locale persists between sessions, as long as the cookies aren’t cleared.
Using CookieLocaleResolver
To make our application persist the locale between sessions, we’ll use the CookieLocaleResolver
. Create a new file MyBeansConfig.java
inside the src/main/java/com/lokalise/i18nboot/
folder:
package com.lokalise.i18nboot; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.i18n.CookieLocaleResolver; import java.util.Locale; import java.util.TimeZone; @Configuration public class MyBeansConfig { @Bean public LocaleResolver localeResolver() { CookieLocaleResolver localeResolver = new CookieLocaleResolver(); localeResolver.setDefaultLocale(Locale.ENGLISH); localeResolver.setDefaultTimeZone(TimeZone.getTimeZone("UTC")); return localeResolver; } }
In this configuration, we implement the LocaleResolver using Spring’s built-in CookieLocaleResolver. We also set the default locale to English and the default time zone to UTC.
Add a LocaleChangeInterceptor
Now that the application knows how to resolve and store locales, we need a way to switch the locale when different users visit. We’ll add an interceptor that looks for a locale
parameter in the HTTP request. If found, the interceptor will update the current user’s locale using the localeResolver
we defined earlier.
Add this code to the same MyBeansConfig.java
file:
// other imports ... import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; // other code ... @Bean public LocaleChangeInterceptor localeChangeInterceptor() { LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); localeChangeInterceptor.setParamName("localeData"); return localeChangeInterceptor; }
This LocaleChangeInterceptor
will intercept every request and check for the locale
parameter. If the parameter is present, it will update the current locale accordingly.
Register the Interceptor
To make sure the interceptor catches all incoming requests, we need to add it to Spring’s InterceptorRegistry
. Open the I18nbootApplication.java
file and modify it as follows:
// other imports ... import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; @SpringBootApplication public class I18nbootApplication implements WebMvcConfigurer { private final LocaleChangeInterceptor localeChangeInterceptor; public I18nbootApplication(LocaleChangeInterceptor localeChangeInterceptor) { this.localeChangeInterceptor = localeChangeInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(localeChangeInterceptor); } public static void main(String[] args) { SpringApplication.run(I18nbootApplication.class, args); } @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasenames("lang/messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } }
Here, we implement WebMvcConfigurer
and add the LocaleChangeInterceptor
to the interceptor registry. This ensures that all incoming requests are intercepted and checked for the locale
parameter.
Create a controller
Let’s start by creating a controller to handle our root URL. Create a new file named HelloController.java
in the src/main/java/com/lokalise/i18nboot
directory:
package com.lokalise.i18nboot; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HelloController { @GetMapping("/") public String hello() { return "hello"; // This returns the hello.html view } }
This controller maps the root URL (/
) to the hello
method, which returns the hello.html
view located in the templates/
directory.
Implement a view
Now, we need to create a simple view for our application. Create a templates
directory under src/main/resources
, and then create a new file named hello.html
inside that directory:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <!-- 1 --> <head> <meta charset="UTF-8"> <title th:text="#{welcome}"></title> <!-- 2 --> </head> <body> <span th:text="#{hello}"></span><br> <span th:text="#{welcome}"></span><br> <!-- Locale switch buttons --> <button type="button" th:text="#{switch-en}" onclick="window.location.href='http://localhost:8080/?locale=en'"></button> <!-- 3 --> <button type="button" th:text="#{switch-it}" onclick="window.location.href='http://localhost:8080/?locale=it'"></button> </body> </html>
Main points to note:
- We declare the Thymeleaf namespace
xmlns:th
to supportth:*
attributes in the HTML. - The
th:text
attribute is used to fetch translations from the message properties files (e.g.,#{welcome}
pulls the value of thewelcome
key from the translation files). - The locale switch buttons reload the page with the selected language by adding the
locale
parameter (locale=en
orlocale=it
) to the URL. This triggers ourLocaleChangeInterceptor
, which changes the language accordingly.
Test functionality
Now, let’s test if our Spring Boot application handles internationalization as expected. Run the project and navigate to http://localhost:8080
. Click on the language switch buttons to see if the text is correctly translated between English and Italian.
As an added bonus, after switching to a different locale, try closing and reopening the browser. When you visit the root URL again, you’ll notice that your previous locale choice has been retained. This is thanks to the CookieLocaleResolver
, which stores the locale in the browser’s cookies, allowing it to persist across sessions.
Scour Spring Boot i18n
Let’s explore a few more useful features to further enhance the internationalization (i18n) of our Spring Boot application.
Pluralization
Handling pluralization is an often overlooked but important aspect of internationalization. For instance, when displaying text related to quantities, such as apples, the text must adapt to the provided number.
For example, in English:
0 apples
1 apple
2 apples
To handle pluralization in Spring Boot, we can use the spring-icu library, which integrates ICU4J message formatting into Spring.
Add the spring-icu library
For Gradle, add the following repository and dependency to your build.gradle
file:
repositories { mavenCentral() maven { url 'https://jitpack.io' } } dependencies { implementation 'com.github.transferwise:spring-icu:0.3.0' }
This will include the spring-icu library, enabling advanced message formatting features like pluralization.
Configure ICUMessageSource
Now, open the MyBeansConfig.java
file and modify it to use the ICUMessageSource for message resolution. Add the following imports:
import com.transferwise.icu.ICUMessageSource; import com.transferwise.icu.ICUReloadableResourceBundleMessageSource;
Next, configure the ICUMessageSource
bean:
@Bean public ICUMessageSource messageSource() { ICUReloadableResourceBundleMessageSource messageSource = new ICUReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:lang/messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; }
This bean ensures that Spring uses the ICU message format features when resolving messages, including plural forms. Having done that, don’t forget to remove the old messageSource()
from the I18nbootApplication.java
.
Handle pluralization in message properties
Let’s now add pluralization support to our messages.properties
file. In the src/main/resources/lang/messages.properties
, define the following:
plural={0} {0, plural, zero{apples}one{apple}other{apples}}
This format allows us to display the correct form of “apple” based on the quantity. The pattern {0}
is replaced with the actual number, and plural
handles different cases such as zero, one, and other numbers.
For the Italian translation, update messages_it.properties
:
plural={0} {0, plural, zero{mele}one{mela}other{mele}}
Update the view
Finally, update the hello.html
template to demonstrate pluralization. Add the following lines to display the text based on different quantities:
<br><span th:text="#{plural(0)}"></span> <br><span th:text="#{plural(1)}"></span> <br><span th:text="#{plural(22)}"></span>
This will display the correct form of “apple” in both English and Italian based on the provided numbers: 0
, 1
, and 22
.
Date and time
Now, let’s dive into date-time localization in Spring Boot. We can use the @DateTimeFormat
annotation provided by Spring to parse (or deserialize) string date-time inputs into LocalDate
or LocalDateTime
objects.
Add a date-time endpoint
First, open the HelloController.java
file and add a new GET
mapping to handle date and time inputs:
// other imports... import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; import java.time.LocalDateTime; // ... @GetMapping("/datetime") @ResponseBody public String dateTime(@RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam("datetime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime datetime) { return date.toString() + "<br>" + datetime.toString(); }
Explanation:
@RequestParam("date")
: Captures thedate
parameter from the request URL.@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
: Parses thedate
parameter in the standard ISO format (yyyy-MM-dd
) into aLocalDate
object.@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
: Parses thedatetime
parameter in the ISO DateTime format (yyyy-MM-dd'T'HH:mm:ss.SSSXXX
) into aLocalDateTime
object.
Test the date-time endpoint
To test this, run the application and call the /datetime
endpoint with the following parameters:
http://localhost:8080/datetime?date=1993-04-25&datetime=2018-11-22T01:30:00.000-05:00
date=1993-04-25
: This is the date in the ISO Date format (yyyy-MM-dd
).datetime=2018-11-22T01:30:00.000-05:00
: This is the date-time in ISO DateTime format (yyyy-MM-dd'T'HH:mm:ss.SSSXXX
), including a time zone offset (-05:00
).
Lokalise to the rescue
By now, you might be thinking…
“Wow, I get it. Internationalization is crucial for reaching my audience, but isn’t there an easier way to handle all of this?”
Meet Lokalise—the translation management system that takes care of all your Spring Boot app’s internationalization needs. With features like:
- Easy integration with various services
- Collaborative translation tools
- Built-in quality assurance for translations
- Centralized management of translations via a user-friendly dashboard
And many more! Lokalise simplifies the process of expanding your web application to support all the locales you aim to reach.
Get started with Lokalise in a few steps
- Sign up for a free trial (no credit card needed).
- Log in to your Lokalise account.
- Create a new project with any name you prefer.
- Upload your translation files and edit them as needed.
That’s it! You’ve just completed the first steps toward Lokalise-ing your web application. For more in-depth guidance, visit the Getting Started section on Lokalise, which offers helpful articles to kick-start your journey. Be sure to check out the Lokalise API Documentation for a complete list of REST commands you can use with your translation project.
Conclusion
Huge thanks to my colleague Anton Malich for technical review.
In this tutorial, we explored how to localize a Spring Boot project to support multiple locales. We covered simple translations using MessageSource
implementations, handling language resolution with LocaleResolver
and LocaleChangeInterceptor
, and allowing users to switch between languages with the click of a button.
We also delved into advanced topics like pluralization, date and time localization, and persisting the chosen language using CookieLocaleResolver
to ensure a seamless internationalized experience for your users.