Spring WebFlux Rest API internationalization i18n
--
The purpose of this story is to explain how to configure and handles internationalization in a Spring WebFlux Rest API.
· Prerequisites
· Overview
· What is Internationalization?
· Getting Started
∘ MessageSource Bean
∘ Custom class for LocaleContextResolver
∘ MessageTranslator class
∘ Translation files
· Testing
· Conclusion
· References
Prerequisites
This is the list of all the prerequisites for following this story:
- Java 17
- Spring Boot / Starter WebFlux 3.0.5
- Maven 3.6.3
- Postman
Overview
When you produce public APIs, you don’t know which clients will consume your services, so it’s important to standardize your API’s return messages so you can communicate effectively with your consumers.
The Java Platform provides a rich set of APIs for developing global applications. These internationalization APIs are based on the Unicode standard and include the ability to adapt text, numbers, dates, currency, and user-defined objects to any country’s conventions.
What is Internationalization?
If you internationalize, you design or develop your content, application, specification, and so on, in a way that ensures it will work well for, or can be easily adapted for, users from any culture, region, or language.
The word ‘Internationalization’ is often abbreviated to ‘i18n’. This is widely used abbreviation, derived from the fact that there are 18 letters between the ‘i’ and the ’n’. — https://www.w3.org/standards/webdesign/i18n
Getting Started
We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Reactive Web, Lombok, and Validation.
MessageSource Bean
MessageSource is a strategy interface for resolving messages, with support for the parameterization and internationalization of such messages.
Spring provides two out-of-the-box implementations for production:
ResourceBundleMessageSource
: built on top of the standardResourceBundle
, sharing its limitations.ReloadableResourceBundleMessageSource
: highly configurable, in particular with respect to reloading message definitions.
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:messages");
messageSource.setDefaultEncoding("ISO-8859-1");
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
The important thing is the basename because it specifies an array of locale filenames that will resolve to the name provided. Regular and XML properties files are supported: e.g. “messages” will find a “messages.properties”, “messages_en.properties” etc arrangement as well as “messages.xml”, “messages_en.xml” etc.
Custom class for LocaleContextResolver
We need to create a custom class that implements the LocaleContextResolver interface where we’ll override resolveLocaleContext which resolves the current locale context via the given request.
@Component
public class LocaleResolver implements LocaleContextResolver {
@Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
String language = exchange.getRequest().getHeaders().getFirst("Accept-Language");
return new SimpleLocaleContext(StringUtils.isNotEmpty(language) ? Locale.forLanguageTag(language) : Locale.getDefault());
}
@Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
throw new UnsupportedOperationException("Not Supported");
}
}
MessageTranslator class
Let’s create a new MessageTranslator class responsible for choosing the correct message based on the specified locale.
@Component
public class MessageTranslator {
private static MessageSource messageSource;
private static LocaleResolver localeResolver;
@Autowired
MessageTranslator(MessageSource messageSource, LocaleResolver localeResolver) {
MessageTranslator.messageSource = messageSource;
MessageTranslator.localeResolver = localeResolver;
}
/**
* @param msg message code that should be translated.
* @param args message parameters
* @return translated message
*/
public static String getMessage(String msg, Object[] args, ServerWebExchange exchange) {
LocaleContext localeContext = localeResolver.resolveLocaleContext(exchange);
return messageSource.getMessage(msg, args, msg, Objects.requireNonNull(localeContext.getLocale()));
}
/**
* @param msg message code that should be translated.
* @param args message parameters
* @return translated message
*/
public static String getMessage(String msg, Object[] args) {
LocaleContext localeContext = new SimpleLocaleContext(Locale.getDefault());
return messageSource.getMessage(msg, args, msg, Objects.requireNonNull(localeContext.getLocale()));
}
/**
* @param msg message code that should be translated.
* @return translated message
*/
public static String getMessage(String msg, ServerWebExchange exchange) {
LocaleContext localeContext = localeResolver.resolveLocaleContext(exchange);
return messageSource.getMessage(msg, null, msg, Objects.requireNonNull(localeContext.getLocale()));
}
/**
* @param msg message code that should be translated.
* @return translated message
*/
public static String getMessage(String msg) {
LocaleContext localeContext = new SimpleLocaleContext(Locale.getDefault());
return messageSource.getMessage(msg, null, msg, Objects.requireNonNull(localeContext.getLocale()));
}
}
Translation files
We can add translation files in the project resources directory.
messages.properties is the default translation file when no match message file was found.
Now, we can set custom messages in our API.
MessageTranslator.getMessage("internal.server.exception", echange)
without parameters.MessageTranslator.getMessage("error.id.not.found", arg,echange)
with argument parameters in the translator.
Testing
We are all done with our code. We can run our application and test it.
Conclusion
In this story, We have seen how to configure and handle internationalization in a Spring WebFlux Rest API.
The complete source code is available on GitHub.