跳至主要内容

國際化翻譯模組

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 支援取得語系相關的格式化模式,可搭配 DateTimeFormatterDecimalFormat 使用。

日期時間格式

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

下一步

  • Error 模組 - 統一錯誤處理(搭配 I18n 翻譯錯誤訊息)
  • Mail 模組 - 郵件發送(多語系郵件模板)
  • Image 模組 - 圖像處理(搭配 fontFamily 設定字型)