Spring MVC Internationalization (i18n) and Localization (L10n) Example

Filed Under: Spring

Welcome to the Spring Internationalization (i18n) tutorial. Any web application with users all around the world, internationalization (i18n) or localization (L10n) is very important for better user interaction.

Most of the web application frameworks provide easy ways to localize the application based on user locale settings. Spring also follows the pattern and provides extensive support for internationalization (i18n) through the use of Spring interceptors, Locale Resolvers and Resource Bundles for different locales.

Some earlier articles about i18n in java.

Spring Internationalization i18n

Let’s create a simple Spring MVC project where we will use request parameter to get the user locale and based on that set the response page label values from locale specific resource bundles.

Create a Spring MVC Project in the Spring Tool Suite to have the base code for our application. If you are not familiar with Spring Tool Suite or Spring MVC Projects, please read Spring MVC Example.

Our final project with localization changes looks like below image. We will look into all the parts of the application one by one.

Spring i18n Example, spring i18n, spring localization, spring Internationalization, spring resource bundle

Spring i18n Maven Configuration

Our Spring MVC pom.xml looks like below.


<?xml version="1.0" encoding="UTF-8"?>
<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.journaldev</groupId>
	<artifactId>spring</artifactId>
	<name>Springi18nExample</name>
	<packaging>war</packaging>
	<version>1.0.0-BUILD-SNAPSHOT</version>
	<properties>
		<java-version>1.6</java-version>
		<org.springframework-version>4.0.2.RELEASE</org.springframework-version>
		<org.aspectj-version>1.7.4</org.aspectj-version>
		<org.slf4j-version>1.7.5</org.slf4j-version>
	</properties>
	<dependencies>
		<!-- Spring -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${org.springframework-version}</version>
			<exclusions>
				<!-- Exclude Commons Logging in favor of SLF4j -->
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				 </exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
				
		<!-- AspectJ -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>	
		
		<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${org.slf4j-version}</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>jcl-over-slf4j</artifactId>
			<version>${org.slf4j-version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>${org.slf4j-version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.15</version>
			<exclusions>
				<exclusion>
					<groupId>javax.mail</groupId>
					<artifactId>mail</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.jms</groupId>
					<artifactId>jms</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jdmk</groupId>
					<artifactId>jmxtools</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jmx</groupId>
					<artifactId>jmxri</artifactId>
				</exclusion>
			</exclusions>
			<scope>runtime</scope>
		</dependency>

		<!-- @Inject -->
		<dependency>
			<groupId>javax.inject</groupId>
			<artifactId>javax.inject</artifactId>
			<version>1</version>
		</dependency>
				
		<!-- Servlet -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>
	
		<!-- Test -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.7</version>
			<scope>test</scope>
		</dependency>        
	</dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.9</version>
                <configuration>
                    <additionalProjectnatures>
                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                    </additionalProjectnatures>
                    <additionalBuildcommands>
                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                    </additionalBuildcommands>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <configuration>
                    <mainClass>org.test.int1.Main</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Most of the code is auto generated by STS, except that I have updated the Spring version to use the latest one as 4.0.2.RELEASE. We can remove dependencies or update the versions of other dependencies too but I have left them as it is for simplicity.

Spring Resource Bundle

For simplicity, let’s assume that our application supports only two locales – en and fr. If no user locale is specified, we will use english as default locale. Let’s create spring resource bundles for both these locales that will be used in the JSP page.

messages_en.properties code:


label.title=Login Page
label.firstName=First Name
label.lastName=Last Name
label.submit=Login

messages_fr.properties code:


label.title=Connectez-vous page
label.firstName=Pr\u00E9nom
label.lastName=Nom
label.submit=Connexion

Note that I am using unicode for special characters in the french locale resource bundles, so that it gets interpreted properly in the response HTML sent to client requests.

Another important point to note is that both the resource bundles are in the classpath of the application and their name has pattern as “messages_{locale}.properties”. We will see why these are important later on.

Spring i18n Controller Class

Our controller class is very simple, it just logs the user locale and return the home.jsp page as response.


package com.journaldev.spring;

import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Handles requests for the application home page.
 */
@Controller
public class HomeController {
	
	private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
	
	/**
	 * Simply selects the home view to render by returning its name.
	 */
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String home(Locale locale, Model model) {
		logger.info("Welcome home! The client locale is {}.", locale);
	
		return "home";
	}
	
}

Spring i18n JSP Page

Our home.jsp page code looks like below.


<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ page session="false"%>
<html>
<head>
<title><spring:message code="label.title" /></title>
</head>
<body>
	<form method="post" action="login">
		<table>
			<tr>
				<td><label> <strong><spring:message
								code="label.firstName" /></strong>
				</label></td>
				<td><input name="firstName" /></td>
			</tr>
			<tr>
				<td><label> <strong><spring:message
								code="label.lastName" /></strong>
				</label></td>
				<td><input name="lastName" /></td>
			</tr>
			<tr>
				<spring:message code="label.submit" var="labelSubmit"></spring:message>
				<td colspan="2"><input type="submit" value="${labelSubmit}" /></td>
			</tr>
		</table>
	</form>
</body>
</html>

The only part worth mentioning is the use of spring:message to retrieve the message with the given code. Make sure Spring tag libraries are configured using taglib jsp directive.Spring takes care of loading the appropriate resource bundle messages and make it available for the JSP pages to use.

Spring Internationalization i18n – Bean Configuration File

Spring Bean configuration file is the place where all the magic happens. This is the beauty of Spring framework as it helps us to focus more on business logic rather than coding for trivial tasks. Let’s see how our spring bean configuration file looks and we will look at each of the beans one be one.

servlet-context.xml code:


<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing 
		infrastructure -->

	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving 
		up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources 
		in the /WEB-INF/views directory -->
	<beans:bean
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

	<beans:bean id="messageSource"
		class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
		<beans:property name="basename" value="classpath:messages" />
		<beans:property name="defaultEncoding" value="UTF-8" />
	</beans:bean>

	<beans:bean id="localeResolver"
		class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
		<beans:property name="defaultLocale" value="en" />
		<beans:property name="cookieName" value="myAppLocaleCookie"></beans:property>
		<beans:property name="cookieMaxAge" value="3600"></beans:property>
	</beans:bean>

	<interceptors>
		<beans:bean
			class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
			<beans:property name="paramName" value="locale" />
		</beans:bean>
	</interceptors>

	<context:component-scan base-package="com.journaldev.spring" />

</beans:beans>
  1. annotation-driven tag enables the Controller programming model, without it Spring won’t recognize our HomeController as handler for client requests.
  2. context:component-scan provides the package where Spring will look for the annotated components and register them automatically as Spring bean.
  3. messageSource bean is configured to enable i18n for our application. basename property is used to provide the location of resource bundles. classpath:messages means that resource bundles are located in the classpath and follows name pattern as messages_{locale}.properties. defaultEncoding property is used to define the encoding used for the messages.
  4. localeResolver bean of type org.springframework.web.servlet.i18n.CookieLocaleResolver is used to set a cookie in the client request so that further requests can easily recognize the user locale. For example, we can ask user to select the locale when he launches the web application for the first time and with the use of cookie, we can identify the user locale and automatically send locale specific response. We can also specify the default locale, cookie name and maximum age of the cookie before it gets expired and deleted by the client browser.

    If your application maintains user sessions, then you can also use org.springframework.web.servlet.i18n.SessionLocaleResolver as localeResolver to use a locale attribute in the user’s session. The configuration is similar to CookieLocaleResolver.

    
    <bean id="localeResolver"
    	class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
    	<property name="defaultLocale" value="en" />
    </bean>
    

    If we don’t register any “localeResolver”, AcceptHeaderLocaleResolver will be used by default, which resolves user locale by checking the accept-language header in the client HTTP request.

  5. org.springframework.web.servlet.i18n.LocaleChangeInterceptor interceptor is configured to intercept the user request and identify the user locale. The parameter name is configurable and we are using request parameter name for locale as “locale”. Without this interceptor, we won’t be able to change the user locale and send the response based on the new locale settings of the user. It needs to be part of interceptors element otherwise Spring won’t configure it as an interceptor.

If you are wondering about the configuration that tells Spring framework to load our context configurations, it’s present in deployment descriptor of our MVC application.


<servlet>
	<servlet-name>appServlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>
		
<servlet-mapping>
	<servlet-name>appServlet</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>

We can change the location or name of the context file by changing the web.xml configuration.

Our Spring i18n application is ready, just deploy it in any servlet container. Usually I export it as WAR file in a standalone tomcat web server webapps directory.

Here are the screenshots of our application home page with different locales.

Default Home Page (en locale):
Spring localization, spring resource bundle, spring i18n example

Passing Locale as parameter (fr locale):
Spring localization example, spring i18n, spring mvc i18n

Further requests without locale:
Spring localization example, spring mvc i18n, spring i18n, sprin L10n

As you can see in the above image that we are not passing locale information in the client request but still our application identifies the user locale. You must have guessed by now that it’s because of the CookieLocaleResolver bean that we configured in our spring bean configuration file. However you can check your browser cookies data to confirm it. I am using chrome and below image shows the cookie data stored by the application.

Spring i18n example

Notice that cookie expiry time is one hour i.e 3600 seconds as configured by cookieMaxAge property.

If you will check server logs, you can see that locale is getting logged.


INFO : com.journaldev.spring.HomeController - Welcome home! The client locale is en.
INFO : com.journaldev.spring.HomeController - Welcome home! The client locale is fr.
INFO : com.journaldev.spring.HomeController - Welcome home! The client locale is fr.

That’s all for Spring i18n example application, download the example project from below link and play around with it to learn more.

Comments

  1. srikanth says:

    suppose if need to support 30 different regional language. Will we creating 30 properites file ? Please advice how will design the application.

    1. srikanth says:

      Please reply above mentioned query.

      suppose if need to support 30 different regional language. Will we creating 30 properites file ? Please advice how will design the application.

      1. Sean says:

        A viable alternative could be to look into a text translator service such as the Google Cloud Translation API (https://cloud.google.com/translate/docs/), though I believe convention in business is to use native speaker translations rather than relying on a framework in order to account for any linguistic nuances

  2. SVK PETO says:

    How to do it, if my site dont allow GET requests, but only POST?

  3. Vamshi says:

    Hi Friends,
    I am facing problem in implementing Internationalization in spring, as per my knowledge I have configured all the required resources. Please help me, the following is my project location.
    https://github.com/VamshiKrishna2828/SpringI18NDemo

    Thanks in advance.

  4. Jeferson says:

    Hello. How can I retrieve message from properties file programmatically??

  5. cindy says:

    Hello, how can I change the language of a calendar..datepicker in spring??

  6. Vishal Wagh says:

    1.7
    1.7
    …………..

    updated the pom.xml with above chages.
    But I am getting below error using Jdk 7.

    org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.5.1:compile (default-compile) on project spring: Fatal error compiling

    Please help me…

  7. zoli says:

    I think you don’t need the annotation-driven tag in your app config as context:component-scan does the same:

    Element : component-scan
    Scans the classpath for annotated components that will be auto-registered as Spring beans. By default, the
    Spring-provided @Component, @Repository, @Service, and @Controller stereotypes will be detected. Note: This
    tag implies the effects of the ‘annotation-config’ tag, activating @Required, @Autowired, @PostConstruct,
    @PreDestroy, @Resource, @PersistenceContext and @PersistenceUnit annotations in the component classes

  8. daj says:

    Thank you very much ! you have just saved my life 😀

  9. Håvard says:

    According to Google, it is bad practice to use request parameters and cookies to show translated versions of the page: https://support.google.com/webmasters/answer/182192

    1. Lluis says:

      So what’s your recomendation in order to internationalize your site with spring?

  10. Vince says:

    I got following exceptions when I copied and ran this example using IntelliJ. If I go to my tomcat directory, I can see messages_X.properties are under WEB-INF/classes and no spelling error. Please help. Thanks.

    SEVERE: Servlet.service() for servlet [jsp] in context with path [/spring] threw exception [javax.servlet.ServletException: javax.servlet.jsp.JspTagException: No message found under code ‘label.title’ for locale ‘en_US’.] with root cause
    javax.servlet.jsp.JspTagException: No message found under code ‘label.title’ for locale ‘en_US’.
    at org.springframework.web.servlet.tags.MessageTag.doEndTag(MessageTag.java:202)
    at org.apache.jsp.home_jsp._jspx_meth_spring_005fmessage_005f0(home_jsp.java:135)
    at org.apache.jsp.home_jsp._jspService(home_jsp.java:73)
    at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
    at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:432)
    at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390)
    at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:950)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:314)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:744)

    1. Deepak says:

      Have you given the id as messageSource for the class-RelodableResourceBundleMessageSource? Please check if not given, provide the id.

  11. rajan says:

    Hai pankaj,
    I compiled this project without any error, what is the url to access the home page?
    http://localhost:8080/spring/ it is not showing any page, How do I access the home page(controller)
    please help

    1. vivek says:

      Change the name of the project.

  12. Acid Serg says:

    Good example! Thanx a lot!

  13. Anibal Gomez says:

    Thanks for the 100% working example!

  14. praveen says:

    it’s good , it resolved my problem .. good example . thanks pankaj 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages