Using MessageSource In Spring Boot Application

Overview

Nowadays, creating applications that serve users from different countries with different languages and dialects is a common practice. How to make your application friendly to users across the globle? That’s what we are going to find out today.

I18n and l10n

Internationalization (i18n) is the process of designing your software ready to serve people from different languages and regions (think translation and, not limited to, currency format).

Localization (l10n) is the process of making your software display correctly to specific language and region.

Sounds too complex? How about some examples.

Internationalization

If your software is written like this:

System.out.println("User not found")

There is no way to make this message understandable by users who don’t understand English.

However, if you write:

String errorMessage = messageSource.getMessage("user.not.found", new Object[]{id}, locale);
System.out.println(errorMessage)

Then, with the appropriate setup, your application can display different text to different users based on their locale.

Localization

Localization, think simply in the view of a software developer is to provide the language, format that used in a specific locale. For example, your application simply say hello to the users. Let’s say it currently supports two locales: common english and common german. Here is the code the get the messages:

        Locale locale = LocaleContextHolder.getLocale();
        String greeting = messageSource.getMessage("greeting", null, locale);
        String dateFormat = messageSource.getMessage("date.format", null, locale);
        String numberFormat = messageSource.getMessage("number.format", null, locale);

In your resource files, you prepare two files:

messages.properties (default to english)

greeting=Hello
date.format=MM/dd/yyyy
number.format=#,##0.## 

messages_de.properties

greeting=Hallo
date.format=dd.MM.yyyy
number.format=#,##0.##

For english users, the message would be like this:

 "greeting": "Hello",
 "date": "06/29/2024",
 "number": "12,345.68"

But the german users would see this text:

"greeting": "Hallo",
"date": "29.06.2024",
"number": "12.345,68"

Still confusing? Let’s get your hand dirty to make your mind less cloudy.

MessageSource in spring boot

Key concepts

MessageSource

MessageSource is an interface in Spring framework. Its main purpose is to resolving messages. It allows you to get messages from a resource bundle based on a message code and the current locale.

Message Bundles

Message Bundles are sets of resource files containing localized messages.

Common MessageSource implementations

ResourceBundleMessageSource:

Loads messages from resource bundles (properties files).

ReloadableResourceBundleMessageSource:

Extends ResourceBundleMessageSource with the capability to reload properties files without restarting the application.

Structure of message bundles

The message bundles have a base property file serves as the default file, usually English. Specific locales have the file name set accordingly

messages.properties

messages_en.properties

messages_de.properties

messages_en_GB.properties

MessageSource basic example

Let’s consider an example where you need to write an api that serve English and German. The api simply says hello in the user’s language.

Configure the MessageSource bean

Before using the MessageSource bean, you need to configure one. It can be simple like this:

@Configuration
public class LocaleConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}

Next, let’s prepare the resource bundle files:

messages_de.properties

greeting=Hallo
farewell=Auf Wiedersehen
date.format=dd.MM.yyyy
number.format=#,##0.## 

messages.properties

greeting=Hello there
farewell=Goodbye
date.format=MM/dd/yyyy
number.format=#,##0.##

Now let’s create a controller to serve the greeting message:

@RestController
@RequestMapping("/hello")
public class HelloController {

    private final MessageSource messageSource;

    public HelloController(MessageSource messageSource) {

        this.messageSource = messageSource;
    }


    @GetMapping
    public String hello(Locale locale) {
        return messageSource.getMessage("greeting", null, locale);
    }
}

Now, if you send a request to the endpoint, you will see the greeting in english.

How do you get the german greeting? In the http request, specify ‘Accept-Language’ header has ‘de’

Specifying Locale

When making requests to the backend, the client has multiple way to specify the locale. One is using the Accept-Language as mentioned above. The other one is to use a query param.

Let’s see how you can use query param to specify the locale.

@Configuration
public class LocaleConfig implements WebMvcConfigurer {

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.ENGLISH); // Set a default locale if none is provided
        return localeResolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}

Using this, you can specify the locale in the URL:

The current implementation only let you use ‘lang’ to specify the locale, not the accept-header. If you want to have a more flexible way to handle the locale (lang in the url is the main resolver but fall back to accept-language when lang is not available), you can create a custom resolver.

Creating custom resolver

With the following resolver, you can have the fallback strategy as mentioned above:

public class CustomLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String lang = request.getParameter("lang");
        if (StringUtils.hasText(lang)) {
            return Locale.forLanguageTag(lang);
        }

        String acceptLanguageHeader = request.getHeader("Accept-Language");
        if (StringUtils.hasText(acceptLanguageHeader)) {
            return request.getLocale();
        }

        return Locale.getDefault();
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

Cheat Sheet for Writing Messages in Message Bundles

Here is the cheat sheet for writing messages in message bundles

Basic usage

# messages.properties
greeting=Hello
farewell=Goodbye

Parameterized Messages

You can use placeholder and replace them with variables in code

# messages.properties
welcome.user=Welcome, {0}!
item.count=You have {0} items in your cart.
order.status=Order {0} is {1}.
String message = messageSource.getMessage("welcome.user", new Object[]{"John"}, locale);
String message2 = messageSource.getMessage("order.status", new Object[]{"12345", "shipped"}, locale);

Default Messages

You can provide a default message when the message key is not found:

String message = messageSource.getMessage("non.existent.key", null, "Default Message", locale);

Multiline Messages

Your messages don’t have to be in one line. If you want to have multiple lines message, use the backslash to separate lines.

# messages.properties
long.message=This is a very long message that needs to be broken \
into multiple lines for better readability.

Escaping Special Characters

You can also use the backslash to escape special characters

# messages.properties
special.chars=This message contains special characters: \{, \}, \=, \:, \!, \#

Using Spring Expression Language (SpEL)

# messages.properties
dynamic.message=Today is #{T(java.time.LocalDate).now()}

Conclusion

In this post, I’ve shown you how to use message sources for internationalization in your spring boot application.

Leave a Comment