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
22 changes: 17 additions & 5 deletions content/docs/ui/translations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default defineStack({
| Action labels, confirm text, success messages | `objects.<name>._actions.<action>` |
| Action result dialogs (title / description / acknowledge / field labels) | `objects.<name>._actions.<action>.resultDialog` |
| Form sections | `objects.<name>._sections.<section>` |
| Custom validation-rule messages | `objects.<name>._validations.<rule>.message` |
| App navigation | `apps.<app>.navigation.<id>.label` |
| Dashboard label / description | `dashboards.<name>.label` / `description` |
| Dashboard widget title / description / sub-caption | `dashboards.<name>.widgets.<widgetId>.title` / `description` / `subCaption` |
Expand Down Expand Up @@ -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.<name>._validations.<rule>.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
Expand Down
54 changes: 53 additions & 1 deletion docs/adr/0049-no-unenforced-security-properties.md
Original file line number Diff line number Diff line change
@@ -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.<object_name>._validations.<rule_name>.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).
Expand Down Expand Up @@ -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.<object_name>._validations.<rule_name>.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.
Loading