Part 4: Internationalization in Spring Boot

One of the many features provided by Spring Boot’s automatic configuration is a ResourceBundleMessageSource. This is the foundation of the support for internationalization provided by Spring as part of Spring Boot. However, resolving the user’s current locale, and being able to switch locales on demand is a little trickier, and is something that I had a hard time finding a comprehensive example of. In this installment, we’ll cover how to set up a resource bundle for messages, how to name them to support locales, and finally, how to wire a LocaleResolver and LocaleChangeInterceptor into our test application so that we can implement and test Spring’s internationalization support.

As usual, for this installment, I’ve created a copy of the code from Part 3 and created a new project called Part 4. It’s committed to Github, ready for cloning.

The Resource Bundle
First, we’ll create a resource bundle with message files for English and Spanish in our sample application. When provided a locale, the auto-configured message source can dynamically look up a message file using a default base file name – for example, the default messages file will be named “messages.properties”, whereas the file for Spanish messages will be called “messages_es.properties”. Likewise, the file for English language messages will be “messages_en.properties”. Basically, the message source resolves the file name in which to look for message properties by concatenating Spring’s default base file name “messages”, with an underscore and the locale name. The default location for these files, as specified by the auto-configuration, is “/src/main/resources”.

Create two files in /src/main/resources: messages_en.properties, and messages_es.properties.

We’ll add a couple of messages to each file, and we’ll use them to change the labels on our sample application’s based on the client’s locale.

Update “/src/main/resources/messages_en.properties”, adding the following properties and values:

/src/main/resources/messages_en.properties:

field1 = Field 1
field2 = Field 2

and likewise, for our Spanish clients, update “src/main/resources/messages_es.properties” to include the Spanish versions of the same properties and values (note that I do not speak Spanish. I typed these names into Google Translate. I think they are accurate enough for this example)

/src/main/resources/messages_es.properties:

field1 = El Campo 1
field2 = El Campo 2

Updating The View Template
The view template will need to be updated with placeholders containing the keys that Thymeleaf can use to swap in values from our message files. We’ve seen usages of ${} and *{} in these tutorials. The next one we’ll use is #{}, which is the placeholder Thymeleaf uses to bind messages, and can be used to do general string manipulation within the view.

Update our view template to include two new placeholders such that:

/src/main/resources/hello.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8" />
    <title>HELLO</title>
</head>
<body>
<p th:text="${message}"></p>
<form id="gizmo-form" role="form" th:action="@{/save}" method="post" th:object="${gizmo}">
    <div>
        <label for="field1" th:text="#{field1}"></label>
        <input type="text" id="field1" name="field1" th:field="${gizmo.field1}"/>
    </div>
    <div>
        <label for="field2" th:text="#{field2}"></label>
        <input type="text" id="field2" name="field2" th:field="${gizmo.field2}"/>
    </div>
    <div>
        <ul>
            <li th:each="item, stat : *{children}" class="itemRow">
                <div>
                    <label th:for="${'childField1-'+stat.index}">Field 1</label>
                    <input type="text" class="form-control quantity" name="childField1"
                           th:field="*{children[__${stat.index}__].childField1}" th:id="${'childField1-'+stat.index}"/>

                    <label th:for="${'childField2-'+stat.index}">Field 2</label>
                    <input type="text" class="form-control quantity" name="childField2"
                           th:field="*{children[__${stat.index}__].childField2}" th:id="${'childField2-'+stat.index}"/>
                </div>
            </li>
        </ul>
    </div>
    <div>
        <button type="submit">Save</button>
    </div>
</form>
</body>
</html>

You can see we’ve removed the label text (formerly “Field 1” and “Field 2″ for the ‘field1’ and ‘field2’ properties on the Gizmo object and added new th:text references containing our new binding types. In these cases, our goal will be to replace #{field1} with the value of the “field1” property in the appropriate messages file, and to replace the value of #{field2} in a similar fashion. If you were to start the server at this point, the solution would still not work. The last step ties the views to the message files.

Configuring The LocaleResolver
In order for Spring to know which message file’s values to make available to Thymeleaf, the application needs to be able to determine which locale the application is currently running in. For this, we need to configure a LocaleResolver.

In ‘src/main/java/com.rodenbostel.sample.Application.java’ file, configure a new LocaleResolver bean.

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.US);
        return slr;
    }

This will configure a locale resolver that fits the following description, according to the Spring API:

“Implementation of LocaleResolver that uses a locale attribute in the user’s session in case of a custom setting, with a fallback to the specified default locale or the request’s accept-header locale”

The only item remaining is how to switch the locale without updating the configuration of the bean above and restarting our server or changing our default or accept-header specified locale.

Configuring a LocaleChangeInterceptor
Configuring an interceptor that is responsible or swapping out the current locale allows for easy testing by a developer, and also gives you the option of including a select list in your UI that lets the user pick the locale they prefer. Add the following bean to your ‘src/main/java/com.rodenbostel.sample.Application.java’ file:

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }

As you can see, this interceptor will look for a request parameter named ‘lang’ and will use its value to determine which locale to switch to. For example, adding ‘lang=en’ to the end of any request will render the messages from default English locale’s message file. Changing that parameter to ‘lang=es’ will render the Spanish version. For any interceptor to take effect, we need to add it to the application’s interceptor registry. In order to do that, we need to get a handle and override Spring Boot Web’s addInterceptor configuration method.

In order to do that, we need to update our ‘src/main/java/com.rodenbostel.sample.WebApplication.java’ file to extend WebMvcConfigurerAdapter. The WebMvcConfigurerAdapter’s existence is based on around activities such as this. It provides hooks to override base Spring configuration. Let’s update the class to extend this super class and add our interceptor:

package com.rodenbostel.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application extends WebMvcConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.US);
        return slr;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }

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

}

Testing
Start your server and observe the default locale in action:
Screen Shot 2014-05-13 at 5.20.00 PM
then, add our request parameter and refresh the page to change the locale so that our Spanish language file is loaded:
Screen Shot 2014-05-13 at 5.20.52 PM
As Spring Boot matures, I’m sure the available examples will be updated to include end to end samples such as this. In the mean time, I hope you found this simple example useful. Check back soon for Spring Security integration in Part 5.

Advertisements

28 comments

  1. Hey man, great post!!
    Do you know how can I use locales as part of the URL? E.g.: example.com/en/page and not example.com/page?lang=en

    1. Hey Jin,

      The section of the post titled ‘Testing’ contains that information. The LocaleChangeInterceptor and the SessionLocaleResolver work together to both detect the url param that controls the locale, and also to detect when it changes to load the correct locale.

      1. Hi Justin, maybe I’ve not explained well my question. I mean, how can I use subdirectories (/en or /es ….) in URLs in place of parameters (?lang=en or ?lang=es).

      2. Hey Jin,

        Sorry about my misunderstanding. To accomplish what you’re describing, you would write your own LocaleChangeInterceptor implementation, which would be an extension of the HandlerInterceptorAdapter class, and override the preHandle method. If you look at the source for Spring’s LocaleChangeInterceptor, you can see that you can get a handle on the HttpRequest in the preHandle method, and instead of looking for a specific param like LocaleChangeInterceptor does, you can look for a specific path, as you have suggested.

  2. Hi, great post!
    I feel relieved when I saw your post here.
    I have some things in my mind, I am using neo4j graphDatabase as my database and it affect my application.java.
    My application.java now is extending Neo4jConfiguration.
    My question is:
    1. How I can extend the WebMvcConfigurerAdapter when I have extend my appliction.java to Neo4jConfiguration?
    2. The replacement of the words is really work, but it won’t change even if I put the parameter “http://localhost:8080/?lang=es”. Is the fault is on my application.java didn’t extend the WebMvcConfigurerAdapter so it won’t resolving the parameter (question no 1)?

  3. Spectacular post, Justin. Thank you so much for sharing. What I planned a day for, I’ve accomplished in an hour, so thank you for the other 7 hours of my life.

  4. Justin,

    Thanks for the post .
    I do have a question, more so with my implementation .
    I receive the language information on a trigger, say change in the DropDown value , from the DB.
    Now on the change of the value, the controller is called which invokes the service layer to get the language . Now how can I make it part of my URL for the the interceptor to pick it up and refresh the page with the new language ?

    1. Couldn’t you use javascript to listen to a change of that drop down on the front-end, and then trigger a refresh of the page with the language param included in the URL query string? It’s a bit heavy-handed, but it would work (assuming I understand your implementation)

  5. Hi Justin!
    Thank you for yours posts, I’ve learned a lot of themnot just this one.

    Is there a way to implement a fallback on a per-message-basis? So I could maintain a file for e.g. locale “de” and another one with locale “de_ch”, with just the strings, that differ in German and Swiss German. I didn’t find any hints, right now I have to maintain two, three, … files.

    Karin

  6. Hi Justin,

    Thank you for your post and uploading your example application. I have your application working on my machine. I tried to do exactly what you did in my application and the i18n is not working. I found one key difference I am using spring boot 1.3.3 where as you have 1.0.1. When I upgrade your project to 1.3.3 the i18n also stops working. I was wondering if you had any insight as to why?

    Thanks!

    Tom

    1. I figured out the issue … you need to include messages.properties in the src/main/resources otherwise the MessageSourceAutoConfiguration won’t load the resource bundles. (The example only had messages_en.properties and messages_es.properties)

  7. Thanks for a great post. I learned a lot!

    If I may have a comment, here goes.

    I would not invoke the localeChangeInterceptor() method in the addInterceptors method, as it creates a new LocaleChangeInterceptor object, which might not be seen as the same bean that Spring would provide elsewhere (different Object references, cause Spring at some point would also invoke localeChangeInterceptor() method).
    Unless, Spring is “clever” enough to scan JVM for already existing beans of such class. Not sure about that, though. Please, correct me if I’m wrong.

    I would do the following:
    – add @Autowired private WebApplicationContext context
    – change registry.addInterceptor(localeChangeInterceptor()); to registry.addInterceptor(context.getBean(“localeChangeInterceptor”, LocaleChangeInterceptor.class));

  8. If I missed the default message.properties file, the text on UI will be shown like: ??profile.title_en_US??. What’s wrong?

  9. Hi Justin,

    Great post. How can I change the .setDefaultLocale(Locale.US); to spanish (or any other country) in the first place.

    There is no Spanish in the list!

    Thanks

  10. Thanks for this. How do I implement this inside a Java class because I need to add this functionality on server side validation?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s