DEV Community

Cover image for Internationalization: a practical example with Spring Boot
Juan M. Altamirano for Cloud(x);

Posted on • Updated on

Internationalization: a practical example with Spring Boot

If you started coding a long time ago in a not so far away galaxy, the idea of being globally connected and exposing your web services to the world was only a dream. And that’s why you probably wouldn't consider adding internationalization (I18N) to your service.

Over the years and with global exposure in mind, frameworks started to include tools to ease the addition of I18N in our APIs.


What is I18N


You may already know what I18N is, but for those of you who are thinking of taking a quick peek at Wikipedia to find out, here's a brief definition instead:

“Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes.”


A practical example: ResourceBundleMessageSource


Needless to say, each framework has its own way to tackle I18N.
In this post, we will see how easy it is to add I18N to our API, by reviewing a possible solution for Spring Boot.
Considering that the solution is very simple, I will show you examples in Java and Kotlin.

Overall process

The process consists of four steps:

  1. Creating our message service
  2. Adding a resource bundle for all the languages we want to support
  3. Adding variables to the application properties
  4. Start using our message service

1. Creating the service

Let's start by creating a class that will work as a message service.

Java
@Service
public class MessageService {

    @Autowired
    private ResourceBundleMessageSource messageSource;

    @Value("${custom.app.locale}")
    private String systemLanguage;

    private Locale locale;

    @PostConstruct
    private void init() {
        locale = new Locale(systemLanguage);
        LocaleContextHolder.setDefaultLocale(locale);
    }

    public String getMessage(String prefix, String key){
        return getMessage(prefix, key, "");
    }

    public String getMessage(String prefix, String key, String value){
        List<String> listOfValues = Collections.singletonList(value);
        return getMessage(prefix, key, listOfValues);
    }

    public String getMessage(String prefix, String key, List<String> args){
        return messageSource.getMessage(concatPrefixAndKey(prefix, key), args.toArray(), locale);
    }

    public String getMessage(String prefix, String key, List<String> args, Locale requestedLocale){
        if (requestedLocale == null){
            return getMessage(prefix, key, args);
        } else {
            return messageSource.getMessage(concatPrefixAndKey(prefix, key), args.toArray(), requestedLocale);
        }
    }

    public String getRequestLocalizedMessage(String prefix, String key){
        return getRequestLocalizedMessage(prefix, key, new ArrayList<>());
    }

    public String getRequestLocalizedMessage(String prefix, String key, List<String> args){
        return getMessage(prefix, key, args, LocaleContextHolder.getLocale());
    }

    private String concatPrefixAndKey(String prefix, String key){
        return prefix.concat(".").concat(key);
    }
}
Enter fullscreen mode Exit fullscreen mode
Kotlin
@Service
class MessageService {
    @Autowired
    private lateinit var messageSource: ResourceBundleMessageSource

    @Value("\${app.locale}")
    private lateinit var localLanguage: String

    private lateinit var locale: Locale

    @PostConstruct
    fun init() {
        locale = Locale(localLanguage)
        LocaleContextHolder.setDefaultLocale(locale)
    }

    fun getMessage(prefix: String, key: String): String {
        return getMessage(prefix, key, emptyList())
    }

    fun getMessage(prefix: String, key: String, value: String): String {
        return getMessage(prefix, key, listOf(value))
    }

    fun getMessage(prefix: String, key: String, args: List<String>): String {
        return messageSource.getMessage("$prefix.$key", args.toTypedArray(), locale)
    }

    fun getMessage(prefix: String, key: String, args: List<String>, locale: Locale?): String {
        return locale?.let {
            messageSource.getMessage("$prefix.$key", args.toTypedArray(), locale)
        } ?: getMessage(prefix, key, args)
    }

    fun getRequestLocalizedMessage(prefix: String, key: String): String {
        return getRequestLocalizedMessage(prefix, key, emptyList())
    }

    fun getRequestLocalizedMessage(prefix: String, key: String, args: List<String>): String {
        return getMessage(prefix, key, args, LocaleContextHolder.getLocale())
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Adding the messages

Now, we will create a resource bundle inside our resources folder and we will add a properties file for each language we want to support.

Resource bundle menu

Resource bundle creation

messages.properties
## GENERIC MESSAGES
generic.message.i18n=Hey, this is a message in english.
Enter fullscreen mode Exit fullscreen mode
messages_en.properties
## GENERIC MESSAGES
generic.message.i18n=Hey, this is a message in english.
Enter fullscreen mode Exit fullscreen mode
messages_es.properties
## GENERIC MESSAGES
generic.message.i18n=Hey, este es un mensaje en español.
Enter fullscreen mode Exit fullscreen mode
messages_fr.properties
## GENERIC MESSAGES
generic.message.i18n=Salut, c'est un message en français.
Enter fullscreen mode Exit fullscreen mode

3. Updating the application.properties

Here we will add two variables, one for the default language and one for the application encoding.

# The default locale for our API
custom.app.locale=fr

# This sets the Spring boot encoding used for the API responses
spring.messages.encoding=ISO-8859-1
Enter fullscreen mode Exit fullscreen mode

4. Using the service

And the last step, we need to wire our message service and use it. For this, I created a simple controller that calls the getRequestLocalizedMessage method.
The method uses the LocalContextHolder to detect the Accept-Language header. If the header is present, it returns the response in the requested language. If not, it returns the message in the default language.

Java
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private MessageService messageService;

    private final String prefixKey = "generic.message";

    @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<String> getTest(){
        return ResponseEntity.status(HttpStatus.OK).body(messageService.getRequestLocalizedMessage(prefixKey, "i18n"));
    }
}
Enter fullscreen mode Exit fullscreen mode
Kotlin
@RestController
@RequestMapping("/test")
class TestController {

    @Autowired
    private lateinit var messageService: MessageService

    private val prefixKey = "generic.message"

    @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
    fun getTest(): ResponseEntity<String> = ResponseEntity
        .status(HttpStatus.OK)
        .body(messageService.getRequestLocalizedMessage(prefixKey, "i18n"))
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use the other methods in the MessageService to achieve the same goal.

// You can pass the Locale as an argument to the getMessage function to get the message in a specific language

Locale requestedLocale = new Locale("es");
messageService.getMessage(prefixKey, "i18n", new ArrayList<>(), requestedLocale);

// OR

// You can use the getMessage without a specific locale to get the message in the default language
messageService.getMessage(prefixKey, "i18n");
Enter fullscreen mode Exit fullscreen mode

Final comments

As we’ve seen in this post, adding I18N to our API is very simple. So, if you are thinking about creating APIs to be globally consumed (and if you are not already doing it), it is not a bad idea to add this to your other good practices (clean code, documentation, testing…).

If you are working on the JVM platform and using Spring Boot in your projects, I hope this post was helpful.
If you are working with other frameworks/languages, don’t worry, the internet is full of examples and information that will help you to implement it in your project.

Top comments (0)