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

11 KiB
Raw Blame History

下拉選項雙語顯示(English/中文)— 設計文件

日期: 2026-05-29 狀態: 已核准設計,待寫實作計畫 範圍: 前端(Angular)。不動資料庫、不動 schema、不動既有後端 API 回傳的列表名稱。


1. 目標

針對大多數下拉選單,選項同時顯示英文與中文,格式為 英文/中文(例:Worship/敬拜Cash/現金)。

設計決定(已與使用者確認):

  • 顯示時拼接、資料分開存 — 中英文在資料層維持分開(DB 兩欄 / 前端 value 不變),只在「顯示」時組成 英文/中文。送出/儲存的值完全不變。
  • 全站套用 + 未來約定 — 現有所有業務下拉都改;並定成往後新增下拉的統一慣例。
  • 回顯一致性 — 下拉改雙語後,前端自己組得出來的已選值回顯也一起改(目前僅奉獻錄入頁下方明細的 Type 欄)。後端帶 name_en 過來的列表欄位(如支出列表)本次不動。

格式規則:

  • 英文在前、中文在後,中間以 / 分隔、無空格
  • 沒有中文時只顯示英文(如品牌字 ZellePayPal)。

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 categoriesGivingCategoryApiService.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 ministriesMinistryApiService.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:36givings-page.component.ts:44 純字串陣列(直接綁字串)
ExpenseStatus expense.model.ts:2;陣列 expenses-page.component.ts:34 純字串陣列
Member Status member-form-dialog.component.ts:30members-page.component.ts:39 純字串陣列
Gender member-form-dialog.component.ts:31-35 {text,value} 物件陣列
Language member-form-dialog.tsusers/create-user-dialog.tsusers/edit-user-dialog.tsmembers/create-user-dialog.ts {text,value} 物件陣列(已含「中文」)
Rolesmultiselect roleOptions: string[] = [...ALL_ROLES]ALL_ROLESusers/models/user.model.ts:48 純字串陣列(13 個 snake_case 代碼)

Dashboard 範本殘留的示範下拉(dashboard.html)與未使用的通用元件不在範圍內。


3. 做法:方案 A(共用工具 + 兩處收斂)

評估過三種做法:

  • 方案 A(採用) — 共用 bilingual() 工具;DB 查表類在 service 層算出 labelenum 類集中到 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

/** 顯示用雙語標籤:英文在前、中文在後,無中文則只回英文。 */
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(可空,顯示用):GivingCategoryDtoMinistryDtoExpenseCategoryGroupDtoExpenseSubCategoryDto 各加 label?: string;
  2. Service 載入時計算 label:在 GivingCategoryApiServiceMinistryApiServiceExpenseCategoryApiService(含 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.tsaddLine()categoryName: cat?.name_en ?? '' 改為 categoryName: cat?.label ?? '',使下方明細 Type 欄與下拉一致。

4.3 第 2 類:寫死 enum 下拉(集中到 option-lists.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 計算的 labelbilingual(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. 確認送出後實際儲存的值不變(仍為 CashMember、角色代碼、類別 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.tsGivingCategoryDtolabel
  • app/features/expense/models/expense.model.tsMinistryDtoExpenseCategoryGroupDtoExpenseSubCategoryDtolabel
  • GivingCategoryApiServiceMinistryApiServiceExpenseCategoryApiServicemap 計算 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(新增約定一節)