Software localization

Database-Stored Messages for I18n in Spring Boot

Having more alternatives is always good: Let's explore the possibility of using a database to store localized messages for i18n in Spring Boot.
Software localization blog category featured image | Phrase

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.

Project Setup

First of all, we will walk through all the necessary settings for our project.

Dependency

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:

...

<parent>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-parent</artifactId>

    <version>2.1.1.RELEASE</version>

    <relativePath/>

</parent>

<dependencies>

    <dependency>

        <groupId>com.h2database</groupId>

	<artifactId>h2</artifactId>

    </dependency>

    <dependency>

	<groupId>org.springframework.boot</groupId>

	<artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

	<artifactId>spring-boot-starter-thymeleaf</artifactId>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

	<artifactId>spring-boot-starter-data-jpa</artifactId>

    </dependency>

</dependencies>

<build>

    <finalName>spring-boot-db-messageresource</finalName>

    <plugins>

        <plugin>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-maven-plugin</artifactId>

        </plugin>

    </plugins>

</build>

...

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.

Database

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:

create table IF NOT EXISTS languages (

	id integer auto_increment,

	locale varchar(2),

	messagekey varchar(255),

	messagecontent varchar(255),

	primary key (id)

);

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:

INSERT INTO languages (locale, messagekey,messagecontent) VALUES

('en', 'home.welcome','Welcome'),

('en', 'home.info','This page is displayed in English.'),

('en', 'home.changelanguage','Supported languages : '),

('en', 'home.lang.en','English'),

('en', 'home.lang.de','German'),

('en', 'home.lang.zh','Chinese'),

('de', 'home.welcome','Welcome'),

('de', 'home.info','Diese Seite wird in deutscher Sprache angezeigt.'),

('de', 'home.changelanguage','Unterstützte Sprachen : '),

('de', 'home.lang.en','Englisch'),

('de', 'home.lang.de','Deutsch'),

('de', 'home.lang.zh','Chinesisch'),

('zh', 'home.welcome','Welcome'),

('zh', 'home.info','此頁面以中文顯示.'),

('zh', 'home.changelanguage','支持的語言  : '),

('zh', 'home.lang.en','英語'),

('zh', 'home.lang.de','德語'),

('zh', 'home.lang.zh','普通話')

;

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:

spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE

spring.datasource.username=sa

spring.datasource.password=

spring.datasource.driverClassName=org.h2.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect

spring.h2.console.path=/myconsole

spring.h2.console.enabled=true

spring.datasource.initialize=true

spring.datasource.schema=classpath:data/database.sql

spring.datasource.data=classpath:data/data.sql

Additionally, we declare the H2 database connection in the same file.

Entity And Repository

Let's declare the Entity for our table as below:

@Entity

@Table(name = "languages")

public class LanguageEntity {

    @GeneratedValue(strategy = GenerationType.AUTO)

    @Id

    @Column

    private Integer id;

    @Column

    private String locale;

    @Column(name = "messagekey")

    private String key;

    @Column(name = "messagecontent")

    private String content;

  //Getter & Setter

}

Furthermore, we create the following repository class to be able to perform CRUD action on the LanguageEntity:

@Repository

public interface LanguageRepository extends JpaRepository<LanguageEntity, Integer> {

    LanguageEntity findByKeyAndLocale(String key, String locale);

}

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:

@Configuration

public class WebConfig implements WebMvcConfigurer {

    @Bean

    public LocaleResolver localeResolver() {

        return new CookieLocaleResolver();

    }

    @Override

    public void addInterceptors(InterceptorRegistry registry) {

        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();

        localeChangeInterceptor.setParamName("lang");

        registry.addInterceptor(localeChangeInterceptor);

    }

}

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.

Custom DBMessageSource

Now, we reach the main part of our topic. We are going to create a custom MessageSource called DBMessageSource as below:

@Component("messageSource")

public class DBMessageSource extends AbstractMessageSource {

    @Autowired

    private LanguageRepository languageRepository;

    private static final String DEFAULT_LOCALE_CODE = "en";

    @Override

    protected MessageFormat resolveCode(String key, Locale locale) {

        LanguageEntity message = languageRepository.findByKeyAndLocale(key,locale.getLanguage());

	if (message == null) {

	    message = languageRepository.findByKeyAndLocale(key,DEFAULT_LOCALE_CODE);

	}

	return new MessageFormat(message.getContent(), locale);

    }

}

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:

@Controller

public class HomeController {

    @RequestMapping("/")

    public String welcome(Map<String, Object> model) {

        return "index";

    }

}

Next, we add below index.html page under main/resources/templates:

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<title>I18N Spring Boot</title>

</head>

<body>

	<h2 data-th-text="#{home.welcome}"></h2>

	<p data-th-text="#{home.info}"></p>

	<p data-th-text="#{home.changelanguage}"></p>

	<ul>

		<li><a href="?lang=en" data-th-text="#{home.lang.en}"></a></li>

		<li><a href="?lang=de" data-th-text="#{home.lang.de}"></a></li>

		<li><a href="?lang=zh" data-th-text="#{home.lang.zh}"></a></li>

	</ul>

</body>

</html>

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:

@SpringBootApplication

public class I18NWebMVCApplication {

    public static void main(String[] args) {

        SpringApplication.run(I18NWebMVCApplication.class, args);

    }

}

Overall, our project structure will be as below:

Project structure | Phrase

Start the application and access http://localhost:8080/, we will see below page:

localhost:8080 demo page English | Phrase

Click on German to switch language:

localhost:8080 demo page German | Phrase

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.