Trong thời đại toàn cầu hóa, việc xây dựng ứng dụng web hỗ trợ đa ngôn ngữ không chỉ là một lợi thế cạnh tranh mà còn là yếu tố thiết yếu để tiếp cận và phục vụ người dùng từ khắp nơi trên thế giới.

I18n mang đến giải pháp hoàn hảo để xây dựng các ứng dụng web đa ngôn ngữ một cách dễ dàng và hiệu quả.

1. Khái niệm

Internationalization (i18n) là quá trình thiết kế và phát triển một ứng dụng phần mềm sao cho nó có thể dễ dàng thích nghi với các ngôn ngữ, khu vực và văn hóa khác nhau mà không cần thay đổi mã nguồn cốt lõi.

"i" là chữ cái đầu, "n" là chữ cái cuối của từ "Internationalization", "18" biểu thị số chữ cái ở giữa (từ chữ thứ 2 đến chữ thứ 18).

Cho phép ứng dụng hỗ trợ nhiều ngôn ngữ Ví dụ: tiếng Anh, tiếng Việt, tiếng Pháp.

Đáp ứng các định dạng địa phương ngày tháng, tiền tệ, v.v..

Tăng tính tiếp cận và trải nghiệm người dùng trên toàn cầu.

2. Sử dụng trong tệp HTML

  • Tạo các tệp ngôn ngữ message_<tên ngôn ngữ>.properties trong thư mục src/main/resources/messages. Lưu ý tệp messages.properties sẽ là tệp mặc định và bằng tiếng anh.

locale đây là một khu vực địa lý hoặc văn hóa cụ thể. Nó thường là một ký hiệu ngôn ngữ được theo sau bởi một ký hiệu quốc gia, phân biệt nhau bởi dấu gạch dưới. Ví dụ: en_US biểu diễn English locale cho US

Tên ngôn ngữ được ký hiệu bằng 2 chữ cái thông qua bảng sau

  • Sử dụng [[#{message_key}]] nếu trong phương pháp thẻ, ngoài ra còn có thể sử dụng th:<tên_thuộc_tính>="#{message_key}"

Ví dụ: <span>[[#{message_key}]]</span>, <span th:text="#{message_key}"></span>

  • Nếu khóa thông báo của bạn là một biến, bạn có thể sử dụng [[#{${tên_biến}}]] bên trong thẻ hoặc ${tên_biến} bên trong thuộc tính.
  • Nếu khóa thông báo của bạn có tham số ví dụ hello=Hello {0} {1} bạn có thể sử dụng bên trong thẻ: [[${#messages.msg('message_key', value0, value1)}]]hoặc bên trong thuộc tính: ${#messages.msg('message_key', value0, value1)}
  • Nếu bạn muốn chuyển đổi thông báo thành chữ thường, bạn có thể sử dụng bên trong thẻ: [[${#strings.toLowerCase(#messages.msg('message_key'))}]] hoặc bên trong thuộc tính: ${#strings.toLowerCase(#messages.msg('message_key'))}
  • Đối với các thông báo có định dạng HTML, bạn có thể sử dụng thuộc tính th:utext ví dụ: <p th:utext="#{home.welcome}"></p>

Bạn có thể tham khảo tài liệu Thymeleaf để biết thêm chi tiết

3. Sử dụng trong JavaScript

- Đối với admin, chúng ta cần sử dụng bên trong thẻ <script> như sau:
<script th:fragment="common" th:inline="javascript">
/*<![CDATA[*/
ezyadmin.messages.message_key1 = /*[[#{message_key1}]]*/ '';
/*]]>*/
// other javascript code
</script>
- Đối với web, chúng ta cũng cần sử dụng bên trong thẻ <script> như sau:
<script th:fragment="common" th:inline="javascript">
/*<![CDATA[*/
ezyweb.messages.message_key1 = /*[[#{message_key1}]]*/ '';
/*]]>*/
// other javascript code
</script>

4. Ví dụ

hình ảnh_2025-03-13_101501563.png
  • Người dùng truy cập URL http://localhost:8080/login

Hành động: Gửi yêu cầu GET đến /login.

Xử lý: LoginController.loginGet được gọi.

Nhận tham số lang (mặc định = "en") và error (mặc định = rỗng).

Trả về View:

Template: user-login.html.

Biến: lang (ngôn ngữ), error (có lỗi hay không).

Kết quả: Trang đăng nhập hiển thị với ngôn ngữ mặc định (tiếng Anh).

  • Người dùng chuyển đổi ngôn ngữ

Hành động: Nhấp vào liên kết "Tiếng Việt" (/login?lang=vi) hoặc "English" (/login?lang=en).

Xử lý: Yêu cầu GET mới được gửi với tham số lang.

LoginController.loginGet cập nhật locale dựa trên lang.

Trả về View với template user-login.html và ngôn ngữ mới.

Kết quả: Trang đăng nhập được làm mới với ngôn ngữ đã chọn VD: "Đăng nhập" thay vì "Login".

  • Thêm các dependency cần thiết
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>3.1.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.tvd12</groupId>
            <artifactId>ezyhttp-server-thymeleaf</artifactId>
            <version>1.3.7</version>
        </dependency>

        <dependency>
            <groupId>com.tvd12</groupId>
            <artifactId>ezyhttp-server-core</artifactId>
            <version>1.3.7</version>
        </dependency>

        <dependency>
            <groupId>com.tvd12</groupId>
            <artifactId>ezyhttp-server-boot</artifactId>
            <version>1.3.7</version>
        </dependency>
- Tạo file messages_en.propertiesfile messages_vi.properties trong thư mục src/main/resources/messages hình ảnh_2025-03-13_092840208.png

File messages_en.properties

login=Login
username=Username
username.placeholder=Enter your username
password=Password
password.placeholder=Enter your password
login.button=Login
login.language.en=English
login.language.vi=Tiếng Việt
login.error=Invalid username or password
File file messages_vi.properties
login=Đăng nhập
username=Tên người dùng
username.placeholder=Nhập tên người dùng
password=Mật khẩu
password.placeholder=Nhập mật khẩu
login.button=Đăng nhập
login.language.en=English
login.language.vi=Tiếng Việt
login.error=Tên người dùng hoặc mật khẩu không đúng
- Tạo trang user-login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="#{login}">Login</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
<div class="container mt-5 mx-auto" style="max-width: 600px;">
    <div class="mb-3 text-end">
        <a th:href="'/login?lang=vi'" th:text="#{login.language.vi}">Vietnamese</a> |
        <a th:href="'/login?lang=en'" th:text="#{login.language.en}">English</a>
    </div>
    <h2 class="text-center mb-4" th:text="#{login}">Login</h2>
    <form action="/login" method="post">
        <input type="hidden" name="lang" th:value="${lang}">
        <div class="mb-3">
            <label for="username" class="form-label" th:text="#{username}">Username</label>
            <input type="text" class="form-control" id="username" name="username"
                   th:placeholder="#{username.placeholder}" required>
        </div>
        <div class="mb-3">
            <label for="password" class="form-label" th:text="#{password}">Password</label>
            <input type="password" class="form-control" id="password" name="password"
                   th:placeholder="#{password.placeholder}" required>
        </div>
        <button type="submit" class="btn btn-primary w-100" th:text="#{login.button}">Login</button>
    </form>
    <div th:if="${error}" class="alert alert-danger mt-3 text-center" th:text="#{login.error}">
        Invalid username or password
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
        crossorigin="anonymous"></script>
</body>
</html>
- Tạo LoginController.java
package org.hello.controller;

import com.tvd12.ezyhttp.server.core.annotation.Controller;
import com.tvd12.ezyhttp.server.core.annotation.DoGet;
import com.tvd12.ezyhttp.server.core.annotation.DoPost;
import com.tvd12.ezyhttp.server.core.annotation.RequestParam;
import com.tvd12.ezyhttp.server.core.view.Redirect;
import com.tvd12.ezyhttp.server.core.view.View;

@Controller
public class LoginController {

    @DoGet("/login")
    public View loginGet(
            @RequestParam(value = "lang", defaultValue = "en") String language,
            @RequestParam(value = "error", defaultValue = "") String error) {
        return View.builder()
                .locale(language)
                .template("user-login")
                .addVariable("lang", language)
                .addVariable("error", !error.isEmpty())
                .build();
    }

    @DoPost("/login")
    public Redirect loginPost(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            @RequestParam(value = "lang", defaultValue = "en") String language) {
        if ("admin".equals(username) && "password".equals(password)) {
            return Redirect.to("/book-store/books");
        } else {
            return Redirect.to("/login?lang=" + language + "&error=1");
        }
    }
}
- Khởi chạy ứng dụng và truy cập http://localhost:8080/login
hình ảnh_2025-03-13_095043250.png
hình ảnh_2025-03-13_095136507.png

5. Tóm lại

Trên đây là những thông tin liên quan đến đa ngôn ngữ Internationalization| i18n . Hy vọng với những chia sẻ trên đây giúp bạn nắm được đặc điểm và ứng dụng.