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
Next, 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 appswitch-en=Switch to Englishswitch-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 appswitch-en=Passa all'Ingleseswitch-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:
The default resource file should only use the base name (e.g., messages.properties or res.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;@SpringBootApplicationpublic 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;@Componentpublic 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;@Configurationpublic 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 ...@Beanpublic 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;@SpringBootApplicationpublic 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;@Controllerpublic 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:
We declare the Thymeleaf namespace xmlns:th to support th:* 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 the welcome key from the translation files).
The locale switch buttons reload the page with the selected language by adding the locale parameter (locale=en or locale=it) to the URL. This triggers our LocaleChangeInterceptor, 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:
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:
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:
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:
@RequestParam("date"): Captures the date parameter from the request URL.
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE): Parses the date parameter in the standard ISO format (yyyy-MM-dd) into a LocalDate object.
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME): Parses the datetime parameter in the ISO DateTime format (yyyy-MM-dd'T'HH:mm:ss.SSSXXX) into a LocalDateTime object.
Test the date-time endpoint
To test this, run the application and call the /datetime endpoint with the following parameters:
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.
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.
Ilya is a lead of content/documentation/onboarding at Lokalise, an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, goes in for sports and plays music.
Ilya is a lead of content/documentation/onboarding at Lokalise, an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, goes in for sports and plays music.
Libraries and frameworks to translate JavaScript apps
In our previous discussions, we explored localization strategies for backend frameworks like Rails and Phoenix. Today, we shift our focus to the front-end and talk about JavaScript translation and localization. The landscape here is packed with options, which makes many developers a
Build a smooth translation pipeline with Lokalise and Vercel
Internationalization can sometimes feel like a massive headache. Juggling multiple JSON files, keeping translations in sync, and redeploying every time you tweak a string… What if you could offload most of that grunt work to a modern toolchain and let your CI/CD do the heavy lifting? In this guide, we’ll wire up a Next.js 15 project hosted on Vercel. It will load translation files on demand f
Hands‑on guide to GitHub Actions for Lokalise translation sync: A deep dive
In this tutorial, we’ll set up GitHub Actions to manage translation files using Lokalise: no manual uploads or downloads, no reinventing a bicycle. Instead of relying on the Lokalise GitHub app, we’ll use open-source GitHub Actions. These let you push and pull translation files directly via the API in an automated way. You’ll learn how to: Push translation files from your repo to LokalisePull translated content back and open pull requests automaticallyWork w