MessageSource (Spring Boot)
Introduction
MessageSource helps application developers handle various complex scenarios, such as environment-scpecific configuration, internationalization or configurable values.
References
- Custom Validation MessageSource in Spring Boot
https://www.baeldung.com/spring-custom-validation-message-source
- Guide to Internationalization in Spring Boot
https://www.baeldung.com/spring-boot-internationalization
Configuration
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
MessageSourceConfig.java
package edu.cou.app;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
/**
* I18n configuration (MessageSource).<br>
* <br>
* An application context delegates the message resolution to a bean with the exact name
* messageSource.
*/
@Configuration
public class MessageSourceConfig {
/**
* ReloadableResourceBundleMessageSource is the most common MessageSource implementation that
* resolves messages from resource bundles for different locales.
*
* @return The MessageSource instance
*/
@Bean
public MessageSource messageSource() {
val messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
/**
* To use custom name messages in a properties file like we need to define a
* LocalValidatorFactoryBean and register the messageSource.<br>
* <br>
* Example:<br>
*
* <pre>
* @NotEmpty(message = "{email.notempty}")<br>
* @Email<br>
* private String email;<br>
* </pre>
*
* @return The LocalValidatorFactoryBean instance
*/
@Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
}
I18nService.java
package edu.cou.app.service;
import java.util.Locale;
import javax.inject.Inject;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import edu.cou.app.exception.AppException;
import lombok.extern.slf4j.Slf4j;
/**
* i18n service.
*/
@Slf4j
@Service
public final class I18nService {
/** i18n literals */
private MessageSource messageSource;
/**
* Constructor.
*
* @param messageSource I18n message source
*/
@Inject
public I18nService(final MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* $(key, langIso639p1Code, arguments).<br>
* <br>
* Sample pattern: "S''ha detectat que la petició {0} és repetida"<br>
* where {0} will be replaced by the actual argument.<br>
* <br>
*
* @param key Eg: "argument_blank"
* @param langIso639p1Code Eg: "en"
* @param args Nullable. N arguments for replacing pattern place holders {0}, {1},
* etc.
* @return parameterized translated text
* @throws AppException -
*/
@SuppressWarnings({ "checkstyle:methodname" })
public String $( // NOSONAR
final String key, final String langIso639p1Code, @Nullable final Object... args) {
//
String ret;
try {
ret = this.messageSource.getMessage(key, args, this.getLocale(langIso639p1Code));
} catch (NoSuchMessageException t) {
log.warn(String.format("No message exists for key=%s, lang=%s", key, langIso639p1Code), t);
ret = key;
}
return ret;
}
/**
* $(key, langIso639p1Code).<br>
* <br>
* <b>Eclipse config</b> | Preferences > Java > Compiler > Error/Warnings > Unhandled token in
* '@SuppresWarnings': Ignore<br>
* <br>
*
* @param key Eg: "http_504"
* @param langIso639p1Code Eg: "cmn"
* @return Message in lang 'langIanaCode' or, if not found, the key
*/
@SuppressWarnings({ "checkstyle:methodname" })
public String $(final String key, final String langIso639p1Code) { // NOSONAR
return this.$(key, langIso639p1Code, (Object[]) null);
}
/**
* $(key).<br>
*
* @param key Eg: "argument_blank" translated in default language
* @return fixed translated text
*/
@SuppressWarnings({ "checkstyle:methodname" })
public String $(final String key) { // NOSONAR
return this.$(key, LangService.DEFAULT_ISO639P1_CODE, (Object[]) null);
}
/**
*
* @param langIso639p1Code
* @return Locale for 'langIso639p1Code'
*/
private Locale getLocale(final String langIso639p1Code) {
Locale ret;
if (LangService.ENGLISH_ISO639P1_CODE.equals(langIso639p1Code)) {
ret = Locale.ENGLISH;
} else if (LangService.CASTILIAN_ISO639P1_CODE.equals(langIso639p1Code)) {
ret = new Locale("es", "ES");
} else if (LangService.CATALAN_ISO639P1_CODE.equals(langIso639p1Code)) {
ret = new Locale("ca", "ES");
} else {
log.warn("App does not have message source for langIso639p1Code=" + langIso639p1Code);
ret = this.getLocale(LangService.DEFAULT_ISO639P1_CODE);
}
return ret;
}
}
Property files
Properties file must be created in the src/main/resources directory with the name provided in the basename in MessageSourceConfig.java
# messages.properties
email.nonempty=Please provide a valid email address
# messages_pl.properties
email.nonempty=Prosimy o wprowadzenie poprawnego adresu e-mailowego
Example
public class Example {
@NotEmpty(message = "{email.nonempty}")
@Email
private email;
}
The example adds validation constrains that verify if an email is not provided at all, or provided, but not following the standard email address style.
For demostrating custom and locale-specific message, the @NotEmpty annotation provides the placeholder {email.nonempty}.
The email.nonempty property will be resolved from a properties files by the MessageSource configuration.
Locale change interceptor
We need to add an interceptor bean that will switch to a new locale based on the value of the lang parameter of REST operations requests.
In order to do so, see "Guide to Internationalization in Spring Boot" by Baeldung:
https://www.baeldung.com/spring-boot-internationalization#localechangeinterceptor
Example:
package edu.cou.app;
import javax.inject.Inject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
/**
* Determine which locale is currently being used.<br>
* It allows, eg, that the validation constrain annotations pick up the proper MessageSource
* language.<br>
*
*/
@Configuration
public class LocaleConfig implements WebMvcConfigurer {
private I18nService i18nService;
/**
* In order for our application to be able to determine which locale is currently being used.
*
* @return -
*/
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.ENGLISH);
return slr;
}
/**
* Add an interceptor bean that will switch to a new locale based on the value of the "lang" query
* string parameter appended to a request.<br>
* Eg: /entities/27?lang=pl<br>
* <br>
* Note: In order to take effect, this bean needs to be added to the application's interceptor
* registry
*
* @return -
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
/**
* Register the LocaleChangeInterceptor
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}