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.ResourceBundle và java.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
- 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.propertieschứ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).
- Mã ngôn ngữ: Theo chuẩn ISO 639, viết thường hai chữ cái, như
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)
- Khu vực ngôn ngữ (Locale)
Đối tượngjava.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 choResourceBundlebiết nên tải file tài nguyên nào. - Tải tập tin tài nguyên (ResourceBundle Loading)
Phương thứcResourceBundle.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ênLocaletruyề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:
TextResources_vi_VN.properties(khớp chính xác)TextResources_vi.properties(chỉ khớp ngôn ngữ)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.
- 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ớpjava.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ứcMessageFormat.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.
TextResources.properties(file mặc định)
greeting=Hello, {0}!
farewell=Goodbye.
TextResources_vi_VN.properties(Tiếng Việt)
greeting=Xin chào, {0}!
farewell=Tạm biệt.
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:
- Không phụ thuộc vào
ResourceBundlecủa JDK
Khác vớiResourceBundleMessageSource,ReloadableResourceBundleMessageSourcekhông sử dụng lớpjava.util.ResourceBundlecủa JDK để tải tài nguyên. Bởi vìResourceBundlecủ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ệnPropertiesPersistervàResourceLoaderđể đọc trực tiếp file.properties, từ đó kiểm soát hoàn toàn quá trình tải file. - 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,
ReloadableResourceBundleMessageSourcesẽ 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.
- 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ínhcacheSeconds:
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ệtResourceBundleMessageSource, 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à60thì 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ị.
- 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),ReloadableResourceBundleMessageSourcesẽ 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,ResourceBundlemặ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:
ResourceBundlecó cơ chế cache nội bộ, việc tải lại cùngLocalenhiề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.