Table of Contents
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.
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.