Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
32 KiB
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/valueFieldare 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 tomain). The working tree already has unrelated modified/deleted files; stage only the files each task names — nevergit add -A.
File Structure
New files
APP/src/app/shared/i18n/bilingual.ts— thebilingual(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.tsAPP/src/app/features/expense/models/expense.model.tsAPP/src/app/features/giving/services/giving-category-api.service.tsAPP/src/app/features/expense/services/ministry-api.service.tsAPP/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(addlabel?toGivingCategoryDto) - Modify:
APP/src/app/features/expense/models/expense.model.ts(addlabel?toMinistryDto,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
labelinGivingCategoryApiService.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
labelin 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
labelfor 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
defaultItemto 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 withreadonly 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 itsdefaultItem
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'];withreadonly 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 withreadonly genderOptions = GENDER_OPTIONS; -
Replace
readonly statusOptions = ['Member', 'Visitor', 'Inactive', 'Former'];withreadonly 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". ChangetextField="text"→textField="label". Ensure[valuePrimitive]="true"is present (add it if missing). If there is a Gender[defaultItem]="{ text: '-- Select --', value: null }", changetext→labeland 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
langOptionsarray and aroleOptions = [...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' }]withreadonly langOptions = LANGUAGE_OPTIONS; -
Replace
roleOptions: string[] = [...ALL_ROLES];withreadonly roleOptions = ROLE_OPTIONS;(remove the now-unusedALL_ROLESimport 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". ChangetextField="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 showsCash/現金. 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 fromfeatures/<area>/pages/<page>/(4 levels up →../../../../shared/i18n/...) and fromfeatures/<area>/components/<comp>/(also 4 levels up). Services underfeatures/<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.