@dforge-core/metadata 0.0.31 → 0.0.32

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.
@@ -42,6 +42,11 @@
42
42
  "type": "boolean",
43
43
  "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
44
  },
45
+ "auditHistory": {
46
+ "type": "string",
47
+ "enum": ["basic", "fields", "full"],
48
+ "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."
49
+ },
45
50
  "traits": {
46
51
  "type": "array",
47
52
  "description": "Entity traits expanded during module install (authoring shortcut). Trait definitions loaded from DB (seeded by metadata module).",
@@ -364,6 +369,14 @@
364
369
  "serverDefault": {
365
370
  "$ref": "#/$defs/serverDefault"
366
371
  },
372
+ "currency": {
373
+ "type": "string",
374
+ "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."
375
+ },
376
+ "currencyColumn": {
377
+ "type": "string",
378
+ "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."
379
+ },
367
380
  "pattern": {
368
381
  "type": "string",
369
382
  "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 +401,7 @@
388
401
  "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
402
  },
390
403
  "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."
404
+ "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
405
  }
393
406
  },
394
407
  "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.",
36
+ "additionalProperties": {
37
+ "$ref": "#/$defs/labelOnly"
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
+ }
@@ -74,22 +74,27 @@ export const BUILTINS: Builtin[] = [
74
74
  // Messaging
75
75
  {
76
76
  name: "error",
77
- signature: "error(message)",
77
+ signature: "error(message, values?)",
78
78
  returns: "never",
79
- doc: "Abort the action and show an error. Rolls back all changes.",
79
+ doc: "Abort the action and show an error. Rolls back all changes. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`.",
80
+ },
81
+ {
82
+ name: "warn",
83
+ signature: "warn(message, values?, opts?)",
84
+ returns: "void",
85
+ doc: "Show a warning and continue. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`.",
80
86
  },
81
- { name: "warn", signature: "warn(message)", returns: "void", doc: "Show a warning and continue." },
82
87
  {
83
88
  name: "info",
84
- signature: "info(message)",
89
+ signature: "info(message, values?, opts?)",
85
90
  returns: "void",
86
- doc: "Show an informational message — a receipt for work the action did, not a way to publish a computed value.",
91
+ doc: "Show an informational message — a receipt for work the action did, not a way to publish a computed value. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`. `opts.links` adds record links.",
87
92
  },
88
93
  {
89
94
  name: "exit",
90
- signature: "exit(message?, level?)",
95
+ signature: "exit(message?, level?, values?)",
91
96
  returns: "never",
92
- doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default) or `'warn'`. This is the DSL's early return — the block compiles to a bare script, so a `return` outside a function is a syntax error wherever it sits.",
97
+ doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default), `'warning'`, `'danger'` or `'success'`. The message is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`. This is the DSL's early return — the block compiles to a bare script, so a `return` outside a function is a syntax error wherever it sits.",
93
98
  },
94
99
  {
95
100
  name: "notify",
package/src/dsl/check.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  // omits, and the rules that depend on it stand down. See ./types.
12
12
 
13
13
  import { BUILTIN_BY_NAME } from "./builtins";
14
+ import { extractMessages } from "./messages";
14
15
  import { CONTROL_KEYWORDS, type Token } from "./lexer";
15
16
  import {
16
17
  type BlockKind,
@@ -25,7 +26,7 @@ import type { ColumnLookup, DslContext, DslIssue, DslSeverity } from "./types";
25
26
  * Check a DSL body. Returns [] for a clean script.
26
27
  *
27
28
  * Issues come back in source-feature order (field reads, params, execution
28
- * mode, block spelling, built-ins, top-level returns, inline assignments)
29
+ * mode, block spelling, built-ins, top-level returns, inline assignments, messages)
29
30
  * rather than sorted by position — a host that wants them in file order can
30
31
  * sort on `start`.
31
32
  */
@@ -573,9 +574,49 @@ export function checkDsl(text: string, ctx: DslContext = {}): DslIssue[] {
573
574
  }
574
575
 
575
576
  out.push(...inlineAssignmentIssues(parsed, positionAt));
577
+
578
+ // ── messages ─────────────────────────────────────────────────────
579
+ // Install reports the same, module-wide (ActionMessageScanner).
580
+ const locales = Object.entries(ctx.messageTranslations ?? {});
581
+ for (const m of extractMessages(parsed)) {
582
+ if (m.kind === "concatenated" && locales.length > 0) {
583
+ add(
584
+ m.span,
585
+ `This ${m.builtin}() message joins text and values, so it cannot be translated. Put the values in {placeholders}: ${m.builtin}('Only {n} left', ${m.builtin === "exit" ? "'warning', " : ""}{ n: qty }).`,
586
+ "warning",
587
+ "dsl/message-concatenated",
588
+ );
589
+ }
590
+ if (m.text === undefined) continue;
591
+
592
+ // A call without values keeps a literal "{x}" as text, as before placeholders existed.
593
+ if (m.valueNames && m.valueNames.length > 0) {
594
+ for (const name of placeholders(m.text)) {
595
+ if (!m.valueNames.includes(name))
596
+ add(m.span, `{${name}} has no value in this call, so it shows as written.`, "warning", "dsl/message-placeholder");
597
+ }
598
+ }
599
+
600
+ // A blank translation is dropped at install, so the message still shows in English.
601
+ const missing = locales
602
+ .filter(([, messages]) => !(Object.hasOwn(messages, m.text!) && messages[m.text!]?.trim()))
603
+ .map(([locale]) => locale);
604
+ if (missing.length > 0) {
605
+ add(
606
+ m.span,
607
+ `No ${missing.join(", ")} translation for this message — add it to "messages" in translations/<locale>.json, keyed by the exact text.`,
608
+ "warning",
609
+ "dsl/message-untranslated",
610
+ );
611
+ }
612
+ }
576
613
  return out;
577
614
  }
578
615
 
616
+ function placeholders(text: string): string[] {
617
+ return [...new Set([...text.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((x) => x[1]!))];
618
+ }
619
+
579
620
  /**
580
621
  * Column matching is case-insensitive at the other end — `EntityColumnLookup`
581
622
  * hands the compiler an OrdinalIgnoreCase set — but a host passes us whatever
package/src/dsl/index.ts CHANGED
@@ -27,6 +27,9 @@ export type {
27
27
  export { BUILTINS, BUILTIN_BY_NAME, BUILTIN_VALUES } from "./builtins";
28
28
  export type { Builtin } from "./builtins";
29
29
 
30
+ export { extractMessages } from "./messages";
31
+ export type { DslMessage, DslMessageKind } from "./messages";
32
+
30
33
  export { checkDsl } from "./check";
31
34
  export type {
32
35
  ColumnLookup,
package/src/dsl/lexer.ts CHANGED
@@ -366,11 +366,12 @@ export function stringValue(token: Token): string {
366
366
  /**
367
367
  * The literal text of one template chunk, delimiters removed. A chunk opens on
368
368
  * a backtick or on the `}` that closed the hole before it, and closes on a
369
- * backtick, on the `${` of the next hole, or on the end of the file.
369
+ * backtick, on the `${` of the next hole, or on the end of the file. A line
370
+ * break in it reads as \n, as JavaScript reads a template written with CRLF.
370
371
  */
371
372
  export function templateChunkValue(token: Token): string {
372
373
  if (token.kind !== "template") return token.text;
373
- const body = token.text.slice(1);
374
+ const body = token.text.slice(1).replace(/\r\n?/g, "\n");
374
375
  if (body.endsWith("${")) return unescape(body.slice(0, -2));
375
376
  if (body.endsWith("`")) return unescape(body.slice(0, -1));
376
377
  return unescape(body);
@@ -379,16 +380,40 @@ export function templateChunkValue(token: Token): string {
379
380
  // A switch, not a lookup table: the escaped character comes out of the script,
380
381
  // and an object would answer `\c` from Object.prototype.
381
382
  function unescape(body: string): string {
382
- return body.replace(/\\(.)/g, (_, ch: string) => {
383
- switch (ch) {
384
- case "n":
385
- return "\n";
386
- case "t":
387
- return "\t";
388
- case "r":
389
- return "\r";
390
- default:
391
- return ch;
392
- }
393
- });
383
+ return body.replace(
384
+ /\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|\r\n|[\s\S])/g,
385
+ (_, esc: string) => {
386
+ switch (esc[0]) {
387
+ case "n":
388
+ return "\n";
389
+ case "t":
390
+ return "\t";
391
+ case "r":
392
+ return "\r";
393
+ case "b":
394
+ return "\b";
395
+ case "f":
396
+ return "\f";
397
+ case "v":
398
+ return "\v";
399
+ case "0":
400
+ return "\0";
401
+ case "\r":
402
+ case "\n":
403
+ case "\u2028":
404
+ case "\u2029":
405
+ return "";
406
+ case "u":
407
+ case "x":
408
+ if (esc.length > 1) {
409
+ const hex = esc[1] === "{" ? esc.slice(2, -1) : esc.slice(1);
410
+ const cp = parseInt(hex, 16);
411
+ return cp <= 0x10ffff ? String.fromCodePoint(cp) : esc;
412
+ }
413
+ return esc;
414
+ default:
415
+ return esc;
416
+ }
417
+ },
418
+ );
394
419
  }