Motivation
In Java world there is a common approach to store localisation messages in property files. I want to have messages stored in database so users can manage them on runtime. I also want better plural forms handling provided by project ICU.
Introduction to Spring's localisation process
Following figure shows localisation processing in a Spring application.

-
Requested locale (language and country codes) is passed to the Spring application as part of
ServletRequest
object. InServletRequest
there are several properties that can hold locale value. For example HTTP headerAccept-Language
, a cookie or query string parameter. Locale is resolved from the request object inDispatcherServlet.render
method by instance ofLocaleResolver
class. There are several resolvers available in the Spring. You can check outorg.springframework.web.servlet.i18n
package for more details. -
Locale value is send by user's browser by default (as a
Accept-Language
header). To allow your application to change locale you have to configureLocaleChangeInterceptor
. The interceptor readslocale
value from query string and sets it to the request. Please read my older article Request mapping on demand in Spring MVC if you want to know more about request processing. -
Message code and resolved locale are passed to
MessageSource
via a view. Message source is responsible for loading message from storage, processing it and returning localised message back to the view. The view incorporates processed message into template. -
A message can be simple text or a pattern consisted of placeholders that will be replaced while pattern is processed. Placeholders are used to render text in a locale sensitive way. Patterns are processed by
java.text.MessageFormat
by default.In this article I will also describe enhanced version of message format (
com.ibm.icu.text.MessageFormat
) provided by ICU.
LocaleChangeInterceptor and LocaleResolver configuration
AcceptHeaderLocaleResolver
is Spring Boot's default locale resolver. The resolver reads locale from Accept-Language
header. Unfortunately it does not support locale change on runtime because it's setLocale
method is not implemented.
No locale change interceptor is configured by default.
Locale resolver can be changed by creating LocaleResolver
bean. I've decided to use CookieLocaleResolver
which resolves locale stored in a cookie.
Locale change interceptor can be added by extending WebMvcConfigurerAdapter
. Following code snippet shows configuration of locale change interceptor that reads new language code from query string. By adding lang
parameter to a query string user can change requested locale.
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
@Bean
LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.US);
return resolver;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang"); // Query string parameter name
registry.addInterceptor(localeChangeInterceptor);
super.addInterceptors(registry);
}
}
Database aware MessageSource
To create message source you just need to implement MessageSource
interface.
@Component
public class DatabaseMessageSource implements MessageSource {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
return resolveMessage(code, args, locale);
}
@Override
public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
return resolveMessage(code, args, locale);
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
for (String code : resolvable.getCodes()) {
String message = resolveMessage(code, resolvable.getArguments(), locale);
if (message != null) {
return message;
}
}
return null;
}
private String resolveMessage(String code, Object[] args, Locale locale) {
return ""; // TODO Load message from database...
}
}
And then configure template engine so it will load messages from newly created message source. Thymeleaf template engine is used in this article.
@Configuration
public class ThymeleafConfiguration {
@Autowired
private DatabaseMessageSource databaseMessageSource;
@Bean
public SpringTemplateEngine thymeleafTemplateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
// ...
engine.setTemplateEngineMessageSource(databaseMessageSource);
return engine;
}
}
Message patterns and plural forms handling
Plural forms handling is very common case in multilingual applications. Plural forms handling has to be robust because some languages have pretty complicated plural rules. Here is a example of three sentences that should be generated from a single message pattern:
- There is 1 apple in 1 basket.
- There are 2 apples in 2 baskets.
- There are 0 apples in 2 baskets.
Standard MessageFormat
To deal with plurals Java offers MessageFormat
formatter. This formatter can evaluate conditions in message pattern which can be used for plural forms handling. Let's revisit DatabaseMessageSource.resolveMessage
method to incorporate the formatter into the application.
private String resolveMessage(String code, Object[] args, Locale locale) {
String message = ""; // TODO Load message from database...
MessageFormat messageFormat = new MessageFormat(message, locale)
return messageFormat.format(args);
}
Then you need to create a message pattern with choice
conditions. Curly braces are used as a placeholders for message parameters and can contain message formatting syntax.
There {0,choice,0#are|1#is|2#are} {0} {0,choice,0#apples|1#apple|2#apples} in {1} {1,choice,0#baskets|1#basket|2#baskets}.
To pass values into message you just need to add braces with values at the end of message code.
<p th:text="#{apple.message(1,1)}"></p>
<p th:text="#{apple.message(2,2)}"></p>
<p th:text="#{apple.message(0,2)}"></p>
Shown example is official Java recommended approach for plural forms handling. Unfortunately it's choice conditions are quite complicated even for English language which has only two plural forms. For languages like Polish it's almost unreadable.
IBM ICU MessageFormat
Project ICU focus on dealing with internationalisation. It offers its own implementation of MessageFormat
that is more robust and easier to use. To incorporate this formatter to the application you need to slightly change DatabaseMessageSource.resolveMessage
method.
private String resolveMessage(String code, Object[] args, Locale locale) {
String message = ""; // TODO Load message from database...
MessageFormat messageFormat = new MessageFormat(message, locale);
StringBuffer formattedMessage = new StringBuffer();
messageFormat.format(args, formattedMessage, null);
return formattedMessage.toString();
}
For plural forms handling ICU formatter offers purpose built plural
pattern argument.
There {0,plural,one{is # apple}other{are # apples}} in {1,plural,one{# basket}other{# baskets}}.
Localised URLs
In the Request Mapping on Demand article I've described possibilities of URL localisation on the fly.
Conclusion
There are several more internationalisation approaches available in Java world. For example there is possibility to use GNU gettext in Java. But it's not that easy to implement it in Spring. I've done some research and found out that combination of ICU and message codes seems to be the best choice.