As mentioned at the end of the last tutorial, some applications need to let users pick their own locale rather than relying on the browser's Accept-Language header. This tutorial demonstrates SessionLocaleResolver, which supports exactly that.
|
SessionLocaleResolver stores a Locale instance in the user's HttpSession. As the diagram shows, it also implements LocaleContextResolver, which adds methods for reading time zone information from the session — a feature we'll cover in a future tutorial. Here, we'll focus on Locale alone. |
Example
There are several scenarios where a Locale needs to be stored in the user's session. This tutorial walks through one common flow:
- A user selects a desired language from a dropdown component on the client side.
- On the server side, a corresponding Locale is constructed and populated in session using SessionLocaleResolver.
- From that point on the Locale is retrieved from HttpSession for further interaction until the end of the session.
Registering SessionLocaleResolver and other config
package com.logicbig.example;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import java.util.Locale;
@EnableWebMvc
@Configuration
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/pages/", ".jsp");
}
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasenames("msgs/msg");
source.setDefaultEncoding("UTF-8");
return source;
}
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver r = new SessionLocaleResolver();
r.setDefaultLocale(Locale.US);
return r;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor l = new LocaleChangeInterceptor();
l.setParamName("localeCode");
registry.addInterceptor(l);
}
}
Populating session with custom Locale using LocaleChangeInterceptor
When a user picks a language from the dropdown, the resulting Locale needs to be passed to LocaleResolver#setLocale(..). You could do this directly in a controller's handler method — but that only works if the dropdown appears on a single page. If several pages need the same behavior, a custom implementation of HandlerInterceptor is a better fit, and Spring already provides one such implementation LocaleChangeInterceptor. On every request, it checks for a specific request parameter (configured via LocaleChangeInterceptor.setParamName(..)) and, if present, updates the current locale on the underlying LocaleResolver.
We have already registered LocaleChangeInterceptor in above configuration class.
The Controller
package com.logicbig.example;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
@Controller
public class TheController {
@RequestMapping(value = "/",
method = {RequestMethod.POST, RequestMethod.GET})
public String handleGet(Model model,
Locale locale) {
FormData formData = new FormData();
formData.setLocaleCode(locale.toString());
model.addAttribute("formData", formData);
//map's key/value equivalent to
// html's <option value='key'>value</option>
Map<String, String> localeChoices = new LinkedHashMap<>();
Locale l = Locale.US;
localeChoices.put(l.toString(), l.getDisplayLanguage());
l = Locale.GERMANY;
localeChoices.put(l.toString(), l.getDisplayLanguage());
l = Locale.FRANCE;
localeChoices.put(l.toString(), l.getDisplayLanguage());
model.addAttribute("localeChoices", localeChoices);
return "main";
}
@GetMapping(value = "/page1")
public String handlePage1() {
return "page1";
}
@GetMapping(value = "/page2")
public String handlePage2() {
return "page2";
}
public static class FormData {
private String localeCode;
public String getLocaleCode() {
return localeCode;
}
public void setLocaleCode(String localeCode) {
this.localeCode = localeCode;
}
}
}
JSP pages
src/main/webapp/WEB-INF/pages/main.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="fm"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<html>
<body style="margin:20px;">
<h3> <spring:message code="label.mainPage"/></h3>
<h4><spring:message code="label.lang"/>:
<spring:message code="text.lang"/></h4>
<fm:form method="post" action="/" modelAttribute="formData">
<fm:select path="localeCode">
<fm:options items="${localeChoices}"/>
</fm:select>
<input type="submit" value="Change Language" />
</fm:form>
<br/>
<P><spring:message code="text.mainPage"/></p>
<a href="/page1"><spring:message code="label.page1"/></a>
<br/>
<a href="/page2"><spring:message code="label.page2"/></a>
</body>
</html>
The page above submits a POST request with a 'localeCode' parameter — the same name configured via LocaleChangeInterceptor.setParamName()— carrying the selected locale's string value (the same format produced by Locale.toString() in the controller). Detecting that parameter in the request, LocaleChangeInterceptor's logic is triggered, which parses it back into a Locale and passes it to LocaleResolver#setLocale(...). If interested, check out the method preHandle() of LocaleChangeInterceptor.java
src/main/webapp/WEB-INF/pages/page1.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<html>
<body>
<h3><spring:message code="label.page1"/></h3>
<h4><spring:message code="label.lang"/>:
<spring:message code="text.lang"/></h4>
<p><spring:message code="text.page1"/></p>
<a href="/page2"><spring:message code="label.page2"/></a>
<br/>
<a href="/"><spring:message code="label.mainPage"/></a>
</body>
</html>
src/main/webapp/WEB-INF/pages/page2.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<html>
<body>
<h3><spring:message code="label.page2"/></h3>
<h4><spring:message code="label.lang"/>:
<spring:message code="text.lang"/></h4>
<P><spring:message code="text.page2"/></p>
<a href="/page1"><spring:message code="label.page1"/></a>
<br/>
<a href="/"><spring:message code="label.mainPage"/></a>
</body>
</html>
Resource bundle property files
src/main/resources/msgs/msg.propertieslabel.mainPage = Main Page
label.page1 = Page 1
label.page2 = Page 2
label.lang = language
text.lang = English
text.mainPage = This is the content of main page.
text.page1 = This is the content of page 1.
text.page2 = This is the content of page 2.
Similarly, we have populated two more property files for the German and French languages (see project browser below).
Running the example
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
Output
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitWebConfig(WebConfig.class)
public class ControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvcTester;
@BeforeEach
public void setup() {
this.mockMvcTester = MockMvcTester.from(this.wac);
}
@Test
public void test() {
MockHttpSession session = new MockHttpSession();
MvcTestResult postResult = mockMvcTester.post().uri("/")
.param("localeCode", "de_DE")
.session(session)
.exchange();
assertThat(postResult).hasStatusOk();
assertThat(postResult).model().extractingByKey("formData")
.extracting("localeCode").isEqualTo("de_DE");
MvcTestResult getResult = mockMvcTester.get().uri("/").session(session).exchange();
assertThat(getResult).hasStatusOk();
assertThat(getResult).model().extractingByKey("formData")
.extracting("localeCode").isEqualTo("de_DE");
MvcTestResult page1Result = mockMvcTester.get().uri("/page1")
.session(session)
.exchange();
assertThat(page1Result).hasStatusOk();
assertThat(page1Result).hasViewName("page1");
assertThat(page1Result).hasForwardedUrl("/WEB-INF/pages/page1.jsp");
assertThat(page1Result.getRequest().getSession()
.getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME))
.isEqualTo(Locale.GERMANY);
}
}
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.6 (Spring TestContext Framework)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|