Retrieve locale based on the Accept-Language in Spring Boot - java

I have a Spring Boot (2.1.3.RELEASE) application that uses Jersey to define the (RESTful) endpoints. I'm trying to read and propagate some messages based on the locale being sent by the user-agents.
I've configured these beans:
#Bean
public LocaleResolver localeResolver() {
final AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setSupportedLocales(Arrays.asList(Locale.GERMANY, Locale.US));
resolver.setDefaultLocale(Locale.ENGLISH);
return resolver;
}
#Bean
public MessageSource messageSource() { // Not sure if this is needed
final ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
messageSource.setBasenames("classpath:/messages");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setCacheSeconds(5);
return messageSource;
}
...and also the bundles (inside ../src/main/resources/) like: messages.properties (fallback), messages_en_US.properties, messages_de_DE.properties, etc.
Now, the challenge is that I'm not sure how to "read" the locale sent by the user-agents in order to read the messages from the bundles appropriately. I'm injecting a MessageSource ms, and programmatically reading messages like:
final Locale locale = ???
ms.getMessage("message.duplicate-token", null, locale);
Any clues?
I've tried LocaleContextHolder.getLocale() but it's always en_US. If I hardcode the corresponding locale for the getMessage call, I'm able to retrieve the correct message(s). So I know the setup/configuration works for the most part.
Clients are sending the locale using the Accept-Language header — and values like: de-DE, en-US, etc.

You need add an LocaleChangeInterceptor and configure the bean as follow:
Refer Spring Boot internationalization for more
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
    LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
    lci.setParamName("lang");
    return lci;
}
If you want to use "Accept-Language" header only, then you can extend AcceptHeaderLocaleResolver and can customize:
package com.deb.demo.config;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
public class CustomLocaleResolver extends AcceptHeaderLocaleResolver {
List<Locale> LOCALES = Arrays.asList(new Locale("en"),new Locale("es"),new Locale("fr"));
#Override
public Locale resolveLocale(HttpServletRequest request) {
if (StringUtils.isEmpty(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;
}
}

I m using a bean
#Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver slr = new AcceptHeaderLocaleResolver();
slr.setDefaultLocale(Locale.UK);
return slr;
}
then another one
#Bean
public LanguageUtil languageUtil() {
return new LanguageUtil();
}
with
private Locale getLocale() {
return LocaleContextHolder.getLocale();
}
public String getLocalizedMessage(String messageKey) {
return messageSource.getMessage(messageKey, null, getLocale());
}
The header is saved into the LocaleContextHolder, and you can use it when you need it.

Create a custom AcceptHeaderLocaleResolver
public class AcceptHeaderResolver extends AcceptHeaderLocaleResolver {
List<Locale> LOCALES = Arrays.asList(new Locale("en"), new Locale("ar"));
#Override
public Locale resolveLocale(HttpServletRequest request) {
String headerLang = request.getHeader("Accept-Language");
return headerLang == null || headerLang.isEmpty()
? Locale.getDefault()
: Locale.lookup(Locale.LanguageRange.parse(headerLang), LOCALES);
}
}
And Don't forgot to use it in #Configuration file
#Bean
public LocaleResolver sessionLocaleResolver() {
AcceptHeaderResolver localeResolver = new AcceptHeaderResolver();
return localeResolver;
}

If you want to get this on a REST controller level, you can directly get the locale instance in the REST methods. Spring does this magic
#GetMapping("/status")
public ResponseEntitiy<String> getStatus(final Locale locale) {
}

The LocaleResolver bean that you create only gets used in Spring MVC and not in the Jersey container. It is the Spring's DispatcherServlet that uses the LocaleResolver.
So LocaleContextHolder.getLocale(); will return different local depending on if call in a Jersey controller or in a Spring MVC controller.

There is no need to extend AcceptHeaderLocaleResolver.
Create a bean definition such as:
#Bean
public LocalResolver localeResolver() {
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setSupportedLocales(Arrays.asList(new Locale("fa"), new Locale("en")));
localeResolver.setDefaultLocale(new Locale("fa"));
return localeResolver;
}

I also implemented same scenario and it's works for me. For this, need to override the resolveLocale method in AcceptHeaderLocaleResolver.
Create component LanguageResolver for the custom implementation. Use Locale.forLanguageTag(language) to create locale from accept-header value. This will create a local with language and country code.
#Component
public class LanguageResolver extends AcceptHeaderLocaleResolver {
#Override
public Locale resolveLocale(HttpServletRequest request) {
String language = request.getHeader("Accept-Language");
List<Locale> supportedLocales = getSupportedLocales();
Locale defaultLocale = getDefaultLocale();
Locale requestLocale = Locale.forLanguageTag(language);
if (StringUtils.isEmpty(language)) {
return defaultLocale;
} else if (supportedLocales.contains(requestLocale)) {
return requestLocale;
} else {
return defaultLocale;
}
}
}
In the configuration class create bean using custom LanguageResolver class.
#Configuration
public class Internationalization extends WebMvcConfigurerAdapter {
#Bean
public AcceptHeaderLocaleResolver localeResolver() {
final LanguageResolver resolver = new LanguageResolver();
resolver.setSupportedLocales(Arrays.asList(Locale.GERMANY, Locale.US,Locale.UK));
resolver.setDefaultLocale(Locale.US);
return resolver;
}
#Bean
public ResourceBundleMessageSource messageSource() {
final ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasename("language/messages");
source.setDefaultEncoding("UTF-8");
return source;
}
}
Here LocaleContextHolder.getLocale() will invoke the override method in LanguageResolver class.
#Service
public class LocaleService {
#Autowired
ResourceBundleMessageSource messageSource;
public String getMessage(String code) {
return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
}
}
And property files are in path of resources -> language
messages_en_US.properties
messages_en_GB.properties
messages_de_DE.properties
Content of the file in the format of below
test.hello=Hello GERMANY
Tested method using below.
#RestController
public class TestController {
private LocaleService localeService;
#Autowired
public TestController(LocaleService localeService) {
this.localeService = localeService;
}
#GetMapping("/local")
public String getMessageForLocal() {
return localeService.getMessage("test.hello");
}
}

You have to add Accept-Language header in your api endpoint to get desired locale output. Then you have to add following configuration to parse and set the Accept-Language header value from incoming request.
#Configuration
public class I18NConfiguration {
#Value("${i18n.locale.default:en-US}")
private String defaultLocale;
#Value("#{'${i18n.locale.supported: }'.split(',\\s*')}")
private List<String> supportedLocales;
#Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
acceptHeaderLocaleResolver.setDefaultLocale(Locale.forLanguageTag(defaultLocale));
if (supportedLocales != null && !supportedLocales.isEmpty()) {
List<Locale> localeList = supportedLocales.stream().map(Locale::forLanguageTag).collect(Collectors.toUnmodifiableList());
acceptHeaderLocaleResolver.setSupportedLocales(localeList);
}
return acceptHeaderLocaleResolver;
}
}

Related

Sending emails in different languages using Thymeleaf

I am sending emails using Thymeleaf. I've set up the templates that I need and it works perfectly. I'm now trying to translate this email depending on the receiver's language. I know the language I need to use through a previously recovered variable.
I'm using properties files to set up the variables that will be translated.
So far, I have three languages: French, English, and Chinese. So I have three properties files :
mail.messages_fr_FR
mail.messages_en_US
mail.messages_zh_CN
I have the following methods in my email configuration class :
#Bean
public ReloadableResourceBundleMessageSource emailMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("mailMessages");
return messageSource;
}
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor);
}
I have enabled the Mvc :
#SpringBootApplication
#EnableWebMvc
public class AccessRequestEventHookApplication extends SpringBootServletInitializer implements WebMvcConfigurer {
I have a method in my email configuration class which is the following :
public void sendMessageUsingThymeleafTemplate(String to, String subject, Map<String, Object> templateModel, String appName)
throws MessagingException {
Context thymeleafContext = new Context();
thymeleafContext.setVariables(templateModel);
String htmlBody;
if (appName == "Test1"){
htmlBody = thymeleafTemplateEngine.process("Test1-template", thymeleafContext);
} else {
htmlBody = thymeleafTemplateEngine.process("Test2-template", thymeleafContext);
}
sendHtmlMessage(to, subject, htmlBody);
}
This function is then called in my controller.
I also have my template resolver and engine as follow :
#Bean
#Primary
public ITemplateResolver thymeleafTemplateResolver() {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("mail-templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML");
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
#Bean
public SpringTemplateEngine thymeleafTemplateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.setTemplateEngineMessageSource(emailMessageSource());
return templateEngine;
}
I tried changing the locale resolver type with cookie, acceptLanguage, it still does not work.
If I change the language on my computer (from French to English for example), it works, the English translation is used. But I can't figure out how to change the locale variable directly in the code.
As it is sent to a user, I can not base myself on their locale.
If I was not clear, please let me know and I will provide any info needed.
Thanks !
You need to set the locale on the Context object you create for Thymeleaf:
Context thymeleafContext = new Context();
thymeleafContext.setLocale(locale);
If you are calling your sendMessageUsingThymeleafTemplate method from a web controller, you can do this as well:
#Controller
public class MyController {
private final ServletContext servletContext;
public MyController(ServletContext servletContext) {
this.servletContext = servletContext;
}
#GetMapping
public void myControllerMethod(HttpServletRequest request,
HttpServletResponse response,
Locale locale) {
WebContext context = new WebContext(request, response, servletContext);
context.setLocale(locale);
service.sendMessageUSingThymeleafTemplate(context, ...);
}
}

Spring boot internalization does not work per request

I know this question might have an answer but am a newbie to spring boot. I need to do internalization to my rest endpoints and I followed this blog to implement internalization but the problem is one. The language does not change per request it only change only after application launch that is suppose at launch in my post man the language was fr that will work but after I change fr to pt (Portuguese) it does not pick pt it still remains with fr. Here is my code that am working with. I have created 4 different messages.properties that is messages_fr.properties, messages_pt.properties, messages_sw.properties under resources dir
#Configuration
public class MyCustomLocaleResolver extends AcceptHeaderLocaleResolver
implements WebMvcConfigurer {
List<Locale> LOCALES = Arrays.asList(
new Locale("en"),
new Locale("pt"),
new Locale("sw"),
new Locale("fr"));
#Override
public Locale resolveLocale(HttpServletRequest request) {
String headerLang = request.getHeader("Accept-Language");
System.out.println("header:"+headerLang);
return headerLang == null || headerLang.isEmpty()
? Locale.getDefault()
: Locale.lookup(Locale.LanguageRange.parse(headerLang), LOCALES);
}
#Bean
public MessageSource messageSource() {
final ResourceBundleMessageSource rs = new ResourceBundleMessageSource();
rs.setDefaultEncoding(StandardCharsets.ISO_8859_1.name());
rs.setBasename("messages");
rs.setUseCodeAsDefaultMessage(true);
return rs;
}
}
And Translator class
#Component
public class LanguageTranslator {
private static ResourceBundleMessageSource messageSource;
#Autowired
LanguageTranslator(ResourceBundleMessageSource messageSource) {
LanguageTranslator.messageSource = messageSource;
}
public static String translate(String msg) {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(msg, null, locale);
}
}
And here is my postman request
and here is messages_fr.properties
and project structure
And here is my controller code
#RestController
#RequestMapping("/home")
public class HomeContoller {
#Autowired
private SystemSettingService settingService;
#GetMapping("/")
public String home() {
return "Home page";
}
#RequestMapping(value = "/demo", method = RequestMethod.POST)
public ResponseEntity all_menu_assignment(HttpServletRequest req) {
return ResponseEntity
.ok().body(new ServerResponse(Common.SUCCESS_CODE,Common.SUCCESS_MESSAGE));
}
}
and here is my Common class
public class Common{
public static String SUCCESS_MESSAGE= LanguageTranslator.translate("SUCCESS_MESSAGE");
}
The LocaleContextHolder holds the locale of the current thread - so referencing this in a static variable will result in the default VM local to be used (because the constant will be initialized during startup).
Something like this should work:
#RestController
#RequestMapping("/home")
public class HomeContoller {
#Autowired
private SystemSettingService settingService;
#GetMapping("/")
public String home() {
return "Home page";
}
#RequestMapping(value = "/demo", method = RequestMethod.POST)
public ResponseEntity all_menu_assignment(HttpServletRequest req) {
return ResponseEntity
.ok().body(new ServerResponse(Common.SUCCESS_CODE, LanguageTranslator.translate("SUCCESS_MESSAGE")));
}
}
Also, it would be better make the translate method in LanguageTranslator non-static and to inject (autowire) the object in the controller.
Translator.toLocale(Common.SUCCESS_MESSAGE) is missing in your controller api method

how to read messages.properties in Annotations

I want to use localization to localize the Swagger Documentation. But I can only provide compile time constants to Annotations. So I'm confused how to provide read messages from messages_**.properties and provide it to annotations.
Message Source:
#Configuration
public class CustomMessageSourceConfig {
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
#Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.ENGLISH);
return slr;
}
}
Reading messages from messages_**.properties:
#Component
public class MessagesByLocaleServiceImpl implements MessagesByLocaleService {
#Autowired
private MessageSource messageSource;
#Override
public String getMessage(String id) {
Locale locale = LocaleContextHolder.getLocale();
return StringEscapeUtils.unescapeJava(messageSource.getMessage(id, null, locale));
}
}
Here is how I'm reading messsages in Java Code:
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(Predicates.not(RequestHandlerSelectors.basePackage("org.springframework.boot"))).build()
.apiInfo(apiInfo())
.tags(new Tag("Netmap Mode Service", messageSource.getMessage(MessageCodes.SWAGGER_WINDOWS_ONLY)));
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title(messageSource.getMessage(MessageCodes.SWAGGER_TITLE))
.description(messageSource.getMessage(MessageCodes.SWAGGER_DESCRIPTION))
.contact(messageSource.getMessage(MessageCodes.SWAGGER_CONTACT)).build();
}
But how can I provide these messages to Swagger Annotations.
#ApiOperation(value = "Add Netmap mode ", notes = "**I want to read properties here**")
#ApiImplicitParams({
#ApiImplicitParam(value = SwaggerSinglePoint.DESC_MODE_NAME, dataType = CDSwaggerPrimitives.STRING, name = SwaggerSinglePoint.MODE_NAME, paramType = CDSwaggerPrimitives.PARAMA_TYPE_QUERY),
#ApiImplicitParam(value = SwaggerSinglePoint.DESC_MODE_BUFFER_SIZE, dataType = CDSwaggerPrimitives.INETEGER, name = SwaggerSinglePoint.BUFFER, paramType = CDSwaggerPrimitives.PARAMA_TYPE_QUERY)})
#RequestMapping(method = RequestMethod.POST, produces = CDConstants.JSON_RESPONSE_DATA_FORMAT, consumes = CDConstants.JSON_REQUEST_DATA_FORMAT)
#SuppressWarnings({ "squid:S3776", "squid:S1319", "unused" })
public String testController(#RequestBody(required = false) HashMap requestParamMap, HttpServletResponse response,
HttpServletRequest request) {
I want to read messages in these annotations. Any guidance or suggestions would be highly appreciated.
It's always better to decouple your documentation comments from your code (reading text from external property file rather than ineserting as plain text)
Use the placeholder like so,instead of
#ApiOperation(value = "Add Netmap mode " ,...)
use
#ApiOperation(value = ${message.addNetMode} ,...)
Here inside "messages_**.properties" file there should be key-value pair
message.addNetMode=Add Netmap mode
Also register the property file in your configuration on class level
#PropertySource("classpath:messages_**.properties")
**Note that values for some annotations might not be supported.Refer docs http://springfox.github.io/springfox/docs/current/#support-for-documentation-from-property-file-lookup
You can get values from application.properties by using SPEL i.e. ${}:
#Annotation(value="${request.reminder.mails.cron.expression}")
Note:- the props name should be complete name from application.properties.

Can not get locale use LocaleContextHolder.getLocale() in Spring Boot

I use Spring boot in my source, and set Locale like bellow:
#Controller
#RequestMapping(value = "/language")
public class LocaleController {
#Bean
public LocaleResolver localeResolver(){
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(new Locale("en", "US"));
return localeResolver;
}
#RequestMapping(value = "/{lang}", method = RequestMethod.GET)
public String changeSessionLanguage(HttpServletRequest request, HttpServletResponse response,
#PathVariable("lang") String lang) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
Locale locale = LocaleContextHolder.getLocale();
if ("zh".equals(lang)) {
localeResolver.setLocale(request, response, new Locale("zh", "CN"));
} else if ("jp".equals(lang)) {
localeResolver.setLocale(request, response, new Locale("ja", "JP"));
} else {
localeResolver.setLocale(request, response, new Locale("en", "US"));
}
return "redirect:/";
}
}
And it works good.
But in Spring security UserDetailsService source, I can't get locale as this:
Locale locale = LocaleContextHolder.getLocale();
It always show my browser language no matter I had changed the locale to what.
How can I get the locale correctly?
Thanks!
UPDATE
I changed code, move LocaleResolver to Configuration class and add RequestContextListener like bellow:
#Configuration
#WebListener
public class Config extends RequestContextListener{
#Bean
public RequestContextListener requestContextListener(){
return new RequestContextListener();
}
#Bean
public LocaleResolver localeResolver(){
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(new Locale("en", "US"));
return localeResolver;
}
}
But it also can't work.

Java, Spring internationalization: how to use values from .properties in simple String?

I use the next code
#Value("${app.user.root}")
private String userRoot;
to get constant value from my application.properties file.
In my GetMapping method I need to redirect to the error page and to pass a String as parameter.
#GetMapping("/user/activate")
public String activate(String activation) {
Users u = usersService.activate(activation);
if (u != null) {
usersService.autoLogin(u);
return "redirect:/";
}
return "redirect:/error?message=Could not activate with this activation code, please contact support";
But I need to have different String values with different languages. So, I am using Spring i18n, but how can I get the value I need at runtime? I need something like this:
return "redirect:/error?message=${errorMessage}";
Thank you, hope you will help me.
First you have to create multiple properties file for multiple languages
messages_en.properties
messages_fr.properties
The configuration of i18n should be following
#Configuration
public class LanguageConfig extends WebMvcConfigurerAdapter {
#Bean
public ReloadableResourceBundleMessageSource messageSource(){
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
#Bean
public LocaleResolver localeResolver() {
SmartLocaleResolver slr = new SmartLocaleResolver();
Locale locale = new Locale("en", "us");
slr.setDefaultLocale(locale); // Set default Locale as en_cos
return slr;
}
#Bean
public LocaleChangeInterceptor localeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang");
return interceptor;
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeInterceptor());
}
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();
}
}
}
Now update your controller code and autowire org.springframework.context.MessageSource and then use it to get localized message.
#Autowired
private MessageSource messageSource;
Then you can get the localized message using following code.
String errorMessage = messageSource.getMessage("project.errorMessage", new Object[]{"John Doe"}, LocaleContextHolder.getLocale());
You can also use Locale object from controller method parameter instead of LocaleContextHolder.getLocale(), but it works just fine.

Categories

Resources