Files
ROLAC/docs/superpowers/specs/2026-05-29-bilingual-dropdown-options-design.md
T
2026-05-29 21:50:31 -07:00

207 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 下拉選項雙語顯示(English/中文)— 設計文件
**日期:** 2026-05-29
**狀態:** 已核准設計,待寫實作計畫
**範圍:** 前端(Angular)。不動資料庫、不動 schema、不動既有後端 API 回傳的列表名稱。
---
## 1. 目標
針對大多數下拉選單,選項**同時顯示英文與中文**,格式為 `英文/中文`(例:`Worship/敬拜``Cash/現金`)。
**設計決定(已與使用者確認):**
- **顯示時拼接、資料分開存** — 中英文在資料層維持分開(DB 兩欄 / 前端 value 不變),只在「顯示」時組成 `英文/中文`。送出/儲存的值完全不變。
- **全站套用 + 未來約定** — 現有所有業務下拉都改;並定成往後新增下拉的統一慣例。
- **回顯一致性** — 下拉改雙語後,**前端自己組得出來的**已選值回顯也一起改(目前僅奉獻錄入頁下方明細的 Type 欄)。後端帶 `name_en` 過來的列表欄位(如支出列表)本次不動。
**格式規則:**
- 英文在前、中文在後,中間以 `/` 分隔、**無空格**。
- 沒有中文時只顯示英文(如品牌字 `Zelle``PayPal`)。
---
## 2. 現況盤點
下拉選項來源分兩類:
### 第 1 類 — DB 查表(已有 `name_en` + `name_zh`,中文已種子,但前端只顯示英文)
後端實體與前端 DTO 皆已具備 `name_en`(非空)+ `name_zh`(可空),種子資料已填中文(如 Worship/敬拜、Tithe/什一奉獻)。前端目前一律綁 `textField="name_en"`,因此中文存在卻未顯示。**故此類為純前端顯示改動,不需動 DB 或 schema。**
涉及下拉:
| 下拉 | 模板位置 | 資料來源 |
|------|----------|----------|
| 奉獻錄入「Type」 | `app/features/giving/pages/offering-session-page/offering-session-page.component.html:39` | `categories``GivingCategoryApiService.getAll` |
| Givings 類型篩選 + 對話框「Type」 | `app/features/giving/pages/givings-page/givings-page.component.html:9, 48` | `categories` |
| 支出「Ministry」篩選 | `app/features/expense/pages/expenses-page/expenses-page.component.html:18` | `ministries``MinistryApiService.getAll` |
| 支出對話框 Ministry / 大類 / 子項(3 個) | `app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html:19, 32, 45` | `ministries` / `groups` / `subs` |
### 第 2 類 — 前端寫死的 enum 陣列(後端僅存 `string`,無 C# enum
散落在各 `*.component.ts`,標籤目前為純英文(部分為純字串陣列、部分已是 `{text,value}` 物件陣列)。
| Enum | 型別/陣列位置 | 綁定形式 |
|------|----------------|----------|
| PaymentMethod | `giving.model.ts:1`;陣列 `offering-session-page.component.ts:36``givings-page.component.ts:44` | 純字串陣列(直接綁字串) |
| ExpenseStatus | `expense.model.ts:2`;陣列 `expenses-page.component.ts:34` | 純字串陣列 |
| Member Status | `member-form-dialog.component.ts:30``members-page.component.ts:39` | 純字串陣列 |
| Gender | `member-form-dialog.component.ts:31-35` | `{text,value}` 物件陣列 |
| Language | `member-form-dialog.ts``users/create-user-dialog.ts``users/edit-user-dialog.ts``members/create-user-dialog.ts` | `{text,value}` 物件陣列(已含「中文」) |
| Rolesmultiselect | `roleOptions: string[] = [...ALL_ROLES]``ALL_ROLES``users/models/user.model.ts:48` | 純字串陣列(13 個 snake_case 代碼) |
> Dashboard 範本殘留的示範下拉(`dashboard.html`)與未使用的通用元件不在範圍內。
---
## 3. 做法:方案 A(共用工具 + 兩處收斂)
評估過三種做法:
- **方案 A(採用)** — 共用 `bilingual()` 工具;DB 查表類在 service 層算出 `label`enum 類集中到 `option-lists.ts`。模板改動最小、與既有 `textField` 寫法一致、可定為全站慣例。
- 方案 B(不採用)— 每個下拉用 Kendo item/value `<ng-template>` + pipe。不動 DTO,但每個下拉需兩段樣板,8 個下拉冗長易漏,未來新下拉也須每次手寫。
- 方案 C(不採用)— 後端 C# 直接回傳合併字串。違反「前端顯示時拼接、資料分開存」,且幫不到第 2 類前端 enum。
---
## 4. 詳細設計
### 4.1 共用工具(新檔)
`APP/src/app/shared/i18n/bilingual.ts`
```ts
/** 顯示用雙語標籤:英文在前、中文在後,無中文則只回英文。 */
export const bilingual = (en: string, zh?: string | null): string =>
zh ? `${en}/${zh}` : en;
```
`APP/src/app/shared/i18n/option-lists.ts` — 集中所有寫死 enum 的雙語選項,型別為 `ReadonlyArray<{ value: string; label: string }>`。供全站及未來新下拉引用。
### 4.2 第 1 類:DB 查表下拉
1. **DTO 加唯讀 `label`**(可空,顯示用):`GivingCategoryDto``MinistryDto``ExpenseCategoryGroupDto``ExpenseSubCategoryDto` 各加 `label?: string;`
2. **Service 載入時計算 `label`**:在 `GivingCategoryApiService``MinistryApiService``ExpenseCategoryApiService`(含 groups 與 subs)的回傳 `map` 中,對每筆資料補 `label: bilingual(x.name_en, x.name_zh)`
- 集中在 service 層的理由:所有現有消費端自動雙語;未來新下拉只要綁同一 service 即免費雙語 → 即「未來約定」。
3. **模板**:對應下拉的 `textField="name_en"` 全部改為 `textField="label"`
4. **defaultItem 提示字改雙語**:例如
- `{ id: null, name_en: 'All types' }``{ id: null, label: 'All types/全部類型' }`
- `{ id: null, name_en: 'All Ministries' }``{ id: null, label: 'All Ministries/全部事工' }`
- 「-- Select ministry --」等 → 改為對應雙語提示。
defaultItem 物件需改為帶 `label` 欄以對應新的 `textField`。)
5. **奉獻明細回顯**`offering-session-page.component.ts``addLine()``categoryName: cat?.name_en ?? ''` 改為 `categoryName: cat?.label ?? ''`,使下方明細 Type 欄與下拉一致。
### 4.3 第 2 類:寫死 enum 下拉(集中到 `option-lists.ts`
預設譯名(**使用者已核可,可日後再微調**):
```ts
export const PAYMENT_METHOD_OPTIONS = [
{ value: 'Cash', label: 'Cash/現金' },
{ value: 'Check', label: 'Check/支票' },
{ value: 'Zelle', label: 'Zelle' },
{ value: 'PayPal', label: 'PayPal' },
{ value: 'Other', label: 'Other/其他' },
] as const;
export const EXPENSE_STATUS_OPTIONS = [
{ value: 'Draft', label: 'Draft/草稿' },
{ value: 'PendingApproval', label: 'PendingApproval/待審核' },
{ value: 'Approved', label: 'Approved/已核准' },
{ value: 'Paid', label: 'Paid/已付款' },
{ value: 'Rejected', label: 'Rejected/已拒絕' },
] as const;
export const MEMBER_STATUS_OPTIONS = [
{ value: 'Member', label: 'Member/會友' },
{ value: 'Visitor', label: 'Visitor/訪客' },
{ value: 'Inactive', label: 'Inactive/未活躍' },
{ value: 'Former', label: 'Former/已離開' },
] as const;
export const GENDER_OPTIONS = [
{ value: 'M', label: 'Male/男' },
{ value: 'F', label: 'Female/女' },
{ value: 'Other', label: 'Other/其他' },
] as const;
export const LANGUAGE_OPTIONS = [
{ value: 'en', label: 'English/英文' },
{ value: 'zh-TW', label: '中文/Chinese' },
] as const;
export const ROLE_OPTIONS = [
{ value: 'super_admin', label: 'Super Admin/系統管理員' },
{ value: 'pastor', label: 'Pastor/牧師' },
{ value: 'board_member', label: 'Board Member/理事' },
{ value: 'coworker_chair', label: 'Coworker Chair/同工會主席' },
{ value: 'ministry_leader', label: 'Ministry Leader/事工領袖' },
{ value: 'district_leader', label: 'District Leader/區長' },
{ value: 'cell_leader', label: 'Cell Leader/小組長' },
{ value: 'coworker', label: 'Coworker/同工' },
{ value: 'finance', label: 'Finance/財務同工' },
{ value: 'secretary', label: 'Secretary/行政秘書' },
{ value: 'worship_leader', label: 'Worship Leader/敬拜領袖' },
{ value: 'member', label: 'Member/一般教友' },
{ value: 'visitor', label: 'Visitor/訪客' },
] as const;
```
**綁定調整:**
- 原本綁純字串陣列者(PaymentMethod、ExpenseStatus、Member Status)改用上述物件陣列,模板補 `textField="label" valueField="value" [valuePrimitive]="true"`。**儲存/送出的值維持原本字串,不變。**
- Gender、Language 已是物件陣列,改引用集中常數;為全站一致,欄名統一用 `label`(取代原本的 `text`),模板對應改 `textField="label"`
- Roles multiselect 改綁 `ROLE_OPTIONS`,加 `textField="label" valueField="value" [valuePrimitive]="true"`,確保 `formControlName="roles"` 仍取得代碼陣列。
- 含篩選用空值的(如 members-page 的 `''` = 全部)保留一個 `{ value: '', label: 'All Status/全部狀態' }` 或沿用 `defaultItem`,行為不變。
### 4.4 約定文件
`docs/PLANNING.md` 新增一節「下拉雙語顯示約定」:
- DB 查表下拉 → 用 service 計算的 `label``bilingual(name_en, name_zh)`),模板綁 `textField="label"`
- 寫死 enum 下拉 → 一律定義在 `shared/i18n/option-lists.ts``{value,label}`,模板綁 `textField="label" valueField="value" [valuePrimitive]="true"`
- 格式 `英文/中文`、無空格、無中文則只英文。
---
## 5. 不在範圍(YAGNI / 本次不做)
- 不導入 Angular i18n / 翻譯框架(使用者要的是「同時顯示兩種語言」,非切換語系)。
- 不改 DB、不改 EF 實體、不改 migration、不改種子資料。
- 不改後端 API 回傳的列表名稱欄位(如支出列表的 ministry/category 名稱),故該等表格欄位本次維持英文。
- Dashboard 範本示範下拉與未使用的通用元件不處理。
---
## 6. 驗證
1. 前端 `build` 通過(型別正確)。
2. 起 dev server,以 preview 截圖確認:
- 奉獻錄入頁:Type 顯示「Worship/敬拜」等、Method 顯示「Cash/現金」等;加入一筆後,下方明細 Type 欄同步雙語。
- 支出對話框:Ministry / 大類 / 子項雙語。
- 會友表單:Gender、Status、Language 雙語。
- 使用者對話框:Roles multiselect 雙語。
3. 確認**送出後實際儲存的值不變**(仍為 `Cash``Member`、角色代碼、類別 id 等)。
---
## 7. 影響檔案清單(預估)
**新增**
- `APP/src/app/shared/i18n/bilingual.ts`
- `APP/src/app/shared/i18n/option-lists.ts`
**修改(DTO / Service**
- `app/features/giving/models/giving.model.ts``GivingCategoryDto``label`
- `app/features/expense/models/expense.model.ts``MinistryDto``ExpenseCategoryGroupDto``ExpenseSubCategoryDto``label`
- `GivingCategoryApiService``MinistryApiService``ExpenseCategoryApiService`map 計算 `label`
**修改(模板 / 元件)**
- `offering-session-page.component.{html,ts}`
- `givings-page.component.{html,ts}`
- `expenses-page.component.{html,ts}`
- `expense-form-dialog.component.{html,ts}`
- `member-form-dialog.component.{html,ts}`
- `members-page.component.{html,ts}`
- `users/create-user-dialog.component.{html,ts}``users/edit-user-dialog.component.{html,ts}`
- `members/create-user-dialog.component.{html,ts}`
**文件**
- `docs/PLANNING.md`(新增約定一節)