Spring boot internalization and exceptions - spring

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.

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.

Springboot locale with Rest request and Thymeleaf

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);
}
}

How to solve `No message found under code 'good.morning.message' for locale 'us'`?

I'm trying to test a internationalization but I keep getting the message "No message found under code 'good.morning.message' for locale 'us'." each time I make a GET request.
I'm using Netbeans IDE for my project. Below are my codes
#SpringBootApplication
public class RestfulWebServicesApplication {
public static void main(String[] args) {
SpringApplication.run(RestfulWebServicesApplication.class, args);
}
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
return localeResolver;
}
#Bean
public ReloadableResourceBundleMessageSource bundleMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("messages");
return messageSource;
}
ControllerClass
#RestController
public class HelloWorldController {
#Autowired
private MessageSource messageSource;
#GetMapping("/hello-world-internationalized")
public String helloWorldInternationalized(#RequestHeader(name="Accept-Language", required=false) Locale locale) {
return messageSource.getMessage("good.morning.message", null, locale);
//return "Good morning";
}
}
Here's my messages.properties file:
good.morning.message=Good Morning
And here's the link to my folder structure
Change bundleMessageSource() name to messageSource().

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();
}

How to configure i18n in Spring boot 2 + Webflux + Thymeleaf?

I just start a new project based on Spring boot 2 + Webflux. On upgrading version of spring boot and replace spring-boot-starter-web with spring-boot-starter-webflux classes like
WebMvcConfigurerAdapter
LocaleResolver
LocaleChangeInterceptor
are missing. How now can I configure defaultLocale, and interceptor to change the language?
Just add a WebFilter that sets the Accept-Language header from the value of a query parameter. The following example gets the language from the language query parameter on URIs like http://localhost:8080/examples?language=es:
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import reactor.core.publisher.Mono;
import static org.springframework.util.StringUtils.isEmpty;
#Component
public class LanguageQueryParameterWebFilter implements WebFilter {
private final ApplicationContext applicationContext;
private HttpWebHandlerAdapter httpWebHandlerAdapter;
public LanguageQueryParameterWebFilter(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
#EventListener(ApplicationReadyEvent.class)
public void loadHttpHandler() {
this.httpWebHandlerAdapter = applicationContext.getBean(HttpWebHandlerAdapter.class);
}
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
final ServerHttpRequest request = exchange.getRequest();
final MultiValueMap<String, String> queryParams = request.getQueryParams();
final String languageValue = queryParams.getFirst("language");
final ServerWebExchange localizedExchange = getServerWebExchange(languageValue, exchange);
return chain.filter(localizedExchange);
}
private ServerWebExchange getServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
return isEmpty(languageValue)
? exchange
: getLocalizedServerWebExchange(languageValue, exchange);
}
private ServerWebExchange getLocalizedServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
final ServerHttpRequest httpRequest = exchange.getRequest()
.mutate()
.headers(httpHeaders -> httpHeaders.set("Accept-Language", languageValue))
.build();
return new DefaultServerWebExchange(httpRequest, exchange.getResponse(),
httpWebHandlerAdapter.getSessionManager(), httpWebHandlerAdapter.getCodecConfigurer(),
httpWebHandlerAdapter.getLocaleContextResolver());
}
}
It uses #EventListener(ApplicationReadyEvent.class) in order to avoid cyclic dependencies.
Feel free to test it and provide feedback on this POC.
With spring-boot-starter-webflux, there are
DelegatingWebFluxConfiguration
LocaleContextResolver
For example, to use a query parameter "lang" to explicitly control the locale:
Implement LocaleContextResolver, so that
resolveLocaleContext() returns a SimpleLocaleContext determined by a GET parameter of "lang". I name this implementation QueryParamLocaleContextResolver. Note that the default LocaleContextResolver is an org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver.
Create a #Configuration class that extends DelegatingWebFluxConfiguration. Override DelegatingWebFluxConfiguration.localeContextResolver() to return QueryParamLocaleContextResolver that we just created in step 1. Name this configuration class WebConfig.
In WebConfig, override DelegatingWebFluxConfiguration.configureViewResolvers() and add the ThymeleafReactiveViewResolver bean as a view resolver. We do this because, for some reason, DelegatingWebFluxConfiguration will miss ThymeleafReactiveViewResolver after step 2.
Also, I have to mention that, to use i18n with the reactive stack, this bean is necessary:
#Bean
public MessageSource messageSource() {
final ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:/messages");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(5);
return messageSource;
}
After creating a natural template, some properties files, and a controller, you will see that:
localhost:8080/test?lang=zh gives you the Chinese version
localhost:8080/test?lang=en gives you the English version
Just don't forget <meta charset="UTF-8"> in <head>, otherwise you may see some nasty display of Chinese characters.
Starting with Spring Boot 2.4.0, the WebFluxAutoConfiguration contains a bean definition for the LocaleContextResolver, which allows us to inject custom LocaleContextResolver. For reference, the following is the default bean definition in Spring Boot 2.5.4 (the implementation may be different in earlier versions):
#Bean
#Override
#ConditionalOnMissingBean(name = WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public LocaleContextResolver localeContextResolver() {
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleContextResolver(this.webProperties.getLocale());
}
AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
localeContextResolver.setDefaultLocale(this.webProperties.getLocale());
return localeContextResolver;
}
You can provide your own LocaleContextResolver implementation to get the locale from the query parameter by providing a custom bean definition:
//#Component("localeContextResolver")
#Component(WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public class RequestParamLocaleContextResolver implements LocaleContextResolver {
#Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
List<String> lang = exchange.getRequest().getQueryParams().get("lang");
Locale targetLocale = null;
if (lang != null && !lang.isEmpty()) {
targetLocale = Locale.forLanguageTag(lang.get(0));
}
if (targetLocale == null) {
targetLocale = Locale.getDefault();
}
return new SimpleLocaleContext(targetLocale);
}
#Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
throw new UnsupportedOperationException(
"Cannot change lang query parameter - use a different locale context resolution strategy");
}
}
Note that the framework consumes the LocaleContextResolver with a specific name localeContextResolver (WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME). You need to provide the bean with the given name. See #24209.
Another solution with spring boot starter web flux, which is much more cleaner, is to define your own HttpHandler using WebHttpHandlerBuilder in which you can set your LocaleContextResolver.
Documentation (see 1.2.2. WebHandler API) : https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-config-customize
MyLocaleContextResolver.java
public class MyLocaleContextResolver implements LocaleContextResolver {
#Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
return new SimpleLocaleContext(Locale.FRENCH);
}
#Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
throw new UnsupportedOperationException();
}
}
Then in a config file (annotated with #Configuration) or in your spring boot application file, defined your own HttpHandler bean.
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public HttpHandler httpHandler(ApplicationContext context) {
MyLocaleContextResolver localeContextResolver = new MyLocaleContextResolver();
return WebHttpHandlerBuilder.applicationContext(context)
.localeContextResolver(localeContextResolver) // set your own locale resolver
.build();
}
}
That's it!
Based on Jonatan Mendoza's answer, but simpliefied and in kotlin:
/**
* Override Accept-Language header by "lang" query parameter.
*/
#Component
class LanguageQueryParameterWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val languageValue = exchange.request.queryParams.getFirst("lang") ?: ""
if (languageValue.isEmpty()) {
return chain.filter(exchange)
}
return chain.filter(
exchange.mutate().request(
exchange.request
.mutate()
.headers {
it[HttpHeaders.ACCEPT_LANGUAGE] = languageValue
}
.build(),
).build(),
)
}
}

Resources