Skip to content

Commit b2eab95

Browse files
os-warrenclaude
andauthored
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>
1 parent 79b6a22 commit b2eab95

21 files changed

Lines changed: 1710 additions & 51 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
'@objectstack/lint': patch
5+
---
6+
7+
feat(spec,objectql): give three authored display surfaces a bundle key — bulk-action defs, custom validation messages, dataset labels (#14253)
8+
9+
Purely additive: three new translation groups, one new dispatch-table entry, one
10+
new resolution step on the write path. No existing key changes shape, no
11+
resolution order changes, and every surface still falls back to the authored
12+
literal when the bundle carries nothing.
13+
14+
Each of the three carried **authored, user-facing display text that no key in
15+
`TranslationDataSchema` could reach** — not a drifted key, no key. Each rendered
16+
in the source locale inside an otherwise fully translated screen, which is the
17+
bad failure mode: it reads as a styling quirk rather than as a missing
18+
translation. Measured on a real `zh-CN` deployment.
19+
20+
**1. A list view's `bulkActionDefs[]`**
21+
`objects.<object>._views.<view>.bulkActions.<def_name>.{label,confirmText,confirmLabel,params.<p>.{label,help,placeholder}}`,
22+
resolved in `translateView` against `config.bulkActionDefs` (the one address a
23+
served def has: both `ViewItemSchema` and `expandViewContainer` nest the whole
24+
ListView under `config`). A def is part of the *view* document, not an action
25+
document, so it never reached `translateAction`; the selection bar read
26+
`已选择 1 项 · Complete · Skip · 清除`. The def's `label` deliberately stays a
27+
plain `z.string()` on the authoring side — the bar renders it as a React child,
28+
so an inline locale map would be a blank cell rather than a parse error — and
29+
overlaying at the metadata boundary keeps the wire value a plain string. The
30+
documented workaround (`bulkActions: ['<name>']`, promoting a declared action)
31+
is not equivalent: it is N elevated per-record dispatches instead of one
32+
data-plane `updateMany`.
33+
34+
**2. A custom validation rule's `message`**
35+
`objects.<object>._validations.<rule_name>.message`, spelled by the new
36+
`objectValidationMessageKey` and read on the write path by the rule evaluator.
37+
⚠️ **This adds a key shape, not a channel**: the lookup runs on the *existing*
38+
`i18nService` hook that has localized built-in field-catalog messages and field
39+
labels since #3957. Before it, a deployment got platform-generated refusals in
40+
the caller's language and author-written refusals in the source language inside
41+
one `400 VALIDATION_FAILED` envelope. All five authored-message emitters route
42+
through one seat; a nested `conditional` branch is addressed by the branch's own
43+
name; a platform-generated rejection (an unevaluable predicate) is deliberately
44+
left alone. `messages['validation.field.*']` is unchanged and still overrides the
45+
built-in catalog only.
46+
47+
**3. Dataset labels**`datasets.<name>.{label,description,dimensions.<d>.label,measures.<m>.label}`
48+
plus `translateDataset` in `METADATA_DOCUMENT_TRANSLATORS`. A dataset reads like
49+
a back-office definition, but a measure label is drawn on the dashboard, under
50+
every metric tile and on every chart axis. Registering the translator is the
51+
whole wiring — `TRANSLATABLE_METADATA_TYPES` is derived from that table and
52+
`@objectstack/rest` reads the derived set (#3786) — so `GET /api/v1/meta/datasets?locale=…`
53+
localizes with nothing else to remember.
54+
55+
Key faces are measured against the authoring schemas rather than mirrored from
56+
the report, so nothing here parses clean and translates nothing: a bulk param's
57+
hint is `help` (not the action-param `helpText`), per-param `options` are refused
58+
because `options[].value` is unconstrained and a value-keyed map cannot address
59+
`true` and `"true"` apart, a def has no `successMessage`, and a dataset dimension
60+
or measure has no `description` — the authoring schema says so itself. Every
61+
exclusion carries `guidance` naming the right home.
62+
63+
Two tombstones stop asserting that no route exists: the retired
64+
`validationMessages` and `errors` guidance now point at
65+
`objects.<object>._validations.<rule>.message`. Retiring `validationMessages`
66+
(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. -->

content/docs/references/api/protocol.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,6 +1565,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
15651565
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
15661566
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
15671567
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object> }>` | optional | Dashboard translations keyed by dashboard name |
1568+
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
15681569
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
15691570
| **flows** | `Record<string, { label?: string; screens?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
15701571
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |

content/docs/references/system/translation.mdx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,11 @@ Translation data for a single object
9090
| **pluralLabel** | `string` | optional | Translated plural label |
9191
| **description** | `string` | optional | Translated object description |
9292
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Field-level translations |
93-
| **_views** | `Record<string, { label?: string; description?: string; emptyState?: object }>` | optional | View translations keyed by view name |
93+
| **_views** | `Record<string, { label?: string; description?: string; emptyState?: object; bulkActions?: Record<string, object> }>` | optional | View translations keyed by view name |
9494
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
9595
| **_sections** | `Record<string, { label?: string; description?: string }>` | optional | Section translations keyed by section name |
9696
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |
97+
| **_validations** | `Record<string, { message?: string }>` | optional | Custom validation-rule messages keyed by rule name (`ValidationRuleSchema.name`) |
9798

9899
### Nested Shape: `ObjectTranslationData.fields[string]`
99100

@@ -113,6 +114,7 @@ Translation data for a single field
113114
| **label** | `string` | optional | Translated view label |
114115
| **description** | `string` | optional | Translated view description |
115116
| **emptyState** | `{ title?: string; message?: string }` | optional | Translated empty-state copy shown when the view has no rows |
117+
| **bulkActions** | `Record<string, { label?: string; confirmText?: string; confirmLabel?: string; params?: Record<string, object> }>` | optional | Selection-bar translations keyed by bulk-action def name (`BulkActionDefSchema.name`) |
116118

117119
### Nested Shape: `ObjectTranslationData._actions[string]`
118120

@@ -138,6 +140,12 @@ Translation data for a single field
138140
| :--- | :--- | :--- | :--- |
139141
| **label** | `string` | optional | Translated tab label |
140142

143+
### Nested Shape: `ObjectTranslationData._validations[string]`
144+
145+
| Property | Type | Required | Description |
146+
| :--- | :--- | :--- | :--- |
147+
| **message** | `string` | optional | Translated rejection message — overlays the rule's authored `message` on every rejected write |
148+
141149

142150
---
143151

@@ -225,6 +233,7 @@ Translation data for objects, apps, and UI messages
225233
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
226234
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
227235
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object> }>` | optional | Dashboard translations keyed by dashboard name |
236+
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
228237
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
229238
| **flows** | `Record<string, { label?: string; screens?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
230239
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
@@ -241,10 +250,11 @@ Translation data for a single object
241250
| **pluralLabel** | `string` | optional | Translated plural label |
242251
| **description** | `string` | optional | Translated object description |
243252
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Field-level translations |
244-
| **_views** | `Record<string, { label?: string; description?: string; emptyState?: object }>` | optional | View translations keyed by view name |
253+
| **_views** | `Record<string, { label?: string; description?: string; emptyState?: object; bulkActions?: Record<string, object> }>` | optional | View translations keyed by view name |
245254
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
246255
| **_sections** | `Record<string, { label?: string; description?: string }>` | optional | Section translations keyed by section name |
247256
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |
257+
| **_validations** | `Record<string, { message?: string }>` | optional | Custom validation-rule messages keyed by rule name (`ValidationRuleSchema.name`) |
248258

249259
### Nested Shape: `TranslationData.apps[string]`
250260

@@ -274,6 +284,15 @@ Translation data for a single object
274284
| **actions** | `Record<string, { label?: string }>` | optional | Header action label translations keyed by action url/key |
275285
| **widgets** | `Record<string, { title?: string; description?: string; subCaption?: string }>` | optional | Widget translations keyed by widget id |
276286

287+
### Nested Shape: `TranslationData.datasets[string]`
288+
289+
| Property | Type | Required | Description |
290+
| :--- | :--- | :--- | :--- |
291+
| **label** | `string` | optional | Translated dataset label |
292+
| **description** | `string` | optional | Translated dataset description |
293+
| **dimensions** | `Record<string, { label?: string }>` | optional | Dimension translations keyed by dimension name (`DatasetDimensionSchema.name`) |
294+
| **measures** | `Record<string, { label?: string }>` | optional | Measure translations keyed by measure name (`DatasetMeasureSchema.name`) |
295+
277296
### Nested Shape: `TranslationData.pages[string]`
278297

279298
| Property | Type | Required | Description |
@@ -364,6 +383,7 @@ One locale of translations — the `translation` metadata type
364383
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
365384
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
366385
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object> }>` | optional | Dashboard translations keyed by dashboard name |
386+
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
367387
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
368388
| **flows** | `Record<string, { label?: string; screens?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
369389
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
@@ -390,10 +410,11 @@ Translation data for a single object
390410
| **pluralLabel** | `string` | optional | Translated plural label |
391411
| **description** | `string` | optional | Translated object description |
392412
| **fields** | `Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Field-level translations |
393-
| **_views** | `Record<string, { label?: string; description?: string; emptyState?: object }>` | optional | View translations keyed by view name |
413+
| **_views** | `Record<string, { label?: string; description?: string; emptyState?: object; bulkActions?: Record<string, object> }>` | optional | View translations keyed by view name |
394414
| **_actions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Action translations keyed by action name |
395415
| **_sections** | `Record<string, { label?: string; description?: string }>` | optional | Section translations keyed by section name |
396416
| **_tabs** | `Record<string, { label?: string }>` | optional | Filter-preset tab translations keyed by tab name |
417+
| **_validations** | `Record<string, { message?: string }>` | optional | Custom validation-rule messages keyed by rule name (`ValidationRuleSchema.name`) |
397418

398419
### Nested Shape: `TranslationItem.apps[string]`
399420

@@ -423,6 +444,15 @@ Translation data for a single object
423444
| **actions** | `Record<string, { label?: string }>` | optional | Header action label translations keyed by action url/key |
424445
| **widgets** | `Record<string, { title?: string; description?: string; subCaption?: string }>` | optional | Widget translations keyed by widget id |
425446

447+
### Nested Shape: `TranslationItem.datasets[string]`
448+
449+
| Property | Type | Required | Description |
450+
| :--- | :--- | :--- | :--- |
451+
| **label** | `string` | optional | Translated dataset label |
452+
| **description** | `string` | optional | Translated dataset description |
453+
| **dimensions** | `Record<string, { label?: string }>` | optional | Dimension translations keyed by dimension name (`DatasetDimensionSchema.name`) |
454+
| **measures** | `Record<string, { label?: string }>` | optional | Measure translations keyed by measure name (`DatasetMeasureSchema.name`) |
455+
426456
### Nested Shape: `TranslationItem.pages[string]`
427457

428458
| Property | Type | Required | Description |

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,4 @@ directory rather than per file.
263263
| `kernel/` | 261 |
264264
| `qa/` | 6 |
265265
| `shared/` | 20 |
266-
| `system/` | 364 |
266+
| `system/` | 370 |

packages/lint/src/validate-translation-references.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,7 @@ describe('validateTranslationReferences — object-branch coverage vs the schema
14481448
fields: { name: { type: 'text', label: 'Name' } },
14491449
fieldGroups: [{ key: 'basics', label: 'Basics' }],
14501450
actions: [{ name: 'convert_lead', label: 'Convert' }],
1451+
validations: [{ name: 'lead_needs_name', type: 'script', message: 'Name is required' }],
14511452
},
14521453
],
14531454
views: [{ name: 'open_leads', objectName: 'crm_lead', label: 'Open Leads' }],
@@ -1503,6 +1504,11 @@ describe('validateTranslationReferences — object-branch coverage vs the schema
15031504
ghost: { _tabs: { ghost_tab: { label: 'Ghost' } } },
15041505
path: 'translations[0].en.objects.crm_lead._tabs.ghost_tab',
15051506
},
1507+
_validations: {
1508+
kind: 'reference-checked',
1509+
ghost: { _validations: { ghost_rule: { message: 'Ghost' } } },
1510+
path: 'translations[0].en.objects.crm_lead._validations.ghost_rule',
1511+
},
15061512
};
15071513

15081514
it('classifies every key `ObjectTranslationDataSchema` declares, and no key it does not', () => {
@@ -1537,6 +1543,7 @@ describe('validateTranslationReferences — object-branch coverage vs the schema
15371543
_sections: { basics: { label: 'Basics' } },
15381544
_actions: { convert_lead: { label: 'Convert' } },
15391545
_tabs: { urgent: { label: 'Urgent' } },
1546+
_validations: { lead_needs_name: { message: 'A name is required' } },
15401547
}),
15411548
);
15421549
expect(findings).toEqual([]);

0 commit comments

Comments
 (0)