Requirement #363
closed[NFR-B.2-支援多國語系] 支援多國語系
50%
Description
需求基本資料¶
| 項目 | 內容 |
|---|---|
| 分組 | B.2(BRD-003 §3) |
| 小節 | 支援多國語系(NFR 項數 1) |
| 對應架構設計 | SDD-007 §7 |
| 驗證方式 | 語系切換測試、資源檔覆蓋率 |
| 主責 | FE |
| WBS 階段 | Phase-4 領域服務 R2/R3 與參數極大化 |
| Owner | SA/SD Agent |
逐項(1 項)符合度回應見建議書附錄 B;本議題以小節層級追蹤歸屬與驗證。
Files
RA Updated by Redmine Admin 9 days ago
RA Updated by Redmine Admin 9 days ago
RA Updated by Redmine Admin 7 days ago
註:本則為 #A-318 當時之快照(部分數字為歷史值);最新狀態見同議題 #A-323 之 Journal。
前端多國語系(i18n)推進記錄(2026-10-18)¶
一、框架與錯誤訊息¶
-
#A-307 導入
i18next+react-i18next(npm audit 0 vulnerabilities;peer 為 react>=16.8,未升 React 19)。
語言解析順序:localStorage → 瀏覽器語系 → 預設zh-TW;只接受已支援語言碼(避免缺檔時靜默退回)。 -
#A-312/#A-313 CR-060 前端錯誤訊息多語化:
- 10 個具名錯誤改為
ApiError(messageKey+code+status);錯誤碼不翻譯(契約)。 - 66 個 fallback 全數改為
errors.action.*語系鍵。 - 順手收斂 10 份重複 的
toErrorMessage/errorText為單一api/errors.ts。
- 10 個具名錯誤改為
二、頁面內文外譯(已完成 8 個)¶
- 營運總覽(DashboardConsole)
- 應用程式外殼(App.tsx;並移除已無人使用之 label/title 死資料)
- 分期作業台(InstallmentConsole)
- 批次作業監控(BatchConsole)
- 授權檢核明細(AuthorizationPolicyConsole)
- 歸戶 360(AccountOverviewConsole)
- 卡片生命週期(CardLifecycleConsole)
- 額度控管(LimitConsole)
三、量測(可稽核,非估算)¶
| 指標 | 起始 | 現況 |
|---|---|---|
| 語系鍵 | 70 | 557 |
| 待遷移檔案 | 32 | 31 |
| 待遷移行數 | 約 1674 | 約 885(完成約 47%) |
- 量測方式:
tools/verify_nfr_ext.py掃描src/**/*.tsx與src/**/*.ts,排除程式註解(/* */、//、*、{/* */)。 - ⚠️ 過程中修正三次量測誤判(註解被當成介面文案),並將掃描範圍由僅
.tsx擴及.ts——
數字曾因此「變差」(29 檔/1209 行 → 50 檔/1275 行),那是把原本看不到的債務掀出來,非工作倒退。
四、如實揭露(未完成項)¶
-
頁面內文仍有 31 檔/約 885 行待遷移(含
CardApplicationConsole、StipConsole等)。 -
後端錯誤訊息未在地化(P3):
toErrorMessage會優先採用後端訊息,故實際畫面仍可能顯示中文;此屬後端契約變更,已於 CR-060 列為 P3。 -
api/*.ts仍餘部分 label 對映(如RECEIVED: '受理'、SINGLE: '單店'),屬頁面標籤遷移,隨各頁處理。
五、提交¶
b1b3be7 → 26c908b → 0ee062e → 31cce20 → c8d1dec → 095b8f9 → 49b639b → 40231f5 → 5e1aa56 → 5f65adf → fc8414d → a21e14d
RA Updated by Redmine Admin 7 days ago
1. #A-323 i18n 內文外譯全部完成(2026-10-18)
1. 結果(單一真實來源:`tools/i18n_pending.py`,與 `verify_nfr_ext.py::check_fe_i18n` 同邏輯)
| 項目 | 完成前 | 完成後 |
|---|---|---|
| 待遷移檔案 | 26 檔(18 頁 + 8 個 .ts) |
0 檔 |
| 待遷移行數 | 761 行 | 0 行 |
| 語系鍵(leaf) | 659 | 1297(zh-TW 與 en-US 同鍵) |
官方檢核器輸出:待遷移 0 檔/約 0 行(跑檢核器,非自評)。
1. 真實渲染驗證(不是「能編譯就算過」)
VITE_AUTH_ENABLED=false npm run build → vite preview → headless Chrome --dump-dom
(英文以注入 localStorage 觸發;Chrome --lang 不會改變 navigator.language)
逐頁比對渲染後文字:18 頁全部 0 個中文殘留(唯一允許的中文是語言切換器的自稱「繁體中文」——
選項以各語系自稱呈現,否則切到英文後無從辨識哪個是中文)。
證據:docs/08-evidence/N4_i18n_批次外譯_DOM驗證_20261018.md
1. 併修的三個真缺陷(非美化)
-
useApiErrorMessage身分不穩定:每次 render 回傳新函式,而多頁把它放進
useCallback([apiErr])依賴 →useEffect每輪重跑 → 無限重新查詢
(畫面看似正常,實際持續打後端)。改useCallback([t])固定身分。 -
API 層以中文字串當語系鍵:
bff.ts/gateway.ts之getJson(..., '查詢健康狀態失敗')→
英文模式顯示中文(t()查無鍵時原樣回傳)。改 6 個errors.action.*鍵。 -
分頁標題盲區:
index.html的<title>為靜態中文,而 i18n 掃描只掃.ts/.tsx→
英文模式分頁標題仍為中文、量測看不到。改由App.tsx以document.title隨語系設定。1. 新增工具(避免重造)
-
tools/i18n_add_keys.py:新增語系鍵唯一入口——深層合併、既有鍵不覆寫(值不同即報錯)、
兩語系鍵集合強制一致(缺一邊會靜默退回中文=假綠)。 -
tools/i18n_pending.py:待遷移量測(全量/單檔)。1. 如實揭露(這些不是「i18n 未完成」) -
CR-060P3 後端錯誤訊息在地化:未開始(需另立 CR)。ApiError.serverMessage目前
優先顯示後端原文且不翻譯,故後端回中文時畫面仍為中文。 -
數字/日期仍固定
toLocaleString('zh-TW')(千分位/日期在 en-US 差異有限,但非 locale-aware)。 -
開發診斷訊息(
authClient/pkce/main.tsx)固定英文:僅進 console、不顯示於介面。1. 驗證陷阱(留給後人) -
--user-data-dir會保留 localStorage:同一 Chrome profile 重跑會一直得到英文,
曾誤判「繁中也變英文」。驗繁中必須換新 profile。 -
行尾註解(
code(); // 中文)仍會被量測計入 → 移到獨立行;不為此放寬規則(寧可多報,不可漏報)。 -
Redmine 6 的
Setting.text_formatting是common_mark(不是 Textile):
用h5./|_. |/@code@會被原樣顯示。本則曾誤用 Textile,已改寫為 CommonMark。1. 三層留痕 -
Wiki(完整敘事):[[ExecutionLog-A-323]]
-
附件:
N4_i18n_批次外譯_DOM驗證_20261018.md(issue #363)
RA Updated by Redmine Admin 7 days ago
- Status changed from In Progress to Closed
#A-325 P3 後端錯誤訊息在地化完成(2026-10-18)¶
CR-060 留下的最後一段:前端已全外譯(#A-323),但只要錯誤來自後端,畫面仍是中文。
做法(規格先行:CR-061)¶
| 項目 | 內容 |
|---|---|
| 決策 | D1 伺服器端在地化(訊息屬 API 契約,B2B/curl 同受益);D2 語言來源 Accept-Language;D3 文件為單一來源;D4 未登錄→回退原訊息;D5 details 不在地化;D6 錯誤碼不翻譯 |
| 訊息表 |
SDD-006 §3.4 新增**「訊息(EN)」欄**(138 列/133 碼),並統一 5 個重複碼措辭(一碼一訊息) |
| 產生器 |
tools/gen_error_i18n.py:--check 驗「bundle 漂移/覆蓋率/缺 EN」,任一不過即非 0 |
| bundle |
common-spring/i18n/messages*.properties(zh-TW/en,各 133 鍵 error.<CODE>) |
| 機制 |
ErrorMessageLocalizer + MessageSource + LocaleResolver(預設 zh-TW、支援 [zh-TW, en]) |
| 前端 |
authFetch 顯式送 Accept-Language(使用者切換器選的語言勝過瀏覽器預設) |
驗證(可失敗,逐項實測)¶
-
gen_error_i18n.py --check→133 碼/使用 133/未登錄 0,bundle 一致 - 單元 7 例(真實 bundle:en/zh/en-US/fr→zh/未登錄回退;回退:空 bundle、code 空白)
- MockMvc 語言整合 4 例(en/zh-TW/未帶→zh-TW/同請求兩語言訊息確實不同)
-
真機 E2E(
bff-web實際執行):
| 請求 | 回應 |
|---|---|
Accept-Language: en |
CC-PLT-4010 Not authenticated
|
Accept-Language: zh-TW |
CC-PLT-4010 未認證
|
Accept-Language: fr(未支援) |
未認證(回預設) |
| 未帶標頭 | 未認證(預設 zh-TW) |
GET /bff-api/v1/bff/nope + en/zh |
CC-PLT-4040 Resource or path not found/資源或路徑不存在
|
-
真實瀏覽器(
vite preview+ headless--dump-dom):同一頁/system/batch,
en-US 顯示 Not authenticated、zh-TW 顯示 未認證 → 前端未為此做任何翻譯,證明訊息語言真的來自後端。
兩個原 CR 沒寫到的坑(已修)¶
-
401/403不經GlobalExceptionHandler——由SecurityAutoConfiguration的
entry point/access denied handler 直接寫回應;只改 handler 的話,最常見的兩種錯誤永遠是中文。 -
@WebMvcTest切片因缺 bean 起不來——改建構子注入後rewards-service8 個測試轉紅;
改為「無參數建構子 +@Autowired(required=false)setter」(切片走 passthrough,行為與在地化前一致)。
如實揭露(未完成項)¶
-
details[]未在地化(欄位名/規則說明仍中文;前端目前未呈現)→ 後續另立。 -
166 處動態組合訊息(如
field + " 不得為空")在地化後由通用措辭取代;欄位資訊仍在details[]。 - 日誌維持中文(維運語料,刻意不隨客戶語言變)。
-
api-gateway(WebFlux)未掃描common.spring,其錯誤不走本機制(現況即此,非本批退步)。
證據:docs/08-evidence/CR-061_後端錯誤訊息在地化_E2E_20261018.md(含 CR-061 §9 執行記錄)。