@dforge-core/dforge-mcp 0.2.31 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/iash44/dForge-core",
@@ -26,8 +26,8 @@
26
26
  "node": ">=18"
27
27
  },
28
28
  "dependencies": {
29
- "@dforge-core/dforge-cli": "^0.2.24",
30
- "@dforge-core/metadata": "^0.0.30",
29
+ "@dforge-core/dforge-cli": "^0.2.26",
30
+ "@dforge-core/metadata": "^0.0.33",
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
32
  "zod": "^4.4.3"
33
33
  },
@@ -512,7 +512,7 @@ Two halves, both required:
512
512
 
513
513
  **`"default"` is the one exempt name** and means "no view" (full column set). It is what the ~100 shipped folder entries write, what `FolderExporter` writes back for a NULL `view_name`, and what the runtime auto-creates (column-less) for an entity reached without a folder binding — so rejecting it would fail every install. A module that really declares a view named `default` still gets it bound.
514
514
 
515
- Don't confuse the three things called "view": an **entity view** (`views`, this section) is column security; a **data view** (`ui/data_views.json`, referenced by a menu's `dataViewCode`) is a grid/kanban/calendar; `isView` + `viewSql` on an entity means it is backed by a SQL view. `folders.json`'s `viewName` binds the first.
515
+ Don't confuse the two things called "view": an **entity view** (`views`, this section) is column security; a **data view** (`ui/data_views.json`, referenced by a menu's `dataViewCode`) is a grid/kanban/calendar. `folders.json`'s `viewName` binds the first. An entity is never backed by a SQL view — `isView` / `viewSql` are rejected; use a data view, a report or a stored procedure instead.
516
516
 
517
517
  ## Column Domains (`domains.json`)
518
518
 
@@ -472,14 +472,16 @@ scalar; denormalise instead of navigating; put the rest in `execute:` behind
472
472
 
473
473
  ### Messaging & notifications
474
474
 
475
- #### `info(message, opts?)`, `warn(message, opts?)`
476
- Queue user-facing toast. `opts.links: [{entity, id}]` adds "open record" buttons.
475
+ #### `info(message, values?, opts?)`, `warn(message, values?, opts?)`
476
+ Queue user-facing toast. `values` fills the message's `{name}` placeholders; `opts.links: [{ label, entity, record }]` makes `label` in the text a link to that record (`pk:` instead of `record:` takes a key value or object). A link label must be data — a record number — never a word, since it is found in the text after translation.
477
+
478
+ **Translatable.** The message text is looked up by its exact English wording in the module's `messages` translations (`dforge://reference/translations`), in the user's language; no entry shows the English. Put varying values in `{name}` placeholders, filled from `values` — never join them into the string, which leaves no fixed text to translate. `links` is not a placeholder name.
477
479
 
478
480
  A message is a **receipt for work the action did**, not a way to publish a computed value. An action whose only output is `info('<some computed number>')` stores nothing — it can't be audited, re-read or reported on, and the value arrives unformatted. Build a **record report** (a report with an `entities` attachment: it lands on the same record toolbar, but shows the numbers *and* the rows behind them, re-read live on every open), a formula column, or an action that writes a result record instead. Do not compute in DSL what a report dataset already aggregates. See `dforge://reference/action-dsl`, *When an action is the wrong tool*, and `dforge://reference/reports`.
479
481
 
480
482
  ```dsl
481
- info('Created PO ' + po.po_number, {
482
- links: [{ entity: 'purchase_order', id: po.purchase_order_id }]
483
+ info('Created PO {po}', { po: po.po_number }, {
484
+ links: [{ label: po.po_number, entity: 'purchase_order', record: po }]
483
485
  })
484
486
  warn('Stock below reorder point — review supplier')
485
487
  ```
@@ -506,7 +508,7 @@ sendEmail([customer_id].[email], 'order_confirmation', {
506
508
  })
507
509
  ```
508
510
 
509
- #### `error(message)`
511
+ #### `error(message, values?)`
510
512
  **Throw** — aborts execution immediately. Use for validation failures. Whether
511
513
  prior `insert()` / writes in this `execute:` block are rolled back **depends on the
512
514
  execution path** and is not something to rely on — validate before writing
@@ -516,10 +518,13 @@ anything (see the anti-pattern near the end of this document).
516
518
  if ([rating] == null || [rating] < 1 || [rating] > 5) {
517
519
  error('Rating must be 1-5')
518
520
  }
521
+ if (available < params[quantity]) {
522
+ error('Only {n} left', { n: available })
523
+ }
519
524
  ```
520
525
 
521
- #### `exit(message?, level?)`
522
- Successful early exit — **commits** work done so far. `level`: `"info"` (default) | `"warning"` | `"danger"` | `"success"`.
526
+ #### `exit(message?, level?, values?)`
527
+ Successful early exit — **commits** work done so far. `level`: `"info"` (default) | `"warning"` | `"danger"` | `"success"`. `values` fills the message's placeholders, as for `info()`.
523
528
 
524
529
  ```dsl
525
530
  if ([status] == 'closed') { exit('Already closed — nothing to do', 'info') }
@@ -21,7 +21,7 @@
21
21
  "type": "object",
22
22
  "description": "Entity key → placement. A key is the entity code as the module's manifest names it, or `module.entity` for an entity of a dependency — the same form a field link's `entity` takes. A key no entity has yet is a planned entity, drawn as a name only. An empty object places the entity automatically.",
23
23
  "propertyNames": {
24
- "minLength": 1
24
+ "$ref": "#/$defs/entityKey"
25
25
  },
26
26
  "additionalProperties": {
27
27
  "$ref": "#/$defs/diagramEntity"
@@ -46,6 +46,11 @@
46
46
  },
47
47
  "additionalProperties": false,
48
48
  "$defs": {
49
+ "entityKey": {
50
+ "type": "string",
51
+ "pattern": "^([a-zA-Z_][a-zA-Z0-9_-]*\\.)*[a-zA-Z_][a-zA-Z0-9_-]*$",
52
+ "description": "An entity code, optionally qualified by its module's code: `entity`, `module.entity`. Each part is an identifier the installer accepts: `[a-zA-Z_][a-zA-Z0-9_-]*`."
53
+ },
49
54
  "diagramEntity": {
50
55
  "type": "object",
51
56
  "properties": {
@@ -77,13 +82,11 @@
77
82
  "required": ["from", "to"],
78
83
  "properties": {
79
84
  "from": {
80
- "type": "string",
81
- "minLength": 1,
85
+ "$ref": "#/$defs/entityKey",
82
86
  "description": "Entity key of the many side — the entity that will carry the foreign key."
83
87
  },
84
88
  "to": {
85
- "type": "string",
86
- "minLength": 1,
89
+ "$ref": "#/$defs/entityKey",
87
90
  "description": "Entity key of the one side — the entity referenced."
88
91
  },
89
92
  "label": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://dforge.dev/schemas/entity.schema.json",
4
4
  "title": "dForge Entity Definition",
5
- "description": "Defines an entity (table/view mapping) for a dForge module",
5
+ "description": "Defines an entity (a table) for a dForge module",
6
6
  "type": "object",
7
7
  "anyOf": [
8
8
  { "required": ["fields"] },
@@ -24,16 +24,7 @@
24
24
  },
25
25
  "dbObject": {
26
26
  "type": "string",
27
- "description": "Database table or view name (defaults to entity code)"
28
- },
29
- "isView": {
30
- "type": "boolean",
31
- "description": "When true, entity maps to a SQL view (read-only, no DDL generation). Default: false (table-backed).",
32
- "default": false
33
- },
34
- "viewSql": {
35
- "type": "string",
36
- "description": "SQL SELECT statement for view-backed entities (isView=true). Executed as CREATE OR REPLACE VIEW during module install."
27
+ "description": "Database table name (defaults to entity code)"
37
28
  },
38
29
  "toString": {
39
30
  "description": "Display pattern using column placeholders, e.g. \"{first_name} {last_name}\" — the caption of a record wherever it is referenced. Every `{placeholder}` must name a field of the entity (trait columns included) and not a set; pack and install fail otherwise. Omitted, the install generates one from the fields and warns. Not declared on extension entities (with `extends`). No `type` constraint here because `toString` collides with Object.prototype.toString in JSON validators — they read the inherited function and reject it, even when the property is absent from the JSON."
@@ -42,6 +33,11 @@
42
33
  "type": "boolean",
43
34
  "description": "Records of this entity accept comments \u2014 the composer and thread appear on the record card, beside the audit history. Opt-in per entity; `comments` in the manifest sets a module-wide default that this overrides in both directions."
44
35
  },
36
+ "auditHistory": {
37
+ "type": "string",
38
+ "enum": ["basic", "fields", "full"],
39
+ "description": "Audit history level for this entity, overriding the manifest's `auditHistory`: 'basic' logs each insert/update/delete without field details; 'fields' adds old/new values of changed fields; 'full' also stores display values (reference and user names). Omit to disable."
40
+ },
45
41
  "traits": {
46
42
  "type": "array",
47
43
  "description": "Entity traits expanded during module install (authoring shortcut). Trait definitions loaded from DB (seeded by metadata module).",
@@ -146,7 +142,7 @@
146
142
  },
147
143
  "views": {
148
144
  "type": "object",
149
- "description": "Entity views keyed by view name — column-level security. Unrelated to 'isView'/'viewSql' (a SQL-view-backed entity) and to ui/data_views.json (grids, kanban, calendars). A folder binds one view per entity via ui/folders.json (entities.<code>.viewName); users working in that folder see ONLY the columns the view lists. Entities without views show the entity's full column set; a folder naming a view that isn't declared here FAILS the install, since resolving it to nothing would show every column \u2014 the opposite of what naming a view asks for. A view can only be declared by the entity's own module: an extension ('extends') declaring 'views' is rejected, because a view lists the complete column set and an extension knows only the columns it adds.",
145
+ "description": "Entity views keyed by view name — column-level security. Unrelated to ui/data_views.json (grids, kanban, calendars). A folder binds one view per entity via ui/folders.json (entities.<code>.viewName); users working in that folder see ONLY the columns the view lists. Entities without views show the entity's full column set; a folder naming a view that isn't declared here FAILS the install, since resolving it to nothing would show every column \u2014 the opposite of what naming a view asks for. A view can only be declared by the entity's own module: an extension ('extends') declaring 'views' is rejected, because a view lists the complete column set and an extension knows only the columns it adds.",
150
146
  "propertyNames": {
151
147
  "pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$",
152
148
  "maxLength": 100
@@ -364,6 +360,14 @@
364
360
  "serverDefault": {
365
361
  "$ref": "#/$defs/serverDefault"
366
362
  },
363
+ "currency": {
364
+ "type": "string",
365
+ "description": "currency field type: fixed ISO 4217 code (e.g. 'USD') the value is formatted in. With currencyColumn, the code used for a record whose currencyColumn is empty."
366
+ },
367
+ "currencyColumn": {
368
+ "type": "string",
369
+ "description": "currency field type: a scalar column of the same entity holding the ISO 4217 code per record, e.g. a 'currency' dropdown. Each value is formatted in its own record's currency; a total over rows in different currencies shows no symbol. A line reads its header's code through a formula column ([invoice].[currency]). Display only — nothing converts between currencies."
370
+ },
367
371
  "pattern": {
368
372
  "type": "string",
369
373
  "description": "Format rule for this column, as an ECMAScript regex source. Overrides the field type's own pattern (field_type.def_params.pattern, which is what makes email/phone/url reject a malformed value). Enforced in the browser and again on data.insert / data.update, so keep to constructs both engines read alike: [0-9] and \\d are the same there, \\s is normalized to JavaScript's set, and a pattern that will not compile is skipped by both rather than enforced by one."
@@ -388,7 +392,7 @@
388
392
  "description": "Reference to a reusable column domain, qualified as 'module_cd.domain_cd' (unqualified resolves within this module, then tenant-created domains). The domain supplies dbDatatype, baseDatatypeCd, fieldTypeCd, maxLen, precision and params; declaring any of those alongside 'domain' is rejected at install. The column keeps its own description, flags, orderNum, isNullable and isPk."
389
393
  },
390
394
  "refFilter": {
391
- "description": "Filter criteria for reference lookups. Condition values support @[field] placeholders resolved against the current record at lookup time (e.g. {\"c\":\"channel\",\"o\":\"eq\",\"v\":\"@[channel]\"} narrows options to rows matching the record's own channel); a condition whose placeholder is unset is dropped rather than matching nothing."
395
+ "description": "Filter criteria for reference lookups. Condition values support @[field] placeholders resolved against the current record at lookup time (e.g. {\"c\":\"channel\",\"o\":\"eq\",\"v\":\"@[channel]\"} narrows options to rows matching the record's own channel), and @[ref].[field] nav placeholders that read through a Reference column, as a formula does (e.g. @[purchase_order].[supplier_id] on an order line narrows to the order's supplier, also in the New dialog of the order's Lines set). A condition whose placeholder is unset is dropped rather than matching nothing."
392
396
  }
393
397
  },
394
398
  "oneOf": [
@@ -104,8 +104,8 @@
104
104
  },
105
105
  "auditHistory": {
106
106
  "type": "string",
107
- "enum": ["none", "minimal", "full"],
108
- "description": "Default audit history mode for entities in this module ('full' captures every field change; 'minimal' captures only inserts/deletes; 'none' disables). Individual entities can override."
107
+ "enum": ["basic", "fields", "full"],
108
+ "description": "Default audit history level for entities in this module: 'basic' logs each insert/update/delete without field details; 'fields' adds old/new values of changed fields; 'full' also stores display values (reference and user names). Omit to disable. Individual entities can override."
109
109
  },
110
110
  "comments": {
111
111
  "type": "boolean",
@@ -56,6 +56,11 @@
56
56
  "formula": { "type": "string" },
57
57
  "orderNum": { "type": "integer" },
58
58
  "description": { "type": "string", "description": "Supports {entity}/{Entity} placeholders" },
59
+ "labels": {
60
+ "type": "object",
61
+ "description": "Translated labels keyed by culture code (e.g. de-DE). Applied to every entity that takes the field from this trait; the module's translations/<locale>.json overrides them per entity. Supports {entity}/{Entity} placeholders.",
62
+ "additionalProperties": { "type": "string" }
63
+ },
59
64
  "maxLen": { "type": "integer" },
60
65
  "link": {
61
66
  "type": "object",
@@ -0,0 +1,267 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://dforge.dev/schemas/translations.schema.json",
4
+ "title": "dForge Translations",
5
+ "description": "A module's translations/<locale>.json, e.g. de-DE.json. English is the base text authored in the other module files, so an en-* file is not applied at install. Every key is optional: a missing entry falls back to the base text. Labels are required only for the locales the manifest lists in supportedLocales (constraint messages and DSL messages are warned about, never required).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "entities": {
13
+ "type": "object",
14
+ "description": "Entity code → labels for the entity, its fields, card sections and constraint messages.",
15
+ "additionalProperties": {
16
+ "$ref": "#/$defs/entity"
17
+ }
18
+ },
19
+ "domains": {
20
+ "type": "object",
21
+ "description": "Column domain code (domains.json) → label and option labels, translated once for every column that uses the domain.",
22
+ "additionalProperties": {
23
+ "$ref": "#/$defs/withOptions"
24
+ }
25
+ },
26
+ "folders": {
27
+ "type": "object",
28
+ "description": "Folder code → label. A sub-folder is keyed by its own flat code, not its path.",
29
+ "additionalProperties": {
30
+ "$ref": "#/$defs/labelOnly"
31
+ }
32
+ },
33
+ "views": {
34
+ "type": "object",
35
+ "description": "Data view code → label and description (the subtitle under the view title).",
36
+ "additionalProperties": {
37
+ "$ref": "#/$defs/labelDesc"
38
+ }
39
+ },
40
+ "menus": {
41
+ "type": "object",
42
+ "description": "Menu code → label and item labels.",
43
+ "additionalProperties": {
44
+ "$ref": "#/$defs/menu"
45
+ }
46
+ },
47
+ "settings": {
48
+ "type": "object",
49
+ "description": "Setting code (settings.json) → label and description.",
50
+ "additionalProperties": {
51
+ "$ref": "#/$defs/labelDesc"
52
+ }
53
+ },
54
+ "roles": {
55
+ "type": "object",
56
+ "description": "Module-qualified role code (e.g. \"crm.admin\") → label.",
57
+ "additionalProperties": {
58
+ "$ref": "#/$defs/labelOnly"
59
+ }
60
+ },
61
+ "reports": {
62
+ "type": "object",
63
+ "description": "Report code → label, dataset captions and parameter labels.",
64
+ "additionalProperties": {
65
+ "$ref": "#/$defs/report"
66
+ }
67
+ },
68
+ "actions": {
69
+ "type": "object",
70
+ "description": "Action code (ui/actions.json) → label, tooltip and parameter labels.",
71
+ "additionalProperties": {
72
+ "$ref": "#/$defs/action"
73
+ }
74
+ },
75
+ "print_templates": {
76
+ "type": "object",
77
+ "description": "Print template code → label. Accepted, but not applied at install yet.",
78
+ "additionalProperties": {
79
+ "$ref": "#/$defs/labelOnly"
80
+ }
81
+ },
82
+ "messages": {
83
+ "type": "object",
84
+ "description": "Text of a DSL info() / warn() / error() / exit() call, exactly as written in the .dsl file → its translation. {name} placeholders are filled from the call's values and may be reordered. See docs/business-logic/message-localization.md.",
85
+ "propertyNames": {
86
+ "minLength": 1
87
+ },
88
+ "additionalProperties": {
89
+ "type": "string",
90
+ "minLength": 1
91
+ }
92
+ }
93
+ },
94
+ "$defs": {
95
+ "label": {
96
+ "type": "string",
97
+ "description": "Translated display label."
98
+ },
99
+ "labelOnly": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "properties": {
103
+ "label": {
104
+ "$ref": "#/$defs/label"
105
+ }
106
+ }
107
+ },
108
+ "labelDesc": {
109
+ "type": "object",
110
+ "additionalProperties": false,
111
+ "properties": {
112
+ "label": {
113
+ "$ref": "#/$defs/label"
114
+ },
115
+ "desc": {
116
+ "type": "string",
117
+ "description": "Translated description or tooltip."
118
+ }
119
+ }
120
+ },
121
+ "option": {
122
+ "description": "A translated option: the label alone, or the label with an icon and color override.",
123
+ "oneOf": [
124
+ {
125
+ "type": "string"
126
+ },
127
+ {
128
+ "type": "object",
129
+ "additionalProperties": false,
130
+ "properties": {
131
+ "label": {
132
+ "type": "string"
133
+ },
134
+ "icon": {
135
+ "type": "string"
136
+ },
137
+ "color": {
138
+ "type": "string"
139
+ }
140
+ }
141
+ }
142
+ ]
143
+ },
144
+ "options": {
145
+ "type": "object",
146
+ "description": "Option value (the stored code) → its translation.",
147
+ "additionalProperties": {
148
+ "$ref": "#/$defs/option"
149
+ }
150
+ },
151
+ "withOptions": {
152
+ "type": "object",
153
+ "additionalProperties": false,
154
+ "properties": {
155
+ "label": {
156
+ "$ref": "#/$defs/label"
157
+ },
158
+ "options": {
159
+ "$ref": "#/$defs/options"
160
+ }
161
+ }
162
+ },
163
+ "entity": {
164
+ "type": "object",
165
+ "additionalProperties": false,
166
+ "properties": {
167
+ "label": {
168
+ "$ref": "#/$defs/label"
169
+ },
170
+ "desc": {
171
+ "type": "string"
172
+ },
173
+ "fields": {
174
+ "type": "object",
175
+ "description": "Column code → label and, for dropdown / radio / flags columns, option labels.",
176
+ "additionalProperties": {
177
+ "$ref": "#/$defs/withOptions"
178
+ }
179
+ },
180
+ "columnGroups": {
181
+ "type": "object",
182
+ "description": "Card-layout section code → heading.",
183
+ "additionalProperties": {
184
+ "$ref": "#/$defs/labelOnly"
185
+ }
186
+ },
187
+ "constraints": {
188
+ "type": "object",
189
+ "description": "Check / unique constraint name → violation message.",
190
+ "additionalProperties": {
191
+ "type": "object",
192
+ "additionalProperties": false,
193
+ "properties": {
194
+ "message": {
195
+ "type": "string"
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ },
202
+ "menu": {
203
+ "type": "object",
204
+ "additionalProperties": false,
205
+ "properties": {
206
+ "label": {
207
+ "$ref": "#/$defs/label"
208
+ },
209
+ "items": {
210
+ "type": "object",
211
+ "description": "Menu item code → label. Flat even when ui/menus.json nests items under children: a group and its children are siblings here.",
212
+ "additionalProperties": {
213
+ "$ref": "#/$defs/labelOnly"
214
+ }
215
+ }
216
+ }
217
+ },
218
+ "params": {
219
+ "type": "object",
220
+ "description": "Parameter code → label and dropdown option labels.",
221
+ "additionalProperties": {
222
+ "$ref": "#/$defs/withOptions"
223
+ }
224
+ },
225
+ "report": {
226
+ "type": "object",
227
+ "additionalProperties": false,
228
+ "properties": {
229
+ "label": {
230
+ "$ref": "#/$defs/label"
231
+ },
232
+ "datasets": {
233
+ "type": "object",
234
+ "description": "Dataset code → caption.",
235
+ "additionalProperties": {
236
+ "type": "object",
237
+ "additionalProperties": false,
238
+ "properties": {
239
+ "caption": {
240
+ "type": "string"
241
+ }
242
+ }
243
+ }
244
+ },
245
+ "params": {
246
+ "$ref": "#/$defs/params"
247
+ }
248
+ }
249
+ },
250
+ "action": {
251
+ "type": "object",
252
+ "additionalProperties": false,
253
+ "properties": {
254
+ "label": {
255
+ "$ref": "#/$defs/label"
256
+ },
257
+ "desc": {
258
+ "type": "string",
259
+ "description": "Translated tooltip."
260
+ },
261
+ "params": {
262
+ "$ref": "#/$defs/params"
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
@@ -60,8 +60,7 @@ correct: a data view describes the presentation, and the folder's entity view de
60
60
  what a user in that folder is allowed to see of it. The grid renders whichever
61
61
  columns survive the view — you do not maintain one data view per role.
62
62
 
63
- Entity views are also unrelated to the two other things called "view" here: a *data
64
- view* (`ui/data_views.json`) is a grid/kanban/calendar, and `isView` + `viewSql` on an
65
- entity means it is backed by a SQL view.
63
+ Entity views are also unrelated to the other thing called "view" here: a *data view*
64
+ (`ui/data_views.json`) is a grid/kanban/calendar.
66
65
 
67
66
  See `dforge://reference/security` → Column-level security for the full rules.
@@ -302,12 +302,19 @@ var queued = select('comm.message', {
302
302
 
303
303
  | Function | Description |
304
304
  |---|---|
305
- | `error('message')` | **Abort** the action and show an error to the user. Rolls back all changes. |
306
- | `warn('message')` | Show a warning but continue execution. |
307
- | `info('message')` | Show an informational message. A **receipt for work the action did** — not a way to publish a computed value; see *When an action is the wrong tool*. |
308
- | `exit('message'?, 'level'?)` | **Stop the script early without error** — changes made so far are kept. Level `'info'` (default) or `'warn'` styles the message. This is the DSL's early-return: a bare `return` is not valid at the top level of `execute:`. |
305
+ | `error('message', values?)` | **Abort** the action and show an error to the user. Rolls back all changes. |
306
+ | `warn('message', values?, opts?)` | Show a warning but continue execution. |
307
+ | `info('message', values?, opts?)` | Show an informational message. A **receipt for work the action did** — not a way to publish a computed value; see *When an action is the wrong tool*. |
308
+ | `exit('message'?, 'level'?, values?)` | **Stop the script early without error** — changes made so far are kept. Level `'info'` (default), `'warning'`, `'danger'` or `'success'` styles the message. This is the DSL's early-return: a bare `return` is not valid at the top level of `execute:`. |
309
309
  | `notify(userId, 'message')` | Send an in-app notification to a specific user. First arg is a user ID (typically `[owner_id]` or similar). |
310
310
 
311
+ The message of `error` / `warn` / `info` / `exit` is translatable: it is looked up by its exact English text in the module's `messages` translations (see `dforge://reference/translations`). Put values in `{name}` placeholders filled from `values` — never join them into the string, which leaves no fixed text to translate. `opts` carries record links (`{ links: [...] }`), so `links` is not a placeholder name:
312
+
313
+ ```javascript
314
+ error('Insufficient stock. Available: {available}, requested: {requested}',
315
+ { available: availableQty, requested: params[quantity] })
316
+ ```
317
+
311
318
  ### Email
312
319
 
313
320
  | Function | Description |
@@ -4,7 +4,7 @@ Data views define how users see and interact with entity data. One entity can ha
4
4
 
5
5
  Lives in: `ui/data_views.json`
6
6
 
7
- > **Not to be confused with entity views.** An *entity view* (`views` on an entity file) is column-level **security** — which columns a folder may see at all — and is what `folders.json`'s `viewName` binds. A *data view* is a presentation of an entity, referenced by a menu's `dataViewCode`. A third thing, `isView` + `viewSql` on an entity, means the entity is backed by a SQL view. See `security.md` → Column-level security.
7
+ > **Not to be confused with entity views.** An *entity view* (`views` on an entity file) is column-level **security** — which columns a folder may see at all — and is what `folders.json`'s `viewName` binds. A *data view* is a presentation of an entity, referenced by a menu's `dataViewCode`. See `security.md` → Column-level security.
8
8
 
9
9
  ## Structure
10
10
 
@@ -318,16 +318,17 @@ Each of these is an install-time error, and each is silent at runtime if it slip
318
318
 
319
319
  `"viewName": "default"` means **no view** (full column set) and declares nothing. It is what every shipped module writes, and what the runtime auto-creates for an entity reached without a folder binding — so it is exempt from the checks above. A module that really declares a view named `default` still gets it bound.
320
320
 
321
- ### Don't confuse the three "views"
321
+ ### Don't confuse the two "views"
322
322
 
323
323
  | Term | Where | What it is |
324
324
  |---|---|---|
325
325
  | **Entity view** | `entities/*.json` → `views` | Column-level security (this section) |
326
326
  | **Data view** | `ui/data_views.json` | A grid / kanban / calendar over an entity (`data-views.md`) |
327
- | **SQL view** | `entities/*.json` → `isView` + `viewSql` | An entity backed by a SQL view instead of a table |
328
327
 
329
328
  `viewName` in `folders.json` binds the first. Menus' `dataViewCode` references the second.
330
329
 
330
+ There is no SQL-view entity. `isView` / `viewSql` are rejected at validate, pack and install: a view entity would duplicate the source entity's metadata, break when that module changes a column, and read around its row and column security. For a read-only slice use a data view (nav paths, `F` columns), a `G` total, the Query Builder, or a stored-procedure report — the last is the one way to show users a slice of an entity they have no rights on, behind an explicit `sp:` grant.
331
+
331
332
  ### When to use subfolders
332
333
 
333
334
  - **Multi-location modules** (warehouses, offices, branches) — one subfolder per location, filtered by location FK
@@ -38,7 +38,7 @@ Translation files mirror the module's content structure. Each translatable objec
38
38
  "crm.sales-rep": { "label": "Sales Representative" }
39
39
  },
40
40
  "views": {
41
- "contacts": { "label": "Contacts" },
41
+ "contacts": { "label": "Contacts", "desc": "People associated with customers" },
42
42
  "contacts_list": { "label": "Contact List" },
43
43
  "contacts_gallery": { "label": "Contact Cards" }
44
44
  },
@@ -80,11 +80,12 @@ Translation files mirror the module's content structure. Each translatable objec
80
80
  | `entities` | Entity labels (`label`, `desc`) and all field labels | Entity codes → `{ label, desc, fields: { field_code: { label } } }` |
81
81
  | `folders` | Folder labels | Folder codes (root folder key from `ui/folders.json`) |
82
82
  | `roles` | Role display labels | Role codes (keys from `security/roles.json`, already module-qualified — e.g. `crm.admin`) → `{ label }` |
83
- | `views` | Data view labels | View codes (keys from `ui/data_views.json`) |
83
+ | `views` | Data view labels and descriptions (`label`, `desc`) | View codes (keys from `ui/data_views.json`) → `{ label, desc }` |
84
84
  | `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
85
85
  | `actions` | Action labels, descriptions, param labels **and param dropdown option labels** | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label, options: { value: … } } } }` |
86
86
  | `reports` | Report dataset captions, param labels **and param dropdown option labels** | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label, options: { value: … } } } }` |
87
87
  | `entities.<e>.constraints` | Check/unique constraint violation messages (opt-in — warned, not enforced) | Constraint codes → `{ message }` under the owning entity |
88
+ | `messages` | DSL `info` / `warn` / `error` / `exit` texts (opt-in — warned, not enforced) | Exact English text → translated text |
88
89
  | `entities.<e>.fields.<f>.options` | Dropdown/radio/flags **option labels** (opt-in) | Option `value` → localized label (string) or `{ label, icon, color }` |
89
90
  | `domains` | Column domain labels **and** their shared option labels (opt-in) | Domain codes → `{ label, options: { value: … } }` |
90
91
 
@@ -174,6 +175,22 @@ A `dropdown` **parameter** carries per-value labels exactly like a column, and t
174
175
  - **This requires base labels in the DSL.** `options=bank_transfer,cash` stores bare codes, and the dialog shows `bank_transfer` in *every* locale including English until you write `options=[bank_transfer:Bank transfer, cash:Cash]` (or the JSON object form). See `action-dsl.md`.
175
176
  - **Prefer a domain.** If the param's choices are the same list a column uses — which they usually are, since the value normally ends up written to that column — declare the param as `domain <domainCd>` and skip this section entirely: it inherits the domain's translated options. Reach for per-param `options` only when the list belongs to that one parameter and matches no column.
176
177
 
178
+ ### Action messages ARE translatable (opt-in)
179
+
180
+ The text an action passes to `info()`, `warn()`, `error()` or `exit()` is the base text **and the key**. Translate it under `messages` in each non-English file, keyed by the exact English text; `{name}` placeholders are filled from the call's second argument (`exit()`: third, after the level) and may be reordered:
181
+
182
+ ```json
183
+ "messages": {
184
+ "Item processed successfully": "Товар успішно оброблено",
185
+ "Recount completed: {count} line(s) checked": "Перерахунок завершено: перевірено рядків — {count}"
186
+ }
187
+ ```
188
+
189
+ - Lookup is in the **action's own module** (a bridge module's action uses the bridge's `messages`), in the user's language; a queued action uses the language of the user who queued it, a scheduled job the workspace default. No entry → the English text. Needs platform 1.26.0.
190
+ - A message joined from text and values (`'Bill ' + [bill_number]`) cannot be translated — rewrite it with placeholders.
191
+ - A link label must be data (a record number), not a word: links are placed after translation.
192
+ - **Opt-in, never mandatory.** With `supportedLocales`, install and `dforge_module_validate` warn about untranslated and joined messages; both always warn about `messages` entries no script uses (an English wording change leaves one behind) and about placeholders with no value. `dforge_translation_sync` adds every fixed message text to the non-English files.
193
+
177
194
  ### Roles ARE translated — and completeness is enforced
178
195
 
179
196
  Role labels are **required and localized** (this changed — older docs said roles were skipped; they are not). `SecurityRegistrar` gives every role a `res_id`, so `TranslationRegistrar` wires the role label exactly like entities/views/menus:
@@ -225,7 +242,7 @@ If you include these sections, the install succeeds (they're silently ignored)
225
242
  "crm.sales-rep": { "label": "Vertriebsmitarbeiter" }
226
243
  },
227
244
  "views": {
228
- "contacts": { "label": "Kontakte" }
245
+ "contacts": { "label": "Kontakte", "desc": "Personen, die mit Kunden verknüpft sind" }
229
246
  },
230
247
  "menus": {
231
248
  "crm_menu": {