Cách thực hiện quốc tế hóa trong Java thuần không sử dụng Spring Framework

Trong môi trường Java, việc triển khai chức năng quốc tế hóa (i18n) có thể được thực hiện dễ dàng mà không cần phụ thuộc vào Spring Framework, thông qua các thư viện chuẩn của JDK như java.util.ResourceBundlejava.util.Locale.

Cơ chế hoạt động chủ yếu là tách rời các chuỗi văn bản (như thông báo giao diện, lỗi, v.v.) khỏi mã nguồn và lưu trữ chúng vào các file tài nguyên bên ngoài. Khi chương trình chạy, hệ thống sẽ tải các tài nguyên tương ứng dựa trên thiết lập khu vực ngôn ngữ (Locale) của người dùng để hiển thị nội dung phù hợp.

Dưới đây là phân tích chi tiết về nguyên lý hoạt động cùng ví dụ hoàn chỉnh sử dụng Java thuần.

Nguyên lý hoạt động cốt lõi

  1. Tập tin tài nguyên (Resource Bundle)
    Đây là nền tảng cơ bản cho quá trình quốc tế hóa, thường là các file định dạng .properties chứa các cặp khóa-giá trị (Key-Value) dưới dạng văn bản.
  • Tên cơ sở (Base Name): Tên chung cho tất cả các file tài nguyên, bao gồm đường dẫn và tiền tố tên chung. Ví dụ: TextResources, C:/AppData/TextResources.
  • Mã ngôn ngữ và quốc gia: Thêm vào sau tên cơ sở để xác định khu vực cụ thể.
    • Mã ngôn ngữ: Theo chuẩn ISO 639, viết thường hai chữ cái, như vi (Tiếng Việt), en (Tiếng Anh).
    • Mã quốc gia: Theo chuẩn ISO 3166, viết hoa hai chữ cái, như VN (Việt Nam), GB (Vương quốc Anh).

Ví dụ, một gói tài nguyên có tên TextResources có thể bao gồm các file sau:

  • TextResources.properties (file mặc định, thường là tiếng Anh)
  • TextResources_vi_VN.properties (Tiếng Việt - Việt Nam)
  • TextResources_en_GB.properties (Tiếng Anh - Vương quốc Anh)
  1. Khu vực ngôn ngữ (Locale)
    Đối tượng java.util.Locale đại diện cho một vùng lãnh thổ, chính trị hoặc văn hóa cụ thể. Chương trình sử dụng đối tượng này để chỉ định cho ResourceBundle biết nên tải file tài nguyên nào.
  2. Tải tập tin tài nguyên (ResourceBundle Loading)
    Phương thức ResourceBundle.getBundle(String baseName, Locale locale) đóng vai trò trung tâm trong việc tải tài nguyên. Nó sẽ tự động tìm kiếm file phù hợp nhất dựa trên Locale truyền vào và chiến lược tìm kiếm nội bộ.

Thứ tự tìm kiếm minh họa:
Giả sử Locale mặc định là vi_VN, khi gọi ResourceBundle.getBundle("TextResources", new Locale("vi", "VN")), Java sẽ tìm kiếm theo thứ tự sau:

  1. TextResources_vi_VN.properties (khớp chính xác)
  2. TextResources_vi.properties (chỉ khớp ngôn ngữ)
  3. TextResources.properties (file mặc định, dự phòng cuối cùng)

Nếu không tìm thấy bất kỳ file nào phù hợp, hệ thống sẽ ném ra ngoại lệ MissingResourceException.

  1. Xử lý văn bản động (MessageFormat)
    Nhiều văn bản chứa nội dung thay đổi, ví dụ như "Chào mừng, Nguyễn Văn A!". Trong trường hợp này, có thể sử dụng lớp java.text.MessageFormat. Trong file tài nguyên, sử dụng các placeholder như {0}, {1} để biểu thị phần thay đổi, sau đó dùng phương thức MessageFormat.format() để thay thế trong mã nguồn.

Triển khai với ResourceBundle

Sau đây là ví dụ hoàn chỉnh về quốc tế hóa trong Java thuần không sử dụng framework nào khác.

Bước 1: Tạo file tài nguyên

Tạo ba file sau trong thư mục src/main/resources của dự án. Đảm bảo định dạng file là UTF-8.

  1. TextResources.properties (file mặc định)
greeting=Hello, {0}!
farewell=Goodbye.
  1. TextResources_vi_VN.properties (Tiếng Việt)
greeting=Xin chào, {0}!
farewell=Tạm biệt.
  1. TextResources_en_US.properties (Tiếng Anh)
greeting=Hi, {0}!
farewell=See you later.

Bước 2: Viết mã Java

Tạo lớp Java để tải và sử dụng các tài nguyên này.

import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;

public class MultiLanguageExample {

    private static final String BASE_RESOURCE_NAME = "TextResources";

    public static void main(String[] args) {
        // 1. Mô phỏng lấy thiết lập khu vực người dùng
        // Trong thực tế, giá trị này có thể đến từ cài đặt người dùng, header yêu cầu trình duyệt, v.v.
        Locale currentLocale = Locale.forLanguageTag("vi-VN"); // Thiết lập sang tiếng Việt

        // 2. Tải tập tin tài nguyên theo khu vực
        ResourceBundle bundle = ResourceBundle.getBundle(BASE_RESOURCE_NAME, currentLocale);

        // 3. Lấy văn bản tĩnh
        String farewellText = bundle.getString("farewell");
        System.out.println(farewellText); // Kết quả: Tạm biệt.

        // 4. Lấy và định dạng văn bản động
        String greetingPattern = bundle.getString("greeting");
        String userName = "Nguyễn Văn A";
        
        // Sử dụng MessageFormat để thay thế placeholder {0}
        String greetingOutput = MessageFormat.format(greetingPattern, userName);
        System.out.println(greetingOutput); // Kết quả: Xin chào, Nguyễn Văn A!

        // --- Chuyển đổi ngôn ngữ ---
        System.out.println("--- Chuyển sang tiếng Anh ---");
        currentLocale = Locale.forLanguageTag("en-US"); // Chuyển sang tiếng Anh
        bundle = ResourceBundle.getBundle(BASE_RESOURCE_NAME, currentLocale);

        farewellText = bundle.getString("farewell");
        System.out.println(farewellText); // Kết quả: See you later.

        greetingPattern = bundle.getString("greeting");
        greetingOutput = MessageFormat.format(greetingPattern, "John Doe");
        System.out.println(greetingOutput); // Kết quả: Hi, John Doe!
    }
}

ReloadableResourceBundleMessageSource

ReloadableResourceBundleMessageSource là thành phần quốc tế hóa mạnh mẽ do Spring Framework cung cấp. Đây là phiên bản nâng cao của ResourceBundleMessageSource, điểm khác biệt chính là hỗ trợ cập nhật và tải lại tài nguyên quốc tế hóa mà không cần khởi động lại ứng dụng.

Đây là tính năng rất hữu ích đối với các hệ thống vận hành lâu dài và không thể tùy tiện dừng để khởi động lại trong môi trường sản xuất.

Nguyên lý hoạt động cốt lõi

Khả năng mạnh mẽ của ReloadableResourceBundleMessageSource dựa trên các thiết kế quan trọng sau:

  1. Không phụ thuộc vào ResourceBundle của JDK
    Khác với ResourceBundleMessageSource, ReloadableResourceBundleMessageSource không sử dụng lớp java.util.ResourceBundle của JDK để tải tài nguyên. Bởi vì ResourceBundle của JDK sau khi tải sẽ cache vĩnh viễn và không nhận biết được sự thay đổi của file. Spring sử dụng giao diện PropertiesPersisterResourceLoader để đọc trực tiếp file .properties, từ đó kiểm soát hoàn toàn quá trình tải file.
  2. Cơ chế làm mới dựa trên dấu thời gian
    Đây là cốt lõi để đạt được "cập nhật nóng". Quy trình hoạt động như sau:
  • Lần đầu tải: Khi ứng dụng lần đầu yêu cầu một thông báo quốc tế hóa, ReloadableResourceBundleMessageSource sẽ tải file tài nguyên tương ứng và lưu cache nội dung file cùng dấu thời gian sửa đổi cuối cùng.
  • Yêu cầu sau: Khi yêu cầu thông báo tiếp theo, nó sẽ kiểm tra xem chu kỳ làm mới đã hết chưa.
  • Kiểm tra thay đổi: Nếu chu kỳ đã hết, nó sẽ đọc lại dấu thời gian sửa đổi mới nhất của file tài nguyên trên đĩa và so sánh với dấu thời gian cũ trong cache.
  • Quyết định tải lại:
    • Nếu dấu thời gian thay đổi, nghĩa là file đã bị sửa, nó sẽ đọc lại nội dung file và cập nhật cache.
    • Nếu dấu thời gian không thay đổi, nó sẽ trả về nội dung từ cache để tránh thao tác I/O không cần thiết, đảm bảo hiệu suất.
  1. Chiến lược cache linh hoạt
    Hành vi làm mới được điều khiển chính xác bởi thuộc tính cacheSeconds:
  • cacheSeconds = -1 (giá trị mặc định): Chỉ định là không bao giờ làm mới. Trong trường hợp này, hành vi giống hệt ResourceBundleMessageSource, tài nguyên chỉ tải một lần duy nhất.
  • cacheSeconds > 0: Chỉ định khoảng thời gian kiểm tra thay đổi file theo giây. Ví dụ đặt là 60 thì mỗi phút sẽ kiểm tra một lần.
  • cacheSeconds = 0: Chỉ định mỗi yêu cầu đều kiểm tra thay đổi file. Hữu ích trong giai đoạn phát triển để thấy ngay kết quả, nhưng gây ảnh hưởng hiệu suất trong môi trường sản xuất nên không khuyến nghị.
  1. Xử lý lỗi vững chắc
    Khi cố gắng tải lại tài nguyên, nếu xảy ra ngoại lệ (ví dụ file bị khóa tạm thời hoặc sai định dạng), ReloadableResourceBundleMessageSource sẽ bắt ngoại lệ và giữ lại dữ liệu cache cũ. Điều này ngăn chặn tình trạng toàn bộ chức năng quốc tế hóa bị hỏng do vấn đề tạm thời, đảm bảo ổn định ứng dụng.

️ Cấu hình và sử dụng

Trong dự án Spring Boot hoặc Spring MVC, thường cấu hình một Bean MessageSource để sử dụng.

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import java.nio.charset.StandardCharsets;

@Configuration
public class LocalizationConfig {

    @Bean
    public MessageSource localizationProvider() {
        ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
        
        // 1. Thiết lập đường dẫn gốc của file tài nguyên, hỗ trợ nhiều file
        source.setBasenames("classpath:i18n/texts");
        
        // 2. Thiết lập mã hóa file, khuyến nghị luôn dùng UTF-8
        source.setDefaultEncoding(StandardCharsets.UTF_8.name());
        
        // 3. Thiết lập chu kỳ làm mới, đơn vị giây. Ở đây đặt 60 giây kiểm tra thay đổi.
        // Trong sản xuất nên đặt giá trị lớn hơn, như 1800 (30 phút) hoặc 3600 (1 giờ)
        source.setCacheSeconds(60);
        
        // 4. Khi không tìm thấy thông báo theo khóa, trả về khóa luôn thay vì ném ngoại lệ
        // Hữu ích trong quá trình phát triển và gỡ lỗi
        source.setUseCodeAsDefaultMessage(true);
        
        return source;
    }
}

Thuộc tính cấu hình phổ biến

Thuộc tính Mô tả Giá trị mặc định
basenames Đường dẫn gốc của file tài nguyên, có thể cấu hình nhiều. Không
cacheSeconds Thời gian hết hạn cache (giây), điều khiển tần suất làm mới. -1 (không bao giờ làm mới)
defaultEncoding Mã hóa ký tự sử dụng khi tải file tài nguyên. ISO-8859-1 (khuyến nghị đặt là UTF-8)
useCodeAsDefaultMessage Khi không tìm thấy thông báo, có trả về khóa hay không. false (ném NoSuchMessageException)
fallbackToSystemLocale Khi không tìm thấy thông báo theo Locale chỉ định, có quay lại dùng Locale hệ thống không. true
alwaysUseMessageFormat Có luôn dùng MessageFormat phân tích thông báo ngay cả khi không có tham số. false

So sánh với ResourceBundleMessageSource

Tính năng ResourceBundleMessageSource ReloadableResourceBundleMessageSource
Cơ chế底层 Phụ thuộc vào java.util.ResourceBundle của JDK Sử dụng PropertiesPersister của Spring
Khả năng làm mới Không hỗ trợ. Tải một lần sau khởi động, không cập nhật. Hỗ trợ. Có thể làm mới động theo cấu hình cacheSeconds.
Hiệu suất Sau lần đầu tải, hiệu suất cực cao (đọc hoàn toàn từ memory). Cần kiểm tra dấu thời gian file định kỳ, có chi phí I/O nhẹ.
Ứng dụng Trường hợp nội dung quốc tế hóa không bao giờ thay đổi trong suốt vòng đời ứng dụng. Trường hợp cần cập nhật trực tuyến nội dung văn bản, thông báo quốc tế hóa.

Sử dụng trong mã Java thuần không phải Bean

  • Danh sách file ví dụ:
| - i18n
  | - productmessages_de_DE.properties
  | - productmessages_ja_JP.properties
  • Mã công cụ:
// Cấu hình
private static final String RESOURCE_PATH = "file:C:/appdata/i18n/productmessages";
private static final ReloadableResourceBundleMessageSource messageProvider = new ReloadableResourceBundleMessageSource();

// Khởi tạo
static {
    messageProvider.setBasenames(RESOURCE_PATH); // Thiết lập đường dẫn file
    messageProvider.setDefaultEncoding("UTF-8");
    messageProvider.setCacheSeconds(3600);
}

// 3. Cung cấp phương thức tĩnh để lớp Java thường gọi
public static String retrieveMessage(String key, Locale targetLocale) {
    return messageProvider.getMessage(key, new Object[0], targetLocale);
}
  • Sử dụng:
// ja_JP
Locale locale = new Locale("ja", "JP");
String localizedText = retrieveMessage("welcome", locale);

Điểm quan trọng

  • Đặt tên file: Tuân thủ nghiêm ngặt quy tắc tên_gốc_ngôn_ngữ_quốc_gia.properties.
  • Mã hóa file: Tài nguyên phải lưu ở định dạng UTF-8, nếu không ký tự không ASCII (như tiếng Việt) sẽ bị lỗi hiển thị. Từ Java 9 trở lên, ResourceBundle mặc định đọc bằng UTF-8.
  • Quay lại mặc định: Luôn cung cấp file tài nguyên mặc định không có hậu tố ngôn ngữ (như TextResources.properties) làm phương án dự phòng khi không tìm thấy ngôn ngữ cụ thể.
  • Quản lý khóa: Sử dụng khóa có ý nghĩa, có cấu trúc (như user.greeting.text) thay vì text1, giúp dễ bảo trì và cộng tác nhóm.
  • Tối ưu hiệu suất: ResourceBundle có cơ chế cache nội bộ, việc tải lại cùng Locale nhiều lần chi phí nhỏ. Trong trường hợp tải cao, có thể cân nhắc chiến lược cache tùy chỉnh.

Thẻ: Java i18n internationalization resourcebundle locale

Đăng vào ngày 13 tháng 7 lúc 02:52