Bài viết hướng dẫn xử lý hiển thị text và các message thông báo cho từng ngôn ngữ khác nhau

1. Giới thiệu

Xử lý hiển thị đa ngôn ngữ là vấn đề thường gặp trong phát triển website, đa ngôn ngữ giúp website của bạn được tiếp xúc và sử dụng bởi nhiều đối tượng người dùng hơn. Đa ngôn ngữ hay còn gọi là quốc tế hóa website, ví dụ website của bạn chỉ đang cung cấp dịch vụ cho khách hàng Tiếng Việt và bạn có nhu cầu mở rộng ra phạm vi khách hàng quốc tế, thì việc đầu tiên cần làm cho website chính là chuyển thể nội dung đang có sang phiên bản Tiếng Anh. Ở đây chúng ta đang đề cập đến việc hỗ trợ thêm 1 ngôn ngữ cho khách hàng quốc tế, chứ không phải mở thêm 1 website nữa, như cách 1 số website vẫn hay làm, ví dụ: vnexpress.vn.

Spring Framework cung cấp chúng ta các công cụ để làm website hỗ trợ nhiều ngôn ngữ khác nhau. Không chỉ hỗ trợ chuyển đổi cho các dạng text hiển thị mà Spring còn hỗ trợ cho các message validation, bài viết này sẽ hướng dẫn các bạn làm 1 website đơn giản hỗ trợ 3 ngôn ngữ khác nhau: Tiếng Việt, Tiếng Anh, Tiếng Nhật, cho text và hiển thị và cách phân chia và quản lý các file resource bundles cho text thông thường và message lỗi.

Trong ví dụ này thông tin địa phương ( Locale) nằm trên tham số của URL. Thông tin Locale sẽ được lưu trữ lại ở Cookie, các trang tiếp theo người dùng không phải lựa chọn lại ngôn ngữ.

 

2. Tools

  • JDK 8
  • Maven 3
  • IDE Eclipse
  • Spring Boot 2.0.5

3. Cấu trúc project

File maven pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>the.code.root</groupId>
    <artifactId>multi-language</artifactId>
    <packaging>jar</packaging>
    <version>0.0.1</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <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>

        <!--validation-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- hot swapping, disable cache for template, enable live reload -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--tomcat embedded-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>9.0.13</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Package as an executable jar/war -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Chú ý rằng chúng ta sẽ có các file chứa các message theo từng ngôn ngữ, ở đây sẽ chia ra nhóm messages dùng để chứa các text hiển thị bình thường, ví dụ như Login hay Logout và nhóm validation chứa các message hiển thị cho validation, ví dụ: "Sai username hoặc password":

  • messages.properties, 
  • messages_vi.properties,  
  • messages_ja.properties 
  • validation.properties
  • validation_vi.properties
  • validation_ja.properties

Các bạn có thể đặt tên bất kỳ cho các nhóm file này, chỉ cần lưu ý là các file cùng 1 nhóm phải cùng tên với nhau và khác nhau ở ký tự ngôn ngữ (_vi, _ja). Cần lưu ý với tên file không có ký hiệu ngôn ngữ thì chúng ta hiểu rằng đó là ngôn ngữ Tiếng Anh, có thể vì Spring Framework là hàng Mỹ hoặc bản thân Java được viết bằng Tiếng Anh nên những nhà phát triển Framework quy định như thế. Không chỉ Spring Framework mà một số các Framework khác như JSF, EJB, ... cũng được quy định như thế.

 

4. Khai báo Locale Resolver và Locale Interceptor

package the.code.root.multi.language.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        Locale vietnamLocale = new Locale("vi", "VI");
        slr.setDefaultLocale(vietnamLocale);
        return slr;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource
                = new ReloadableResourceBundleMessageSource();

        messageSource.setBasenames("classpath:messages", "classpath:validation");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

    @Bean
    public LocalValidatorFactoryBean getValidator() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource());
        return bean;
    }
}

Locale Interceptor đóng vai trò như một filter request trước khi request của Client vào tới tầng Controller. Tại đây các thay đổi về Locale của Client sẽ được xử lý, ví dụ như Client thay đổi Locale từ VN sang US, thì Server phải trả về các message Tiếng Anh.

@Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
}

Khi người dùng muốn chuyển sang ngôn ngữ khác thì sẽ kèm thêm 1 query param, có dạng: 

<url>?<key>=<language> 
VD: 
https://thecoderoot.com?lang=en, 
https://thecoderoot.com?lang=ja,
https://thecoderoot.com?lang=vi 

Server sẽ trả về kết quả của trang web được yêu cầu với ngôn ngữ mong muốn từ người dùng.

Local Resolver:  Các bạn lưu ý phần khai báo. Chúng ta sẽ sử dụng ngôn ngữ mặc định là tiếng mẹ đẻ Việt Nam, phục vụ cho khách hàng Việt Nam đầu tiên. Nếu không có khai báo thì mặc định Spring vẫn sẽ dùng Locale mặc đinh là Tiếng Anh mặc dù Client truy cập từ Việt Nam.

Ngoài ra chúng ta có thể thêm vào xử lý để website tự động detect vùng truy cập của Client nếu không phải là Việt Nam thì sẽ chuyển ngôn ngữ hiển thị là Tiếng Anh. Chức năng sẽ được TheCodeRoot cập nhật trong những bài viết sau.

Locale vietnamLocale = new Locale("vi", "VI");
slr.setDefaultLocale(vietnamLocale);

 

5. messages files

messages.properties

messages.signin=Sign In
messages.register=Register
messages.username=Username
messages.remmberme=Remember me
messages.password=Password

 

messages_vi.properties

messages.signin=Đăng nhập
messages.register=Đăng ký
messages.username=Tên Đăng nhập
messages.remmberme=Nhớ đăng nhập của tôi
messages.password=Password

 

messages_ja.properties

messages.signin=ロギング
messages.register=登録
messages.username=ユーザー名
messages.remmberme=私のロギングを覚えて下さい
messages.password=パスコード

Spring sử dụng chuẩn UTF-8 để lưu trữ và đọc các file messages, encode phải luôn được sử dụng là UTF-8. Các bạn có thể sử dụng tool Message Editor của Eclipse hoặc đơn giản hơn là dùng Notepad++ với phần encode là UTF-8 để đảm bảo các message của chúng ta hiển thị được bình thường và không bị vỡ. 

 

5. Controller

Model LoginForm:

Chúng ta sử dụng package javax.validation.constraints để validate cho các trường email và password, yêu cầu người dùng phải nhập email đúng đinh dạng x@y,  và password không được để trống. 

package the.code.root.multi.language.model;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;

public class LoginForm {
    @Email(message = "{validation.email}")
    private String email;

    @NotEmpty(message = "{validation.password.notempty}")
    private String password;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
package the.code.root.multi.language.controller;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import the.code.root.multi.language.model.LoginForm;

import javax.validation.Valid;

@Controller
public class DefaultController {

    @GetMapping("/")
    public String home1() {
        return "redirect:/login";
    }

    @GetMapping("/login")
    public String login(LoginForm loginForm) {
        return "login";
    }

    @PostMapping(value = "/process-login")
    public String processLogin(@Valid LoginForm form,
                               BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "login";
        }

        //validation is OK, process Login, simply return home
        return "home";
    }

    @GetMapping("/403")
    public String error403() {
        return "error/403";
    }

}

Trong method "/login" các bạn phải inject 1 object LoginForm để Thymeleaf có thể map được các trường input trên HTML vào object Java để chúng ta tiện validate và lấy data để xử lý.

Và ở method "/process-login", các bạn sử dụng class BindingResult để kiểm tra xem các trường của object LoginForm đã được người dùng điền đẩy đủ và hơp lệ chưa bằng method bindingResult.hasErrors() .

 

6. login.html

Cú pháp của Thymeleaf để sử dụng các message là:

th:text="#{message.key}"
VD:
th:text="#{messages.signin}"
<form id="login-form"
      th:object="${loginForm}"
      th:action="@{/process-login}"
      method="post" role="form"
      style="display: block;">
    <div class="form-group">
        <input type="text" name="username" tabindex="1" class="form-control"
               th:field="*{email}"
               th:placeholder="#{messages.username}">
        <span style="color: red" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span>

    </div>
    <div class="form-group">
        <input type="password" name="password" tabindex="2"
               class="form-control"
               th:field="*{password}"
               th:placeholder="#{messages.password}">
        <span style="color: red" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></span>
    </div>
    <div class="form-group text-center">
        <input type="checkbox" tabindex="3" class="" name="remember" id="remember">
        <label for="remember" th:text="#{messages.remmberme}"></label>
    </div>
    <div class="form-group">
        <div class="row">
            <div class="col-sm-6 col-sm-offset-3">
                <input type="submit" name="login-submit" id="login-submit" tabindex="4"
                       class="form-control btn btn-login btn-primary" th:value="#{messages.signin}">
            </div>
        </div>
    </div>
</form>

Và sử dụng cú pháp <th:errors="{filed-name}">  để hiển thị message lỗi.

 

7. Test:

Tiếng Việt

 

Tiếng Nhật

Tiếng Anh

Test Validate Login Form: Chúng ta sẽ test thử các message lỗi khi người dùng nhập thông tin sai trên form login:

Các bạn có thể thấy rằng Spring Framework không chỉ hỗ trợ cơ chế để làm đa ngôn ngữ cho các text hiển thị thông thường mà còn có thể tích hợp được với javax validation để lấy ra các message theo ngôn ngữ tương ứng, tạo ra sự đồng bộ, tiện lợi và thống nhất cho lập trình viên việc quản lý message.

Các bạn có thể tải source code tại đây

Chúc các bạn thành công.

minhnhatict@gmail.com