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 i18n.
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 internationalization 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 ourjava-i18n-spring-boot
project. -
First and foremost, let’s add the
spring-web
starter dependency to integrate Spring Web module-related functionalities with ourjava-i18n-spring-boot
application. In the project’spom.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; }
- Bean annotation is added to mark this method as a Spring bean.
-
LocaleResolver
interface is implemented using Spring’s built-inCookieLocaleResolver
implementation. - 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 }
- 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.
-
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>
-
Make sure to declare Thymeleaf namespace in order to support
th:*
attributes. -
The value for the
welcome
key is retrieved from the applicable language resource file of the specified locale and displayed as the title. -
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 }
-
Placing
name
inside brackets lets Spring identify it as a URI template variable. -
@PathVariable
annotating the
name
variable here binds it to a URI template variable of the same name. -
The
name
variable is added as a Model attribute. -
The
hello
View is called by the Controller passing the Model namedmodel
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!