diff --git a/.changeset/23756-section-translation-tsdoc.md b/.changeset/23756-section-translation-tsdoc.md new file mode 100644 index 00000000000..9735d258608 --- /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`). `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 ec043f0c739..a77972afcca 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` @@ -3912,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. * 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