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>
   * &#64;NotEmpty(message = "{email.notempty}")<br>
   * &#64;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());
  }

}