You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
i18n: give bulk-action defs, custom validation messages and dataset labels a bundle key (#14381)
* feat(spec): translate a list view's bulkActionDefs from the bundle
A `bulkActionDefs` entry is part of the VIEW document, not an action
document, so it never reaches `translateAction` and no bundle group
addressed it. Measured against a fully translated app the selection bar
read `已选择 1 项 · Complete · Skip · 清除` — two English words between
two Chinese ones, which reads as a styling quirk rather than a missing
translation. Not a drifted key: no key.
Adds `objects.<object>._views.<view>.bulkActions.<def_name>` carrying
`label` / `confirmText` / `confirmLabel` and per-param `label` / `help` /
`placeholder`, resolved in `translateView` against `config.bulkActionDefs`
— the one address a served def has (`ViewItemSchema` and
`expandViewContainer` both nest the whole ListView under `config`).
The def's `label` stays `z.string()` on the authoring side: the bar
renders it as a React child, so an inline locale map would be a blank
cell rather than a parse error (`ui/bulk-action.zod.ts` module header).
Overlaying at the metadata boundary keeps the wire value a plain string
and changes only its language.
Key face measured against `BulkActionDefSchema`, not mirrored from the
report. Two exclusions carry `guidance`: `successMessage` (a def declares
none) and per-param `options` (`options[].value` is unconstrained, so a
value-keyed map cannot address `true` and `"true"` apart — the same
measured reason `FLOW_SCREEN_FIELD_NO_OPTIONS` gives). `help`, not
`helpText`: the face follows the authored key and aliases the
neighbouring action-param spelling onto it.
Part of #14253
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
* feat(spec,objectql): give a custom validation rule's message a bundle key
`object.validations[].message` is the sentence a rejected write returns,
and the evaluator emitted it verbatim. A deployment with a complete
`zh-CN` bundle therefore read platform-generated refusals in Chinese and
author-written refusals in English *inside one 400 VALIDATION_FAILED
envelope* — the built-in field catalog has resolved through the engine's
i18n service since #3957, and only the authored half had nowhere to look.
Adds `objects.<object>._validations.<rule_name>.message`, spelled by
`objectValidationMessageKey` (the third member of the
`objectFieldLabelKey` / `objectLabelKey` family) and read on the write
path by a new `authoredRuleMessage` seat in the rule evaluator.
⚠️ No second i18n path into objectql. The lookup runs on the SAME
`ValidationMessageContext.translate` hook — the engine's `i18nService`,
bridged by `ObjectQLPlugin` — that `resolveFieldLabel` and
`renderValidationMessage` already use. What was missing was a key shape,
not a channel. All five authored-message emitters route through the one
seat (`script`/`cross_field`, `state_machine`, `format`, and both
`json_schema` arms); a nested `conditional` branch is addressed by the
BRANCH's own name, and a PLATFORM-generated rejection (an unevaluable
predicate) is deliberately left alone.
This is not `validationMessages` (#4667, ADR-0049) coming back. That group
was keyed by rule name at the bundle's TOP level — it could not tell two
objects' rules apart — and, the reason it was retired, nothing read it.
This address is object-scoped, sits beside `_views`/`_actions`/`_tabs`,
and ships its reader in the same change. Its retired-key guidance is
updated to point here instead of asserting that no route exists, and the
`errors` tombstone with it.
Key face is one key, measured: a rule also declares `label` (the admin
listing entry) and `description` (administrative notes), neither of which
reaches a rejected caller — declaring them would parse clean and translate
nothing, so both carry `guidance`.
Also corrects the `validation.message` liveness row, whose overrides
clause named `validationMessages` — a route removed a major version ago.
Part of #14253
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
* feat(spec): translate dataset labels via translateDataset + a datasets group
A dataset reads like a back-office definition, but a measure label is
drawn ON THE DASHBOARD — under every metric tile and on every chart axis.
`dataset` was neither in `TRANSLATABLE_METADATA_TYPES` nor addressed by
any bundle group, so a translated dashboard rendered Chinese tile titles
(those are `dashboards.<name>.widgets.<id>.*`) with `Untouched > 14 days`
directly beneath them. Not a drifted key: no key.
Adds `datasets.<name>.{label,description,dimensions.<d>.label,
measures.<m>.label}` and `translateDataset`, registered in
`METADATA_DOCUMENT_TRANSLATORS`. That registration is the whole wiring:
`TRANSLATABLE_METADATA_TYPES` is DERIVED from the table and
`@objectstack/rest` reads the derived set, so the REST boundary follows
with nothing else to remember (#3786). No second hand-maintained list was
found — the derivation is intact.
Top-level rather than nested under `dashboards` because a dataset is the
ONE definition every presentation binds to by reference (ADR-0021 D1):
the same measure is drawn by N widgets across M dashboards, and a dataset
no dashboard references would otherwise be unaddressable.
The four keys are `I18nLabelSchema` at the authoring site, so a dataset's
copy may already be an inline `{ en, 'zh-CN' }` map (#5728).
`translateDataset` writes ONLY where the bundle answers — the same rule
`translatePage` follows — so an uncovered inline map is left intact
rather than flattened to one language, and the member arrays keep their
identity when nothing matched.
Key face measured against `DatasetSchema`: a dimension and a measure each
declare `label` and nothing else display-shaped, which `dataset.zod.ts`
states at the authoring site too ("its author-facing text is `label`.
`description` is declared on the DATASET itself"). `description` therefore
lives on the dataset and carries `guidance` below it.
Liveness: the `datasets` group is seeded LIVE and DRILLED (label /
description / dimensions / measures) with its reader in the same change.
The ledger's own header sentence is corrected with it — it still called
`validationMessages` "the one dead group" a major version after #4667
removed it.
Part of #14253
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
* fix(spec): keep the retired-key guidance free of internal issue ids
`check:doc-authoring` caught two `#NNNN` citations in the tombstone text
this branch rewrote. That text is printed AT the author, verbatim, the
moment their bundle is refused — by `os validate`, by a publish gate, by
a parse — and none of those readers has a tracker, so the token resolves
to nothing in the one place the sentence most needs to be actionable.
Maintainer ruling 2026-08-12, verbatim: 「处理 issue 时犯的错应该总结成
经验,保留 issue id没有意义」.
The customer-resolvable references stay: the protocol version, ADR-0049,
the migration command and the replacement key path.
Adds the negative pin the gate's own remedy asks for, beside the twin
that pins the wording: the tombstone must name the live replacement
group AND must not carry an issue id.
Part of #14253
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
* feat(rest): localize a saved dataset at the analytics query door too
Surface 3's second door, found by measuring where a measure label
actually reaches a dashboard rather than assuming it rides the metadata
read.
`AnalyticsResult.fields[].label` is documented as the display label "for
legends/KPIs", and `AnalyticsService` fills it by copying
`dataset.measures[].label` off the definition. That definition arrives
through `POST /analytics/dataset/query`, which resolves a saved dataset
via `getMetaItems` and never passes through `translateMetaItem`. So
`translateDataset` alone closes `/meta/datasets` — the door a dashboard
does NOT draw through — and leaves the one in the issue's screenshot
open: covered at one door, open at the other.
Fixed at the metadata boundary, where every other document is localized:
the resolved saved definition goes through the existing
`translateMetaItem` before it is parsed and compiled, and the existing
field enrichment carries the translated label to the wire untouched.
Nothing downstream learns about bundles — the analytics service stays
free of i18n and there is no second resolution path.
⛔ The INLINE branch is deliberately not translated: a Studio preview
posts the draft the designer is editing, which carries no saved name to
address a bundle entry with, and overwriting its copy would misreport
what is about to be saved. Pinned by test, both ways.
Part of #14253
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
* fix(lint): give `_validations` a reference-checked leg in validate-translation-references
#14253 added `objects.<obj>._validations.<rule>.message` to
`ObjectTranslationDataSchema`; the lint rule's per-group coverage pin
(`classifies every key the schema declares, and no key it does not`)
correctly went red because the rule had no leg for it — the exact
"second hand-maintained list drifted" class the PR exists to close.
The walker now registers `objects[].validations[].name` per object and
reports `translation-target-unknown` for a `_validations` key naming a
rule the object does not declare, with the same guidance shape as
`_tabs`. The coverage pin and the all-real-names control both carry the
new group; the fixture declares one rule so the control stays non-vacuous.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(17.0.0, #4667, ADR-0049) is **not** reversed — that group was keyed by rule name
67
+
at the bundle's top level, so it could not tell two objects' rules apart, and,
68
+
the reason it was retired, nothing read it. Its ADR-0087 conversion still strips
69
+
it from stored bundles. The replacement is object-scoped and ships its reader in
70
+
the same change.
71
+
72
+
Authors upgrading need do nothing; a bundle that writes none of the three new
73
+
groups behaves exactly as before.
74
+
75
+
<!-- adr-0087: not-required (unpublished) Purely additive: three new optional groups on `TranslationData` / `TranslationItem`, one new dispatch-table entry, and one new lookup on the write path. No authorable key is removed, renamed or re-shaped, so there is no tombstone, no stored shape to rewrite, and nothing mechanical for `objectstack migrate meta` to prescribe. The retired `validationMessages` conversion entry is untouched and still strips the key it always stripped — its guidance text now names a live replacement instead of asserting none exists, which changes what an author is told, not what a stored bundle becomes. -->
0 commit comments