Skip to content

Commit 5afdc0d

Browse files
committed
Merge origin/main into claude/issue-25504-showcase-money-format
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
2 parents 2a70334 + 5ea211c commit 5afdc0d

6 files changed

Lines changed: 1378 additions & 953 deletions

File tree

‎examples/app-showcase/src/system/translations/index.ts‎

Lines changed: 154 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -372,16 +372,38 @@ export const ShowcaseTranslationBundle = {
372372
showcase_project: {
373373
label: '项目',
374374
pluralLabel: '项目',
375+
description: '为某个客户交付的项目。',
376+
// Every field the project grid and record surface as a column, and
377+
// every option a status / health / labels cell renders — the grid
378+
// that shows 进行中 for a task must not show "Active" for a project.
379+
// `planned` reads 计划中 because `_validations.project_status_flow`
380+
// below already says so; `active` reads 进行中 for the same reason
381+
// `apps…nav_active_projects` does. `task_count` is a count column,
382+
// hence 任务数; `total_estimate` reuses showcase_task's 预计工时.
375383
fields: {
376384
name: { label: '项目名称' },
377385
account: { label: '客户' },
378386
owner: { label: '负责人' },
379-
status: { label: '状态' },
380-
health: { label: '健康度' },
387+
status: {
388+
label: '状态',
389+
options: { planned: '计划中', active: '进行中', on_hold: '已暂停', completed: '已完成', cancelled: '已取消' },
390+
},
391+
health: {
392+
label: '健康度',
393+
options: { green: '绿色', yellow: '黄色', red: '红色' },
394+
},
381395
budget: { label: '预算' },
382396
spent: { label: '已花费' },
397+
budget_remaining: { label: '剩余预算' },
383398
start_date: { label: '开始日期' },
384399
end_date: { label: '结束日期' },
400+
labels: {
401+
label: '标签',
402+
options: { frontend: '前端', backend: '后端', design: '设计', qa: '测试', devops: '运维' },
403+
},
404+
team_members: { label: '团队成员' },
405+
task_count: { label: '任务数' },
406+
total_estimate: { label: '总预计工时' },
385407
},
386408
// The zh-CN mirror of the `en` `_validations` block — see the note
387409
// there. Without these two keys the write path's own refusals arrive in
@@ -503,6 +525,18 @@ export const ShowcaseTranslationBundle = {
503525
// The hook-refusal specimen: every click is refused by
504526
// showcase_guard_task_reopen, so it carries no successMessage.
505527
showcase_reopen_task: { label: '重新打开' },
528+
// The rest of the task's row / toolbar / bulk actions, so a row menu
529+
// never lists 重算工时 beside an English "Mark Done". 工时 is the
530+
// word the recalc entries above use; 重新分配 is the reassign
531+
// wizard's own flow label (`flows.showcase_reassign_wizard`); 新建…
532+
// is the shape a create entry takes in this bundle.
533+
showcase_mark_done: { label: '标记为完成', successMessage: '任务已标记为完成。' },
534+
showcase_open_docs: { label: '打开文档' },
535+
showcase_bulk_reassign: { label: '重新分配…' },
536+
showcase_quick_view: { label: '快速查看' },
537+
showcase_log_time: { label: '记录工时' },
538+
showcase_new_task: { label: '新建任务' },
539+
showcase_archive_task: { label: '归档', successMessage: '任务已归档(演示——未修改任何数据)。' },
506540
},
507541
// Section headings of the six form-view projections in
508542
// `ui/views/task.view.ts` (edit / tabbed / wizard / split / quick).
@@ -579,14 +613,23 @@ export const ShowcaseTranslationBundle = {
579613
showcase_contact: {
580614
label: '联系人',
581615
pluralLabel: '联系人',
616+
// The list header renders the object's description, so leaving it out
617+
// put one English sentence above an otherwise translated grid. The
618+
// ADR reference is kept, as `showcase_client_brief.description` keeps
619+
// its own.
620+
description:
621+
'演示「默认推导 + 稀疏覆盖」表单:一套扁平、分组、带意图标记的字段集,同时投影为完整的编辑表单和精简的新建表单(ADR-0047)。',
582622
fields: {
583623
name: { label: '姓名' },
584624
email: { label: '邮箱' },
585625
phone: { label: '电话' },
586626
company: { label: '公司' },
587627
title: { label: '职务' },
588628
account: { label: '客户' },
589-
stage: { label: '阶段' },
629+
stage: {
630+
label: '阶段',
631+
options: { new: '新建', working: '跟进中', qualified: '合格', closed: '已关闭' },
632+
},
590633
lead_score: { label: '线索评分' },
591634
notes: { label: '备注' },
592635
},
@@ -700,8 +743,35 @@ export const ShowcaseTranslationBundle = {
700743
},
701744
},
702745
},
746+
// The expense report list is a sidebar entry (`nav_expense_reports`,
747+
// 报销单), so its object label, description, every column and every
748+
// status option are translated here — the sidebar's word and the list's
749+
// header now agree. 草稿 / 已提交 / 已批准 follow the 已… shape this
750+
// bundle uses for a reached state; 合计 is showcase_invoice's `total`.
751+
showcase_expense_report: {
752+
label: '报销单',
753+
pluralLabel: '报销单',
754+
description: '报销单的汇总金额由其明细行按条件汇总而来。',
755+
fields: {
756+
name: { label: '报销单标题' },
757+
employee: { label: '员工' },
758+
status: {
759+
label: '状态',
760+
options: { draft: '草稿', submitted: '已提交', approved: '已批准', reimbursed: '已报销' },
761+
},
762+
submitted_on: { label: '提交日期' },
763+
total_amount: { label: '合计' },
764+
approved_amount: { label: '已批准金额' },
765+
reimbursable_amount: { label: '可报销金额' },
766+
line_count: { label: '明细行数' },
767+
rejected_count: { label: '已驳回行数' },
768+
over_limit_count: { label: '超过 $500' },
769+
},
770+
},
703771
// See the `en` side for why this entry translates exactly ONE field and
704-
// no more (check-i18n-coverage is a two-sided ratchet).
772+
// no more (check-i18n-coverage is a two-sided ratchet). The line's other
773+
// labels are still in the baseline: only the report list is a first
774+
// screen.
705775
showcase_expense_line: {
706776
fields: {
707777
incurred_at: { label: '发生时间' },
@@ -751,10 +821,11 @@ export const ShowcaseTranslationBundle = {
751821
label: '分类', pluralLabel: '分类',
752822
fields: { name: { label: '名称' }, parent: { label: '上级' }, color: { label: '颜色' }, sort_order: { label: '排序' } },
753823
},
754-
// `_views` only: these two objects have no zh-CN block of their own, and
755-
// their object/field debt is inside the ratchet's baseline. Translating
756-
// the VIEW labels the coverage fix newly surfaces is what keeps the gate
757-
// from widening; the rest is left exactly as it was.
824+
// `_views` only: this object has no zh-CN block of its own, and its
825+
// object/field debt is inside the ratchet's baseline. Translating the
826+
// VIEW labels the coverage fix newly surfaces is what kept the gate from
827+
// widening; the rest is left exactly as it was. (showcase_business_unit
828+
// below started the same way and now carries its object block too.)
758829
// (`default` = the container's DEFAULT list — see showcase_project.)
759830
showcase_inquiry: {
760831
_views: {
@@ -769,7 +840,24 @@ export const ShowcaseTranslationBundle = {
769840
tell_us_about_yourself: { label: '介绍一下您自己' },
770841
},
771842
},
843+
// Object, fields and `kind` options as well as the views below: the
844+
// sidebar entry (`nav_business_units`, 业务单元 — the platform bundle's
845+
// word for `sys_business_unit`) opens this list. 上级单元 follows
846+
// showcase_category's 上级.
772847
showcase_business_unit: {
848+
label: '业务单元',
849+
pluralLabel: '业务单元',
850+
description: '组织架构层级——在自引用对象上演示树形 / 树形表格视图。',
851+
fields: {
852+
name: { label: '名称' },
853+
parent: { label: '上级单元' },
854+
kind: {
855+
label: '类型',
856+
options: { company: '公司', division: '事业部', department: '部门', team: '团队' },
857+
},
858+
manager: { label: '经理' },
859+
headcount: { label: '人数' },
860+
},
773861
_views: {
774862
default: { label: '全部单元' },
775863
org_chart: { label: '组织架构图' },
@@ -781,6 +869,31 @@ export const ShowcaseTranslationBundle = {
781869
unit: { label: '单元' },
782870
},
783871
},
872+
// The B3 dynamic-options fixture, opened from `nav_cascade` (级联选择).
873+
// Its description is a technical statement about the fixture, so the
874+
// zh-CN copy states the same thing and keeps the metadata keys
875+
// (`visibleWhen`, `dependsOn`) in their source spelling.
876+
showcase_cascade: {
877+
label: '级联选择',
878+
pluralLabel: '级联选择',
879+
description:
880+
'B3 动态选项夹具:国家 → 省份级联(逐选项 visibleWhen + dependsOn),外加按角色门控的等级;在客户端(可选集合)和服务端(objectql 拒绝集合外的提交)同时强制执行。',
881+
fields: {
882+
name: { label: '名称' },
883+
country: {
884+
label: '国家',
885+
options: { cn: '中国', us: '美国' },
886+
},
887+
province: {
888+
label: '省 / 州',
889+
options: { zj: '浙江', gd: '广东', ca: '加利福尼亚', tx: '得克萨斯' },
890+
},
891+
tier: {
892+
label: '等级',
893+
options: { standard: '标准', restricted: '受限(仅管理员)' },
894+
},
895+
},
896+
},
784897
// `_sections` only, on the same footing as the two `_views`-only blocks
785898
// above: this ADR-0085 fixture has no zh-CN block of its own and its
786899
// object/field debt sits inside the ratchet's baseline. Its two
@@ -945,9 +1058,9 @@ export const ShowcaseTranslationBundle = {
9451058
nav_accounts: { label: '客户' },
9461059
nav_contacts: { label: '联系人' },
9471060
nav_invoices: { label: '发票' },
948-
// `showcase_expense_report` has no translated object label (it sits
949-
// inside check-i18n-coverage's frozen baseline), so this renders the
950-
// nav label directly; 报销单 is the standard word for the document.
1061+
// 报销单 is the standard word for the document, and the word
1062+
// `objects.showcase_expense_report` above gives the object, so the
1063+
// sidebar entry and the list it opens read the same.
9511064
nav_expense_reports: { label: '报销单' },
9521065
nav_products: { label: '产品' },
9531066
nav_teams: { label: '团队' },
@@ -1050,6 +1163,25 @@ export const ShowcaseTranslationBundle = {
10501163
},
10511164
},
10521165
showcase_ops_dashboard: {
1166+
// The first dashboard a visitor opens (`nav_ops`, 交付运营 — the same
1167+
// word here). Every widget title, so the KPI row never reads English
1168+
// under a translated header. Each title says the word of the value it
1169+
// filters on: 进行中的项目 is `fields.status.options.active` on
1170+
// showcase_project, 红色 its health option, 待评审 the task's
1171+
// 评审中, 总预算 the command center's `cc_k6`.
1172+
label: '交付运营',
1173+
description: '运营首页——核心 KPI、项目健康度与任务吞吐量。',
1174+
widgets: {
1175+
kpi_active_projects: { title: '进行中的项目' },
1176+
kpi_at_risk: { title: '风险项目(红色)' },
1177+
kpi_awaiting_review: { title: '待评审' },
1178+
kpi_total_budget: { title: '总预算' },
1179+
col_health: { title: '项目健康度分布' },
1180+
bar_status: { title: '任务状态分布' },
1181+
donut_priority: { title: '优先级构成' },
1182+
line_created: { title: '任务吞吐量(按月)' },
1183+
table_spend: { title: '各客户预算与已花费' },
1184+
},
10531185
globalFilters: {
10541186
task_status: {
10551187
label: '任务状态',
@@ -1071,9 +1203,18 @@ export const ShowcaseTranslationBundle = {
10711203
// Page component copy became declared surface with `pages.<name>.components`
10721204
// (#6080), so these keys are born under the ratchet: leaving any of them
10731205
// untranslated widens the frozen baseline and fails `check-i18n-coverage`.
1074-
// The pages' own label/title/subtitle predate the ratchet and stay in the
1075-
// frozen baseline, same as Revenue Pulse's older widget titles above.
1206+
// Most pages' own label/title/subtitle predate the ratchet and stay in the
1207+
// frozen baseline, same as Revenue Pulse's older widget titles above. The
1208+
// exceptions are the pages a visitor lands on first, whose `label` is the
1209+
// page heading: each reads the word its sidebar entry already uses
1210+
// (`apps.showcase_app.navigation`), so the heading matches the link that
1211+
// opened it.
10761212
pages: {
1213+
showcase_capability_map: { label: '能力地图' },
1214+
showcase_my_work: { label: '我的工作', subtitle: '你的待办队列、团队产出,以及需要关注的事项。' },
1215+
showcase_review_queue: { label: '审批' },
1216+
showcase_task_board: { label: '任务看板' },
1217+
showcase_active_projects: { label: '进行中的项目' },
10771218
// `ui/pages/invoice-document.page.ts` — the invoice print page.
10781219
showcase_invoice_document: { label: '发票' },
10791220
showcase_contact_form: {

‎examples/app-showcase/test/seed.test.ts‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,106 @@ describe('showcase form + page section i18n coverage (#8231)', () => {
314314
}
315315
});
316316

317+
/**
318+
* #25505 — the same two halves as the section sweep above, for the screens a
319+
* zh-CN visitor opens first: the landing pages, the first dashboard, the
320+
* object lists the sidebar links to, and the task's row / toolbar actions.
321+
* They used to read English under a fully translated sidebar.
322+
*
323+
* `check:i18n-coverage` measures this app with ONE count, so it cannot see a
324+
* first-screen key traded for an obscure one. This names the surfaces and
325+
* DERIVES what each declares from the composed stack — a field, option, widget
326+
* or action added to one of them later has to arrive translated too.
327+
*/
328+
describe('showcase zh-CN first screens are translated (#25505)', () => {
329+
const zh = (stack.translations?.[0] as any)?.['zh-CN'] as Record<string, any> | undefined;
330+
331+
const OBJECTS = [
332+
'showcase_project',
333+
'showcase_contact',
334+
'showcase_expense_report',
335+
'showcase_business_unit',
336+
'showcase_cascade',
337+
];
338+
const PAGES = [
339+
'showcase_capability_map',
340+
'showcase_my_work',
341+
'showcase_review_queue',
342+
'showcase_task_board',
343+
'showcase_active_projects',
344+
];
345+
const DASHBOARDS = ['showcase_ops_dashboard'];
346+
const ACTIONS_OF = ['showcase_task'];
347+
348+
/** One declared string: its bundle path, and what the zh-CN bundle holds there. */
349+
interface Entry { path: string; value: unknown }
350+
const entry = (path: string): Entry => ({
351+
path,
352+
value: path.split('.').reduce<any>((node, key) => node?.[key], zh),
353+
});
354+
355+
function declaredEntries(): Map<string, Entry[]> {
356+
const out = new Map<string, Entry[]>();
357+
for (const name of OBJECTS) {
358+
const obj = (stack.objects ?? []).find((o: any) => o.name === name) as any;
359+
const list: Entry[] = [];
360+
for (const attr of ['label', 'pluralLabel', 'description']) {
361+
if (obj?.[attr]) list.push(entry(`objects.${name}.${attr}`));
362+
}
363+
for (const [field, def] of Object.entries<any>(obj?.fields ?? {})) {
364+
if (def?.label) list.push(entry(`objects.${name}.fields.${field}.label`));
365+
for (const opt of Array.isArray(def?.options) ? def.options : []) {
366+
if (opt?.label) list.push(entry(`objects.${name}.fields.${field}.options.${opt.value}`));
367+
}
368+
}
369+
out.set(`object ${name}`, list);
370+
}
371+
for (const name of PAGES) {
372+
const page = (stack.pages ?? []).find((p: any) => p.name === name) as any;
373+
out.set(`page ${name}`, page?.label ? [entry(`pages.${name}.label`)] : []);
374+
}
375+
for (const name of DASHBOARDS) {
376+
const dash = (stack.dashboards ?? []).find((d: any) => d.name === name) as any;
377+
const list: Entry[] = [];
378+
for (const attr of ['label', 'description']) {
379+
if (dash?.[attr]) list.push(entry(`dashboards.${name}.${attr}`));
380+
}
381+
for (const w of Array.isArray(dash?.widgets) ? dash.widgets : []) {
382+
if (w?.title) list.push(entry(`dashboards.${name}.widgets.${w.id}.title`));
383+
}
384+
out.set(`dashboard ${name}`, list);
385+
}
386+
for (const object of ACTIONS_OF) {
387+
const list: Entry[] = [];
388+
for (const action of (stack.actions ?? []) as any[]) {
389+
if (action.objectName !== object) continue;
390+
for (const attr of ['label', 'successMessage']) {
391+
if (action[attr]) list.push(entry(`objects.${object}._actions.${action.name}.${attr}`));
392+
}
393+
}
394+
out.set(`actions of ${object}`, list);
395+
}
396+
return out;
397+
}
398+
399+
const surfaces = declaredEntries();
400+
401+
it('every named surface exists and declares strings (guards against a vacuous sweep)', () => {
402+
for (const [surface, list] of surfaces) {
403+
expect(list.length, `${surface} declares nothing — renamed or removed?`).toBeGreaterThan(0);
404+
}
405+
});
406+
407+
for (const [surface, list] of surfaces) {
408+
it(`${surface}: every declared string has a real zh-CN translation`, () => {
409+
const untranslated = list
410+
.filter((e) => typeof e.value !== 'string' || !e.value || /^[\x20-\x7e]+$/.test(e.value))
411+
.map((e) => e.path);
412+
expect(untranslated, 'missing from the zh-CN bundle, or echoed as ASCII').toEqual([]);
413+
});
414+
}
415+
});
416+
317417
/**
318418
* Static shadow of the seed contract after #3433: a seed write is a curated
319419
* end-state fact, so the platform EXEMPTS it from the object's `state_machine`

0 commit comments

Comments
 (0)