From 4e2976276aacf5ae53b217830cc7d407582aadca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 04:12:01 +0000 Subject: [PATCH 1/3] docs(spec): name both translation addresses for a form section, and the `_sections` readers `FormSectionSchema.name`'s TSDoc sent every form section to `metadataForms..sections.`, which holds only for a metadata-editing form (`defineForm({ schemaId })`, `resolveMetadataFormLabels`). An object-bound form view's section is keyed at `objects.._sections.`. The TSDoc now names both addresses and which form each serves; the group-key comment on the same schema names the object anchor `deriveFieldGroupLayout` keys. `TranslationDataSchema._sections` named `record:details` as its only reader. It now names the three surfaces that author a key there and the three readers on main (`walkObjectSections`, `collectViewRecord`, `translateView`). `translateFormSections`' docblock said all three rules were its siblings'. The authored-copy rule is the sections' own, and the sentence now says so. Comments only: no `.describe()`, no schema, no code. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01RD4nZChNGxRzyyWJtvZ8NJ --- packages/spec/src/system/i18n-resolver.ts | 6 +++++- packages/spec/src/system/translation.zod.ts | 16 +++++++++++++--- packages/spec/src/ui/view.zod.ts | 19 ++++++++++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/spec/src/system/i18n-resolver.ts b/packages/spec/src/system/i18n-resolver.ts index ec043f0c739..189e6fe2e8f 100644 --- a/packages/spec/src/system/i18n-resolver.ts +++ b/packages/spec/src/system/i18n-resolver.ts @@ -1052,7 +1052,11 @@ function formSectionObjectName(config: Record, viewObject: stri * nothing matched (so {@link translateView} rebuilds `config` only when a * section moved). * - * Three rules, each the one its sibling translators already follow: + * Three rules. The first and the third are the ones its sibling translators + * already follow ({@link translateBulkActionDefs} keys each def by its own + * `name` and keeps a diverged string); the second is the sections' own — + * {@link resolveViewDescription} returns a catalog description the view never + * authored, and {@link translateBulkActionDefs} has no authored-copy guard: * * - **The key is the section's own `name`**, ⛔ never a slug of its label: a * section without a `name` has no `_sections` key (`FormSectionSchema.name` diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index d66a623cbef..a11143b16c5 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -392,9 +392,19 @@ export const ObjectTranslationDataSchema = lazySchema(() => strictObject({ * Section translations keyed by section name (snake_case). * Convention: * objects.._sections..label - * Used by `record:details` to translate per-section labels on detail pages - * (e.g. "Opportunity Information" → "商机信息"). Each section in the - * page schema must declare a stable `name` for the lookup to fire. + * objects.._sections..description + * Three authoring surfaces name a section under this key (e.g. + * "Opportunity Information" → "商机信息"): an object-bound form view's + * `sections[].name` (`FormSectionSchema.name`), a `record:details` + * component's `sections[].name` on a record page, and an object's + * `fieldGroups[].key`. A section without a stable name has no key here. + * + * Read by the i18n extractor (`walkObjectSections`, which scaffolds the key + * from all three surfaces), the translation linter (`collectViewRecord`, + * which accepts the names a form view declares) and `translateView` (which + * overlays a served form view's `config.sections`). A metadata-editing + * form's sections are keyed elsewhere, at + * `metadataForms..sections.`. */ _sections: z.record(z.string(), strictObject({ surface: 'this section translation', diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 3dba72ea387..6247a6df825 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -3676,8 +3676,20 @@ export const FormSectionSchema = lazySchema(() => strictObject({ }, { /** * Stable identifier for translation lookup. snake_case convention. - * When provided, translation bundles can target this section's `label` - * and `description` via `metadataForms..sections.`. + * Translation bundles target this section's `label` and `description` at + * one of two addresses, decided by what the form edits: + * + * - **An object-bound form view**: `objects.._sections.`, + * where `` is the form's own `data.object`, else its view's. The + * i18n extractor (`walkObjectSections`) scaffolds this key, the + * translation linter (`collectViewRecord`) accepts it and `translateView` + * serves it. A section without a `name` has no key there and keeps its + * authored text in every locale. + * - **A metadata-editing form** (`defineForm({ schemaId })`): + * `metadataForms..sections.`, read by + * `resolveMetadataFormLabels`. A section without a `name` is looked up + * there under a slug of its `label`. + * * Optional for backward-compat with sections that only have a `label`. */ name: z.string().optional().describe('Stable section identifier for i18n lookup (snake_case)'), @@ -3859,7 +3871,8 @@ export const FormSectionSchema = lazySchema(() => strictObject({ surface: 'this form section', // Exactly the keys `deriveFieldGroupLayout` fills from the group. `name` is // in the list because the derived section's `key` IS the group key, which is - // already this surface's i18n anchor (`metadataForms..sections.`) + // already the object's i18n anchor for that heading + // (`objects.._sections.`, `deriveFieldGroupLayout`'s `key`) // — a second name would give one section two lookup identities. `visibleOn` // rides along because this refinement runs BEFORE the `.transform` that folds // it onto `visibleWhen`, so the deprecated spelling must be named here or it From 58c3d6070fd5788f14c4a7e62c5d9a24ad443609 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 04:33:27 +0000 Subject: [PATCH 2/3] docs(changeset): patch changeset for the form-section translation TSDoc The edited TSDoc ships: `src/**/*.zod.ts` is in `@objectstack/spec`'s `files[]`, and the new view and translation docblocks appear in the built `dist/**` bundles. So the diff publishes, and it takes a patch changeset rather than `skip-changeset`. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01RD4nZChNGxRzyyWJtvZ8NJ --- .changeset/23756-section-translation-tsdoc.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/23756-section-translation-tsdoc.md diff --git a/.changeset/23756-section-translation-tsdoc.md b/.changeset/23756-section-translation-tsdoc.md new file mode 100644 index 00000000000..6b40e9a86d3 --- /dev/null +++ b/.changeset/23756-section-translation-tsdoc.md @@ -0,0 +1,7 @@ +--- +'@objectstack/spec': patch +--- + +Clause-②: no + +The published TSDoc now sends a form section's translations to the right key. `FormSectionSchema.name` used to name only `metadataForms..sections.`, which is the key for a metadata-editing form (`defineForm({ schemaId })`, read by `resolveMetadataFormLabels`). A section of an object-bound form view is keyed at `objects.._sections.`, where `` is the form's own `data.object`, else its view's. The docblock now names both keys and which form each one serves. `TranslationDataSchema._sections` now names the three surfaces that author its key (form-view sections, `record:details` sections, object `fieldGroups`) and its readers (`walkObjectSections`, `collectViewRecord`, `translateView`). These are comment changes only: no key, `.describe()` text or behaviour changes. An object-form translation bundle written from the old docblock under `metadataForms.…` never translated anything. Move its section entries to `objects.._sections.`. From e7ac53b9a1393fb5ea9bf7699a0630566d01d708 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 04:48:20 +0000 Subject: [PATCH 3/3] docs(spec): `resolveMetadataFormLabels`' docblock states the label-slug section lookup The docblock said a section without a stable `name` is returned unchanged for section-level attributes. `translateSection` does otherwise: with no `name` it looks the section up under a slug of its string `label`, and only a section with neither keeps its section-level text. Fields are translated either way. The docblock ships in `dist/system/index.d.ts`, so the changeset gains one sentence. Comment only: no code, no `.describe()`. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01RD4nZChNGxRzyyWJtvZ8NJ --- .changeset/23756-section-translation-tsdoc.md | 2 +- packages/spec/src/system/i18n-resolver.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.changeset/23756-section-translation-tsdoc.md b/.changeset/23756-section-translation-tsdoc.md index 6b40e9a86d3..9735d258608 100644 --- a/.changeset/23756-section-translation-tsdoc.md +++ b/.changeset/23756-section-translation-tsdoc.md @@ -4,4 +4,4 @@ Clause-②: no -The published TSDoc now sends a form section's translations to the right key. `FormSectionSchema.name` used to name only `metadataForms..sections.`, which is the key for a metadata-editing form (`defineForm({ schemaId })`, read by `resolveMetadataFormLabels`). A section of an object-bound form view is keyed at `objects.._sections.`, where `` is the form's own `data.object`, else its view's. The docblock now names both keys and which form each one serves. `TranslationDataSchema._sections` now names the three surfaces that author its key (form-view sections, `record:details` sections, object `fieldGroups`) and its readers (`walkObjectSections`, `collectViewRecord`, `translateView`). These are comment changes only: no key, `.describe()` text or behaviour changes. An object-form translation bundle written from the old docblock under `metadataForms.…` never translated anything. Move its section entries to `objects.._sections.`. +The published TSDoc now sends a form section's translations to the right key. `FormSectionSchema.name` used to name only `metadataForms..sections.`, which is the key for a metadata-editing form (`defineForm({ schemaId })`, read by `resolveMetadataFormLabels`). A section of an object-bound form view is keyed at `objects.._sections.`, where `` is the form's own `data.object`, else its view's. The docblock now names both keys and which form each one serves. `TranslationDataSchema._sections` now names the three surfaces that author its key (form-view sections, `record:details` sections, object `fieldGroups`) and its readers (`walkObjectSections`, `collectViewRecord`, `translateView`). `resolveMetadataFormLabels`' docblock no longer says that a section without a `name` keeps its section-level text: such a section is looked up under a slug of its `label`, as the function has always done. These are comment changes only: no key, `.describe()` text or behaviour changes. An object-form translation bundle written from the old docblock under `metadataForms.…` never translated anything. Move its section entries to `objects.._sections.`. diff --git a/packages/spec/src/system/i18n-resolver.ts b/packages/spec/src/system/i18n-resolver.ts index 189e6fe2e8f..a77972afcca 100644 --- a/packages/spec/src/system/i18n-resolver.ts +++ b/packages/spec/src/system/i18n-resolver.ts @@ -3916,9 +3916,13 @@ function translateFormField( * Translate the labels, descriptions, helpTexts, and placeholders of a * metadata-type configuration form against the active translation bundle. * - * Sections without a stable `name` are returned unchanged for section-level - * attributes — only field labels are translated (field names are always - * stable identifiers). + * A section's `label` and `description` are looked up at + * `metadataForms..sections.`, where `` is + * the section's own `name` or, when it has none, a slug of its string `label` + * (lowercased, `&` → `and`, each run of non-alphanumerics → `_`, edge + * underscores trimmed). Only a section with neither keeps its section-level + * text. Its fields are translated either way, since field names are always + * stable identifiers. * * Returns a new form object; the input is not mutated. *