e37aade69f
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
207 lines
11 KiB
Markdown
207 lines
11 KiB
Markdown
# 下拉選項雙語顯示(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}` 物件陣列(已含「中文」) |
|
||
| Roles(multiselect) | `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`(新增約定一節)
|