Internationalization

Internationalization, frequently abbreviated i18n, is the process of designing an application that can be translated into various languages.

This generally involves placing translations in properties bundles with their names suffixed with the target locale, for instance, the messages_en.properties, messages_en_US.properties, and messages_fr.properties files.

The correct property bundle is resolved by trying the most specific locale first and then falling back to the less specific ones.

For U.S English, if you try to get a translation from a bundle named x, the application would first look in the x_en_US.properties file, then the x_en.properties file, and finally, the x.properties file.

The first thing we will do is translate our error messages into French. To do this, we will rename our existing messages.properties file to messages_en.properties.

We will also create a second bundle named messages_fr.properties:

Size.profileForm.twitterHandle=Veuillez entrer votre identifiant Twitter
Email.profileForm.email=Veuillez spécifier une adresse mail valide
NotEmpty.profileForm.email=Veuillez spécifier votre adresse mail
PastLocalDate.profileForm.birthDate=Veuillez donner votre vraie date de naissance
NotNull.profileForm.birthDate=Veuillez spécifier votre date de naissance

typeMismatch.birthDate = Date de naissance invalide.

We saw in Chapter 1, Setting Up a Spring Web Application in No Time that by default, Spring Boot uses a fixed LocaleResolver interface. The LocaleResolver is a simple interface with two methods:

public interface LocaleResolver {

    Locale resolveLocale(HttpServletRequest request);

    void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}

Spring provides a bunch of implementations of this interface, such as FixedLocaleResolver. This local resolver is very simple; we can configure the application locale via a property and cannot change it once it is defined. To configure the locale of our application, let's add the following property to our application.properties file:

spring.mvc.locale=fr

This will add our validation messages in French.

If we take a look at the different LocaleResolver interfaces that are bundled in Spring MVC, we will see the following:

  • FixedLocaleResolver: This fixes the locale defined in configuration. It cannot be changed once fixed.
  • CookieLocaleResolver: This allows the locale to be retrieved and saved in a cookie.
  • AcceptHeaderLocaleResolver: This uses the HTTP header sent by the user's browser to find the locale.
  • SessionLocaleResolver: This finds and stores the locale in an HTTP session.

These implementations cover a number of use cases, but in a more complex application one might implement LocaleResolver directly to allow more complex logic such as fetching the locale from the database and falling back to browser locale, for instance.

Changing the locale

In our application, the locale is linked to the user. We will save their profile in session.

We will allow the user to change the language of the site using a small menu. That's why we will use the SessionLocaleResolver. Let's edit WebConfiguration once more:

package masterSpringMvc.config;

import masterSpringMvc.date.USLocalDateFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
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.time.LocalDate;

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatterForFieldType(LocalDate.class, new USLocalDateFormatter());
    }

    @Bean
    public LocaleResolver localeResolver() {
        return new SessionLocaleResolver();
    }

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

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

We declared a LocaleChangeInterceptor bean as a Spring MVC interceptor. It will intercept any request made to Controller and check for the lang query parameter. For instance, navigating to http://localhost:8080/profile?lang=fr would cause the locale to change.

Tip

Spring MVC Interceptors can be compared to Servlet filters in a web application. Interceptors allow custom preprocessing, skipping the execution of a handler, and custom post-processing. Filters are more powerful, for example, they allow for exchanging the request and response objects that are handed down the chain. Filters are configured in a web.xml file, while interceptors are declared as beans in the application context.

Now, we can change the locale by entering the correct URL ourselves, but it would be better to add a navigation bar allowing the user to change the language. We will modify the default layout (templates/layout/default.html) to add a drop-down menu:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no"/>
    <title>Default title</title>

    <link href="/webjars/materializecss/0.96.0/css/materialize.css" type="text/css" rel="stylesheet" media="screen,projection"/>
</head>
<body>

<ul id="lang-dropdown" class="dropdown-content">
    <li><a href="?lang=en_US">English</a></li>
    <li><a href="?lang=fr">French</a></li>
</ul>
<nav>
    <div class="nav-wrapper indigo">
        <ul class="right">
            <li><a class="dropdown-button" href="#!" data-activates="lang-dropdown"><i class="mdi-action-language right"></i> Lang</a></li>
        </ul>
    </div>
</nav>

<section layout:fragment="content">
    <p>Page content goes here</p>
</section>

<script src="/webjars/jquery/2.1.4/jquery.js"></script>
<script src="/webjars/materializecss/0.96.0/js/materialize.js"></script>
<script type="text/javascript">
    $(".dropdown-button").dropdown();
</script>
</body>
</html>

This will allow the user to choose between the two supported languages.

Changing the locale

Translating the application text

The last thing we need to do in order to have a fully bilingual application is to translate the titles and labels of our application. To do this, we will edit our web pages and use the th:text attribute, for instance, in profilePage.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/default">
<head lang="en">
    <title>Your profile</title>
</head>
<body>
<div class="row" layout:fragment="content">

    <h2 class="indigo-text center" th:text="#{profile.title}">Personal info</h2>

    <form th:action="@{/profile}" th:object="${profileForm}" method="post" class="col m8 s12 offset-m2">

        <div class="row">
            <div class="input-field col s6">
                <input th:field="${profileForm.twitterHandle}" id="twitterHandle" type="text" th:errorclass="invalid"/>
                <label for="twitterHandle" th:text="#{twitter.handle}">Twitter handle</label>

                <div th:errors="*{twitterHandle}" class="red-text">Error</div>
            </div>
            <div class="input-field col s6">
                <input th:field="${profileForm.email}" id="email" type="text" th:errorclass="invalid"/>
                <label for="email" th:text="#{email}">Email</label>

                <div th:errors="*{email}" class="red-text">Error</div>
            </div>
        </div>
        <div class="row">
            <div class="input-field col s6">
                <input th:field="${profileForm.birthDate}" id="birthDate" type="text" th:errorclass="invalid"/>
                <label for="birthDate" th:text="#{birthdate}" th:placeholder="${dateFormat}">Birth Date</label>

                <div th:errors="*{birthDate}" class="red-text">Error</div>
            </div>
        </div>
        <div class="row s12 center">
            <button class="btn indigo waves-effect waves-light" type="submit" name="save" th:text="#{submit}">Submit
                <i class="mdi-content-send right"></i>
            </button>
        </div>
    </form>
</div>
</body>
</html>

The th:text attribute will replace the contents of a HTML element with an expression. Here, we use the #{} syntax, which indicates we want to display a message coming from a property source like messages.properties.

Let's add the corresponding translations to our English bundle:

NotEmpty.profileForm.tastes=Please enter at least one thing
profile.title=Your profile
twitter.handle=Twitter handle
email=Email
birthdate=Birth Date
tastes.legend=What do you like?
remove=Remove
taste.placeholder=Enter a keyword
add.taste=Add taste
submit=Submit

Now to the French ones:

NotEmpty.profileForm.tastes=Veuillez saisir au moins une chose
profile.title=Votre profil
twitter.handle=Pseudo twitter
email=Email
birthdate=Date de naissance
tastes.legend=Quels sont vos goûts ?
remove=Supprimer
taste.placeholder=Entrez un mot-clé
add.taste=Ajouter un centre d'intérêt
submit=Envoyer

Some of the translations are not used yet, but will be used in just a moment. Et voilà! The French market is ready for the Twitter search flood.

A list in a form

We now want the user to enter a list of "tastes", which are, in fact, a list of keywords we will use to search tweets.

A button will be displayed, allowing our user to enter a new keyword and add it to a list. Each item of this list will be an editable input text and will be removable thanks to a remove button:

A list in a form

Handling list data in a form can be a chore with some frameworks. However, with Spring MVC and Thymeleaf it is relatively straightforward, when you understand the principle.

Add the following lines in the profilePage.html file right below the row containing the birth date, and just over the submit button:

<fieldset class="row">
    <legend th:text="#{tastes.legend}">What do you like?</legend>
    <button class="btn teal" type="submit" name="addTaste" th:text="#{add.taste}">Add taste
        <i class="mdi-content-add left"></i>
    </button>

    <div th:errors="*{tastes}" class="red-text">Error</div>

    <div class="row" th:each="row,rowStat : *{tastes}">
        <div class="col s6">
            <input type="text" th:field="*{tastes[__${rowStat.index}__]}" th:placeholder="#{taste.placeholder}"/>
        </div>

        <div class="col s6">
            <button class="btn red" type="submit" name="removeTaste" th:value="${rowStat.index}" th:text="#{remove}">Remove
                <i class="mdi-action-delete right waves-effect"></i>
            </button>
        </div>
    </div>
</fieldset>

The purpose of this snippet is to iterate over the tastes variable of our LoginForm. This can be achieved with the th:each attribute, which looks a lot like a for…in loop in java.

Compared to the search result loop we saw earlier, the iteration is stored in two variables instead of one. The first one will actually contain each row of the data. The rowStat variable will contain additional information on the current state of the iteration.

The strangest thing in the new piece of code is:

th:field="*{tastes[__${rowStat.index}__]}"

This is quite a complicated syntax. You could come up with something simpler on your own, such as:

th:field="*{tastes[rowStat.index]}"

Well, that wouldn't work. The ${rowStat.index} variable, which represents the current index of the iteration loop, needs to be evaluated before the rest of the expression. To achieve this, we need to use preprocessing.

The expression surrounded by double underscores will be preprocessed, which means that it will be processed before the normal processing phase, allowing it to be evaluated twice.

There are two new submit buttons on our form now. They all have a name. The global submit button we had earlier is called save. The two new buttons are called addTaste and removeTaste.

On the controller side, this will allow us to easily discriminate the different actions coming from our form. Let's add two new actions to our ProfileController:

@Controller
public class ProfileController {

    @ModelAttribute("dateFormat")
    public String localeFormat(Locale locale) {
        return USLocalDateFormatter.getPattern(locale);
    }

    @RequestMapping("/profile")
    public String displayProfile(ProfileForm profileForm) {
        return "profile/profilePage";
    }

    @RequestMapping(value = "/profile", params = {"save"}, method = RequestMethod.POST)
    public String saveProfile(@Valid ProfileForm profileForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "profile/profilePage";
        }
        System.out.println("save ok" + profileForm);
        return "redirect:/profile";
    }

    @RequestMapping(value = "/profile", params = {"addTaste"})
    public String addRow(ProfileForm profileForm) {
        profileForm.getTastes().add(null);
        return "profile/profilePage";
    }

    @RequestMapping(value = "/profile", params = {"removeTaste"})
    public String removeRow(ProfileForm profileForm, HttpServletRequest req) {
        Integer rowId = Integer.valueOf(req.getParameter("removeTaste"));
        profileForm.getTastes().remove(rowId.intValue());
        return "profile/profilePage";
    }
}

We added a param parameter to each of our post actions to differentiate them. The one we had previously is now bound to the save parameter.

When we click on a button, its name will automatically be added to the form data sent by the browser. Note that we specified a particular value with the remove button: th:value="${rowStat.index}". This attribute will indicate which value the associated parameter should specifically take. A blank value will be sent if this attribute is not present. This means that when we click on the remove button, a removeTaste parameter will be added to the POST request, containing the index of the row we would like to remove. We can then get it back into the Controller with the following code:

Integer rowId = Integer.valueOf(req.getParameter("removeTaste"));

The only downside with this method is that the whole form data will be sent every time we click on the button, even if it is not strictly required. Our form is small enough, so a tradeoff is acceptable.

That's it! The form is now complete, with the possibility of adding one or more tastes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.12.34