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
7 changes: 7 additions & 0 deletions .changeset/23756-section-translation-tsdoc.md
Original file line number Diff line number Diff line change
@@ -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.<type>.sections.<name>`, 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.<object>._sections.<name>`, where `<object>` 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.<object>._sections.<name>`.
16 changes: 12 additions & 4 deletions packages/spec/src/system/i18n-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,11 @@ function formSectionObjectName(config: Record<string, unknown>, 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`
Expand Down Expand Up @@ -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.<type>.sections.<section_name>`, where `<section_name>` 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.
*
Expand Down
16 changes: 13 additions & 3 deletions packages/spec/src/system/translation.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,19 @@ export const ObjectTranslationDataSchema = lazySchema(() => strictObject({
* Section translations keyed by section name (snake_case).
* Convention:
* objects.<object>._sections.<section_name>.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.<object>._sections.<section_name>.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.<type>.sections.<section_name>`.
*/
_sections: z.record(z.string(), strictObject({
surface: 'this section translation',
Expand Down
19 changes: 16 additions & 3 deletions packages/spec/src/ui/view.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<type>.sections.<name>`.
* 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.<object>._sections.<name>`,
* where `<object>` 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.<type>.sections.<name>`, 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)'),
Expand Down Expand Up @@ -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.<type>.sections.<name>`)
// already the object's i18n anchor for that heading
// (`objects.<object>._sections.<key>`, `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
Expand Down
Loading