Spring Boot internationalization i18n: Step-by-step with examples

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 i18nboot. Head over to Spring Initializr and generate a new project with the following settings:

    • 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 trial

    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 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 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;
    
    @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:

    1. We declare the Thymeleaf namespace xmlns:th to support th:* attributes in the HTML.
    2. 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).
    3. 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:

    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 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:

    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

    1. Sign up for a free trial (no credit card needed).
    2. Log in to your Lokalise account.
    3. Create a new project with any name you prefer.
    4. 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.

    Further reading

    Related articles
    Stop wasting time with manual localization tasks. 

    Launch global products days from now.