Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .changeset/crm-showcase-section-name-i18n.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
---
"@objectstack/example-crm": patch
"@objectstack/example-showcase": patch
---

fix(examples): name the form/page sections that had a label but no `name`, and translate the headings into zh-CN (#8231)

`translation-section-name-missing` fired on every build of both example apps: a
form or `record:details` section that declares a `label` but no `name` has no
key a bundle can carry (`objects.<object>._sections.<name>.label`), so its
heading renders in the source locale in EVERY locale — permanently, and
invisibly, because every neighbouring field label on the same object
translates fine. `app-crm` ships en + zh-CN; `app-showcase` ships the same.

21 of the 24 flagged sections now declare a stable snake_case `name` and
resolve a real (non-echoed) zh-CN label:

- **app-crm** (9/9): `crm_activity` (`activity_details`, `related_records`,
`notes`), `crm_lead` (`contact_us`, `lead_information`, `qualification`,
`conversion`, `notes`), `crm_opportunity` (`opportunity`).
- **app-showcase** (12/15): `showcase_project` form (`project`,
`budget_schedule`) and its detail page (`overview`, `financials`,
`timeline`); `showcase_task`'s detail page (`overview`, `schedule`,
`details` — reusing the same names and zh-CN copy its `tabbed` form view
already declares, so no new bundle entries were needed there);
`showcase_inquiry` (`tell_us_about_yourself`); `showcase_business_unit`
(`unit`); `showcase_preference`'s settings page (`appearance`,
`notifications`).

**Not named here — a `packages/**` conflict, out of this PR's scope.** Three
`app-showcase` sections (`showcase_task`'s `formViews.edit`/`Task` and
`formViews.quick`/`Quick Edit`, `showcase_contact`'s `formViews.create`/`Who is
this?`) are pinned NAMELESS as regression fixtures by
`packages/lint/src/validate-translatable-sections.test.ts` and
`validate-translation-references.test.ts`, which import `TaskViews` /
`ContactViews` directly from this app and assert on their current unnamed
shape. Naming them requires a coordinated `packages/lint` test update; #8231
remains open for that follow-up.

Adding a `name` alone would have silenced the warning with zero translation
delivered, so both apps also gain a generalized i18n-coverage sweep test
(`examples/app-crm/test/i18n-sections.test.ts`,
`examples/app-showcase/test/seed.test.ts`) asserting every section this PR
touches BOTH has a `name` AND resolves a real, non-ASCII zh-CN
`_sections.<name>.label` — not just that the section has a name.
28 changes: 28 additions & 0 deletions examples/app-crm/src/translations/crm.translation.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,6 +136,14 @@ export const CrmTranslationBundle = defineTranslationBundle({
all: { label: '全部商机' },
pipeline: { label: '商机看板' },
},
// Form section heading of `ui/views/opportunity.view.ts` `default`.
// Declares a stable `name`, which is the only thing that makes the
// heading translatable — otherwise it renders the English `label` in
// every locale (#8231). Bare object word, matching this bundle's own
// `label` above.
_sections: {
opportunity: { label: '商机' },
},
},
crm_lead: {
label: '线索',
Expand All@@ -153,6 +161,18 @@ export const CrmTranslationBundle = defineTranslationBundle({
all: { label: '全部线索' },
pipeline: { label: '线索看板' },
},
// Form section headings of `ui/views/lead.view.ts` (public web-to-lead
// `contact_us`, plus the four `default` groups). Each section declares
// a stable `name`, the only thing that makes the heading translatable
// (#8231). `conversion` reuses this bundle's own `messages` vocabulary
// for "convert" (`crm.lead.convert.success` already reads 转化).
_sections: {
contact_us: { label: '联系我们' },
lead_information: { label: '线索信息' },
qualification: { label: '资格审查' },
conversion: { label: '转化' },
notes: { label: '备注' },
},
},
crm_activity: {
label: '活动',
Expand All@@ -170,6 +190,14 @@ export const CrmTranslationBundle = defineTranslationBundle({
all: { label: '全部活动' },
calendar: { label: '活动日历' },
},
// Form section headings of `ui/views/activity.view.ts` `default`. Each
// declares a stable `name`, the only thing that makes the heading
// translatable (#8231).
_sections: {
activity_details: { label: '活动详情' },
related_records: { label: '相关记录' },
notes: { label: '备注' },
},
},
},
apps: {
Expand Down
3 changes: 3 additions & 0 deletions examples/app-crm/src/views/activity.view.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,7 @@ export const ActivityViews = defineView({
type: 'simple',
sections: [
{
name: 'activity_details',
label: 'Activity Details',
columns: 2,
fields: [
Expand All@@ -64,6 +65,7 @@ export const ActivityViews = defineView({
],
},
{
name: 'related_records',
label: 'Related Records',
columns: 2,
fields: [
Expand All@@ -73,6 +75,7 @@ export const ActivityViews = defineView({
],
},
{
name: 'notes',
label: 'Notes',
columns: 1,
fields: [
Expand Down
5 changes: 5 additions & 0 deletions examples/app-crm/src/views/lead.view.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,7 @@ export const LeadViews = defineView({
data: { provider: 'object', object: 'crm_lead' },
sections: [
{
name: 'contact_us',
label: 'Contact us',
columns: 1,
fields: [
Expand All@@ -81,6 +82,7 @@ export const LeadViews = defineView({
type: 'simple',
sections: [
{
name: 'lead_information',
label: 'Lead Information',
columns: 2,
fields: [
Expand All@@ -93,6 +95,7 @@ export const LeadViews = defineView({
],
},
{
name: 'qualification',
label: 'Qualification',
columns: 2,
fields: [
Expand All@@ -103,6 +106,7 @@ export const LeadViews = defineView({
],
},
{
name: 'conversion',
label: 'Conversion',
columns: 2,
fields: [
Expand All@@ -111,6 +115,7 @@ export const LeadViews = defineView({
],
},
{
name: 'notes',
label: 'Notes',
columns: 1,
fields: [{ field: 'notes' }],
Expand Down
1 change: 1 addition & 0 deletions examples/app-crm/src/views/opportunity.view.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,7 @@ export const OpportunityViews = defineView({
data: { provider: 'object', object: 'crm_opportunity' },
sections: [
{
name: 'opportunity',
label: 'Opportunity',
columns: 2,
fields: [
Expand Down
93 changes: 93 additions & 0 deletions examples/app-crm/test/i18n-sections.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect } from 'vitest';
import stack from '../objectstack.config.js';

/**
* #8231 — a form section can silence `translation-section-name-missing`
* completely just by getting a `name`, with ZERO translation delivered: the
* bundle simply never gains a matching `_sections.<name>` entry, and the
* heading keeps rendering in the source locale in every locale. Naming the
* section is necessary but not sufficient.
*
* This is the `examples/app-showcase/test/seed.test.ts` "keys the contact
* form sections to exactly what the container declares" harness, generalized
* to every CRM view container instead of one hand-picked object: for each
* container, every section any of its `formViews` declares must (a) carry a
* `name` and (b) resolve to a REAL zh-CN `_sections.<name>.label` — non-empty
* and not an echoed English string.
*
* Read on the COMPOSED stack (`objectstack.config.ts`'s `views`/`translations`
* arrays), not the imported view/bundle modules directly — what the platform's
* i18n resolver and lint gates see is the composed stack, which is exactly the
* reachability this issue is about.
*/
describe('app-crm form section i18n coverage (#8231)', () => {
const zhBundle = (stack.translations ?? [])[0] as
| { 'zh-CN'?: { objects?: Record<string, { _sections?: Record<string, { label?: string }> }> } }
| undefined;

interface FormSection {
name?: unknown;
label?: unknown;
}
interface FormView {
data?: { object?: unknown };
sections?: FormSection[];
}
interface ViewContainer {
list?: { data?: { object?: unknown } };
formViews?: Record<string, FormView>;
}

const containers = (stack.views ?? []) as ViewContainer[];

for (const container of containers) {
const containerObject = container.list?.data?.object;
if (typeof containerObject !== 'string') continue;
const formViews = container.formViews ?? {};

for (const [viewKey, formView] of Object.entries(formViews)) {
const sections = Array.isArray(formView.sections) ? formView.sections : [];
if (sections.length === 0) continue;
const objectName = typeof formView.data?.object === 'string' ? formView.data.object : containerObject;

sections.forEach((section, i) => {
const label = typeof section.label === 'string' ? section.label : '(untitled)';
it(`${objectName} · formViews.${viewKey} · section "${label}" declares a name and resolves in zh-CN`, () => {
expect(
typeof section.name === 'string' && section.name.length > 0,
`${objectName}.formViews.${viewKey}.sections[${i}] ("${label}") has no \`name\` — ` +
'it can never be translated (translation-section-name-missing).',
).toBe(true);
const name = section.name as string;

const zhLabel = zhBundle?.['zh-CN']?.objects?.[objectName]?._sections?.[name]?.label;
expect(
zhLabel,
`objects.${objectName}._sections.${name}.label is missing from the zh-CN bundle — ` +
'the section has a name but no real translation.',
).toBeTruthy();
// An echoed English label (or anything ASCII-only) is not a real
// zh-CN translation — it satisfies the key set while faking coverage.
expect(zhLabel, `zh-CN _sections.${name}.label reads as untranslated ASCII`).not.toMatch(
/^[\x20-\x7e]+$/,
);
});
});
}
}

it('exercised at least one container with named sections (guards against a vacuous sweep)', () => {
const total = containers.reduce(
(n, c) =>
n +
Object.values(c.formViews ?? {}).reduce(
(m, fv) => m + (Array.isArray(fv.sections) ? fv.sections.length : 0),
0,
),
0,
);
expect(total).toBeGreaterThanOrEqual(9);
});
});
39 changes: 39 additions & 0 deletions examples/app-showcase/src/system/translations/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,24 @@ export const ShowcaseTranslationBundle = {
by_status: { label: '按状态' },
budget_chart: { label: '按客户预算' },
},
// Section headings from two surfaces: `ui/views/project.view.ts`'s
// `edit` form (`project` / `budget_schedule`) and
// `ui/pages/project-detail.page.ts`'s `record:details` tab
// (`overview` / `financials` / `timeline`). Each declares a stable
// `name`, the only thing that makes the heading translatable
// (#8231). Wording reuses vocabulary this bundle already established
// elsewhere: `预算` (fields.budget above), `排期` (showcase_task
// `_sections.schedule` below), `概览` (showcase_task
// `_sections.overview`), `财务信息` (showcase_semantic_zoo
// `_sections.money`), `时间线` (showcase_task `_views.timeline`,
// "活动时间线").
_sections: {
project: { label: '项目' },
budget_schedule: { label: '预算与排期' },
overview: { label: '概览' },
financials: { label: '财务信息' },
timeline: { label: '时间线' },
},
},
showcase_task: {
label: '任务',
Expand DownExpand Up@@ -438,6 +456,14 @@ export const ShowcaseTranslationBundle = {
notifications_enabled: { label: '启用通知' },
compact_density: { label: '紧凑密度' },
},
// `ui/pages/settings.page.ts` `record:details` sections. Each
// declares a stable `name`, the only thing that makes the heading
// translatable (#8231). `通知` reuses `fields.notifications_enabled`
// ("启用通知") above.
_sections: {
appearance: { label: '外观' },
notifications: { label: '通知' },
},
},
showcase_product: {
label: '产品', pluralLabel: '产品',
Expand DownExpand Up@@ -465,12 +491,25 @@ export const ShowcaseTranslationBundle = {
default: { label: '客户询问' },
triage: { label: '询问分流' },
},
// The public web-to-lead form's single section
// (`ui/views/inquiry.view.ts` `formViews.contact`). Declares a stable
// `name`, the only thing that makes the heading translatable
// (#8231).
_sections: {
tell_us_about_yourself: { label: '介绍一下您自己' },
},
},
showcase_business_unit: {
_views: {
default: { label: '全部单元' },
org_chart: { label: '组织架构图' },
},
// `ui/views/business-unit.view.ts` `edit` form's single section.
// Declares a stable `name`, the only thing that makes the heading
// translatable (#8231). Bare object word, matching `全部单元` above.
_sections: {
unit: { label: '单元' },
},
},
// `_sections` only, on the same footing as the two `_views`-only blocks
// above: this ADR-0085 fixture has no zh-CN block of its own and its
Expand Down
6 changes: 3 additions & 3 deletions examples/app-showcase/src/ui/pages/project-detail.page.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,9 +56,9 @@ export const ProjectDetailPage = definePage({
type: 'record:details',
properties: {
sections: [
{ label: 'Overview', columns: 2, fields: ['name', 'account', 'owner', 'status'] },
{ label: 'Financials', columns: 2, fields: ['budget', 'spent'] },
{ label: 'Timeline', columns: 2, fields: ['start_date', 'end_date'] },
{ name: 'overview', label: 'Overview', columns: 2, fields: ['name', 'account', 'owner', 'status'] },
{ name: 'financials', label: 'Financials', columns: 2, fields: ['budget', 'spent'] },
{ name: 'timeline', label: 'Timeline', columns: 2, fields: ['start_date', 'end_date'] },
],
},
},
Expand Down
4 changes: 2 additions & 2 deletions examples/app-showcase/src/ui/pages/settings.page.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,8 +25,8 @@ export const SettingsPage = definePage({
type: 'record:details',
properties: {
sections: [
{ label: 'Appearance', columns: 2, fields: ['theme', 'compact_density', 'default_landing', 'items_per_page'] },
{ label: 'Notifications', columns: 2, fields: ['notifications_enabled', 'email_digest'] },
{ name: 'appearance', label: 'Appearance', columns: 2, fields: ['theme', 'compact_density', 'default_landing', 'items_per_page'] },
{ name: 'notifications', label: 'Notifications', columns: 2, fields: ['notifications_enabled', 'email_digest'] },
],
},
},
Expand Down
11 changes: 8 additions & 3 deletions examples/app-showcase/src/ui/pages/task-detail.page.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,9 +71,14 @@ export const TaskDetailPage = definePage({
type: 'record:details',
properties: {
sections: [
{ label: 'Overview', columns: 2, fields: ['title', 'project', 'assignee', 'status', 'priority'] },
{ label: 'Schedule', columns: 2, fields: ['start_date', 'end_date', 'due_date', 'estimate_hours'] },
{ label: 'Details', columns: 1, fields: ['labels', 'location', 'notes'] },
// Reuses `objects.showcase_task._sections.{overview,schedule,details}` —
// the same three names `ui/views/task.view.ts`'s `tabbed` form
// already declares and `system/translations/index.ts` already
// translates, so this page's headings resolve in zh-CN with no
// new bundle entries (#8231).
{ name: 'overview', label: 'Overview', columns: 2, fields: ['title', 'project', 'assignee', 'status', 'priority'] },
{ name: 'schedule', label: 'Schedule', columns: 2, fields: ['start_date', 'end_date', 'due_date', 'estimate_hours'] },
{ name: 'details', label: 'Details', columns: 1, fields: ['labels', 'location', 'notes'] },
],
},
},
Expand Down
2 changes: 1 addition & 1 deletion examples/app-showcase/src/ui/views/business-unit.view.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ export const BusinessUnitViews = defineView({
type: 'simple',
data,
sections: [
{ label: 'Unit', columns: 2, fields: ['name', 'parent', 'kind', 'manager', 'headcount'] },
{ name: 'unit', label: 'Unit', columns: 2, fields: ['name', 'parent', 'kind', 'manager', 'headcount'] },
],
},
},
Expand Down
1 change: 1 addition & 0 deletions examples/app-showcase/src/ui/views/inquiry.view.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,6 +56,7 @@ export const InquiryViews = defineView({
data,
sections: [
{
name: 'tell_us_about_yourself',
label: 'Tell us about yourself',
columns: 1,
fields: [
Expand Down
4 changes: 2 additions & 2 deletions examples/app-showcase/src/ui/views/project.view.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -178,8 +178,8 @@ export const ProjectViews = defineView({
type: 'simple',
data,
sections: [
{ label: 'Project', columns: 2, fields: ['name', 'account', 'status', 'health', 'owner'] },
{ label: 'Budget & Schedule', columns: 2, fields: ['budget', 'spent', 'start_date', 'end_date'] },
{ name: 'project', label: 'Project', columns: 2, fields: ['name', 'account', 'status', 'health', 'owner'] },
{ name: 'budget_schedule', label: 'Budget & Schedule', columns: 2, fields: ['budget', 'spent', 'start_date', 'end_date'] },
],
// No subforms here: the Tasks subtable is derived from the data model —
// showcase_task.project declares `inlineEdit: true`, so every standard
Expand Down
Loading
Loading