Spring Boot internationalization: Step-by-step

A short time ago we looked into the basics of Java i18n. In this article, let’s take a step into the web application realm and see how the Spring Boot framework handles internationalization.

When developing a web application, we tend to code it using a collection of the most efficient, the most popular, and the most sought-after programming languages for both our front end and back end. But what about spoken languages? Most of the time, with or without our knowledge, we depend on the built-in translation engines of our customers’ browsers to handle the required translations. Don’t we?

In the ever-globalizing world we live in, we need our web applications to reach as wide an audience as possible. Here enters the much-required concept of internationalization. In this article, we will be looking at how i18n works on the popular Spring Boot framework.

We will be covering the following topics in this tutorial:

  • I18n on Spring Boot.
  • MessageSource interface and its uses.
  • Locale resolving through LocaleResolver, LocaleChangeInterceptor classes.
  • Storing the user-preferred locale in cookies.
  • Switching between languages.
  • Interpolation using placeholders.
  • Pluralization with the help of ICU4J standards.
  • Date-time localization using @DateTimeFormat annotation.

The source code is available on GitHub.

Prerequisites

  • Spring Framework 5.2.7+
  • Spring Boot 2.3.1+
  • Java SDK 8+
  • Maven 3.3+

The mentioned dependency versions were extracted from system requirements for the latest Spring Boot version at the time of writing and hence are subject to change over time.

I18n on Spring Boot

First off, let us create a simple Spring Boot example project using Maven to get a grasp of how internationalization works on Spring.

Let’s go ahead and make a new Spring Boot application named java-i18n-spring-boot. To achieve this, head over to Spring Initializr and generate a new Spring Boot project with the following set up:

Group:     com.lokalise
Artifact:  java-i18n-spring-boot
Packaging: Jar
Java:      8

Save the generated ZIP file and extract it to a local directory of your choice. Next, simply start your favorite IDE and open the extracted java-i18n-spring-boot project as a Maven project.

Add Language Resources

Firstly, we need to add some language resources to our app. In the project resources, create a new lang directory. Create a simple Java properties file res.properties inside this, and add a few words or sentences that you plan to internationalize on your application:

hello=Hello!
welcome=Welcome to my app
switch-en=Switch to English
switch-it=Switch to Italian

Here 'res' will act as the base name for our set of language resources. Make sure to take note of this term as we will be using it quite frequently as we advance through the tutorial. Also, note that your base name can be whatever you like.

res.properties portrays the default resource file that our Spring Boot application will resort to in the case that no match was found.

Secondly, let’s add another res_it.properties file to the same lang directory to hold localization resource data for an Italian locale. Duplicate the same keys of the default resource file on res_it.properties file. As for the values, add the corresponding Italian translations according to each of those keys as follows:

hello=Ciao!
welcome=Benvenuti nella mia app
switch-en=Passa all'Inglese
switch-it=Passa all'italiano

Meet MessageSource

Spring developers created the MessageSource interface for internationalization purposes within Spring Boot applications. We will be using its ResourceBundleMessageSource implementation for our language resource resolving purposes.

ResourceBundleMessageSource acts as somewhat of an extension to the standard ResourceBundle class in Java. If you take a quick look into its source code, you’ll notice it uses the Java inbuilt Locale class-type parameters within its methods.

Language Resource Naming Rules

I’m sure by now you must have wondered…

What’s with all the ‘res’, ‘res_it’ blah blah blah? Can’t I just name them whatever I want?

A glance at the source code of Spring Boot’s Auto-configuration class for MessageSource itself answers this question, see below:

String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");

As you can see, Spring Boot picks the base name from a Spring property named spring.messages.basename, or in the absence of this, it tries to pick it up from a properties file named messages.properties. Therefore, since none of our language resource files are named messages, we will have to add a Spring property to inform Spring where and what our base name is.

Add a new spring.messages.basename property in the application.properties file indicating the base name for the language resources. Make sure to include the path within the resources directory leading to this default resource file, like so:

spring.messages.basename=lang/res

Since the ResourceBundleMessageSource class relies on the underlying JDK’s ResourceBundle implementation, you will also have to follow the same naming rules as used by Java’s built-in i18n functions when naming language resource files:

  • All resource files must reside in the same package.
  • All resource files must share a common base name.
  • The default resource file should simply have the base name:
res.properties
  • Additional resource files must be named following this pattern:
base name _ language suffix
res_it.properties
  • Let’s assume that at least one resource file with a language suffix already exists. However, for a particular language, you might like to narrow down the target locale to specific countries as well. In this case, you can add more resource files with additional country suffixes. For example:
base name _ language suffix _ country suffix
res_en_US.properties
  • Likewise, following the same logic, you may narrow it down to resource files with an additional variant suffix as well. For instance:
base name _ language suffix _ country suffix _ variant suffix
res_th_TH_TH.properties

Test Out ResourceBundleMessageSource

Let’s see how ResourceBundleMessageSource works. Insert this code snippet into the main method of the java-i18n-spring-boot application and run it:

ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("lang/res");

System.out.println(messageSource.getMessage("hello", null, Locale.ITALIAN));

You’ll see it successfully retrieves the localized value for the 'hello' key and prints it out on the console.

Spring Boot Web Application

We’ve seen how easy it is to do simple translations in a Spring project. Let’s go ahead and see how we can perform i18n on a Spring Boot Web application:

  • Open up the pom.xml file within our java-i18n-spring-boot project.
  • First and foremost, let’s add the spring-web starter dependency to integrate Spring Web module-related functionalities with our java-i18n-spring-boot application. In the project’s pom.xml file, add the following within the <dependencies> tag:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • Then, add the thymeleaf starter dependency along with this within the <dependencies> tag to use Thymeleaf as our template engine, as follows:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Alright! Thanks to the magic of Spring Boot we have already completed building the skeleton of our Spring Boot internationalization example project. Now, it is time to give it some i18n functionalities.

It’s Bean Time!

Initially, let’s add some i18n-related Spring beans to our java-i18n-spring-boot project.

Head into the JavaI18nSpringBootApplication class annotated with @SpringBootApplication. Note that the class with this annotation usually acts as the main class of a Spring application. Since @SpringBootApplication includes the @Configuration annotation within it, we can use this class to place our beans.

Meet LocaleResolver

The LocaleResolver interface deals with locale resolution required when localizing web applications to specific locales. Spring aptly ships with a few LocaleResolver implementations that may come in handy in various scenarios:

  • FixedLocaleResolver

Always resolves the locale to a singular fixed language mentioned in the project properties. Mostly used for debugging purposes.

  • AcceptHeaderLocaleResolver

Resolves the locale using an “accept-language” HTTP header retrieved from an HTTP request.

  • SessionLocaleResolver

Resolves the locale and stores it in the HttpSession of the user. But as you might have wondered, yes, the resolved locale data is persisted only for as long as the session is live.

  • CookieLocaleResolver

Resolves the locale and stores it in a cookie stored on the user’s machine. Now, as long as browser cookies aren’t cleared by the user, once resolved the resolved locale data will last even between sessions. Cookies save the day!

Use CookieLocaleResolver

Let’s see how we can use CookieLocaleResolver in our java-i18n-spring-boot application. Simply add a LocaleResolver bean within the JavaI18nSpringBootApplication class annotated with @SpringBootApplication and set a default locale. For instance:

@Bean // <--- 1
public LocaleResolver localeResolver() {
    CookieLocaleResolver localeResolver = new CookieLocaleResolver(); // <--- 2
    localeResolver.setDefaultLocale(Locale.US); // <--- 3

    return localeResolver;
}
  1. Bean annotation is added to mark this method as a Spring bean.
  2. LocaleResolver interface is implemented using Spring’s built-in CookieLocaleResolver implementation.
  3. The default locale is set for this locale resolver to return in the case that no cookie is found.

Add LocaleChangeInterceptor

Okay, now our application knows how to resolve and store locales. However, when users from different locales visit our app, who’s going to switch the application’s locale accordingly? Or in other words, how do we localize our web application to the specific locales it supports?

For this, we’ll add an interceptor – or interceptor? – bean that will intercept each request that the application receives, and eagerly check for a localeData parameter on the HTTP request. If found, the interceptor uses the localeResolver we coded earlier to register the locale it found as the current user’s locale. Let’s add this bean within the JavaI18nSpringBootApplication class:

@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
    LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
    // Defaults to "locale" if not set
    localeChangeInterceptor.setParamName("localeData");

    return localeChangeInterceptor;
}

Now, to make sure this interceptor properly intercepts all incoming requests, we should add it to the Spring InterceptorRegistry:

1. Set the main class in your project, which is the JavaI18nSpringBootApplication class annotated with @SpringBootApplication, to implement WebMvcConfigurer, like so:

@SpringBootApplication
public class JavaI18nSpringBootApplication implements WebMvcConfigurer {..}

2. Override the addInterceptors method and add our locale change interceptor to the registry. We can do this simply by passing its bean localeChangeInterceptor as a parameter to interceptorRegistry.addInterceptor method. Let’s add this overriding method to our main class JavaI18nSpringBootApplication. For example:

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

Create a Controller

Add a class named HelloController within the same package and annotate it with @Controller. This will mark this class as a Spring Controller which holds Controller endpoints on Spring MVC architecture, as below:

import org.springframework.stereotype.Controller;

@Controller
public class HelloController {
}

Now, let’s add a GET mapping to the root URL. Add this to HelloController:

@GetMapping("/")
public String hello() { // <--- 1
    return "hello"; // <--- 2
}
  1. The method name is insignificant here since the Spring IoC Container resolves the mapping by looking at the annotation type, method parameters, and method return value.
  2. The hello View is called by the Controller.

Implement a View

Next, it’s time to create a simple View on our java-i18n-spring-boot application. Let’s make a hello.html file within the project’s resources/templates directory, like so:

<!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>
    <button type="button" th:text="#{switch-en}" onclick="window.location.href='http://localhost:8080/?localeData=en'"></button>
    <button type="button" th:text="#{switch-it}" onclick="window.location.href='http://localhost:8080/?localeData=it'"></button>  <!-- 3 -->
</body>
</html>
  1. Make sure to declare Thymeleaf namespace in order to support th:* attributes.
  2. The value for the welcome key is retrieved from the applicable language resource file of the specified locale and displayed as the title.
  3. The button has the value of a switch-it property key of the specified locale.

Upon clicking the button, the page is reloaded with an additional localeData=it parameter. This in turn causes our LocaleChangeInterceptor to kick in and resolve the template in the Italian language.

Test Functionality

Let’s see if our Spring Boot application correctly performs internationalization. Run the project, then open up a browser and hit the GET mapping URL we coded on our application’s Controller, which in this case would be the root URL localhost:8080/. Click different ‘language switch’ buttons to see if the page now reloads with its content properly localized in the requested locale.

As a nifty bonus, switch to one locale, close and reopen the browser, and navigate to the root URL again; since we used CookieLocaleResolver as our LocaleResolver implementation, you’ll see that the chosen locale choice has been retained.

Scour Spring Boot I18n

Let’s skim through a few more features that could turn out to be useful when internationalizing our Spring Boot application.

Interpolation

Additionally, to make our java-i18n-spring-boot application a tiny bit more interesting, let’s add a name path variable to our GET mapping. Then, we’ll add it to the MVC model object to eventually be passed on to our View. Open up the HelloController class and insert a new mapping method to it as follows:

@GetMapping("/{name}") // <--- 1
public String hello(@PathVariable String name, Model model) { // <--- 2
    model.addAttribute("name", name); // <--- 3

    return "hello"; // <--- 4
}
  1. Placing name inside brackets lets Spring identify it as a URI template variable.
  2. @PathVariable annotating the name variable here binds it to a URI template variable of the same name.
  3. The name variable is added as a Model attribute.
  4. The hello View is called by the Controller passing the Model named model along with it.

Note that {name} here acts as a placeholder. The user of this application can replace it by calling the root URL suffixed with an additional text.

Pluralization

With the internationalization of our Spring Boot app aiming to support various locales, pluralization can become a somewhat overlooked, yet crucial step.

To demonstrate the point, let’s suppose we need to handle text representing some apples based on a provided quantity. So, for the English language, it would take this form:

  • 0 apples
  • 1 apple
  • 2 apples

In order to handle pluralization, we can take the help of the spring-icu library which introduces ICU4J message formatting features into Spring. Since the project on GitHub is a Gradle project, we’ll have to take its Maven project available on the JitPack repository. Follow the steps mentioned there to add the spring-icu dependency onto our java-i18n-spring-boot application.

Firstly, head over to the JavaI18nSpringBootApplication class of your project, and add a new ICUMessageSource bean. Make sure to set its base name correctly with a classpath: prefix, like so:

@Bean
public ICUMessageSource messageSource() {
    ICUReloadableResourceBundleMessageSource messageSource = new ICUReloadableResourceBundleMessageSource();
    messageSource.setBasename("classpath:lang/res");

    return messageSource;
}

Secondly, add a plural property to the res.properties file indicating how to deal with particular quantities of apples:

plural={0} {0, plural, zero{apples}one{apple}other{apples}}

Note that this follows the FormatElement: { ArgumentIndex , FormatType , FormatStyle } pattern mentioned on MessageFormat with a 'plural' FormatType added by the spring-icu library.

Finally, add these lines to the hello.html after the main body:

.
.
<button type="button" th:text="#{switch-en}" onclick="window.location.href='http://localhost:8080/?localeData=en'"></button>
<button type="button" th:text="#{switch-it}" onclick="window.location.href='http://localhost:8080/?localeData=it'"></button>

<br><span th:text="#{plural(0)}"></span>
<br><span th:text="#{plural(1)}"></span>
<br><span th:text="#{plural(22)}"></span>

Run the java-i18n-spring-boot application and test out how the plurals are calculated when representing zero apples, one apple, and multiple quantities of apples.

Date and Time

We can use the @DateTimeFormat Spring annotation to parse – or in other terms, deserialize – a String date-time input into a LocalDate or LocalDateTime object.

Open up HelloController in our java-i18n-spring-boot application and add a new GET mapping:

@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();
}

Run the application and call the /datetime GET endpoint passing parameters as follows:

  • date param: String in the most common ISO Date Format – yyyy-MM-dd

e.g. 1993-12-16

  • datetime param: String in the most common ISO DateTime Format – yyyy-MM-dd'T'HH:mm:ss.SSSXXX

e.g. 2018-11-22T01:30:00.000-05:00

Lokalise to the Rescue

By now you must be thinking…

Wow, okay I get it. This is an invaluable task that my web app will require to reach my expected audience. But isn’t there an easier way to get all this done?

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 other services
  • Collaborative translations
  • Quality Assurance tools for translations
  • Easy management of your translations through a central dashboard

Plus, loads of others, Lokalise will make your life a whole lot easier by letting you expand your web application to all the locales you’ll ever plan to reach.

Get started with Lokalise in just a few steps:

  • Sign up for a free trial (no credit card information required).
  • Log in to your account.
  • Create a new project under any name you like.
  • Upload your translation files and edit them as required.

That’s it! You have already completed the baby steps to Lokalise-ing your web application. See the Getting Started section for a collection of articles that will give all the help you’ll need to kick-start the Lokalise journey. Also, refer Lokalise API Documentation for a complete list of REST commands you can call on your Lokalise translation project.

Conclusion

In conclusion, in this tutorial we looked into how we can localize to several locales and integrate internationalization into a Spring Boot project. We learned how to perform simple translations using MessageSource implementations, use LocaleResolver, LocaleChangeInterceptor classes to resolve languages using the details of incoming HTTP requests, and how we can switch to a different language at the click of a button in our internationalized Spring Boot web application.

Additionally, we reviewed ways to perform interpolation using placeholders, manage pluralization of values, localize date and time, conduct language switching, and store the chosen language on a Spring Boot web application.

And with that, I’ll be signing off. Don’t hesitate to leave a comment if you have any questions.

Till we meet again, have a safe day, and don’t forget to wash your hands before and after coding!

Related posts

Sign up to our newsletter

Get the latest articles on all things localization and translation management delivered straight to your inbox.

Read also
Localization made easy. Why wait?
The preferred localization tool of 1500+ leading global companies