The most common approach to i18n in Spring Boot is to use the messages.properties file to store all the messages for a specific language. We talked about this topic in the previous blog post on internalization with Spring Boot.
However, this approach depends on the access to the application’s resource files when adding a new supported language or modify the existing message files. In case an end-user is responsible for this job – this is not an optimal approach.
Hence, we are going to explore how to move all of our localized messages to a database. This enables the end-user to add a new language or update existing localized messages at runtime.
First of all, we will walk through all the necessary settings for our project.
As mentioned earlier, we will use Spring Boot, Thymeleaf, Spring Data JPA and H2 in our application. Thus, our pom.xml needs to have all those dependencies. In addition, we also need to declare the project parent to the spring-boot-starter-parent.
Altogether, our pom.xml will be:
Here, we also declare the spring-boot-maven-plugin to add Spring Boot support in Maven.
As usual, the latest Spring Boot version can be found over on Maven Central.
We use a table to store all the localized messages for our application. The table has the following columns:
- id: an auto-increment value
- locale: the language code
- messagekey: the key of the message which is used in the HTML page to refer to the target message
- messagecontent: the content of the message
We put our script in database.sql under the main/resources/data folder:
Let’s say we are going to support English, German and Chinese for our application at the beginning. The sample data for localized messages are as below:
The file data.sql, which contains the script above, is also put in main/resources/data folder.
At this point, we need to tell Spring Boot where to locate our scripts by putting the configuration in the application.properties file under main/resources:
Additionally, we declare the H2 database connection in the same file.
Entity And Repository
Let’s declare the Entity for our table as below:
Furthermore, we create the following repository class to be able to perform CRUD action on the LanguageEntity:
We will need to get a message based on the message key and the locale code. For this reason, we add the method findByKeyAndLocale() in the repository.
I18N with the Database-Stored Message
Up until here, we have our messages in the database. We will explore how to use those messages for localization.
LocaleResolver and LocaleChangeInterceptor
If yo recall what did in the previous blog post on I18N in Spring MVC, we need a LocaleResolver and LocaleChangeInterceptor. We will put these settings in a @Confugration class which implements the WebMvcConfigurer:
The LocaleResolver helps to identify which locale is being used. In this post, we still use CookieLocaleResolver as an example.
Besides, the LocaleChangeInterceptor allows the application to switch to another locale. Here, we will use the request parameter lang to decide the target locale.
Now, we reach the main part of our topic. We are going to create a custom MessageSource called DBMessageSource as below:
The DBMessageSource extends AbstractMessageSource and overrides the method resolveCode. The method accepts two parameters: the message key and the target locale. It returns an instance of MessageFormat. When this method is executed, it invokes the findByKeyAndLocale() method from LanguageRepository to look for the entity which has the matching key and locale code. If no entity is found, it then looks for the message which has the same key and the default locale (en in this example).
As a result, to resolve each message by key and locale, we need to make one call to the database. This absolutely isn’t a good approach in term of performance. Considering this issue, we can add a caching mechanism to our application. For example, using Hibernate Secondary Level Cache with query cache. However, our article is mainly about the solution for internationalization and localization, we will not discuss the caching solution here.
To get back to the topic, please be noted that we need to declare the DBMessageSource as a bean name messageSource. This tells Spring to use our implementation as MessageSource instance whenever in need.
Controller and View
At this point, we declare a HomeController to handle any access to the application root. It returns the index page on execution:
Next, we add below index.html page under main/resources/templates:
The index.html uses Thymleaf Syntax to refer to each message by the key. This is exactly the same as what we have done when using message.properties files for localization. Thus, the presence of DBMessageSource is totally transparent for the View layer.
Running the Application
Now, let’s create below I18NWebMVCApplication before testing our application:
Overall, our project structure will be as below:
Start the application and access http://localhost:8080/, we will see below page:
Click on German to switch language:
As we can see, our application works exactly as the expectation.
To Wrap Things Up
What we did in this article was exploring the possibility of using a database-stored message as a localized message in a Spring Boot application.
We achieved that by creating a custom DBMessageSource and declaring it as messageSource bean. The rest of the configuration is actually the same as what we need when employing the messages.properties file approach. By putting all the messages for supported languages into a database, we open the ability to modify the message content at runtime. Not to mention that we can add a new supported language at runtime as well. More importantly, this job can be done by an end-user without the need for accessing project resources.
Although the database approach may bring a concern about the performance, we can overcome this by applying a cache at the persistence layer.
Finally, we can find the whole project on our GitHub.