diff --git a/content/docs/ui/translations.mdx b/content/docs/ui/translations.mdx index 862de85f7a..eca5a297c9 100644 --- a/content/docs/ui/translations.mdx +++ b/content/docs/ui/translations.mdx @@ -69,6 +69,7 @@ export default defineStack({ | Action labels, confirm text, success messages | `objects.._actions.` | | Action result dialogs (title / description / acknowledge / field labels) | `objects.._actions..resultDialog` | | Form sections | `objects.._sections.
` | +| Custom validation-rule messages | `objects.._validations..message` | | App navigation | `apps..navigation..label` | | Dashboard label / description | `dashboards..label` / `description` | | Dashboard widget title / description / sub-caption | `dashboards..widgets..title` / `description` / `subCaption` | @@ -245,11 +246,22 @@ up-to-date bundle re-extracts byte-identically. Honest limits worth knowing before you plan around them: -- **Validation messages are not translated through bundles.** The - `validationMessages` group was removed in 17.0.0 (#4667): it had no runtime - consumer, so a translated rule message was stored and never shown. Author the - message on the rule itself (`object.validations[].message`), which the engine - returns on every rejected write. +- **Validation messages are translatable, but substituted whole — there is no + interpolation.** Author the message on the rule + (`object.validations[].message`), which the engine returns on every rejected + write, and translate it under `objects.._validations..message` + (the row in the table above) — live since 17.3.0. The write path swaps the + whole sentence for the bundle's, so only the language changes: an authored + message has no parameter contract, so `{variable}` placeholders in it are not + filled, and a key the bundle does not carry falls back to the authored + literal. + + The retired top-level `validationMessages` group is **not** what came back. + It was removed in 17.0.0 (#4667) because nothing read it — a translated rule + message was stored and never shown — and that retirement stands. It was keyed + by rule name alone and so could not tell two objects' rules apart; the route + above is object-scoped and shipped with its reader. ADR-0049's 2026-09-04 + amendment carries that record. - **The `flows` group is declared, not yet applied.** A screen flow's copy has somewhere to live (#7646) and the keys are addressed the way the runner resolves them — flow name, screen node id, screen field name — but no shipped diff --git a/docs/adr/0049-no-unenforced-security-properties.md b/docs/adr/0049-no-unenforced-security-properties.md index 921bf39bc3..d2a3dc3d38 100644 --- a/docs/adr/0049-no-unenforced-security-properties.md +++ b/docs/adr/0049-no-unenforced-security-properties.md @@ -1,6 +1,6 @@ # ADR-0049: Spec must not declare security properties the runtime does not enforce (enforce-or-remove gate) -**Status**: Accepted (2026-06-15) — implemented: fail-closed `DESTRUCTIVE_OPERATIONS` (`packages/plugins/plugin-security/src/permission-evaluator.ts#DESTRUCTIVE_OPERATIONS`), lifecycle bits RBAC-gated, `apiEnabled` enforced (`runtime/src/api-exposure.ts`), `PolicySchema` removed, EXPERIMENTAL tag convention live. Two gate-valid disposition deviations: agent access-control shipped experimental-tagged (not enforced), `flow.runAs` kept + enforced (not removed). `action.disabled` CEL enforcement to confirm in objectui. +**Status**: Accepted (2026-06-15) — implemented: fail-closed `DESTRUCTIVE_OPERATIONS` (`packages/plugins/plugin-security/src/permission-evaluator.ts#DESTRUCTIVE_OPERATIONS`), lifecycle bits RBAC-gated, `apiEnabled` enforced (`runtime/src/api-exposure.ts`), `PolicySchema` removed, EXPERIMENTAL tag convention live. Two gate-valid disposition deviations: agent access-control shipped experimental-tagged (not enforced), `flow.runAs` kept + enforced (not removed). `action.disabled` CEL enforcement to confirm in objectui. **Amended 2026-09-04** (#14402) — recording that an author-written validation rule's `message` became translatable in 17.3.0 (#14381) under the object-scoped bundle key `objects.._validations..message`. That is this gate being **enforced, not reversed**: the 17.0.0 retirement of the top-level `validationMessages` group stands, and the two are not the same route. See **"Amendment (2026-09-04): validation-rule messages are translatable again — enforced, not reversed"** at the end. **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (artifact vs runtime overlay), [ADR-0010](./0010-metadata-protection-model.md) (package provenance), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (authoring lifecycle) **Consumers**: `@objectstack/spec` (security/identity schemas), `@objectstack/plugin-security` (`PermissionEvaluator`, `SecurityPlugin`), spec authors, the metadata-property liveness audit follow-ups (#1878 P0 cluster). @@ -114,3 +114,55 @@ property — a smaller spec surface is the stronger default pre-MVP. their respective issues. - The P1 (ADR-0021 analytics migration) and P2 (spec hygiene) clusters of #1878 — non-security, governed separately. + +--- + +## Amendment (2026-09-04): validation-rule messages are translatable again — enforced, not reversed + +**This records a fact, it does not change the decision.** As of **17.3.0** +(#14381, merged 2026-09-02 — `@objectstack/spec@17.3.0`, +`@objectstack/objectql@17.3.0`), an author-written validation rule's `message` +is translatable through the object-scoped bundle key +`objects.._validations..message`. **The 17.0.0 +retirement of the top-level `validationMessages` group (#4667) stands, and the +two are not the same route.** + +It is recorded because Prime Directive #13 makes reversing a recorded decision a +decision in its own right, and until now the 17.0.0 retirement lived only in a +retired-key tombstone (the ADR-0087 conversion +`translation-validation-messages-removed`) and a changeset — precisely the shape +#13 warns is easy to miss. It is recorded *here*, as an amendment rather than a +new ADR, because the reader who would be misled is the one already reading this +gate. + +### Why this is the gate being enforced, not reversed + +- **The policy is enforce-or-remove.** `validationMessages` was removed because + **nothing read it** — a translated rule message was stored and never shown. + Removal was the correct disposition then and is not revisited now. +- **The new key ships with its reader in the same change** — the other half of + the same policy, not an exception to it. + `packages/objectql/src/validation/rule-validator.ts#authoredRuleMessage` + resolves the key as the violation is built, over the existing + `ValidationMessageContext.translate` hook (#3957) that already localizes + built-in messages and field labels. No second i18n path into objectql. +- **It is not the same key.** The retired group was keyed by rule name at the + **top level** and so could not tell two objects' rules apart. The new one is + **object-scoped**, spelled by + `packages/spec/src/system/i18n-resolver.ts#objectValidationMessageKey`, and + sits beside `_views` / `_actions` / `_tabs`. +- **The ADR-0087 conversion is untouched.** The stored-bundle rehydration seam + still strips the old key. Nothing migrates from the retired group into the new + one; an author who wants the new route writes it. + +### Scope note + +`validationMessages` is a translation key, not a security property, so its +retirement was never part of this ADR's original P0 cluster — the Non-goals +above place the non-security clusters elsewhere. The record lands here because +the repo cites this ADR as the enforce-or-remove policy for spec-property +retirement generally, including in the prescription an author sees on writing +the retired key +(`packages/spec/src/system/translation.zod.ts#TRANSLATION_KEY_GUIDANCE`). **No +new scope is claimed for this ADR by this amendment**; the decision above is +unchanged in every respect.