國際化翻譯模組
Package:
io.leandev.appfuse.nls.*
AppFuse Server 提供國際化 (I18n) 工具,支援多語言翻譯、格式化模式(日期、數字、貨幣),以及多層級回退機制。
核心特色
1. 架構圖
2. 工具類別
| 類別 | 用途 |
|---|---|
| I18n | 主要國際化工具類別 |
| Translation | 儲存單一語系的翻譯鍵值對 |
| Dictionary | 儲存特定 Locale 的翻譯集合 |
| ChineseTranslator | 阿拉伯數字轉中文數字 |
3. 命名空間
| 命名空間 | 用途 | 檔案範例 |
|---|---|---|
term | 術語、欄位名稱 | nls/en/term.json |
message | 訊息、提示文字 | nls/zh-TW/message.json |
app | 應用程式設定、格式 | nls/zh/app.json |
基本用法
建立 I18n 實例
import io.leandev.appfuse.nls.I18n;
import java.util.Locale;
// 使用系統預設語系
I18n i18n = new I18n();
// 指定語系
I18n i18n = new I18n(Locale.TRADITIONAL_CHINESE);
// 使用單例(推薦)
I18n i18n = I18n.getInstance();
翻譯文字
// 自動搜尋 term 和 message 命名空間
String text = i18n.translate("user"); // "使用者" 或 "User"
// 指定命名空間
String term = i18n.translate("password", "term"); // 術語
String message = i18n.translate("login.success", "message"); // 訊息
// 指定語系
String text = i18n.translate("user", "term", Locale.ENGLISH); // "User"
String text = i18n.translate("user", "term", "zh-TW"); // "使用者"
便捷方法
// term() - 翻譯術語
String term = i18n.term("password"); // 使用實例語系
String term = i18n.term("password", Locale.ENGLISH); // 指定語系
// message() - 翻譯訊息
String msg = i18n.message("login.success"); // 使用實例語系
String msg = i18n.message("login.success", Locale.ENGLISH); // 指定語系
切換語系
I18n i18n = I18n.getInstance();
// 設定語系
i18n.setLocale(Locale.TRADITIONAL_CHINESE);
// 取得目前語系
Locale currentLocale = i18n.locale();
翻譯檔案格式
翻譯檔案使用 JSON 格式,存放於 classpath 的 nls/ 目錄下。
目錄結構
src/main/resources/
└── nls/
├── en/ # 英文
│ ├── term.json
│ ├── message.json
│ └── app.json
├── zh/ # 簡體中文
│ ├── term.json
│ ├── message.json
│ └── app.json
├── zh-TW/ # 繁體中文
│ ├── term.json
│ ├── message.json
│ └── app.json
└── fr/ # 法文
├── term.json
└── app.json
JSON 檔案範例
nls/en/term.json:
{
"user": "User",
"password": "Password",
"login": "Login",
"logout": "Logout",
"email": "Email",
"address": "Address",
"company": "Company",
"department": "Department"
}
nls/zh-TW/term.json:
{
"user": "使用者",
"password": "密碼",
"login": "登入",
"logout": "登出",
"email": "電子郵件"
}
nls/en/message.json:
{
"welcome.message": "Welcome to AppFuse!",
"login.success": "Login successful",
"login.failed": "Login failed",
"validation.phone": "Please enter a valid phone number",
"validation.email": "Please enter a valid email address"
}
nls/zh-TW/message.json:
{
"welcome.message": "歡迎使用 AppFuse!",
"login.success": "登入成功",
"login.failed": "登入失敗"
}
回退機制
I18n 提供多層級回退機制,確保總能找到翻譯:
查詢順序:特定地區 → 語言 → 英文 → 原始 key
回退範例
I18n i18n = new I18n(Locale.TRADITIONAL_CHINESE); // zh-TW
// 1. "user" 在 zh-TW/term.json 中存在
i18n.term("user"); // "使用者"
// 2. "address" 在 zh-TW 中不存在,回退到 zh
i18n.term("address"); // "地址"(來自 zh/term.json)
// 3. "department" 在 zh-TW 和 zh 中都不存在,回退到 en
i18n.term("department"); // "Department"(來自 en/term.json)
// 4. "nonexistent.key" 在所有語系中都不存在,返回原始 key
i18n.term("nonexistent.key"); // "nonexistent.key"
回退流程圖
格式化模式
I18n 支援取得語系相關的格式化模式,可搭配 DateTimeFormatter 或 DecimalFormat 使用。
日期時間格式
I18n i18n = new I18n(Locale.TRADITIONAL_CHINESE);
// 取得格式字串
String datetimePattern = i18n.datetimePattern(); // "yyyy-MM-dd HH:mm:ss"
String datePattern = i18n.datePattern(); // "yyyy-MM-dd"
String timePattern = i18n.timePattern(); // "HH:mm"
// 使用自訂預設值
String pattern = i18n.datetimePattern("MM/dd/yyyy HH:mm:ss");
// 指定語系
String pattern = i18n.datePattern(Locale.US);
// 搭配 DateTimeFormatter
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(i18n.datePattern());
String formatted = LocalDate.now().format(formatter);
數字格式
// 取得格式字串
String numberPattern = i18n.numberPattern(); // "#,##0.##"
String integerPattern = i18n.integerPattern(); // "#,##0"
String floatPattern = i18n.floatPattern(); // "#,##0.##"
String moneyPattern = i18n.moneyPattern(); // "$#,##0.##"
// 搭配 DecimalFormat
DecimalFormat formatter = new DecimalFormat(i18n.moneyPattern());
String formatted = formatter.format(1234.56); // "$1,234.56"
字型設定
// 取得語系對應的字型
String fontFamily = i18n.fontFamily(); // "Arial"
// 使用自訂預設值
String fontFamily = i18n.fontFamily("Noto Sans CJK TC");
// 指定語系
String fontFamily = i18n.fontFamily(Locale.JAPANESE, "Noto Sans JP");
在 app.json 中設定格式
nls/zh-TW/app.json:
{
"datetimePattern": "yyyy/MM/dd HH:mm:ss",
"datePattern": "yyyy/MM/dd",
"timePattern": "HH:mm",
"numberPattern": "#,##0.##",
"integerPattern": "#,##0",
"floatPattern": "#,##0.##",
"moneyPattern": "NT$#,##0",
"fontFamily": "Microsoft JhengHei"
}
ChineseTranslator 中文數字
ChineseTranslator 將阿拉伯數字轉換為中文數字。
基本用法
import io.leandev.appfuse.nls.ChineseTranslator;
ChineseTranslator translator = new ChineseTranslator();
// 詳細模式(預設)
translator.translate(123); // "一百二十三"
translator.translate(1000); // "一千"
translator.translate(2024); // "二千零二十四"
translator.translate(10); // "十"(省略前面的一)
// 簡潔模式
translator.translate(123, false); // "一二三"
translator.translate(2024, false); // "二〇二四"
支援範圍
- 支援 0 至 9999 的整數
- 詳細模式:使用「百」「千」等單位
- 簡潔模式:逐位轉換,適合年份等場景
Spring Boot 整合
配置類別
@Configuration
public class I18nConfig {
@Bean
public I18n i18n() {
return I18n.getInstance();
}
}
根據請求語系翻譯
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final I18n i18n;
@GetMapping("/{id}")
public ResponseEntity<ProductDto> getProduct(
@PathVariable Long id,
@RequestHeader(value = "Accept-Language", defaultValue = "en") String language) {
// 根據請求語系翻譯
Locale locale = Locale.forLanguageTag(language);
Product product = productService.findById(id);
ProductDto dto = new ProductDto();
dto.setId(product.getId());
dto.setName(product.getName());
// 翻譯狀態
dto.setStatusDisplay(i18n.term(product.getStatus().name(), locale));
return ResponseEntity.ok(dto);
}
}
錯誤訊息國際化
@ControllerAdvice
public class GlobalExceptionHandler {
private final I18n i18n;
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(
ValidationException ex,
@RequestHeader(value = "Accept-Language", defaultValue = "en") String language) {
Locale locale = Locale.forLanguageTag(language);
ErrorResponse error = new ErrorResponse();
error.setCode(ex.getErrorCode());
error.setMessage(i18n.message(ex.getMessageKey(), locale));
return ResponseEntity.badRequest().body(error);
}
}
完整範例
多語系報表產生
@Service
public class ReportService {
private final I18n i18n;
public byte[] generateReport(ReportRequest request, Locale locale) {
// 設定語系
I18n reportI18n = new I18n(locale);
// 取得格式
String datePattern = reportI18n.datePattern();
String moneyPattern = reportI18n.moneyPattern();
String fontFamily = reportI18n.fontFamily("Noto Sans CJK TC");
// 翻譯欄位標題
String titleProduct = reportI18n.term("product");
String titleQuantity = reportI18n.term("quantity");
String titleAmount = reportI18n.term("amount");
String titleDate = reportI18n.term("date");
// 建立報表...
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(datePattern);
DecimalFormat moneyFormatter = new DecimalFormat(moneyPattern);
// 產生 PDF/Excel...
}
}
中文發票號碼
@Service
public class InvoiceService {
private final ChineseTranslator chineseTranslator = new ChineseTranslator();
public String generateChineseInvoiceNumber(int year, int month, int sequence) {
// 年份使用簡潔模式
String chineseYear = chineseTranslator.translate(year, false); // "二〇二四"
// 月份使用詳細模式
String chineseMonth = chineseTranslator.translate(month); // "十二"
// 流水號
String chineseSeq = chineseTranslator.translate(sequence); // "一百二十三"
return String.format("中華民國%s年%s月第%s號", chineseYear, chineseMonth, chineseSeq);
}
}
Translation 類別
Translation 類別是大小寫不敏感的翻譯儲存容器:
import io.leandev.appfuse.nls.Translation;
Translation translation = new Translation();
// 存入(key 自動轉小寫)
translation.put("User", "使用者");
translation.put("PASSWORD", "密碼");
// 取出(key 自動轉小寫)
translation.get("user"); // "使用者"
translation.get("password"); // "密碼"
// 其他操作
translation.containsKey("user"); // true
translation.size(); // 2
translation.isEmpty(); // false
translation.remove("user");
translation.clear();
最佳實踐
1. 使用單例
// 推薦:使用單例避免重複載入翻譯檔
I18n i18n = I18n.getInstance();
// 不推薦:每次都建立新實例
I18n i18n = new I18n(); // 會重新載入所有翻譯檔
2. 命名空間分離
// 術語放在 term 命名空間
i18n.term("product");
i18n.term("customer");
// 訊息放在 message 命名空間
i18n.message("login.success");
i18n.message("validation.required");
// 應用程式設定放在 app 命名空間
i18n.translate("app.title", "app");
3. Key 命名規範
{
// 術語:使用單一單詞或 camelCase
"user": "使用者",
"firstName": "名字",
// 訊息:使用點分隔的階層結構
"login.success": "登入成功",
"login.failed": "登入失敗",
"validation.email.invalid": "請輸入有效的電子郵件",
"validation.phone.required": "電話號碼為必填"
}
4. 提供完整的英文翻譯
// 英文作為最終回退,確保所有 key 都有英文翻譯
// nls/en/term.json 應包含所有可能的 key