Software localization
A Step-by-Step Guide to Spring Boot Internationalization
We will start off our guide to Spring Boot internationalization by setting dependencies and project configuration. We will then ensure the necessary setup for successful localization. Finally, we will also add the capability to switch between languages on the page. Our project will use Thymleaf as the template engine, as well as Maven to build the project. To best understand this article, readers should have basic knowledge of the Spring Framework, Thymleaf, and Maven.
Resource » The source code used in this guide is available on GitHub.
Project setup
Let's have a brief look at how best to set up your project.
Dependencyz
Use any IDE to make a Maven-based project. To implement our project in Spring Boot and the Thymeleaf templating engine, we will make use of Maven for managing dependencies.
The pom.xml file is as below:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.phraseapp.i18n</groupId> <artifactId>spring-boot-i18n</artifactId> <packaging>jar</packaging> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-i18n</name> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath /> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project>
As we are using Spring Boot, we specify the project parent to spring-boot-starter-parent. This helps to provide dependency and plugin management for applications based on Spring Boot. We can check for the latest version at Maven Central.
Additionally, we specify spring-boot-starter-web and spring-boot-starter-thymeleaf as project dependencies. The spring-boot-starter-parent also contains default versions of dependencies that Spring Boot uses. Hence, we do not need to specify the version for those two dependencies in our pom file.
MVC configuration
Next, we will add all the configuration to enable Spring Web MVC on our project:
@EnableWebMvc @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private WebApplicationContext context; @Bean public ServletContextTemplateResolver templateResolver() { final ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(context.getServletContext()); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".html"); resolver.setTemplateMode(TemplateMode.HTML); return resolver; } @Bean public SpringTemplateEngine templateEngine() { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); return templateEngine; } @Bean public ThymeleafViewResolver viewResolver() { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine()); return viewResolver; } //... }
We add the @EnableWebMVC annotation here to import the Spring MVC configuration. Moreover, we also implement the WebMvcConfigurer to be able to customize Spring MVC's default configuration.
The @Configuration indicates that we will have some @Bean declarations in this class. So, we declared Template Resolver, a Template Engine, and a View Resolver here.
HTML pages and controller
Next, we create a simple welcome.html page as below:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>I18N Spring Boot</title> </head> <body> <h2>Hello, your Spring Boot application is ready</h2> </body> </html>
Based on the configuration of the templateResolver bean in WebConfig class, we will put our HTML page under WEB-INF/views.
Together with the HTML page, we declare a simple Controller method as below:
@Controller public class HomeController { @GetMapping("/") public String welcome() { return "welcome"; } //... }
Start the application
Now, we can create the main class to start the Spring Boot Application:
package com.phrase.i18n.springboot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class I18NSpringBootApplication { public static void main(String[] args) { SpringApplication.run(I18NSpringBootApplication.class, args); } }
Run this main class, and access the URL http://localhost:8080/, we will be able to see below result:
Internationalization and localization setup
We can now take care of our Internationalization and localization setup.
Message properties files
Now it is time to dive ourselves into language-specific messages.
With Spring Boot, we can use the messages.properties file to store localized messages for the default locale.
To add more locale-specific messages, we can add more files such as messages_XX.properties, where XX is the locale code, like the US is for US English, SQ is for Albanian, etc. You can have a look at all the language codes here.
Let us define some points to be noted before we move on to defining the messages:
- We identify a message with a key. The same message will use the same key in all locale-specific files
- When retrieving a message, Spring Boot will look at the current set locale and will use that locale’s message file to pick up the String against that key
- If a key is not present in the current locale message file, the framework will switch to the default file only for that key
With the above points in mind, let us move to define the message files now. Define the following files in the src/main/resources/lang folder which is in the classpath for this application.
Here is the default messages.properties file:
label.welcome = Welcome label.content = This is sample project for Internalization and Localization in SpringBoot label.changeLang = Supported languages label.lang.en = English label.lang.fr = French label.lang.cn = Chinese label.lang.de = German
Message resources bean
Next, we define a MessageResource bean to tell Spring where to look for our localized messages:
@Bean("messageSource") public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasenames("lang/messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; }
As we store our localized messages in the properties file, we use the ResourceBundleMessageSource implementation here. Thus, the configuration specifies that we put our localized messages under classpath:lang folder.
We can consider using another implementation which is ReloadableResourceBundleMessageSource if our localized messages will be changed at runtime.
Locale resolver
To make our project capable of determining the locale which is currently being used, we will be using a LocaleResolver Spring bean:
@Bean public LocaleResolver localeResolver() { return new CookieLocaleResolver(); }
The LocaleResolver comes from very smart interface definitions which makes use of not one but four different techniques to determine current locale, these are:
- SessionLocaleResolver: uses a locale attribute in the user's session
- CookieLocaleResolver: store current locale in the browser's cookie
- AcceptHeaderLocaleResolver: uses the primary locale specified in the "accept-language" header of the HTTP request
- FixedLocaleResolver: always return a fixed default locale
Note that these techniques are used in the same order we mentioned here, as more than one value can be present at any given time.
In the above bean definition, we used the CookieLocaleResolver.
Locale change interceptor
Our application supports multiple locales. For this reason, we will add a LocaleChangeInterceptor to be able to specify a locale for an HTTP Request.
@Override public void addInterceptors(InterceptorRegistry registry) { LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); localeChangeInterceptor.setParamName("lang"); registry.addInterceptor(localeChangeInterceptor); }
As stated, we add a parameter named lang in all requests to provide information about the locale for these requests.
Implementing a localization page
Now, let's define the below index.html page using Thymleaf syntax to refer to our localized messages:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>I18N Spring Boot</title> </head> <body> <h2 data-th-text="#{label.welcome}"></h2> <p data-th-text="#{label.content}"></p> <p data-th-text="#{label.changeLang}"></p> <ul> <li> <a href="/index?lang=en" data-th-text="#{label.lang.en}"></a> </li> <li> <a href="/index?lang=de" data-th-text="#{label.lang.de}"></a> </li> <li> <a href="/index?lang=fr" data-th-text="#{label.lang.fr}"></a> </li> </ul> </body> </html>
Of course, we need a controller method for this page:
@GetMapping("/index") public String index() { return "index"; }
Now, start our application again. Then access http://localhost:8080/index, we will see below page:
Click on French link, the page will be reloaded as below:
Exceptions with locale messages
When using Spring Boot, we can also use Global Exception Handler, which provides Exception messages in the user's locale itself. This is failry easy to do and repetitive. We won’t be covering in this section, but we will cover a more interesting aspect.
In the file messages.properties and other locale-specific files, we can define messages for Spring Boot Validation API as well. Once we apply validation to our models, Spring Boot will pick these messages and construct the locale message itself, based on available locales. Let’s see how to do this:
NotNull.user.name=Please provide a name. NotNull.user.pwd=Please, provide a password to sign up. Size.user.title=Title must contain between {2} and {1} chars. Size.user.pwd=Password must contain {2}-{1} characters. Exception.notFound=No record for {0} was found with ID: {1}. Exception.unexpected=Request processing failed.
Now, whenever validation fails for the conditions mentioned above, Spring Boot will return the mentioned messages for the mentioned entity (user) and property (name, pwd, etc.). In length validation cases, strings like {2}-{1} says that these will be replaced with the minimum and maximum limit mentioned during validation annotations.
Two messages, namely Exception.notFound and Exception.unexpected are global messages.
Using the Phrase Spring starter
To simplify the integration of the Phrase in-context editor into your Spring Boot application, there is an existing Phrase Spring Boot starter that you can use to get started quickly. Learn here how you can start using this project.
Summing it up
In this guide, we integrated the internationalization process in our demo Spring-Boot-based application, processed with Thymeleaf to present a layout to the user. We saw how easily we can process a user request and extract locale information from various techniques.
Spring Boot can manage any number of locales in an application flawlessly. If you need a challenge, try localization in an application with a Dynamic API like Google Translate which converts the messages at runtime. It can be slow but it is worth a try.
Internationalization is a great way to increase the number of users of a product so there are no limits in terms of how users use your product. However, writing code to localize your app is one task. Working with translations, on the other hand, comes with a whole new set of challenges. Fortunately, a localization solution, such as Phrase, can make your life as a developer easier.
A professional localization solution, the Phrase Localization Platform features a flexible API and CLI, and a beautiful web platform for your translators. With GitHub, GitLab, and Bitbucket sync, Phrase does the heavy lifting in your localization pipeline, so you can stay focused on the code you love.
Check out all Phrase features for developers and see for yourself how it can help you take your apps global.