MessageSource helps application developers handle various complex scenarios, such as environment-scpecific configuration, internationalization or configurable values.
https://www.baeldung.com/spring-custom-validation-message-source
https://www.baeldung.com/spring-boot-internationalization
<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>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. */@Configurationpublic 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; }}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@Servicepublic 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; }}Properties file must be created in the src/main/resources directory with the name provided in the basename in MessageSourceConfig.java
# messages.propertiesemail.nonempty=Please provide a valid email address# messages_pl.propertiesemail.nonempty=Prosimy o wprowadzenie poprawnego adresu e-mailowegopublic 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.
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> * */@Configurationpublic 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()); }}