Springboot locale with Rest request and Thymeleaf - spring-boot

So another application directs user to my server. The redirect is Post request (application/json) with value language in the JSON. How should I set the locale value in RestController? So that Thymeleaf could render the correct text.
Setting locale with LocaleContextHolder doesn't do the trick.

You should follow this guide here since Internationalization is a common task in spring-boot. In case if you need a short answer:
First configure a LocaleResolver in your Application.java:
#Bean(name = "localeResolver")
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(new Locale("tr", "TR"));
return slr;
}
Then again in your Application.java file configure a LocaleChangeInterceptor:
#Bean(name = "localeChangeInterceptor")
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
And finally register your LocaleChangeInterceptor (also in Application.java):
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
Now if you send a request parameter named "lang" with your POST request spring will use its value to determine the desired locale and change it accordingly.

Ended up with the following solution:
WebMvcConfigurer has these
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver r = new SessionLocaleResolver();
r.setDefaultLocale(new Locale("jp"));
return r;
}
And in the controller I call this classes public method:
#Component
public class WebLanguage {
public void setLocale(HttpServletRequest request, HttpServletResponse response) {
if (!request.getParameterMap().containsKey("lang")) return;
LocaleResolver localeResolver = localeResolver(request);
localeResolver.setLocale(request, response, new Locale(request.getParameterMap().get("lang")[0]));
}
LocaleResolver localeResolver(HttpServletRequest request) {
return RequestContextUtils.getLocaleResolver(request);
}
}

Related

Spring Boot, Hibernate validator language based on LocaleContextHolder

I've a Spring Boot 2.4.2 REST application using JPA, Hibernate, etc.
So far I use a MessageSource for applications errors (located in i18n/messages), and the default ValidationMessagesfor bean validations.
This is part of my configuration:
public static Set<Locale> LOCALES = Set.of(new Locale("en"), new Locale("it"));
#Bean
public LocaleResolver localeResolver() {
SmartLocaleResolver localeResolver = new SmartLocaleResolver();
return localeResolver;
}
public class SmartLocaleResolver extends AcceptHeaderLocaleResolver {
#Override
public Locale resolveLocale(HttpServletRequest request) {
if (StringUtils.isBlank(request.getHeader("Accept-Language"))) {
return Locale.getDefault();
}
List<Locale.LanguageRange> list = Locale.LanguageRange.parse(request.getHeader("Accept-Language"));
Locale locale = Locale.lookup(list, LOCALES);
return locale;
}
}
#Primary
#Bean("messageSource")
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setBasenames("classpath:/i18n/messages");
// set to true only for debugging
messageSource.setUseCodeAsDefaultMessage(false);
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
My application supports 2 languages so far: it and en.
The problem right now is that application's messages are correctly localized in the agent's language (browser) but Validations errors are not.
I found out that Hibernate uses the default locale (Locale.getDefault()) and to customize the behaviour I should customize the locale resolution.
So I tried creating a custom hibernateValidator (that I set in my entityFactory) :
#Bean
public MessageSource validationMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setBasenames("classpath:/ValidationMessages");
// set to true only for debugging
messageSource.setUseCodeAsDefaultMessage(false);
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
#Bean("hibernateValidator")
public LocalValidatorFactoryBean hibernateValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setValidationMessageSource(validationMessageSource());
return factoryBean;
}
and the resolver:
public class HibernateLocaleResolver implements LocaleResolver {
#Override
public Locale resolve(LocaleResolverContext context) {
return LocaleContextHolder.getLocale();
}
}
Doing this, the locale resolution works fine, but the parameter replacement doesn't. What I mean is for messages like this:
server.validators.ArraySize.message = The number of values must be between [{min}] and [{max}].
I've an exception:
"exception": "java.lang.IllegalArgumentException", "message": "can't parse argument number: min"
So I changed the configuration above adding the MessageInterpolator:
factoryBean.setMessageInterpolator(new ResourceBundleMessageInterpolator(LOCALES, Locale.ENGLISH, new HibernateLocaleResolver(), false));
At this point the parameters are resolved correctly, but again the locale resolution doesn't work.
Can you point me out in the right direction, trying to explain the best practice to follow for the combination Spring Boot - Hibernate Validator?
I solved the problem. I hope this can help someone else. This is my configuration file:
#Primary
#Bean("messageSource")
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setBasenames("classpath:/i18n/messages");
// set to true only for debugging
messageSource.setUseCodeAsDefaultMessage(false);
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
#Bean
public MessageSource validationMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setBasenames("classpath:/ValidationMessages");
// set to true only for debugging
messageSource.setUseCodeAsDefaultMessage(false);
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
#Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setValidationMessageSource(validationMessageSource());
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
and the LocaleConfiguration:
#Configuration
public class LocaleConfiguration implements WebMvcConfigurer {
#Bean
public LocaleResolver localeResolver() {
SmartLocaleResolver localeResolver = new SmartLocaleResolver();
return localeResolver;
}
public class SmartLocaleResolver extends AcceptHeaderLocaleResolver {
#Override
public Locale resolveLocale(HttpServletRequest request) {
if (StringUtils.isBlank(request.getHeader("Accept-Language"))) {
return Locale.getDefault();
}
List<Locale.LanguageRange> list = Locale.LanguageRange.parse(request.getHeader("Accept-Language"));
Locale locale = Locale.lookup(list, Constants.LOCALES);
return locale;
}
}
}
the important part I saw made the difference are these lines:
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
In this way the interpolator works fine as well as the localization of the message.

Enum conversion doesn't fallback to rest-messages

I'm using Spring Boot 1.5.8, Spring Data REST, Spring HATEOAS. In my application exposing REST endpoints I enabled:
spring.data.rest.enable-enum-translation=true
In this way when I ask for an enum it is translated acconding to my locale.
Some more configuration stuff:
#Bean
public LocaleResolver localeResolver() {
return new SmartLocaleResolver();
}
public class SmartLocaleResolver extends CookieLocaleResolver {
#Override
public Locale resolveLocale(HttpServletRequest request) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) {
return super.determineDefaultLocale(request);
}
return request.getLocale();
}
}
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:/i18n/messages");
// messageSource.setDefaultEncoding("UTF-8");
// set to true only for debugging
messageSource.setUseCodeAsDefaultMessage(false);
messageSource.setCacheSeconds((int) TimeUnit.HOURS.toSeconds(1));
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
#Bean
public MessageSourceAccessor messageSourceAccessor() {
return new MessageSourceAccessor(messageSource());
}
As you can see I set also message source in order to translate also exceptions coming from the server.
My server locale is it-IT and I've rest-messages.properties (US translation) and rest-messages_it.properties (IT translation). My goal is to use rest-messages.properties when the language is not recognized and rest-messages_it.properties when the language is IT.
Right now it doesn't work. Spring Data REST read rest-messages_it.properties when there isn't a corrispondent file for the language selected.
I solved this problem with messages.properties using messageSource.setFallbackToSystemLocale(false);. Is there a way to do the same thing for rest-messages files?
What if you subclass the RepositoryRestMvcConfiguration, override and copy its method resourceDescriptionMessageSourceAccessor, but set fallbackToSystemLocale to false for messageSource?
#Override
#Bean
public MessageSourceAccessor resourceDescriptionMessageSourceAccessor() {
try {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("rest-default-messages.properties"));
propertiesFactoryBean.afterPropertiesSet();
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:rest-messages");
messageSource.setCommonMessages(propertiesFactoryBean.getObject());
messageSource.setDefaultEncoding("UTF-8");
// Adding this line:
messageSource.setFallbackToSystemLocale(false);
return new MessageSourceAccessor(messageSource);
} catch (Exception o_O) {
throw new BeanCreationException("resourceDescriptionMessageSourceAccessor", "", o_O);
}
}
And what if you create the rest-default-messages.properties file with values for the default locale?..
Update from the question author
To preserve spring.data.rest.* properties it's necessary to create a RepositoryRestConfiguration Bean as described in this post:
#Bean
#ConfigurationProperties(prefix = "spring.data.rest")
#Override
public RepositoryRestConfiguration config() {
return super.config();
}

spring boot Locale Resolver default spanish

I can not find a way to change the default locale from Locale.US to Spanish. It is not in the list and I can not find anything that explains how.
#Configuration
public class SpringMvcConfiguration extends WebMvcConfigurerAdapter {
#Bean
public LocaleResolver localeResolver(){
SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
sessionLocaleResolver.setDefaultLocale(Locale.US);
return sessionLocaleResolver;
}
#Bean
LocaleChangeInterceptor localeChangeInterceptor(){
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
return localeChangeInterceptor;
}
#Override
public void addInterceptors(InterceptorRegistry interceptorRegistry){
interceptorRegistry.addInterceptor(localeChangeInterceptor());
}
}
There is no constant like Locale.US to help you. But you can instantiate a new Locale passing the correct languange information like new Locale("es") as explained in the documentation. Or use the Locale.Builder that was added in Java 8.

Spring boot internalization and exceptions

I'm making an API using Spring boot and trying to making it suit many languages, to do so i'm using this code :
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.ENGLISH);
return slr;
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
#Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
String[] baseNames = { "messages/messages", "messages/messages_errors" };
source.setBasenames(baseNames);
source.setDefaultEncoding(StandardCharsets.UTF_8.toString());
return source;
}
So logically in my controller i get the lang parameter to know which language the user has chosen and it works great.
The problem is that i'm throwing an exception from a method called by the controller, here is the code :
public User getUser(final Long pIdUser) throws EntityNotFound {
User vUser = userRepository.findOne(pIdUser);
if (vUser == null) {
throw new EntityNotFound("entity.notFound.byId", new Object[] { pIdUser });
}
return vUser;
}
and i'm using a #ControllerAdvice to get the exception and switch the exception message to the right language like that :
#ControllerAdvice
public class GlobalExceptionHandler {
#Autowired
private MessageSource messageSource;
#ExceptionHandler(value = EntityNotFound.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
protected EntityNotFound EntityNotFound(EntityNotFound pException, Locale lang) {
return new EntityNotFound(messageSource.getMessage(pException.getMessage(), pException.getArgs(), lang));
}
But i can't have the right message, i have "entity.notFound.byId" in the response of the controller. Someone knows how to deal with internationalization and errors ?
I think that if i make the lang variable as globale, i could have the right message at the first call of the EntityNotFound exception but i will have to set lang in every controller and it's dirty.
Thank you for your time guys.
For those interested by the solution, i did that using a global variable which stores the language to use : public static Locale LANG = Locale.ENGLISH; and created my own LocaleChangeInterceptor class to set LANG variable with the given language from the request.

Spring REST Handle locale change

I'm trying to handle locale change in a Spring 3 REST application.
But the locale is not changed to fr.
The console log shows:
2014-05-19 14:29:46,214 DEBUG [AbstractExceptionHandler] locale: en
Here is my configuration:
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:messages/messages", "classpath:messages/validation");
// If true, the key of the message will be displayed if the key is not found, instead of throwing an exception
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setDefaultEncoding("UTF-8");
// The value 0 means always reload the messages to be developer friendly
messageSource.setCacheSeconds(0);
return messageSource;
}
// The locale interceptor provides a way to switch the language in any page just by passing the lang=’en’, lang=’fr’, and so on to the url
#Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor);
}
#Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(new Locale("en"));
return localeResolver;
}
Here is my exception handler:
#ControllerAdvice
public class AdminExceptionHandler extends AbstractExceptionHandler {
#ExceptionHandler(NullPointerException.class)
#ResponseBody
public ResponseEntity<ErrorInfo> nullPointerException(HttpServletRequest request, NullPointerException e) {
String url = request.getRequestURL().toString();
String errorMessage = localizeErrorMessage("error.npe", new Object[] { e.getMessage() });
return new ResponseEntity<ErrorInfo>(new ErrorInfo(url, errorMessage), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public class AbstractExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(AbstractExceptionHandler.class);
#Autowired
private MessageSource messageSource;
protected String localizeErrorMessage(String errorCode, Object args[]) {
Locale locale = LocaleContextHolder.getLocale();
logger.debug("locale: " + locale);
return messageSource.getMessage(errorCode, args, locale);
}
protected String localizeErrorMessage(String errorCode) {
return localizeErrorMessage(errorCode, null);
}
protected String extractAdminIdFromUrl(String url) {
String adminId = null;
try {
URI uri = new URI(url);
String path = uri.getPath();
adminId = path.substring(path.lastIndexOf('/') + 1);
} catch (URISyntaxException e1) {
e1.printStackTrace();
}
return adminId;
}
}
And here is my test:
#Test
public void testExceptionLocalizedMessage() throws Exception {
HttpHeaders httpHeaders = Common.createAuthenticationHeaders("stephane" + ":" + PASSWORD);
MvcResult resultGet = this.mockMvc.perform(
get("/error/npe").headers(httpHeaders)
.param("lang", "fr")
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value("Une erreur inconnue s'est produite. Veuillez nous excuser."))
.andReturn();
httpHeaders.add("Accept-Language", "fr");
resultGet = this.mockMvc.perform(
get("/error/npe").headers(httpHeaders)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value("Une erreur inconnue s'est produite. Veuillez nous excuser."))
.andReturn();
}
I would like to handle the locale argument in the url as in ?lang=en and the Accept-Language header as a fall back.
As a REST application I was thinking of using the AcceptHeaderLocaleResolver class but it does not support the setting of the locale via the url parameter.
I reckoned using the SessionLocaleResolver class makes little sense in a REST application.
That leaves my with the CookieLocaleResolver class which I'm not specially convinced is the one that should be used in a REST application.
Anyway, the retrieved locale is still en and not fr as I expect it to be.
EDIT:
In the test, using the statement:
httpHeaders.add("Accept-Language", Locale.FRENCH.getLanguage());
does not set the locale.
But using the locale() does.
This test passes:
this.mockMvc.perform(
get("/error/npe").headers(httpHeaders).locale(Locale.FRENCH)
.accept(MediaType.APPLICATION_JSON))
.andDo(print()
)
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value(localizeErrorMessage("error.npe", Locale.FRENCH)))
.andReturn();
I found the solution. Now the Accept-Language header is being used and the cookie as well.
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Bean
public LocaleResolver localeResolver() {
return new SmartLocaleResolver();
}
}
public class SmartLocaleResolver extends CookieLocaleResolver {
#Override
public Locale resolveLocale(HttpServletRequest request) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) {
return super.determineDefaultLocale(request);
}
return request.getLocale();
}
}
UPDATE: Following Thor's comment, here is a resolver that checks first for the cookie, and if not found checks for the request header:
#Override
public Locale resolveLocale(HttpServletRequest request) {
Locale locale = super.determineDefaultLocale(request);
if (null == locale) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage != null && !acceptLanguage.trim().isEmpty()) {
locale = request.getLocale();
}
}
return locale;
}
Or with a simpler implementation (not tested):
private AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
#Override
public Locale resolveLocale(HttpServletRequest request) {
Locale locale = super.determineDefaultLocale(request);
if (null == locale) {
locale = acceptHeaderLocaleResolver.resolveLocale(request);
}
return locale;
}
UPDATE: This above solution is not working any longer.
I'm now trying to pass the accepted language in a header:
httpHeaders.add(HttpHeaders.ACCEPT_LANGUAGE, "fr_FR");
and retrieve it in this locale resolver:
#Override
public Locale resolveLocale(HttpServletRequest request) {
for (String httpHeaderName : Collections.list(request.getHeaderNames())) {
logger.debug("===========>> Header name: " + httpHeaderName);
}
String acceptLanguage = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE);
logger.debug("===========>> acceptLanguage: " + acceptLanguage);
Locale locale = super.resolveLocale(request);
logger.debug("===========>> acceptLanguage locale: " + locale.getDisplayCountry());
if (null == locale) {
locale = getDefaultLocale();
logger.debug("===========>> Default locale: " + locale.getDisplayCountry());
}
return locale;
}
But there is no Accept-Language in the output of the ===========>> Header name logger, and the acceptLanguage logger is empty.
when we are using
#Bean
public SessionLocaleResolver localeResolver(){
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
return localeResolver;
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}
it is able to accept locale from query parameter
{{url}}/com-manh-cp-ext-order/api/ext/ex23order/greeting?language=es

Resources