From e37aade69fab8c22164c332d16f5be996fb38085 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Fri, 29 May 2026 21:50:31 -0700 Subject: [PATCH] docs: spec for bilingual (English/Chinese) dropdown options Co-Authored-By: Claude Opus 4.8 --- ...05-29-bilingual-dropdown-options-design.md | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-29-bilingual-dropdown-options-design.md diff --git a/docs/superpowers/specs/2026-05-29-bilingual-dropdown-options-design.md b/docs/superpowers/specs/2026-05-29-bilingual-dropdown-options-design.md new file mode 100644 index 0000000..d75425c --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-bilingual-dropdown-options-design.md @@ -0,0 +1,206 @@ +# 下拉選項雙語顯示(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 `` + 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`(新增約定一節)