@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.
- package/CHANGELOG.md +51 -0
- package/dist/dsl/index.d.ts +25 -3
- package/dist/dsl/index.js +249 -23
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +84 -3
- package/dist/index.js.map +1 -1
- package/package.json +67 -66
- package/schemas/entity.schema.json +14 -1
- package/schemas/manifest.schema.json +2 -2
- package/schemas/traits.schema.json +5 -0
- package/schemas/translations.schema.json +267 -0
- package/src/dsl/builtins.ts +12 -7
- package/src/dsl/check.ts +42 -1
- package/src/dsl/index.ts +3 -0
- package/src/dsl/lexer.ts +39 -14
- package/src/dsl/messages.ts +196 -0
- package/src/dsl/types.ts +7 -0
- package/src/entity.ts +5 -0
- package/src/index.ts +11 -1
- package/src/manifest.ts +4 -2
- package/src/translations.ts +61 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ All notable changes to `@dforge-core/metadata` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.0.32] — 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`translations.schema.json`** for `translations/<locale>.json` (#1299): every section
|
|
15
|
+
the installer reads (`entities` with `fields` / `columnGroups` / `constraints` and option
|
|
16
|
+
translations, `domains`, `folders`, `views`, `menus`, `settings`, `roles`, `reports`,
|
|
17
|
+
`actions`, `print_templates`) plus the new `messages` block. Strict at every level, so
|
|
18
|
+
the mistakes install ignores silently are errors: a misspelled or singular section
|
|
19
|
+
(`entity`, `menu`), nested menu `items`, `description` where `desc` is meant. Type:
|
|
20
|
+
`TranslationsFile`.
|
|
21
|
+
- **`extractMessages(doc)`** (`@dforge-core/metadata/dsl`): the `info` / `warn` / `error` /
|
|
22
|
+
`exit` calls of a script, classified `literal` / `concatenated` / `dynamic`, with their
|
|
23
|
+
placeholder-value names — the list the server's `ActionDslCompiler` extracts. A shared
|
|
24
|
+
fixture (`test/fixtures/dsl-messages.json`) runs in both test suites.
|
|
25
|
+
- **Message rules in `checkDsl`:** `dsl/message-placeholder` (a `{name}` the call gives no
|
|
26
|
+
value) always; `dsl/message-concatenated` and `dsl/message-untranslated` when the host
|
|
27
|
+
passes the new `DslContext.messageTranslations`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **String escapes are read as JavaScript reads them.** The lexer unescaped only `\n`, `\t`
|
|
32
|
+
and `\r`, so `'\u00e9'` came out as `u00e9`. It now handles `\uXXXX`, `\u{…}`, `\xHH`,
|
|
33
|
+
`\b`, `\f`, `\v`, `\0` and line continuations.
|
|
34
|
+
|
|
35
|
+
- **`auditHistory` levels are `basic` / `fields` / `full`.** `manifest.schema.json` and
|
|
36
|
+
`ManifestDef` listed `none` / `minimal` / `full`, which the platform never accepted: the
|
|
37
|
+
install failed on the entity table's check constraint. `basic` logs each insert, update
|
|
38
|
+
and delete; `fields` adds the changed values; `full` also stores display names. Omit the
|
|
39
|
+
property to turn audit off. The levels are one type, `AuditHistoryLevel`.
|
|
40
|
+
- **`entity.schema.json` accepts `auditHistory`.** The per-entity override of the
|
|
41
|
+
manifest default was missing from the schema, so editors flagged it as an unknown
|
|
42
|
+
property. `EntityDef.auditHistory` is added to match.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **DSL message built-ins take placeholder values** (`DSL_BUILTINS`, #1299):
|
|
47
|
+
`info(message, values?, opts?)`, `warn(message, values?, opts?)`, `error(message, values?)`,
|
|
48
|
+
`exit(message?, level?, values?)`. The message is looked up in the module's `messages`
|
|
49
|
+
translations and `{name}` placeholders are filled from `values`; record links move to
|
|
50
|
+
`opts`. `exit()` levels are documented as `info` / `warning` / `danger` / `success` — the
|
|
51
|
+
doc said `warn`, which the client does not recognise.
|
|
52
|
+
- **`entity.schema.json` documents `params.currency` and `params.currencyColumn`.** A
|
|
53
|
+
`currency` field can name a column of the same entity that holds the ISO code per
|
|
54
|
+
record, instead of one fixed code (#1220).
|
|
55
|
+
- **`entity.schema.json` documents nav placeholders in `refFilter`.** A condition value
|
|
56
|
+
`@[ref].[field]` reads a field through a Reference column of the current record, e.g.
|
|
57
|
+
`@[purchase_order].[supplier_id]` on an order line (#1279).
|
|
58
|
+
|
|
8
59
|
## [0.0.31] — 2026-09-24
|
|
9
60
|
|
|
10
61
|
### Changed
|
package/dist/dsl/index.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ declare function stringValue(token: Token): string;
|
|
|
18
18
|
/**
|
|
19
19
|
* The literal text of one template chunk, delimiters removed. A chunk opens on
|
|
20
20
|
* a backtick or on the `}` that closed the hole before it, and closes on a
|
|
21
|
-
* backtick, on the `${` of the next hole, or on the end of the file.
|
|
21
|
+
* backtick, on the `${` of the next hole, or on the end of the file. A line
|
|
22
|
+
* break in it reads as \n, as JavaScript reads a template written with CRLF.
|
|
22
23
|
*/
|
|
23
24
|
declare function templateChunkValue(token: Token): string;
|
|
24
25
|
|
|
@@ -152,6 +153,20 @@ declare const BUILTIN_BY_NAME: Map<string, Builtin>;
|
|
|
152
153
|
/** Bare identifiers the runtime injects — no parentheses. */
|
|
153
154
|
declare const BUILTIN_VALUES: Record<string, string>;
|
|
154
155
|
|
|
156
|
+
type DslMessageKind = "literal" | "concatenated" | "dynamic";
|
|
157
|
+
interface DslMessage {
|
|
158
|
+
builtin: "info" | "warn" | "error" | "exit";
|
|
159
|
+
kind: DslMessageKind;
|
|
160
|
+
/** The message text for `literal`, exactly as the runtime receives it; otherwise undefined. */
|
|
161
|
+
text?: string;
|
|
162
|
+
/** Keys of the placeholder-values object; empty without one, null when it is not an object literal. */
|
|
163
|
+
valueNames: string[] | null;
|
|
164
|
+
/** The first argument. */
|
|
165
|
+
span: Span;
|
|
166
|
+
}
|
|
167
|
+
/** Message calls in `execute:` and `onBeforeStart:`, in source order. */
|
|
168
|
+
declare function extractMessages(doc: DslDocument): DslMessage[];
|
|
169
|
+
|
|
155
170
|
type DslSeverity = "error" | "warning" | "info";
|
|
156
171
|
/** One reported problem. `rule` is stable — hosts filter and suppress on it. */
|
|
157
172
|
interface DslIssue {
|
|
@@ -236,16 +251,23 @@ interface DslContext {
|
|
|
236
251
|
* none: pass `null`, not the fragment. See `EntityShape.columns`.
|
|
237
252
|
*/
|
|
238
253
|
currentEntity?: EntityShape | null;
|
|
254
|
+
/**
|
|
255
|
+
* Each non-English locale the manifest lists in `supportedLocales` → the
|
|
256
|
+
* `messages` block of its `translations/<locale>.json` (`{}` when the file has
|
|
257
|
+
* none). Absent, the untranslated and concatenated message rules stand down,
|
|
258
|
+
* exactly as install only reports them for a module that declares locales.
|
|
259
|
+
*/
|
|
260
|
+
messageTranslations?: Record<string, Record<string, string>>;
|
|
239
261
|
}
|
|
240
262
|
|
|
241
263
|
/**
|
|
242
264
|
* Check a DSL body. Returns [] for a clean script.
|
|
243
265
|
*
|
|
244
266
|
* Issues come back in source-feature order (field reads, params, execution
|
|
245
|
-
* mode, block spelling, built-ins, top-level returns, inline assignments)
|
|
267
|
+
* mode, block spelling, built-ins, top-level returns, inline assignments, messages)
|
|
246
268
|
* rather than sorted by position — a host that wants them in file order can
|
|
247
269
|
* sort on `start`.
|
|
248
270
|
*/
|
|
249
271
|
declare function checkDsl(text: string, ctx?: DslContext): DslIssue[];
|
|
250
272
|
|
|
251
|
-
export { BLOCK_KINDS, BUILTINS, BUILTIN_BY_NAME, BUILTIN_VALUES, type BlockKind, type Builtin, type CallRef, type ColumnLookup, type DslBlock, type DslContext, type DslDocument, type DslIssue, type DslParam, type DslSeverity, type EntityShape, type FieldRef, type GlobalRef, type Span, type StringArg, type Token, type TokenKind, checkDsl, parseDsl, stringValue, templateChunkValue, tokenize };
|
|
273
|
+
export { BLOCK_KINDS, BUILTINS, BUILTIN_BY_NAME, BUILTIN_VALUES, type BlockKind, type Builtin, type CallRef, type ColumnLookup, type DslBlock, type DslContext, type DslDocument, type DslIssue, type DslMessage, type DslMessageKind, type DslParam, type DslSeverity, type EntityShape, type FieldRef, type GlobalRef, type Span, type StringArg, type Token, type TokenKind, checkDsl, extractMessages, parseDsl, stringValue, templateChunkValue, tokenize };
|
package/dist/dsl/index.js
CHANGED
|
@@ -273,24 +273,48 @@ function stringValue(token) {
|
|
|
273
273
|
}
|
|
274
274
|
function templateChunkValue(token) {
|
|
275
275
|
if (token.kind !== "template") return token.text;
|
|
276
|
-
const body = token.text.slice(1);
|
|
276
|
+
const body = token.text.slice(1).replace(/\r\n?/g, "\n");
|
|
277
277
|
if (body.endsWith("${")) return unescape(body.slice(0, -2));
|
|
278
278
|
if (body.endsWith("`")) return unescape(body.slice(0, -1));
|
|
279
279
|
return unescape(body);
|
|
280
280
|
}
|
|
281
281
|
function unescape(body) {
|
|
282
|
-
return body.replace(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
282
|
+
return body.replace(
|
|
283
|
+
/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|\r\n|[\s\S])/g,
|
|
284
|
+
(_, esc) => {
|
|
285
|
+
switch (esc[0]) {
|
|
286
|
+
case "n":
|
|
287
|
+
return "\n";
|
|
288
|
+
case "t":
|
|
289
|
+
return " ";
|
|
290
|
+
case "r":
|
|
291
|
+
return "\r";
|
|
292
|
+
case "b":
|
|
293
|
+
return "\b";
|
|
294
|
+
case "f":
|
|
295
|
+
return "\f";
|
|
296
|
+
case "v":
|
|
297
|
+
return "\v";
|
|
298
|
+
case "0":
|
|
299
|
+
return "\0";
|
|
300
|
+
case "\r":
|
|
301
|
+
case "\n":
|
|
302
|
+
case "\u2028":
|
|
303
|
+
case "\u2029":
|
|
304
|
+
return "";
|
|
305
|
+
case "u":
|
|
306
|
+
case "x":
|
|
307
|
+
if (esc.length > 1) {
|
|
308
|
+
const hex = esc[1] === "{" ? esc.slice(2, -1) : esc.slice(1);
|
|
309
|
+
const cp = parseInt(hex, 16);
|
|
310
|
+
return cp <= 1114111 ? String.fromCodePoint(cp) : esc;
|
|
311
|
+
}
|
|
312
|
+
return esc;
|
|
313
|
+
default:
|
|
314
|
+
return esc;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
);
|
|
294
318
|
}
|
|
295
319
|
|
|
296
320
|
// src/dsl/parse.ts
|
|
@@ -435,10 +459,10 @@ function templateArg(tokens, start) {
|
|
|
435
459
|
for (let j = start; j < tokens.length; j++) {
|
|
436
460
|
const t = tokens[j];
|
|
437
461
|
if (t.kind !== "template") continue;
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
462
|
+
const opens2 = t.text.startsWith("`");
|
|
463
|
+
if (opens2) depth++;
|
|
440
464
|
if (depth === 1) {
|
|
441
|
-
if (!
|
|
465
|
+
if (!opens2) parts.push(" ");
|
|
442
466
|
parts.push(templateChunkValue(t));
|
|
443
467
|
last = t;
|
|
444
468
|
endIndex = j;
|
|
@@ -761,22 +785,27 @@ var BUILTINS = [
|
|
|
761
785
|
// Messaging
|
|
762
786
|
{
|
|
763
787
|
name: "error",
|
|
764
|
-
signature: "error(message)",
|
|
788
|
+
signature: "error(message, values?)",
|
|
765
789
|
returns: "never",
|
|
766
|
-
doc: "Abort the action and show an error. Rolls back all changes."
|
|
790
|
+
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`."
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: "warn",
|
|
794
|
+
signature: "warn(message, values?, opts?)",
|
|
795
|
+
returns: "void",
|
|
796
|
+
doc: "Show a warning and continue. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`."
|
|
767
797
|
},
|
|
768
|
-
{ name: "warn", signature: "warn(message)", returns: "void", doc: "Show a warning and continue." },
|
|
769
798
|
{
|
|
770
799
|
name: "info",
|
|
771
|
-
signature: "info(message)",
|
|
800
|
+
signature: "info(message, values?, opts?)",
|
|
772
801
|
returns: "void",
|
|
773
|
-
doc: "Show an informational message \u2014 a receipt for work the action did, not a way to publish a computed value."
|
|
802
|
+
doc: "Show an informational message \u2014 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."
|
|
774
803
|
},
|
|
775
804
|
{
|
|
776
805
|
name: "exit",
|
|
777
|
-
signature: "exit(message?, level?)",
|
|
806
|
+
signature: "exit(message?, level?, values?)",
|
|
778
807
|
returns: "never",
|
|
779
|
-
doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default) or `'
|
|
808
|
+
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 \u2014 the block compiles to a bare script, so a `return` outside a function is a syntax error wherever it sits."
|
|
780
809
|
},
|
|
781
810
|
{
|
|
782
811
|
name: "notify",
|
|
@@ -890,6 +919,172 @@ var BUILTIN_VALUES = {
|
|
|
890
919
|
old: "Pre-change values, in a trigger-invoked action \u2014 read with `old[field]`. Read-only."
|
|
891
920
|
};
|
|
892
921
|
|
|
922
|
+
// src/dsl/messages.ts
|
|
923
|
+
var VALUES_INDEX = /* @__PURE__ */ new Map([
|
|
924
|
+
["info", 1],
|
|
925
|
+
["warn", 1],
|
|
926
|
+
["error", 1],
|
|
927
|
+
["exit", 2]
|
|
928
|
+
]);
|
|
929
|
+
function extractMessages(doc) {
|
|
930
|
+
const out = [];
|
|
931
|
+
const { tokens } = doc;
|
|
932
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
933
|
+
const t = tokens[i];
|
|
934
|
+
const valuesIndex = VALUES_INDEX.get(t.text);
|
|
935
|
+
if (t.kind !== "ident" || valuesIndex === void 0 || tokens[i + 1].text !== "(") continue;
|
|
936
|
+
if (tokens[i - 1]?.text === ".") continue;
|
|
937
|
+
const block = doc.blocks.find((b) => t.start >= b.body.start && t.start < b.body.end)?.kind;
|
|
938
|
+
if (block !== "execute" && block !== "onBeforeStart") continue;
|
|
939
|
+
const args = splitArgs(tokens, i + 1);
|
|
940
|
+
if (!args[0]?.length) continue;
|
|
941
|
+
const [kind, text] = classify(args[0]);
|
|
942
|
+
const valuesArg = args[valuesIndex];
|
|
943
|
+
out.push({
|
|
944
|
+
builtin: t.text,
|
|
945
|
+
kind,
|
|
946
|
+
text,
|
|
947
|
+
valueNames: valuesArg ? valueNames(valuesArg) : [],
|
|
948
|
+
span: { start: args[0][0].start, end: args[0][args[0].length - 1].end }
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
function splitArgs(tokens, open) {
|
|
954
|
+
const args = [];
|
|
955
|
+
let current = [];
|
|
956
|
+
let depth = 0;
|
|
957
|
+
for (let j = open + 1; j < tokens.length; j++) {
|
|
958
|
+
const t = tokens[j];
|
|
959
|
+
if (t.kind === "comment") continue;
|
|
960
|
+
if (depth === 0 && t.kind === "punct" && closes(t)) break;
|
|
961
|
+
if (depth === 0 && t.kind === "punct" && t.text === ",") {
|
|
962
|
+
args.push(current);
|
|
963
|
+
current = [];
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
depth += depthDelta(t);
|
|
967
|
+
current.push(t);
|
|
968
|
+
}
|
|
969
|
+
if (current.length > 0) args.push(current);
|
|
970
|
+
return args;
|
|
971
|
+
}
|
|
972
|
+
var isWholeTemplate = (t) => t.kind === "template" && t.text.length > 1 && t.text.startsWith("`") && t.text.endsWith("`");
|
|
973
|
+
function classify(arg) {
|
|
974
|
+
const t = unwrap(arg);
|
|
975
|
+
if (t.length === 1 && t[0].kind === "string") return ["literal", stringValue(t[0])];
|
|
976
|
+
if (t.length === 1 && isWholeTemplate(t[0])) return ["literal", templateChunkValue(t[0])];
|
|
977
|
+
return [joinsText(t) ? "concatenated" : "dynamic"];
|
|
978
|
+
}
|
|
979
|
+
function joinsText(tokens) {
|
|
980
|
+
const t = unwrap(tokens);
|
|
981
|
+
if (t.length === 1 && t[0].kind === "string" || isTemplate(t)) return true;
|
|
982
|
+
let depth = 0;
|
|
983
|
+
let root = -1;
|
|
984
|
+
for (let j = 0; j < t.length; j++) {
|
|
985
|
+
const tok = t[j];
|
|
986
|
+
if (depth === 0) {
|
|
987
|
+
if (isLooserThanPlus(t, j)) return false;
|
|
988
|
+
if (tok.kind === "punct" && (tok.text === "+" || tok.text === "-") && endsOperand(t[j - 1])) root = j;
|
|
989
|
+
}
|
|
990
|
+
depth += depthDelta(tok);
|
|
991
|
+
}
|
|
992
|
+
if (root < 0 || t[root].text === "-") return false;
|
|
993
|
+
return joinsText(t.slice(0, root)) || joinsText(t.slice(root + 1));
|
|
994
|
+
}
|
|
995
|
+
var LOOSER_THAN_PLUS = /* @__PURE__ */ new Set([
|
|
996
|
+
"?",
|
|
997
|
+
":",
|
|
998
|
+
"&&",
|
|
999
|
+
"||",
|
|
1000
|
+
"=",
|
|
1001
|
+
"==",
|
|
1002
|
+
"!=",
|
|
1003
|
+
"===",
|
|
1004
|
+
"!==",
|
|
1005
|
+
"<",
|
|
1006
|
+
">",
|
|
1007
|
+
"<=",
|
|
1008
|
+
">=",
|
|
1009
|
+
"&",
|
|
1010
|
+
"|",
|
|
1011
|
+
"^",
|
|
1012
|
+
",",
|
|
1013
|
+
"=>",
|
|
1014
|
+
"+=",
|
|
1015
|
+
"-=",
|
|
1016
|
+
"*=",
|
|
1017
|
+
"/=",
|
|
1018
|
+
"%="
|
|
1019
|
+
]);
|
|
1020
|
+
function isLooserThanPlus(t, j) {
|
|
1021
|
+
const tok = t[j];
|
|
1022
|
+
if (tok.kind === "ident") return tok.text === "in" || tok.text === "instanceof";
|
|
1023
|
+
if (tok.kind !== "punct" || !LOOSER_THAN_PLUS.has(tok.text)) return false;
|
|
1024
|
+
return !(tok.text === "?" && t[j + 1]?.text === ".");
|
|
1025
|
+
}
|
|
1026
|
+
function endsOperand(prev) {
|
|
1027
|
+
if (!prev) return false;
|
|
1028
|
+
switch (prev.kind) {
|
|
1029
|
+
case "number":
|
|
1030
|
+
case "string":
|
|
1031
|
+
case "regex":
|
|
1032
|
+
return true;
|
|
1033
|
+
case "ident":
|
|
1034
|
+
return !["return", "typeof", "void", "delete", "new", "in", "instanceof", "case"].includes(prev.text);
|
|
1035
|
+
case "template":
|
|
1036
|
+
return prev.text.endsWith("`");
|
|
1037
|
+
case "punct":
|
|
1038
|
+
return [")", "]", "}", "++", "--"].includes(prev.text);
|
|
1039
|
+
default:
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function isTemplate(tokens) {
|
|
1044
|
+
if (tokens[0]?.kind !== "template" || !tokens[0].text.startsWith("`")) return false;
|
|
1045
|
+
let depth = 0;
|
|
1046
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
1047
|
+
depth += depthDelta(tokens[j]);
|
|
1048
|
+
if (depth === 0) return j === tokens.length - 1;
|
|
1049
|
+
}
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
var opens = (t) => t.kind === "punct" ? "([{".includes(t.text) : t.kind === "template" && t.text.endsWith("${");
|
|
1053
|
+
var closes = (t) => t.kind === "punct" ? ")]}".includes(t.text) : t.kind === "template" && t.text.startsWith("}");
|
|
1054
|
+
var depthDelta = (t) => Number(opens(t)) - Number(closes(t));
|
|
1055
|
+
function unwrap(tokens) {
|
|
1056
|
+
while (tokens[0]?.text === "(" && isGroup(tokens)) tokens = tokens.slice(1, -1);
|
|
1057
|
+
return tokens;
|
|
1058
|
+
}
|
|
1059
|
+
function isGroup(tokens) {
|
|
1060
|
+
if (tokens.length < 2 || !opens(tokens[0])) return false;
|
|
1061
|
+
let depth = 0;
|
|
1062
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
1063
|
+
depth += depthDelta(tokens[j]);
|
|
1064
|
+
if (depth === 0) return j === tokens.length - 1;
|
|
1065
|
+
}
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
function valueNames(arg) {
|
|
1069
|
+
const t = unwrap(arg);
|
|
1070
|
+
if (t[0]?.text !== "{" || !isGroup(t)) return null;
|
|
1071
|
+
const names = [];
|
|
1072
|
+
let depth = 0;
|
|
1073
|
+
for (let j = 0; j < t.length; j++) {
|
|
1074
|
+
const tok = t[j];
|
|
1075
|
+
const at = depth;
|
|
1076
|
+
depth += depthDelta(tok);
|
|
1077
|
+
if (at !== 1 || tok.kind !== "ident" && tok.kind !== "string") continue;
|
|
1078
|
+
const prev = t[j - 1].text;
|
|
1079
|
+
const next = t[j + 1]?.text;
|
|
1080
|
+
if ((prev === "{" || prev === ",") && (next === ":" || next === "," || next === "}")) {
|
|
1081
|
+
const name = tok.kind === "string" ? stringValue(tok) : tok.text;
|
|
1082
|
+
if (name !== "links") names.push(name);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return names;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
893
1088
|
// src/dsl/check.ts
|
|
894
1089
|
function checkDsl(text, ctx = {}) {
|
|
895
1090
|
const parsed = parseDsl(text);
|
|
@@ -1230,8 +1425,38 @@ function checkDsl(text, ctx = {}) {
|
|
|
1230
1425
|
}
|
|
1231
1426
|
}
|
|
1232
1427
|
out.push(...inlineAssignmentIssues(parsed, positionAt));
|
|
1428
|
+
const locales = Object.entries(ctx.messageTranslations ?? {});
|
|
1429
|
+
for (const m of extractMessages(parsed)) {
|
|
1430
|
+
if (m.kind === "concatenated" && locales.length > 0) {
|
|
1431
|
+
add(
|
|
1432
|
+
m.span,
|
|
1433
|
+
`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 }).`,
|
|
1434
|
+
"warning",
|
|
1435
|
+
"dsl/message-concatenated"
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
if (m.text === void 0) continue;
|
|
1439
|
+
if (m.valueNames && m.valueNames.length > 0) {
|
|
1440
|
+
for (const name of placeholders(m.text)) {
|
|
1441
|
+
if (!m.valueNames.includes(name))
|
|
1442
|
+
add(m.span, `{${name}} has no value in this call, so it shows as written.`, "warning", "dsl/message-placeholder");
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
const missing = locales.filter(([, messages]) => !(Object.hasOwn(messages, m.text) && messages[m.text]?.trim())).map(([locale]) => locale);
|
|
1446
|
+
if (missing.length > 0) {
|
|
1447
|
+
add(
|
|
1448
|
+
m.span,
|
|
1449
|
+
`No ${missing.join(", ")} translation for this message \u2014 add it to "messages" in translations/<locale>.json, keyed by the exact text.`,
|
|
1450
|
+
"warning",
|
|
1451
|
+
"dsl/message-untranslated"
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1233
1455
|
return out;
|
|
1234
1456
|
}
|
|
1457
|
+
function placeholders(text) {
|
|
1458
|
+
return [...new Set([...text.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((x) => x[1]))];
|
|
1459
|
+
}
|
|
1235
1460
|
function columnMatcher(columns) {
|
|
1236
1461
|
if (!columns) return () => true;
|
|
1237
1462
|
if (typeof columns.keys !== "function")
|
|
@@ -1428,6 +1653,7 @@ export {
|
|
|
1428
1653
|
BUILTIN_BY_NAME,
|
|
1429
1654
|
BUILTIN_VALUES,
|
|
1430
1655
|
checkDsl,
|
|
1656
|
+
extractMessages,
|
|
1431
1657
|
parseDsl,
|
|
1432
1658
|
stringValue,
|
|
1433
1659
|
templateChunkValue,
|