Files
ROLAC/docs/superpowers/plans/2026-05-29-bilingual-dropdown-options.md
T
2026-05-29 21:56:22 -07:00

32 KiB
Raw Blame History

Bilingual Dropdown Options (English/中文) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make business dropdown options display English and Chinese together as English/中文 (e.g. Worship/敬拜, Cash/現金) across the Angular app, without changing stored values or the database.

Architecture: Two seams. (1) DB-backed lookups (giving categories, ministries, expense category groups/subs) already carry name_en + name_zh; their API services compute a display-only label = bilingual(name_en, name_zh) at load time and templates bind textField="label". (2) Hard-coded enum dropdowns (payment method, expense status, member status, gender, language, roles) move to a central shared/i18n/option-lists.ts of {value,label} arrays. A single bilingual() helper produces the 英文/中文 string. Values bound into forms/DB are unchanged.

Tech Stack: Angular 20 (standalone components), Kendo UI for Angular v20 (DropDownList / MultiSelect), Karma + Jasmine (ng test), Tailwind v4 for layout. Spec: docs/superpowers/specs/2026-05-29-bilingual-dropdown-options-design.md.

Conventions to honor:

  • Kendo: whenever textField/valueField are set against an object array, also set [valuePrimitive]="true" so the form binds the scalar value, not the whole object.
  • Format: 英文/中文, no spaces, English first; if no Chinese, show English only.
  • Work directly on main (repo convention — recent feature work is committed straight to main). The working tree already has unrelated modified/deleted files; stage only the files each task names — never git add -A.

File Structure

New files

  • APP/src/app/shared/i18n/bilingual.ts — the bilingual(en, zh) helper. One responsibility: format a bilingual display string.
  • APP/src/app/shared/i18n/bilingual.spec.ts — unit tests for the helper.
  • APP/src/app/shared/i18n/option-lists.ts — central {value,label} option arrays for all hard-coded enums.
  • APP/src/app/shared/i18n/option-lists.spec.ts — guards values/shape of the option arrays.

Modified (DTO / service — compute label)

  • APP/src/app/features/giving/models/giving.model.ts
  • APP/src/app/features/expense/models/expense.model.ts
  • APP/src/app/features/giving/services/giving-category-api.service.ts
  • APP/src/app/features/expense/services/ministry-api.service.ts
  • APP/src/app/features/expense/services/expense-category-api.service.ts

Modified (templates / components — bind labels)

  • APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.{html,ts}
  • APP/src/app/features/giving/pages/givings-page/givings-page.component.{html,ts}
  • APP/src/app/features/expense/pages/expenses-page/expenses-page.component.{html,ts}
  • APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.{html,ts}
  • APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.{html,ts}
  • APP/src/app/features/members/pages/members-page/members-page.component.{html,ts}
  • APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.{html,ts}
  • APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.{html,ts}
  • APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.{html,ts}

Docs

  • docs/PLANNING.md — add the dropdown-bilingual convention section.

Testing note: The helper and option-lists get real Jasmine unit tests (npm run test:ci). The template/wiring tasks are display-only changes verified by a clean ng build (type safety) plus the final browser preview pass (Task 10) — the repo has no Karma DOM tests for these Kendo pages, and adding brittle ones is out of scope.


Task 1: bilingual() helper

Files:

  • Create: APP/src/app/shared/i18n/bilingual.ts

  • Test: APP/src/app/shared/i18n/bilingual.spec.ts

  • Step 1: Write the failing test

Create APP/src/app/shared/i18n/bilingual.spec.ts:

import { bilingual } from './bilingual';

describe('bilingual', () => {
  it('joins English and Chinese with a slash, no spaces', () => {
    expect(bilingual('Worship', '敬拜')).toBe('Worship/敬拜');
  });

  it('returns English only when Chinese is null', () => {
    expect(bilingual('Zelle', null)).toBe('Zelle');
  });

  it('returns English only when Chinese is undefined', () => {
    expect(bilingual('PayPal')).toBe('PayPal');
  });

  it('returns English only when Chinese is an empty string', () => {
    expect(bilingual('Other', '')).toBe('Other');
  });
});
  • Step 2: Run test to verify it fails

Run: cd APP && npm run test:ci Expected: FAIL — Cannot find module './bilingual' (compilation error).

  • Step 3: Write minimal implementation

Create APP/src/app/shared/i18n/bilingual.ts:

/**
 * Display-only bilingual label: English first, Chinese second, joined by '/'
 * with no spaces. Falls back to English alone when no Chinese is provided.
 * Does NOT alter stored values — for display binding only.
 */
export const bilingual = (en: string, zh?: string | null): string =>
  zh ? `${en}/${zh}` : en;
  • Step 4: Run test to verify it passes

Run: cd APP && npm run test:ci Expected: PASS (4 specs in the bilingual suite).

  • Step 5: Commit
git add APP/src/app/shared/i18n/bilingual.ts APP/src/app/shared/i18n/bilingual.spec.ts
git commit -m "feat(i18n): add bilingual() display helper"

Task 2: Central option lists for hard-coded enums

Files:

  • Create: APP/src/app/shared/i18n/option-lists.ts

  • Test: APP/src/app/shared/i18n/option-lists.spec.ts

  • Step 1: Write the failing test

Create APP/src/app/shared/i18n/option-lists.spec.ts:

import {
  PAYMENT_METHOD_OPTIONS, EXPENSE_STATUS_OPTIONS, MEMBER_STATUS_OPTIONS,
  GENDER_OPTIONS, LANGUAGE_OPTIONS, ROLE_OPTIONS,
} from './option-lists';

describe('option-lists', () => {
  it('payment methods preserve raw values and show bilingual labels', () => {
    expect(PAYMENT_METHOD_OPTIONS.map(o => o.value))
      .toEqual(['Cash', 'Check', 'Zelle', 'PayPal', 'Other']);
    expect(PAYMENT_METHOD_OPTIONS.find(o => o.value === 'Cash')!.label).toBe('Cash/現金');
    expect(PAYMENT_METHOD_OPTIONS.find(o => o.value === 'Zelle')!.label).toBe('Zelle');
  });

  it('expense statuses preserve raw enum values', () => {
    expect(EXPENSE_STATUS_OPTIONS.map(o => o.value))
      .toEqual(['Draft', 'PendingApproval', 'Approved', 'Paid', 'Rejected']);
  });

  it('member statuses preserve raw values', () => {
    expect(MEMBER_STATUS_OPTIONS.map(o => o.value))
      .toEqual(['Member', 'Visitor', 'Inactive', 'Former']);
  });

  it('gender values stay M/F/Other', () => {
    expect(GENDER_OPTIONS.map(o => o.value)).toEqual(['M', 'F', 'Other']);
  });

  it('language values stay en/zh-TW', () => {
    expect(LANGUAGE_OPTIONS.map(o => o.value)).toEqual(['en', 'zh-TW']);
  });

  it('role options cover all 13 role codes', () => {
    expect(ROLE_OPTIONS.length).toBe(13);
    expect(ROLE_OPTIONS.map(o => o.value)).toContain('super_admin');
    expect(ROLE_OPTIONS.map(o => o.value)).toContain('visitor');
  });
});
  • Step 2: Run test to verify it fails

Run: cd APP && npm run test:ci Expected: FAIL — Cannot find module './option-lists'.

  • Step 3: Write minimal implementation

Create APP/src/app/shared/i18n/option-lists.ts:

/**
 * Central bilingual option lists for hard-coded enum dropdowns.
 * `value` is the raw value stored/sent to the API (unchanged from before);
 * `label` is the display string `英文/中文`. Bind in templates with
 * textField="label" valueField="value" [valuePrimitive]="true".
 */
export interface BilingualOption { readonly value: string; readonly label: string; }

export const PAYMENT_METHOD_OPTIONS: readonly BilingualOption[] = [
  { value: 'Cash',   label: 'Cash/現金' },
  { value: 'Check',  label: 'Check/支票' },
  { value: 'Zelle',  label: 'Zelle' },
  { value: 'PayPal', label: 'PayPal' },
  { value: 'Other',  label: 'Other/其他' },
];

export const EXPENSE_STATUS_OPTIONS: readonly BilingualOption[] = [
  { value: 'Draft',           label: 'Draft/草稿' },
  { value: 'PendingApproval', label: 'PendingApproval/待審核' },
  { value: 'Approved',        label: 'Approved/已核准' },
  { value: 'Paid',            label: 'Paid/已付款' },
  { value: 'Rejected',        label: 'Rejected/已拒絕' },
];

export const MEMBER_STATUS_OPTIONS: readonly BilingualOption[] = [
  { value: 'Member',   label: 'Member/會友' },
  { value: 'Visitor',  label: 'Visitor/訪客' },
  { value: 'Inactive', label: 'Inactive/未活躍' },
  { value: 'Former',   label: 'Former/已離開' },
];

export const GENDER_OPTIONS: readonly BilingualOption[] = [
  { value: 'M',     label: 'Male/男' },
  { value: 'F',     label: 'Female/女' },
  { value: 'Other', label: 'Other/其他' },
];

export const LANGUAGE_OPTIONS: readonly BilingualOption[] = [
  { value: 'en',    label: 'English/英文' },
  { value: 'zh-TW', label: '中文/Chinese' },
];

export const ROLE_OPTIONS: readonly BilingualOption[] = [
  { 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/訪客' },
];
  • Step 4: Run test to verify it passes

Run: cd APP && npm run test:ci Expected: PASS (all option-lists specs).

  • Step 5: Commit
git add APP/src/app/shared/i18n/option-lists.ts APP/src/app/shared/i18n/option-lists.spec.ts
git commit -m "feat(i18n): add central bilingual option lists for enum dropdowns"

Task 3: DB-backed DTOs gain label; services compute it

Files:

  • Modify: APP/src/app/features/giving/models/giving.model.ts (add label? to GivingCategoryDto)
  • Modify: APP/src/app/features/expense/models/expense.model.ts (add label? to MinistryDto, ExpenseCategoryGroupDto, ExpenseSubCategoryDto)
  • Modify: APP/src/app/features/giving/services/giving-category-api.service.ts
  • Modify: APP/src/app/features/expense/services/ministry-api.service.ts
  • Modify: APP/src/app/features/expense/services/expense-category-api.service.ts

No unit test here — this is a typed data-shaping change verified by ng build. Behavior is exercised end-to-end in Task 10.

  • Step 1: Add label? to the giving DTO

In giving.model.ts, add label?: string; to GivingCategoryDto (keep the existing isActive/sortOrder fields — only the label? line is new):

export interface GivingCategoryDto {
  id: number;
  name_en: string;
  name_zh: string | null;
  description_en: string | null;
  description_zh: string | null;
  isActive: boolean;
  sortOrder: number;
  /** Display-only bilingual label, computed in the API service. */
  label?: string;
}
  • Step 2: Add label? to the expense DTOs

In expense.model.ts, add label?: string; to MinistryDto, ExpenseCategoryGroupDto, and ExpenseSubCategoryDto. Example for MinistryDto:

export interface MinistryDto {
  id: number;
  name_en: string;
  name_zh: string | null;
  sortOrder: number;
  isActive: boolean;
  /** Display-only bilingual label, computed in the API service. */
  label?: string;
}

Do the same (label?: string;) for ExpenseCategoryGroupDto and ExpenseSubCategoryDto.

  • Step 3: Compute label in GivingCategoryApiService.getAll

In giving-category-api.service.ts, the current import is import { Observable } from 'rxjs'; — change it to import { Observable, map } from 'rxjs'; and add import { bilingual } from '../../../shared/i18n/bilingual';. Then pipe the existing getAll (keep the HttpParams exactly as-is, just append .pipe(...)):

  getAll(includeInactive = false): Observable<GivingCategoryDto[]> {
    const params = new HttpParams().set('includeInactive', includeInactive);
    return this.http.get<GivingCategoryDto[]>(this.endpoint, { params }).pipe(
      map(list => list.map(c => ({ ...c, label: bilingual(c.name_en, c.name_zh) }))),
    );
  }
  • Step 4: Compute label in the ministry service

Open ministry-api.service.ts. Find the method that returns Observable<MinistryDto[]> (e.g. getAll). Add the same imports (map from rxjs, bilingual) and pipe each item:

.pipe(map(list => list.map(m => ({ ...m, label: bilingual(m.name_en, m.name_zh) }))))
  • Step 5: Compute label for expense category groups AND their sub-categories

Open expense-category-api.service.ts. Find the method returning Observable<ExpenseCategoryGroupDto[]> (e.g. getAll). Decorate both the group and its nested subCategories:

import { map } from 'rxjs';
import { bilingual } from '../../../shared/i18n/bilingual';
// ...
.pipe(map(groups => groups.map(g => ({
  ...g,
  label: bilingual(g.name_en, g.name_zh),
  subCategories: g.subCategories.map(s => ({ ...s, label: bilingual(s.name_en, s.name_zh) })),
}))))

If sub-categories are fetched by a separate method/endpoint instead of nested, apply the same .pipe(map(list => list.map(s => ({ ...s, label: bilingual(s.name_en, s.name_zh) })))) to that method too.

  • Step 6: Build to verify types

Run: cd APP && npm run build Expected: build succeeds with no TypeScript errors.

  • Step 7: Commit
git add APP/src/app/features/giving/models/giving.model.ts APP/src/app/features/expense/models/expense.model.ts APP/src/app/features/giving/services/giving-category-api.service.ts APP/src/app/features/expense/services/ministry-api.service.ts APP/src/app/features/expense/services/expense-category-api.service.ts
git commit -m "feat(i18n): compute bilingual label on giving/ministry/expense-category lookups"

Task 4: Offering-session page — Type dropdown + line echo

Files:

  • Modify: APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.html

  • Modify: APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.ts

  • Step 1: Bind the Type dropdown to label

In the HTML, the giving Type dropdown currently reads:

<kendo-dropdownlist [data]="categories" textField="name_en" valueField="id" [valuePrimitive]="true"
  [(ngModel)]="entry.givingCategoryId"></kendo-dropdownlist>

Change textField="name_en" to textField="label". Leave valueField, valuePrimitive, and ngModel unchanged.

  • Step 2: Convert the Method (payment) dropdown to bilingual options

The Method dropdown currently binds a bare string array:

<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>

Change it to use the central options with primitive binding:

<kendo-dropdownlist [data]="paymentMethods" textField="label" valueField="value" [valuePrimitive]="true"
  [(ngModel)]="entry.paymentMethod"></kendo-dropdownlist>
  • Step 3: Update the component to use central payment options and bilingual line echo

In offering-session-page.component.ts:

  • Add import: import { PAYMENT_METHOD_OPTIONS } from '../../../../shared/i18n/option-lists';

  • Replace the field readonly paymentMethods: PaymentMethod[] = ['Cash', 'Check', 'Zelle', 'PayPal', 'Other']; with:

    readonly paymentMethods = PAYMENT_METHOD_OPTIONS;
    
  • In addLine(), change the line-echo from English-only to the bilingual label:

    categoryName: cat?.label ?? '',
    

    (was categoryName: cat?.name_en ?? '').

  • Step 4: Build to verify

Run: cd APP && npm run build Expected: build succeeds. (entry.paymentMethod is still a PaymentMethod string; [valuePrimitive] binds value which is that string.)

  • Step 5: Commit
git add APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.html APP/src/app/features/giving/pages/offering-session-page/offering-session-page.component.ts
git commit -m "feat(i18n): bilingual Type/Method dropdowns + line echo on offering session"

Task 5: Givings page — Type filter, Type dialog, Method

Files:

  • Modify: APP/src/app/features/giving/pages/givings-page/givings-page.component.html

  • Modify: APP/src/app/features/giving/pages/givings-page/givings-page.component.ts

  • Step 1: Bind both category dropdowns to label

In the HTML there are two category dropdowns (a filter near line 9 and a dialog near line 48), both textField="name_en". Change textField="name_en"textField="label" on both.

  • Step 2: Fix the filter defaultItem to carry a bilingual label

The filter dropdown has a [defaultItem]="{ id: null, name_en: 'All types' }". Because textField is now label, change it to:

[defaultItem]="{ id: null, label: 'All types/全部類型' }"
  • Step 3: Convert the Method dropdown to bilingual options

Change the payment dropdown from the bare-string form:

<kendo-dropdownlist [data]="paymentMethods" [(ngModel)]="form.paymentMethod"></kendo-dropdownlist>

to:

<kendo-dropdownlist [data]="paymentMethods" textField="label" valueField="value" [valuePrimitive]="true"
  [(ngModel)]="form.paymentMethod"></kendo-dropdownlist>

(Match the existing [(ngModel)] target — it may be form.paymentMethod or similar; keep whatever the file already binds.)

  • Step 4: Update the component to use central payment options

In givings-page.component.ts:

  • Add import: import { PAYMENT_METHOD_OPTIONS } from '../../../../shared/i18n/option-lists';

  • Replace the readonly paymentMethods: PaymentMethod[] = [...] field with readonly paymentMethods = PAYMENT_METHOD_OPTIONS;

  • Step 5: Build to verify

Run: cd APP && npm run build Expected: build succeeds.

  • Step 6: Commit
git add APP/src/app/features/giving/pages/givings-page/givings-page.component.html APP/src/app/features/giving/pages/givings-page/givings-page.component.ts
git commit -m "feat(i18n): bilingual category + method dropdowns on givings page"

Task 6: Expense — list filters + form dialog

Files:

  • Modify: APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html

  • Modify: APP/src/app/features/expense/pages/expenses-page/expenses-page.component.ts

  • Modify: APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html

  • Step 1: Ministry filter → label, fix its defaultItem

In expenses-page.component.html, the Ministry filter is textField="name_en" with [defaultItem]="{ id: null, name_en: 'All Ministries' }". Change textField="name_en"textField="label" and the defaultItem to:

[defaultItem]="{ id: null, label: 'All Ministries/全部事工' }"
  • Step 2: Convert the Status filter to bilingual options

The Status filter binds a bare string array: <kendo-dropdownlist [data]="statuses" [(ngModel)]="filter.status" [defaultItem]="null"> (keep the exact ngModel/defaultItem the file uses). Change it to:

<kendo-dropdownlist [data]="statuses" textField="label" valueField="value" [valuePrimitive]="true"
  [(ngModel)]="filter.status" [defaultItem]="{ value: null, label: 'All Status/全部狀態' }"></kendo-dropdownlist>

(If the existing code used [defaultItem]="null", replace with the object above so the "all" row still renders a label; the bound value stays null.)

  • Step 3: Update the expenses-page component to central status options

In expenses-page.component.ts:

  • Add import: import { EXPENSE_STATUS_OPTIONS } from '../../../../shared/i18n/option-lists';

  • Replace readonly statuses: ExpenseStatus[] = ['Draft', 'PendingApproval', 'Approved', 'Paid', 'Rejected']; with readonly statuses = EXPENSE_STATUS_OPTIONS;

  • Step 4: Form dialog — Ministry / Group / Sub dropdowns → label

In expense-form-dialog.component.html, three dropdowns use textField="name_en" (Ministry ~line 22, Group ~line 34, Sub ~line 47). Change all three to textField="label". For any defaultItem placeholders on these (e.g. { id: null, name_en: '-- Select ministry --' }), change the key to label with bilingual text:

[defaultItem]="{ id: null, label: '-- Select ministry --/請選擇事工' }"

Apply the equivalent bilingual placeholder for the group and sub dropdowns (e.g. -- Select category --/請選擇大類, -- Select sub-category --/請選擇子項).

  • Step 5: Build to verify

Run: cd APP && npm run build Expected: build succeeds. (filter.status remains an ExpenseStatus | null; primitive binding supplies the value.)

  • Step 6: Commit
git add APP/src/app/features/expense/pages/expenses-page/expenses-page.component.html APP/src/app/features/expense/pages/expenses-page/expenses-page.component.ts APP/src/app/features/expense/components/expense-form-dialog/expense-form-dialog.component.html
git commit -m "feat(i18n): bilingual ministry/category/status dropdowns on expense pages"

Task 7: Member form + members page — Gender & Status

Files:

  • Modify: APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html

  • Modify: APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.ts

  • Modify: APP/src/app/features/members/pages/members-page/members-page.component.html

  • Modify: APP/src/app/features/members/pages/members-page/members-page.component.ts

  • Step 1: member-form-dialog — switch Gender + Status to central options

In member-form-dialog.component.ts:

  • Add import: import { GENDER_OPTIONS, MEMBER_STATUS_OPTIONS } from '../../../../shared/i18n/option-lists';

  • Replace the local genderOptions = [{ text:'Male', value:'M' }, ...] field with readonly genderOptions = GENDER_OPTIONS;

  • Replace readonly statusOptions = ['Member', 'Visitor', 'Inactive', 'Former']; with readonly statusOptions = MEMBER_STATUS_OPTIONS;

  • Step 2: member-form-dialog — update HTML bindings

In member-form-dialog.component.html:

  • Gender dropdown currently textField="text" valueField="value". Change textField="text"textField="label". Ensure [valuePrimitive]="true" is present (add it if missing). If there is a Gender [defaultItem]="{ text: '-- Select --', value: null }", change textlabel and make it bilingual: [defaultItem]="{ label: '-- Select --/請選擇', value: null }".

  • Status dropdown currently <kendo-dropdownlist formControlName="status" [data]="statusOptions"> (bare strings). Change to:

    <kendo-dropdownlist formControlName="status" [data]="statusOptions"
      textField="label" valueField="value" [valuePrimitive]="true"></kendo-dropdownlist>
    
  • Step 3: members-page — Status filter to central options + an "all" row

In members-page.component.ts:

  • Add import: import { MEMBER_STATUS_OPTIONS } from '../../../../shared/i18n/option-lists';

  • The current field is readonly statusOptions = ['', 'Member', 'Visitor', 'Inactive', 'Former']; where '' means "all". Replace with:

    readonly statusOptions = [
      { value: '', label: 'All Status/全部狀態' },
      ...MEMBER_STATUS_OPTIONS,
    ];
    
  • Step 4: members-page — update HTML binding

In members-page.component.html, the Status filter is <kendo-dropdownlist [data]="statusOptions" ...> binding bare strings. Add the object bindings (keep the existing [(ngModel)]/filter target):

<kendo-dropdownlist [data]="statusOptions" textField="label" valueField="value" [valuePrimitive]="true"
  ...keep existing ngModel/handlers...></kendo-dropdownlist>

The empty-string value still flows through unchanged as the "all" selection.

  • Step 5: Build to verify

Run: cd APP && npm run build Expected: build succeeds.

  • Step 6: Commit
git add APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.ts APP/src/app/features/members/pages/members-page/members-page.component.html APP/src/app/features/members/pages/members-page/members-page.component.ts
git commit -m "feat(i18n): bilingual gender/status dropdowns on member form + members page"

Task 8: User dialogs — Language & Roles

Files:

  • Modify: APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.{html,ts}
  • Modify: APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.{html,ts}
  • Modify: APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.{html,ts}

Three dialogs share the same shape (a langOptions array and a roleOptions = [...ALL_ROLES]). Apply the identical change to each — they are listed explicitly so you don't skip one.

  • Step 1: users/create-user-dialog — component

In users/components/create-user-dialog/create-user-dialog.component.ts:

  • Add import: import { LANGUAGE_OPTIONS, ROLE_OPTIONS } from '../../../../shared/i18n/option-lists';

  • Replace the local langOptions = [{ text:'English', value:'en' }, { text:'中文', value:'zh-TW' }] with readonly langOptions = LANGUAGE_OPTIONS;

  • Replace roleOptions: string[] = [...ALL_ROLES]; with readonly roleOptions = ROLE_OPTIONS; (remove the now-unused ALL_ROLES import if nothing else uses it).

  • Step 2: users/create-user-dialog — HTML

In the matching .html:

  • Language dropdown is [data]="langOptions" textField="text" valueField="value". Change textField="text"textField="label". Ensure [valuePrimitive]="true" is present.

  • Roles multiselect is <kendo-multiselect formControlName="roles" [data]="roleOptions" ...> (bare strings). Add object bindings:

    <kendo-multiselect formControlName="roles" [data]="roleOptions"
      textField="label" valueField="value" [valuePrimitive]="true" ...keep existing attrs...></kendo-multiselect>
    

    [valuePrimitive]="true" ensures the form control still holds an array of role-code strings.

  • Step 3: users/edit-user-dialog — apply the same component + HTML changes

Repeat Step 1 and Step 2 in users/components/edit-user-dialog/create…→ the edit-user-dialog.component.ts and .html. The edit dialog also pre-selects existing roles; because values are still role-code strings and [valuePrimitive]="true", the existing formControlName="roles" value binds correctly with no further change.

  • Step 4: members/create-user-dialog — apply the same changes

Repeat Step 1 and Step 2 in members/components/create-user-dialog/create-user-dialog.component.ts and .html. (Import path depth is the same four levels: ../../../../shared/i18n/option-lists.)

  • Step 5: Build to verify

Run: cd APP && npm run build Expected: build succeeds. If the build flags an unused ALL_ROLES import, remove it in the offending file.

  • Step 6: Commit
git add APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.html APP/src/app/features/users/components/create-user-dialog/create-user-dialog.component.ts APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.html APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.ts APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.html APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.ts
git commit -m "feat(i18n): bilingual language + roles selectors in user dialogs"

Task 9: Document the convention

Files:

  • Modify: docs/PLANNING.md

  • Step 1: Append the convention section

Add a new section to docs/PLANNING.md (place it under the relevant frontend/UI area; if unsure, append at the end before any trailing footer):

## 下拉雙語顯示約定 (Dropdown Bilingual Display)

所有業務下拉選單的選項同時顯示英文與中文,格式 `英文/中文`(無空格、英文在前;無中文則只顯示英文)。

- **DB 查表下拉**giving categories / ministries / expense category groups & subs):
  在對應的 API service 載入時計算 `label = bilingual(name_en, name_zh)`,模板綁 `textField="label"`。
  不改 DB、不改 schema、不改儲存值。
- **寫死 enum 下拉**payment method / expense status / member status / gender / language / roles):
  選項一律定義在 `APP/src/app/shared/i18n/option-lists.ts`,型別 `{ value, label }`
  模板綁 `textField="label" valueField="value" [valuePrimitive]="true"`。送出/儲存的 `value` 不變。
- 共用工具:`APP/src/app/shared/i18n/bilingual.ts``bilingual(en, zh)`- 新增下拉時沿用以上兩種模式,不要在 component 內各自寫死中文字串。
  • Step 2: Commit
git add docs/PLANNING.md
git commit -m "docs: record dropdown bilingual display convention"

Task 10: Full verification (build + browser preview)

Files: none (verification only).

  • Step 1: Clean build

Run: cd APP && npm run build Expected: succeeds with no errors.

  • Step 2: Run the unit suites

Run: cd APP && npm run test:ci Expected: bilingual and option-lists suites PASS (and no previously-passing suite regresses).

  • Step 3: Start the app and verify in the browser preview

Start the dev server (preview_start / npm start) and, using the preview tools, confirm each of the following renders 英文/中文:

  • Offering-session page: Type shows e.g. Worship/敬拜; Method shows Cash/現金. Add one line → the Type column in the lines grid below also shows the bilingual label.
  • Expense form dialog: Ministry, 大類 (group), 子項 (sub) all bilingual.
  • Expenses page: Ministry and Status filters bilingual.
  • Member form dialog: Gender, Status bilingual. Members page: Status filter bilingual.
  • User dialogs (create/edit): Language bilingual; Roles multiselect options bilingual.

Capture preview_screenshot evidence for the offering-session and expense dialog as proof.

  • Step 4: Confirm stored values are unchanged

In the preview, select bilingual options and submit one offering line and (if feasible) one expense; via preview_network confirm the request payload still sends raw values (paymentMethod: "Cash", givingCategoryId: <int>, status codes, role codes) — NOT the 英文/中文 label.

  • Step 5: Final confirmation

No commit needed (verification only). Report results; if any dropdown still shows English-only or a payload carries a label string, return to the relevant task and fix.


Notes for the implementer

  • Import-path depth: shared/i18n/... is referenced from features/<area>/pages/<page>/ (4 levels up → ../../../../shared/i18n/...) and from features/<area>/components/<comp>/ (also 4 levels up). Services under features/<area>/services/ are 3 levels up (../../../shared/i18n/...). Verify the relative depth per file; the build will catch a wrong path.
  • Don't touch stored values. Every change is display-only. [valuePrimitive]="true" + valueField="value" is what keeps form/DB values identical to today.
  • Out of scope (per spec §5): server-sourced grid name columns (e.g. expense list ministry/category names), Angular i18n framework, DB/seed/migration changes, dashboard template demo dropdowns.