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:
- Creating our message service
- Adding a resource bundle for all the languages we want to support
- Adding variables to the application properties
- 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);
}
}
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())
}
}
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.
messages.properties
## GENERIC MESSAGES
generic.message.i18n=Hey, this is a message in english.
messages_en.properties
## GENERIC MESSAGES
generic.message.i18n=Hey, this is a message in english.
messages_es.properties
## GENERIC MESSAGES
generic.message.i18n=Hey, este es un mensaje en español.
messages_fr.properties
## GENERIC MESSAGES
generic.message.i18n=Salut, c'est un message en français.
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
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"));
}
}
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"))
}
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");
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 (1)
I get property files for labels and headings in a browser app. But how do you best handle application data retrieved from the DB for drop downs and other code description tables.
More tables or fields with alternative languages? Or is there a more clever way? We have a java spring boot app.