@dforge-core/dforge-mcp 0.2.31 → 0.2.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/dist/server.js CHANGED
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
31002
31002
  var fs2 = __toESM(require("fs"));
31003
31003
  var path2 = __toESM(require("path"));
31004
31004
 
31005
- // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.24/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31005
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.25/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31006
31006
  function buildManifest(opts, moduleId) {
31007
31007
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31008
31008
  const manifest = {
@@ -31211,7 +31211,8 @@ var SCHEMA_BINDINGS = [
31211
31211
  { fileMatch: ["logic/webhooks.json"], schema: "webhooks" },
31212
31212
  { fileMatch: ["deps/*.json"], schema: "deps" },
31213
31213
  { fileMatch: ["seed-data/*.json"], schema: "seed-data" },
31214
- { fileMatch: ["settings.json"], schema: "settings" }
31214
+ { fileMatch: ["settings.json"], schema: "settings" },
31215
+ { fileMatch: ["translations/*.json"], schema: "translations" }
31215
31216
  ];
31216
31217
  function schemaEntries() {
31217
31218
  return SCHEMA_BINDINGS.map((b) => ({
@@ -31284,7 +31285,7 @@ function buildZedTasks() {
31284
31285
  var fs = __toESM(require("fs"));
31285
31286
  var path = __toESM(require("path"));
31286
31287
 
31287
- // node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/index.js
31288
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.32/node_modules/@dforge-core/metadata/dist/index.js
31288
31289
  var AggType = {
31289
31290
  Sum: "sum",
31290
31291
  Count: "count",
@@ -34590,9 +34591,13 @@ function entityDelete(args) {
34590
34591
  }
34591
34592
 
34592
34593
  // src/tools/action-add.ts
34594
+ var path10 = __toESM(require("path"));
34595
+
34596
+ // src/tools/validate/translations.ts
34597
+ var fs7 = __toESM(require("fs"));
34593
34598
  var path9 = __toESM(require("path"));
34594
34599
 
34595
- // node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/dsl/index.js
34600
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.32/node_modules/@dforge-core/metadata/dist/dsl/index.js
34596
34601
  var PUNCT3 = ["===", "!=="];
34597
34602
  var PUNCT2 = [
34598
34603
  "==",
@@ -34867,24 +34872,48 @@ function stringValue(token) {
34867
34872
  }
34868
34873
  function templateChunkValue(token) {
34869
34874
  if (token.kind !== "template") return token.text;
34870
- const body = token.text.slice(1);
34875
+ const body = token.text.slice(1).replace(/\r\n?/g, "\n");
34871
34876
  if (body.endsWith("${")) return unescape2(body.slice(0, -2));
34872
34877
  if (body.endsWith("`")) return unescape2(body.slice(0, -1));
34873
34878
  return unescape2(body);
34874
34879
  }
34875
34880
  function unescape2(body) {
34876
- return body.replace(/\\(.)/g, (_, ch) => {
34877
- switch (ch) {
34878
- case "n":
34879
- return "\n";
34880
- case "t":
34881
- return " ";
34882
- case "r":
34883
- return "\r";
34884
- default:
34885
- return ch;
34881
+ return body.replace(
34882
+ /\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|\r\n|[\s\S])/g,
34883
+ (_, esc2) => {
34884
+ switch (esc2[0]) {
34885
+ case "n":
34886
+ return "\n";
34887
+ case "t":
34888
+ return " ";
34889
+ case "r":
34890
+ return "\r";
34891
+ case "b":
34892
+ return "\b";
34893
+ case "f":
34894
+ return "\f";
34895
+ case "v":
34896
+ return "\v";
34897
+ case "0":
34898
+ return "\0";
34899
+ case "\r":
34900
+ case "\n":
34901
+ case "\u2028":
34902
+ case "\u2029":
34903
+ return "";
34904
+ case "u":
34905
+ case "x":
34906
+ if (esc2.length > 1) {
34907
+ const hex3 = esc2[1] === "{" ? esc2.slice(2, -1) : esc2.slice(1);
34908
+ const cp = parseInt(hex3, 16);
34909
+ return cp <= 1114111 ? String.fromCodePoint(cp) : esc2;
34910
+ }
34911
+ return esc2;
34912
+ default:
34913
+ return esc2;
34914
+ }
34886
34915
  }
34887
- });
34916
+ );
34888
34917
  }
34889
34918
  var BLOCK_KINDS = [
34890
34919
  "params",
@@ -35027,10 +35056,10 @@ function templateArg(tokens, start) {
35027
35056
  for (let j = start; j < tokens.length; j++) {
35028
35057
  const t = tokens[j];
35029
35058
  if (t.kind !== "template") continue;
35030
- const opens = t.text.startsWith("`");
35031
- if (opens) depth++;
35059
+ const opens2 = t.text.startsWith("`");
35060
+ if (opens2) depth++;
35032
35061
  if (depth === 1) {
35033
- if (!opens) parts.push(" ");
35062
+ if (!opens2) parts.push(" ");
35034
35063
  parts.push(templateChunkValue(t));
35035
35064
  last = t;
35036
35065
  endIndex = j;
@@ -35351,22 +35380,27 @@ var BUILTINS = [
35351
35380
  // Messaging
35352
35381
  {
35353
35382
  name: "error",
35354
- signature: "error(message)",
35383
+ signature: "error(message, values?)",
35355
35384
  returns: "never",
35356
- doc: "Abort the action and show an error. Rolls back all changes."
35385
+ 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`."
35386
+ },
35387
+ {
35388
+ name: "warn",
35389
+ signature: "warn(message, values?, opts?)",
35390
+ returns: "void",
35391
+ doc: "Show a warning and continue. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`."
35357
35392
  },
35358
- { name: "warn", signature: "warn(message)", returns: "void", doc: "Show a warning and continue." },
35359
35393
  {
35360
35394
  name: "info",
35361
- signature: "info(message)",
35395
+ signature: "info(message, values?, opts?)",
35362
35396
  returns: "void",
35363
- doc: "Show an informational message \u2014 a receipt for work the action did, not a way to publish a computed value."
35397
+ 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."
35364
35398
  },
35365
35399
  {
35366
35400
  name: "exit",
35367
- signature: "exit(message?, level?)",
35401
+ signature: "exit(message?, level?, values?)",
35368
35402
  returns: "never",
35369
- doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default) or `'warn'`. 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."
35403
+ 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."
35370
35404
  },
35371
35405
  {
35372
35406
  name: "notify",
@@ -35473,6 +35507,170 @@ var BUILTINS = [
35473
35507
  }
35474
35508
  ];
35475
35509
  var BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.name, b]));
35510
+ var VALUES_INDEX = /* @__PURE__ */ new Map([
35511
+ ["info", 1],
35512
+ ["warn", 1],
35513
+ ["error", 1],
35514
+ ["exit", 2]
35515
+ ]);
35516
+ function extractMessages(doc) {
35517
+ const out = [];
35518
+ const { tokens } = doc;
35519
+ for (let i = 0; i < tokens.length - 1; i++) {
35520
+ const t = tokens[i];
35521
+ const valuesIndex = VALUES_INDEX.get(t.text);
35522
+ if (t.kind !== "ident" || valuesIndex === void 0 || tokens[i + 1].text !== "(") continue;
35523
+ if (tokens[i - 1]?.text === ".") continue;
35524
+ const block = doc.blocks.find((b) => t.start >= b.body.start && t.start < b.body.end)?.kind;
35525
+ if (block !== "execute" && block !== "onBeforeStart") continue;
35526
+ const args = splitArgs(tokens, i + 1);
35527
+ if (!args[0]?.length) continue;
35528
+ const [kind, text] = classify(args[0]);
35529
+ const valuesArg = args[valuesIndex];
35530
+ out.push({
35531
+ builtin: t.text,
35532
+ kind,
35533
+ text,
35534
+ valueNames: valuesArg ? valueNames(valuesArg) : [],
35535
+ span: { start: args[0][0].start, end: args[0][args[0].length - 1].end }
35536
+ });
35537
+ }
35538
+ return out;
35539
+ }
35540
+ function splitArgs(tokens, open) {
35541
+ const args = [];
35542
+ let current = [];
35543
+ let depth = 0;
35544
+ for (let j = open + 1; j < tokens.length; j++) {
35545
+ const t = tokens[j];
35546
+ if (t.kind === "comment") continue;
35547
+ if (depth === 0 && t.kind === "punct" && closes(t)) break;
35548
+ if (depth === 0 && t.kind === "punct" && t.text === ",") {
35549
+ args.push(current);
35550
+ current = [];
35551
+ continue;
35552
+ }
35553
+ depth += depthDelta(t);
35554
+ current.push(t);
35555
+ }
35556
+ if (current.length > 0) args.push(current);
35557
+ return args;
35558
+ }
35559
+ var isWholeTemplate = (t) => t.kind === "template" && t.text.length > 1 && t.text.startsWith("`") && t.text.endsWith("`");
35560
+ function classify(arg) {
35561
+ const t = unwrap(arg);
35562
+ if (t.length === 1 && t[0].kind === "string") return ["literal", stringValue(t[0])];
35563
+ if (t.length === 1 && isWholeTemplate(t[0])) return ["literal", templateChunkValue(t[0])];
35564
+ return [joinsText(t) ? "concatenated" : "dynamic"];
35565
+ }
35566
+ function joinsText(tokens) {
35567
+ const t = unwrap(tokens);
35568
+ if (t.length === 1 && t[0].kind === "string" || isTemplate(t)) return true;
35569
+ let depth = 0;
35570
+ let root = -1;
35571
+ for (let j = 0; j < t.length; j++) {
35572
+ const tok = t[j];
35573
+ if (depth === 0) {
35574
+ if (isLooserThanPlus(t, j)) return false;
35575
+ if (tok.kind === "punct" && (tok.text === "+" || tok.text === "-") && endsOperand(t[j - 1])) root = j;
35576
+ }
35577
+ depth += depthDelta(tok);
35578
+ }
35579
+ if (root < 0 || t[root].text === "-") return false;
35580
+ return joinsText(t.slice(0, root)) || joinsText(t.slice(root + 1));
35581
+ }
35582
+ var LOOSER_THAN_PLUS = /* @__PURE__ */ new Set([
35583
+ "?",
35584
+ ":",
35585
+ "&&",
35586
+ "||",
35587
+ "=",
35588
+ "==",
35589
+ "!=",
35590
+ "===",
35591
+ "!==",
35592
+ "<",
35593
+ ">",
35594
+ "<=",
35595
+ ">=",
35596
+ "&",
35597
+ "|",
35598
+ "^",
35599
+ ",",
35600
+ "=>",
35601
+ "+=",
35602
+ "-=",
35603
+ "*=",
35604
+ "/=",
35605
+ "%="
35606
+ ]);
35607
+ function isLooserThanPlus(t, j) {
35608
+ const tok = t[j];
35609
+ if (tok.kind === "ident") return tok.text === "in" || tok.text === "instanceof";
35610
+ if (tok.kind !== "punct" || !LOOSER_THAN_PLUS.has(tok.text)) return false;
35611
+ return !(tok.text === "?" && t[j + 1]?.text === ".");
35612
+ }
35613
+ function endsOperand(prev) {
35614
+ if (!prev) return false;
35615
+ switch (prev.kind) {
35616
+ case "number":
35617
+ case "string":
35618
+ case "regex":
35619
+ return true;
35620
+ case "ident":
35621
+ return !["return", "typeof", "void", "delete", "new", "in", "instanceof", "case"].includes(prev.text);
35622
+ case "template":
35623
+ return prev.text.endsWith("`");
35624
+ case "punct":
35625
+ return [")", "]", "}", "++", "--"].includes(prev.text);
35626
+ default:
35627
+ return false;
35628
+ }
35629
+ }
35630
+ function isTemplate(tokens) {
35631
+ if (tokens[0]?.kind !== "template" || !tokens[0].text.startsWith("`")) return false;
35632
+ let depth = 0;
35633
+ for (let j = 0; j < tokens.length; j++) {
35634
+ depth += depthDelta(tokens[j]);
35635
+ if (depth === 0) return j === tokens.length - 1;
35636
+ }
35637
+ return false;
35638
+ }
35639
+ var opens = (t) => t.kind === "punct" ? "([{".includes(t.text) : t.kind === "template" && t.text.endsWith("${");
35640
+ var closes = (t) => t.kind === "punct" ? ")]}".includes(t.text) : t.kind === "template" && t.text.startsWith("}");
35641
+ var depthDelta = (t) => Number(opens(t)) - Number(closes(t));
35642
+ function unwrap(tokens) {
35643
+ while (tokens[0]?.text === "(" && isGroup(tokens)) tokens = tokens.slice(1, -1);
35644
+ return tokens;
35645
+ }
35646
+ function isGroup(tokens) {
35647
+ if (tokens.length < 2 || !opens(tokens[0])) return false;
35648
+ let depth = 0;
35649
+ for (let j = 0; j < tokens.length; j++) {
35650
+ depth += depthDelta(tokens[j]);
35651
+ if (depth === 0) return j === tokens.length - 1;
35652
+ }
35653
+ return false;
35654
+ }
35655
+ function valueNames(arg) {
35656
+ const t = unwrap(arg);
35657
+ if (t[0]?.text !== "{" || !isGroup(t)) return null;
35658
+ const names = [];
35659
+ let depth = 0;
35660
+ for (let j = 0; j < t.length; j++) {
35661
+ const tok = t[j];
35662
+ const at = depth;
35663
+ depth += depthDelta(tok);
35664
+ if (at !== 1 || tok.kind !== "ident" && tok.kind !== "string") continue;
35665
+ const prev = t[j - 1].text;
35666
+ const next = t[j + 1]?.text;
35667
+ if ((prev === "{" || prev === ",") && (next === ":" || next === "," || next === "}")) {
35668
+ const name = tok.kind === "string" ? stringValue(tok) : tok.text;
35669
+ if (name !== "links") names.push(name);
35670
+ }
35671
+ }
35672
+ return names;
35673
+ }
35476
35674
  function checkDsl(text, ctx = {}) {
35477
35675
  const parsed = parseDsl(text);
35478
35676
  const out = [];
@@ -35812,8 +36010,38 @@ function checkDsl(text, ctx = {}) {
35812
36010
  }
35813
36011
  }
35814
36012
  out.push(...inlineAssignmentIssues(parsed, positionAt));
36013
+ const locales = Object.entries(ctx.messageTranslations ?? {});
36014
+ for (const m of extractMessages(parsed)) {
36015
+ if (m.kind === "concatenated" && locales.length > 0) {
36016
+ add(
36017
+ m.span,
36018
+ `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 }).`,
36019
+ "warning",
36020
+ "dsl/message-concatenated"
36021
+ );
36022
+ }
36023
+ if (m.text === void 0) continue;
36024
+ if (m.valueNames && m.valueNames.length > 0) {
36025
+ for (const name of placeholders(m.text)) {
36026
+ if (!m.valueNames.includes(name))
36027
+ add(m.span, `{${name}} has no value in this call, so it shows as written.`, "warning", "dsl/message-placeholder");
36028
+ }
36029
+ }
36030
+ const missing = locales.filter(([, messages]) => !(Object.hasOwn(messages, m.text) && messages[m.text]?.trim())).map(([locale]) => locale);
36031
+ if (missing.length > 0) {
36032
+ add(
36033
+ m.span,
36034
+ `No ${missing.join(", ")} translation for this message \u2014 add it to "messages" in translations/<locale>.json, keyed by the exact text.`,
36035
+ "warning",
36036
+ "dsl/message-untranslated"
36037
+ );
36038
+ }
36039
+ }
35815
36040
  return out;
35816
36041
  }
36042
+ function placeholders(text) {
36043
+ return [...new Set([...text.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((x) => x[1]))];
36044
+ }
35817
36045
  function columnMatcher(columns) {
35818
36046
  if (!columns) return () => true;
35819
36047
  if (typeof columns.keys !== "function")
@@ -36005,6 +36233,190 @@ function lineIndexer(text) {
36005
36233
  };
36006
36234
  }
36007
36235
 
36236
+ // src/tools/validate/translations.ts
36237
+ function resolveTranslationFile(translationsDir, locale) {
36238
+ const exact = path9.join(translationsDir, `${locale}.json`);
36239
+ if (fs7.existsSync(exact)) return exact;
36240
+ if (!fs7.existsSync(translationsDir)) return void 0;
36241
+ const want = `${locale}.json`.toLowerCase();
36242
+ for (const f of fs7.readdirSync(translationsDir)) {
36243
+ if (f.toLowerCase() === want) return path9.join(translationsDir, f);
36244
+ }
36245
+ return void 0;
36246
+ }
36247
+ function hasConstraintOverride(root, entityCd, constraintCd) {
36248
+ if (!root || typeof root !== "object") return false;
36249
+ const entities = root.entities;
36250
+ if (!entities || typeof entities !== "object") return false;
36251
+ const entity = entities[entityCd];
36252
+ if (!entity || typeof entity !== "object") return false;
36253
+ const constraints = entity.constraints;
36254
+ if (!constraints || typeof constraints !== "object") return false;
36255
+ const ck = constraints[constraintCd];
36256
+ if (!ck || typeof ck !== "object") return false;
36257
+ const msg = ck.message;
36258
+ return typeof msg === "string" && msg.trim() !== "";
36259
+ }
36260
+ function checkConstraintTranslations(ctx) {
36261
+ const { paths, warn, entities, supportedLocales } = ctx;
36262
+ if (supportedLocales.length > 0) {
36263
+ const declared = [];
36264
+ for (const [name, e] of Object.entries(entities)) {
36265
+ if (typeof e.extends === "string" && e.extends) continue;
36266
+ const constraints = e.constraints;
36267
+ if (!constraints || typeof constraints !== "object") continue;
36268
+ for (const [cname, c] of Object.entries(constraints)) {
36269
+ if (!c || typeof c !== "object") continue;
36270
+ const msg = c.message;
36271
+ if (typeof msg === "string" && msg.trim() !== "") {
36272
+ declared.push({ entity: name, constraint: cname, message: msg });
36273
+ }
36274
+ }
36275
+ }
36276
+ if (declared.length > 0) {
36277
+ const seen = /* @__PURE__ */ new Set();
36278
+ for (const raw of supportedLocales) {
36279
+ const locale = raw.trim();
36280
+ if (!locale) continue;
36281
+ const lc = locale.toLowerCase();
36282
+ if (lc === "en" || lc.startsWith("en-")) continue;
36283
+ if (seen.has(lc)) continue;
36284
+ seen.add(lc);
36285
+ let tx = null;
36286
+ const abs = resolveTranslationFile(paths.translationsDir, locale);
36287
+ if (abs) {
36288
+ try {
36289
+ tx = JSON.parse(fs7.readFileSync(abs, "utf8"));
36290
+ } catch {
36291
+ tx = null;
36292
+ }
36293
+ }
36294
+ for (const d of declared) {
36295
+ if (!hasConstraintOverride(tx, d.entity, d.constraint)) {
36296
+ warn(
36297
+ `translations/${locale}.json`,
36298
+ `constraint message '${d.entity}.constraints.${d.constraint}.message' has no ${locale} override \u2014 the base message ("${d.message}") will be used as the fallback. Add entities.${d.entity}.constraints.${d.constraint}.message to localize it.`
36299
+ );
36300
+ }
36301
+ }
36302
+ }
36303
+ }
36304
+ }
36305
+ }
36306
+ function checkTranslationCompleteness(ctx) {
36307
+ const { paths, err, warn, roles, supportedLocales } = ctx;
36308
+ const roleCodes = Object.keys(roles);
36309
+ const localeFiles = fs7.existsSync(paths.translationsDir) ? fs7.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
36310
+ for (const raw of supportedLocales) {
36311
+ const locale = raw.trim();
36312
+ if (!locale) continue;
36313
+ if (!resolveTranslationFile(paths.translationsDir, locale)) {
36314
+ err(
36315
+ "translations",
36316
+ `manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
36317
+ );
36318
+ }
36319
+ }
36320
+ if (roleCodes.length > 0 && localeFiles.length === 0) {
36321
+ warn(
36322
+ "translations",
36323
+ `no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
36324
+ );
36325
+ }
36326
+ for (const file2 of localeFiles) {
36327
+ let tx;
36328
+ try {
36329
+ tx = JSON.parse(fs7.readFileSync(path9.join(paths.translationsDir, file2), "utf8"));
36330
+ } catch (ex) {
36331
+ err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
36332
+ continue;
36333
+ }
36334
+ const txRoles = tx.roles ?? {};
36335
+ const missing = roleCodes.filter((rc) => {
36336
+ const entry = txRoles[rc];
36337
+ return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
36338
+ });
36339
+ if (missing.length > 0) {
36340
+ err(
36341
+ `translations/${file2}`,
36342
+ `missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
36343
+ );
36344
+ }
36345
+ }
36346
+ }
36347
+ var isEnglishLocale = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
36348
+ function readLocaleFile(translationsDir, locale) {
36349
+ const abs = resolveTranslationFile(translationsDir, locale);
36350
+ if (!abs) return null;
36351
+ try {
36352
+ return JSON.parse(fs7.readFileSync(abs, "utf8"));
36353
+ } catch {
36354
+ return null;
36355
+ }
36356
+ }
36357
+ function messagesBlock(root) {
36358
+ const block = root?.messages;
36359
+ if (!block || typeof block !== "object" || Array.isArray(block)) return {};
36360
+ return Object.fromEntries(
36361
+ Object.entries(block).filter((e) => typeof e[1] === "string" && e[1].trim() !== "")
36362
+ );
36363
+ }
36364
+ function readMessageTranslations(translationsDir, supportedLocales) {
36365
+ const declared = Array.isArray(supportedLocales) ? supportedLocales.filter((l) => typeof l === "string") : [];
36366
+ const locales = [...new Set(declared.map((l) => l.trim()).filter((l) => l && !isEnglishLocale(l)))];
36367
+ if (locales.length === 0) return void 0;
36368
+ return Object.fromEntries(locales.map((l) => [l, messagesBlock(readLocaleFile(translationsDir, l))]));
36369
+ }
36370
+ function actionMessageTexts(logicDir, actions) {
36371
+ const texts = /* @__PURE__ */ new Map();
36372
+ for (const [code2, def] of Object.entries(actions)) {
36373
+ const declared = def?.script;
36374
+ const script = typeof declared === "string" && declared ? declared : code2;
36375
+ const file2 = path9.join(logicDir, "actions", `${script}.dsl`);
36376
+ if (!fs7.existsSync(file2)) continue;
36377
+ let body;
36378
+ try {
36379
+ body = fs7.readFileSync(file2, "utf8");
36380
+ } catch {
36381
+ continue;
36382
+ }
36383
+ for (const m of extractMessages(parseDsl(body))) {
36384
+ if (m.text !== void 0 && !texts.has(m.text)) texts.set(m.text, `logic/actions/${script}.dsl`);
36385
+ }
36386
+ }
36387
+ return texts;
36388
+ }
36389
+ var placeholdersOf = (text) => new Set([...text.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((m) => m[1]));
36390
+ function checkMessageTranslations(ctx) {
36391
+ const { paths, warn } = ctx;
36392
+ if (!fs7.existsSync(paths.translationsDir)) return;
36393
+ let texts;
36394
+ for (const f of fs7.readdirSync(paths.translationsDir).filter((f2) => f2.toLowerCase().endsWith(".json")).sort()) {
36395
+ const locale = f.slice(0, -".json".length);
36396
+ const block = messagesBlock(readLocaleFile(paths.translationsDir, locale));
36397
+ if (Object.keys(block).length === 0) continue;
36398
+ if (isEnglishLocale(locale)) {
36399
+ warn(`translations/${f}`, "has a `messages` block, which install ignores: English is the text in the .dsl scripts.");
36400
+ continue;
36401
+ }
36402
+ texts ??= actionMessageTexts(paths.logicDir, ctx.actions);
36403
+ for (const [text, translation] of Object.entries(block)) {
36404
+ if (!texts.has(text)) {
36405
+ warn(
36406
+ `translations/${f}`,
36407
+ `messages entry "${text}" matches no info/warn/error/exit text in the action scripts \u2014 it never shows. The key must be the exact English text; an edit to the .dsl wording leaves the old entry behind.`
36408
+ );
36409
+ continue;
36410
+ }
36411
+ const known = placeholdersOf(text);
36412
+ for (const ph of placeholdersOf(translation)) {
36413
+ if (!known.has(ph))
36414
+ warn(`translations/${f}`, `messages entry "${text}": the translation uses {${ph}}, which the English text has no value for \u2014 it shows as written.`);
36415
+ }
36416
+ }
36417
+ }
36418
+ }
36419
+
36008
36420
  // src/tools/dsl-check.ts
36009
36421
  function checkDsl2(src, opts = {}) {
36010
36422
  const raw = opts.executionMode?.trim();
@@ -36013,7 +36425,8 @@ function checkDsl2(src, opts = {}) {
36013
36425
  const ctx = {
36014
36426
  moduleCode: opts.moduleCode,
36015
36427
  action: executionMode || opts.viaJob || opts.actionCode ? { code: opts.actionCode ?? "this action", executionMode, viaJob: opts.viaJob } : void 0,
36016
- currentEntity: opts.entity ?? null
36428
+ currentEntity: opts.entity ?? null,
36429
+ messageTranslations: opts.messageTranslations
36017
36430
  };
36018
36431
  const issues = checkDsl(src, ctx).map(toIssue);
36019
36432
  if (raw && !executionMode) {
@@ -36066,7 +36479,8 @@ function actionAdd(args) {
36066
36479
  executionMode: args.executionMode,
36067
36480
  moduleCode: manifest.code,
36068
36481
  actionCode: args.code,
36069
- entity: entityRecordContext(paths, manifest, args.entityCode, localTraits)
36482
+ entity: entityRecordContext(paths, manifest, args.entityCode, localTraits),
36483
+ messageTranslations: readMessageTranslations(paths.translationsDir, manifest.supportedLocales)
36070
36484
  });
36071
36485
  const dslErrors = dslIssues.filter((i) => i.level === "error");
36072
36486
  if (dslErrors.length > 0) {
@@ -36094,7 +36508,7 @@ function actionAdd(args) {
36094
36508
  actionEntry.icon = args.icon.startsWith("bi-") ? args.icon : `bi-${args.icon}`;
36095
36509
  }
36096
36510
  actionsJson[args.code] = actionEntry;
36097
- const dslPath = path9.join(paths.logicDir, "actions", `${args.code}.dsl`);
36511
+ const dslPath = path10.join(paths.logicDir, "actions", `${args.code}.dsl`);
36098
36512
  const dslBody = args.dslBody.endsWith("\n") ? args.dslBody : args.dslBody + "\n";
36099
36513
  return makeResult(
36100
36514
  `Added action '${args.code}' targeting entity '${args.entityCode}' (executionMode=${args.executionMode}${args.isAsync ? ", async" : ""}).`,
@@ -36109,8 +36523,8 @@ ${dslWarnings.map((i) => ` \u2022 ${i.line ? `line ${i.line}: ` : ""}${i.messag
36109
36523
  }
36110
36524
 
36111
36525
  // src/tools/action-check.ts
36112
- var fs7 = __toESM(require("fs"));
36113
- var path10 = __toESM(require("path"));
36526
+ var fs8 = __toESM(require("fs"));
36527
+ var path11 = __toESM(require("path"));
36114
36528
  var actionCheckSchema = {
36115
36529
  moduleDir: external_exports.string().optional().describe(
36116
36530
  "Module root. Required when checking an on-disk action (actionCode); optional when checking a raw dslBody."
@@ -36137,6 +36551,7 @@ function actionCheck(args) {
36137
36551
  let entityCode = args.entityCode;
36138
36552
  let moduleCode;
36139
36553
  let entity;
36554
+ let messageTranslations;
36140
36555
  const preIssues = [];
36141
36556
  if (args.actionCode && !args.moduleDir) {
36142
36557
  throw new Error("moduleDir is required when checking an on-disk action (actionCode).");
@@ -36144,6 +36559,7 @@ function actionCheck(args) {
36144
36559
  if (args.moduleDir) {
36145
36560
  const { paths, manifest } = loadManifest(args.moduleDir);
36146
36561
  moduleCode = manifest.code;
36562
+ messageTranslations = readMessageTranslations(paths.translationsDir, manifest.supportedLocales);
36147
36563
  if (args.actionCode) {
36148
36564
  const actions = readJsonOrDefault(paths.actions, {});
36149
36565
  const entry = actions[args.actionCode];
@@ -36153,13 +36569,13 @@ function actionCheck(args) {
36153
36569
  );
36154
36570
  }
36155
36571
  const script = entry.script ?? args.actionCode;
36156
- const dslPath = path10.join(paths.logicDir, "actions", `${script}.dsl`);
36157
- if (!fs7.existsSync(dslPath)) {
36572
+ const dslPath = path11.join(paths.logicDir, "actions", `${script}.dsl`);
36573
+ if (!fs8.existsSync(dslPath)) {
36158
36574
  throw new Error(
36159
36575
  `Action '${args.actionCode}' declares script '${script}' but logic/actions/${script}.dsl does not exist.`
36160
36576
  );
36161
36577
  }
36162
- body = fs7.readFileSync(dslPath, "utf8");
36578
+ body = fs8.readFileSync(dslPath, "utf8");
36163
36579
  mode = mode ?? (entry.executionMode ?? entry.mode);
36164
36580
  entityCode = entityCode ?? entry.entityCode;
36165
36581
  label = `logic/actions/${script}.dsl`;
@@ -36188,7 +36604,8 @@ function actionCheck(args) {
36188
36604
  viaJob,
36189
36605
  moduleCode,
36190
36606
  actionCode: args.actionCode,
36191
- entity
36607
+ entity,
36608
+ messageTranslations
36192
36609
  })
36193
36610
  ];
36194
36611
  const errors = issues.filter((i) => i.level === "error");
@@ -36379,8 +36796,8 @@ function menuAdd(args) {
36379
36796
  }
36380
36797
 
36381
36798
  // src/tools/translations.ts
36382
- var fs8 = __toESM(require("fs"));
36383
- var path11 = __toESM(require("path"));
36799
+ var fs9 = __toESM(require("fs"));
36800
+ var path12 = __toESM(require("path"));
36384
36801
  function titleize(code2) {
36385
36802
  return code2.split(/[_-]/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
36386
36803
  }
@@ -36407,21 +36824,21 @@ function buildSkeleton(moduleDir) {
36407
36824
  `traits.json \u2014 ${traitsError} Its traits can't be overlaid, so the entities that use one would be skipped as declaring an unknown trait and their columns would go unlabelled \u2014 which fails the install. Fix the file and sync again.`
36408
36825
  );
36409
36826
  }
36410
- const push = (section, pathParts, english, required2 = false) => {
36411
- entries.push({ pathParts, english, required: required2 });
36827
+ const push = (section, pathParts, english, required2 = false, legacyParts, nonEnglishOnly = false) => {
36828
+ entries.push({ pathParts, english, required: required2, legacyParts, nonEnglishOnly });
36412
36829
  counts[section] = (counts[section] ?? 0) + 1;
36413
36830
  };
36414
36831
  for (const [name, relPath] of Object.entries(manifest.entities ?? {})) {
36415
36832
  if (name.includes(".")) continue;
36416
- const abs = path11.join(paths.root, relPath.replace(/^\.\//, ""));
36417
- if (!fs8.existsSync(abs)) {
36833
+ const abs = path12.join(paths.root, relPath.replace(/^\.\//, ""));
36834
+ if (!fs9.existsSync(abs)) {
36418
36835
  throw new Error(
36419
36836
  `manifest.entities.${name} points at '${relPath}', which does not exist on disk. Fix the manifest or restore the file \u2014 syncing now would silently omit that entity's translation keys and the gap would only surface at install.`
36420
36837
  );
36421
36838
  }
36422
36839
  let e;
36423
36840
  try {
36424
- e = JSON.parse(fs8.readFileSync(abs, "utf8"));
36841
+ e = JSON.parse(fs9.readFileSync(abs, "utf8"));
36425
36842
  } catch (ex) {
36426
36843
  throw new Error(`${relPath} is not valid JSON: ${ex.message}`);
36427
36844
  }
@@ -36475,17 +36892,25 @@ function buildSkeleton(moduleDir) {
36475
36892
  if (dupes.size > 0) {
36476
36893
  const detail = [...dupes].map(([code2, paths_]) => `'${code2}' (${paths_.join(", ")})`).join("; ");
36477
36894
  throw new Error(
36478
- `ui/folders.json reuses folder code(s) across branches: ${detail}. Translation keys are flat (\`folders.<code>.label\`), so duplicates would overwrite each other \u2014 and role rights (\`folder:<code>\`) can't tell them apart either. Rename them to unique codes, then re-run.`
36895
+ `ui/folders.json reuses folder code(s) across branches: ${detail}. Role rights (\`folder:<code>\`) can't tell them apart. Rename them to unique codes, then re-run.`
36479
36896
  );
36480
36897
  }
36481
36898
  push("folders", ["folders", manifest.code, "label"], str(folders, "label") ?? titleize(manifest.code));
36482
36899
  for (const f of walkFolders(folders)) {
36483
- push("folders", ["folders", f.code, "label"], str(f.node, "label") ?? titleize(f.code));
36900
+ const flatCode = [manifest.code, ...f.path.split("/")].join("_");
36901
+ push("folders", ["folders", flatCode, "label"], str(f.node, "label") ?? titleize(f.code), false, [
36902
+ "folders",
36903
+ f.code,
36904
+ "label"
36905
+ ]);
36484
36906
  }
36485
36907
  }
36486
36908
  const settings = readJsonOrDefault(paths.settings, {});
36487
36909
  for (const [code2, s] of Object.entries(settings)) {
36488
- push("settings", ["settings", code2, "label"], str(s, "label") ?? titleize(code2));
36910
+ push("settings", ["settings", code2, "label"], str(s, "label") ?? str(s, "description") ?? str(s, "desc") ?? titleize(code2));
36911
+ }
36912
+ for (const text of actionMessageTexts(paths.logicDir, actions).keys()) {
36913
+ push("messages", ["messages", text], text, false, void 0, true);
36489
36914
  }
36490
36915
  return { entries, counts };
36491
36916
  }
@@ -36525,16 +36950,17 @@ function translationSync(args) {
36525
36950
  );
36526
36951
  }
36527
36952
  const keep = new Set(entries.map((e) => compositeKey(...e.pathParts)));
36953
+ const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
36528
36954
  const files = {};
36529
36955
  const report = [];
36530
36956
  let totalAdded = 0;
36531
36957
  let missingRequired = 0;
36532
36958
  for (const locale of locales) {
36533
- const abs = path11.join(paths.translationsDir, `${locale}.json`);
36959
+ const abs = path12.join(paths.translationsDir, `${locale}.json`);
36534
36960
  let existing = {};
36535
- if (fs8.existsSync(abs)) {
36961
+ if (fs9.existsSync(abs)) {
36536
36962
  try {
36537
- existing = JSON.parse(fs8.readFileSync(abs, "utf8"));
36963
+ existing = JSON.parse(fs9.readFileSync(abs, "utf8"));
36538
36964
  } catch (e) {
36539
36965
  throw new Error(`translations/${locale}.json is not valid JSON: ${e.message}`);
36540
36966
  }
@@ -36542,9 +36968,11 @@ function translationSync(args) {
36542
36968
  const next = args.prune ? pruneTo(existing, keep) : JSON.parse(JSON.stringify(existing));
36543
36969
  let added = 0;
36544
36970
  for (const entry of entries) {
36971
+ if (entry.nonEnglishOnly && isEnglish(locale)) continue;
36545
36972
  const current = getIn(next, entry.pathParts);
36546
36973
  if (typeof current === "string" && current.trim() !== "") continue;
36547
- setIn(next, entry.pathParts, entry.english);
36974
+ const legacy = entry.legacyParts && getIn(existing, entry.legacyParts);
36975
+ setIn(next, entry.pathParts, typeof legacy === "string" && legacy.trim() !== "" ? legacy : entry.english);
36548
36976
  added++;
36549
36977
  if (entry.required) missingRequired++;
36550
36978
  }
@@ -36552,7 +36980,6 @@ function translationSync(args) {
36552
36980
  totalAdded += added;
36553
36981
  report.push(`${locale}: +${added} key(s)${added === 0 ? " (already complete)" : ""}`);
36554
36982
  }
36555
- const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
36556
36983
  const nonEnglish = locales.filter((l) => !isEnglish(l));
36557
36984
  const warning = nonEnglish.length > 0 && totalAdded > 0 ? `Newly-added keys in ${nonEnglish.join(", ")} are seeded with the ENGLISH source text so the file is installable immediately \u2014 they still need real translation. (Untranslated values fall back to en-US at runtime, so this never breaks; it just shows English.)` : void 0;
36558
36985
  const sections = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
@@ -36564,8 +36991,8 @@ function translationSync(args) {
36564
36991
  }
36565
36992
 
36566
36993
  // src/tools/seed.ts
36567
- var fs9 = __toESM(require("fs"));
36568
- var path12 = __toESM(require("path"));
36994
+ var fs10 = __toESM(require("fs"));
36995
+ var path13 = __toESM(require("path"));
36569
36996
  var SYSTEM_USER = 0;
36570
36997
  var seedAddSchema = {
36571
36998
  moduleDir: external_exports.string().describe("Path to the module root."),
@@ -36580,8 +37007,8 @@ var seedAddSchema = {
36580
37007
  };
36581
37008
  function seedAdd(args) {
36582
37009
  const { paths, manifest } = loadManifest(args.moduleDir);
36583
- const entityPath = path12.join(paths.entitiesDir, `${args.entity}.json`);
36584
- if (!fs9.existsSync(entityPath)) {
37010
+ const entityPath = path13.join(paths.entitiesDir, `${args.entity}.json`);
37011
+ if (!fs10.existsSync(entityPath)) {
36585
37012
  throw new Error(
36586
37013
  `Entity '${args.entity}' not found at entities/${args.entity}.json. Existing entities: ${Object.keys(manifest.entities ?? {}).join(", ") || "(none)"}.`
36587
37014
  );
@@ -36659,11 +37086,11 @@ function seedAdd(args) {
36659
37086
  );
36660
37087
  }
36661
37088
  }
36662
- const seedFiles = fs9.existsSync(paths.seedDataDir) ? fs9.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
37089
+ const seedFiles = fs10.existsSync(paths.seedDataDir) ? fs10.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
36663
37090
  const seededPks = {};
36664
37091
  for (const f of seedFiles) {
36665
37092
  try {
36666
- const data = JSON.parse(fs9.readFileSync(path12.join(paths.seedDataDir, f), "utf8"));
37093
+ const data = JSON.parse(fs10.readFileSync(path13.join(paths.seedDataDir, f), "utf8"));
36667
37094
  const ec = data?.entityCode;
36668
37095
  if (typeof ec !== "string" || !Array.isArray(data.records)) continue;
36669
37096
  const set2 = seededPks[ec] ?? (seededPks[ec] = /* @__PURE__ */ new Set());
@@ -36706,7 +37133,7 @@ function seedAdd(args) {
36706
37133
  }
36707
37134
  }
36708
37135
  const fileName = args.fileName ?? `${String(order).padStart(2, "0")}-${args.entity}.json`;
36709
- const seedPath = path12.join(paths.seedDataDir, fileName);
37136
+ const seedPath = path13.join(paths.seedDataDir, fileName);
36710
37137
  const payload = { entityCode: args.entity, records: args.records };
36711
37138
  return makeResult(
36712
37139
  `Seeded ${args.records.length} '${args.entity}' record(s) \u2192 seed-data/${fileName} (load order ${order}).`,
@@ -36720,8 +37147,8 @@ ${warnings.map((w) => ` \u2022 ${w}`).join("\n")}` : void 0
36720
37147
  }
36721
37148
 
36722
37149
  // src/tools/adds.ts
36723
- var fs10 = __toESM(require("fs"));
36724
- var path13 = __toESM(require("path"));
37150
+ var fs11 = __toESM(require("fs"));
37151
+ var path14 = __toESM(require("path"));
36725
37152
  var reportAddSchema = {
36726
37153
  moduleDir: external_exports.string(),
36727
37154
  code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Report code (keys ui/reports.json)."),
@@ -36908,8 +37335,8 @@ function diagramAdd(args) {
36908
37335
  if (problems.length > 0) {
36909
37336
  throw new Error(`Nothing written \u2014 ${problems.join(" ")}`);
36910
37337
  }
36911
- const file2 = path13.join(paths.diagramsDir, `${args.code}.json`);
36912
- const exists = fs10.existsSync(file2);
37338
+ const file2 = path14.join(paths.diagramsDir, `${args.code}.json`);
37339
+ const exists = fs11.existsSync(file2);
36913
37340
  let diagram;
36914
37341
  if (exists) {
36915
37342
  const existing = readJson(file2);
@@ -36967,8 +37394,8 @@ function dependencyAdd(args) {
36967
37394
  `Dependency on '${args.moduleCode}' already exists. Edit manifest.json directly to change the version.`
36968
37395
  );
36969
37396
  }
36970
- const contractPath = path13.join(paths.root, "deps", `${args.moduleCode}.json`);
36971
- if (fs10.existsSync(contractPath)) {
37397
+ const contractPath = path14.join(paths.root, "deps", `${args.moduleCode}.json`);
37398
+ if (fs11.existsSync(contractPath)) {
36972
37399
  throw new Error(
36973
37400
  `deps/${args.moduleCode}.json already exists but '${args.moduleCode}' is not a manifest dependency. Add the manifest entry by hand or delete the stale contract \u2014 this tool will not overwrite it.`
36974
37401
  );
@@ -37061,8 +37488,8 @@ function roleRightSet(args) {
37061
37488
  }
37062
37489
 
37063
37490
  // src/tools/module-inspect.ts
37064
- var fs11 = __toESM(require("fs"));
37065
- var path14 = __toESM(require("path"));
37491
+ var fs12 = __toESM(require("fs"));
37492
+ var path15 = __toESM(require("path"));
37066
37493
  var moduleInspectSchema = {
37067
37494
  moduleDir: external_exports.string()
37068
37495
  };
@@ -37070,7 +37497,7 @@ function moduleInspect(args) {
37070
37497
  const { paths, manifest } = loadManifest(args.moduleDir);
37071
37498
  const entities = manifest.entities ?? {};
37072
37499
  const entitySummaries = Object.entries(entities).map(([name, relPath]) => {
37073
- const abs = path14.join(paths.root, relPath.replace(/^\.\//, ""));
37500
+ const abs = path15.join(paths.root, relPath.replace(/^\.\//, ""));
37074
37501
  const e = readJsonOrDefault(abs, {});
37075
37502
  const fields = e.fields ?? {};
37076
37503
  return {
@@ -37105,11 +37532,11 @@ function moduleInspect(args) {
37105
37532
  const diagramSummaries = diagramFiles(paths.diagramsDir).map((fp) => {
37106
37533
  let d = {};
37107
37534
  try {
37108
- d = JSON.parse(fs11.readFileSync(fp, "utf8"));
37535
+ d = JSON.parse(fs12.readFileSync(fp, "utf8"));
37109
37536
  } catch {
37110
37537
  }
37111
37538
  return {
37112
- code: path14.basename(fp, ".json"),
37539
+ code: path15.basename(fp, ".json"),
37113
37540
  label: typeof d?.label === "string" ? d.label : void 0,
37114
37541
  entities: Object.keys(d?.entities ?? {})
37115
37542
  };
@@ -37166,8 +37593,8 @@ function moduleInspect(args) {
37166
37593
  readJsonOrDefault(paths.printTemplates, {})
37167
37594
  );
37168
37595
  const domains = Object.keys(readJsonOrDefault(paths.domains, {}));
37169
- const seedFiles = fs11.existsSync(paths.seedDataDir) ? fs11.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
37170
- const translations = fs11.existsSync(paths.translationsDir) ? fs11.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
37596
+ const seedFiles = fs12.existsSync(paths.seedDataDir) ? fs12.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
37597
+ const translations = fs12.existsSync(paths.translationsDir) ? fs12.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
37171
37598
  const summary = {
37172
37599
  module: {
37173
37600
  code: manifest.code,
@@ -37212,8 +37639,8 @@ function computeDepth(folder, current = 1) {
37212
37639
  }
37213
37640
 
37214
37641
  // src/tools/validate/context.ts
37215
- var fs12 = __toESM(require("fs"));
37216
- var path15 = __toESM(require("path"));
37642
+ var fs13 = __toESM(require("fs"));
37643
+ var path16 = __toESM(require("path"));
37217
37644
  var SYSTEM_ENTITY_PK = {
37218
37645
  user: "user_id",
37219
37646
  document: "document_id",
@@ -37239,14 +37666,14 @@ function loadContext(moduleDir) {
37239
37666
  const partialColumns = /* @__PURE__ */ new Set();
37240
37667
  for (const [name, relPath] of Object.entries(entityMap)) {
37241
37668
  if (name.includes(".")) continue;
37242
- const abs = path15.join(paths.root, relPath.replace(/^\.\//, ""));
37243
- if (!fs12.existsSync(abs)) {
37669
+ const abs = path16.join(paths.root, relPath.replace(/^\.\//, ""));
37670
+ if (!fs13.existsSync(abs)) {
37244
37671
  err(`manifest.entities.${name}`, `points to '${relPath}' which does not exist on disk`);
37245
37672
  continue;
37246
37673
  }
37247
37674
  let e;
37248
37675
  try {
37249
- e = JSON.parse(fs12.readFileSync(abs, "utf8"));
37676
+ e = JSON.parse(fs13.readFileSync(abs, "utf8"));
37250
37677
  } catch (ex) {
37251
37678
  err(`entities/${name}.json`, `invalid JSON: ${ex.message}`);
37252
37679
  continue;
@@ -37579,7 +38006,7 @@ function checkMenus(ctx) {
37579
38006
  }
37580
38007
 
37581
38008
  // src/tools/validate/security.ts
37582
- var fs13 = __toESM(require("fs"));
38009
+ var fs14 = __toESM(require("fs"));
37583
38010
  function checkRoleRights(ctx) {
37584
38011
  const { paths, manifest, err, deps, isKnownEntity, roles, actions, reports } = ctx;
37585
38012
  const storedProcedures = readJsonOrDefault(paths.storedProcedures, {});
@@ -37595,7 +38022,7 @@ function checkRoleRights(ctx) {
37595
38022
  const code2 = dot < 0 ? ref : ref.slice(dot + 1);
37596
38023
  if (mod !== manifest.code) {
37597
38024
  if (!deps.has(mod)) err(`roles \u2192 ${rcode}`, `grants on '${key}' but '${mod}' is not a declared dependency`);
37598
- } else if (!fs13.existsSync(absFile)) {
38025
+ } else if (!fs14.existsSync(absFile)) {
37599
38026
  err(`roles \u2192 ${rcode}`, `grants on '${key}' but ${file2} is missing \u2014 declare the ${noun} there`);
37600
38027
  } else if (!(code2 in own)) {
37601
38028
  err(`roles \u2192 ${rcode}`, `grants on '${key}' but no such ${noun} exists in ${file2}`);
@@ -37629,120 +38056,6 @@ function checkSelectCoverage(ctx) {
37629
38056
  }
37630
38057
  }
37631
38058
 
37632
- // src/tools/validate/translations.ts
37633
- var fs14 = __toESM(require("fs"));
37634
- var path16 = __toESM(require("path"));
37635
- function resolveTranslationFile(translationsDir, locale) {
37636
- const exact = path16.join(translationsDir, `${locale}.json`);
37637
- if (fs14.existsSync(exact)) return exact;
37638
- if (!fs14.existsSync(translationsDir)) return void 0;
37639
- const want = `${locale}.json`.toLowerCase();
37640
- for (const f of fs14.readdirSync(translationsDir)) {
37641
- if (f.toLowerCase() === want) return path16.join(translationsDir, f);
37642
- }
37643
- return void 0;
37644
- }
37645
- function hasConstraintOverride(root, entityCd, constraintCd) {
37646
- if (!root || typeof root !== "object") return false;
37647
- const entities = root.entities;
37648
- if (!entities || typeof entities !== "object") return false;
37649
- const entity = entities[entityCd];
37650
- if (!entity || typeof entity !== "object") return false;
37651
- const constraints = entity.constraints;
37652
- if (!constraints || typeof constraints !== "object") return false;
37653
- const ck = constraints[constraintCd];
37654
- if (!ck || typeof ck !== "object") return false;
37655
- const msg = ck.message;
37656
- return typeof msg === "string" && msg.trim() !== "";
37657
- }
37658
- function checkConstraintTranslations(ctx) {
37659
- const { paths, warn, entities, supportedLocales } = ctx;
37660
- if (supportedLocales.length > 0) {
37661
- const declared = [];
37662
- for (const [name, e] of Object.entries(entities)) {
37663
- if (typeof e.extends === "string" && e.extends) continue;
37664
- const constraints = e.constraints;
37665
- if (!constraints || typeof constraints !== "object") continue;
37666
- for (const [cname, c] of Object.entries(constraints)) {
37667
- if (!c || typeof c !== "object") continue;
37668
- const msg = c.message;
37669
- if (typeof msg === "string" && msg.trim() !== "") {
37670
- declared.push({ entity: name, constraint: cname, message: msg });
37671
- }
37672
- }
37673
- }
37674
- if (declared.length > 0) {
37675
- const seen = /* @__PURE__ */ new Set();
37676
- for (const raw of supportedLocales) {
37677
- const locale = raw.trim();
37678
- if (!locale) continue;
37679
- const lc = locale.toLowerCase();
37680
- if (lc === "en" || lc.startsWith("en-")) continue;
37681
- if (seen.has(lc)) continue;
37682
- seen.add(lc);
37683
- let tx = null;
37684
- const abs = resolveTranslationFile(paths.translationsDir, locale);
37685
- if (abs) {
37686
- try {
37687
- tx = JSON.parse(fs14.readFileSync(abs, "utf8"));
37688
- } catch {
37689
- tx = null;
37690
- }
37691
- }
37692
- for (const d of declared) {
37693
- if (!hasConstraintOverride(tx, d.entity, d.constraint)) {
37694
- warn(
37695
- `translations/${locale}.json`,
37696
- `constraint message '${d.entity}.constraints.${d.constraint}.message' has no ${locale} override \u2014 the base message ("${d.message}") will be used as the fallback. Add entities.${d.entity}.constraints.${d.constraint}.message to localize it.`
37697
- );
37698
- }
37699
- }
37700
- }
37701
- }
37702
- }
37703
- }
37704
- function checkTranslationCompleteness(ctx) {
37705
- const { paths, err, warn, roles, supportedLocales } = ctx;
37706
- const roleCodes = Object.keys(roles);
37707
- const localeFiles = fs14.existsSync(paths.translationsDir) ? fs14.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
37708
- for (const raw of supportedLocales) {
37709
- const locale = raw.trim();
37710
- if (!locale) continue;
37711
- if (!resolveTranslationFile(paths.translationsDir, locale)) {
37712
- err(
37713
- "translations",
37714
- `manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
37715
- );
37716
- }
37717
- }
37718
- if (roleCodes.length > 0 && localeFiles.length === 0) {
37719
- warn(
37720
- "translations",
37721
- `no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
37722
- );
37723
- }
37724
- for (const file2 of localeFiles) {
37725
- let tx;
37726
- try {
37727
- tx = JSON.parse(fs14.readFileSync(path16.join(paths.translationsDir, file2), "utf8"));
37728
- } catch (ex) {
37729
- err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
37730
- continue;
37731
- }
37732
- const txRoles = tx.roles ?? {};
37733
- const missing = roleCodes.filter((rc) => {
37734
- const entry = txRoles[rc];
37735
- return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
37736
- });
37737
- if (missing.length > 0) {
37738
- err(
37739
- `translations/${file2}`,
37740
- `missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
37741
- );
37742
- }
37743
- }
37744
- }
37745
-
37746
38059
  // src/tools/validate/logic.ts
37747
38060
  var fs15 = __toESM(require("fs"));
37748
38061
  var path17 = __toESM(require("path"));
@@ -37836,9 +38149,10 @@ function checkTriggersJobsWebhooks(ctx) {
37836
38149
  }
37837
38150
  function checkActionDsl(ctx) {
37838
38151
  const { paths, manifest, issues, columnsOf, partialColumns, actions } = ctx;
38152
+ const messageTranslations = readMessageTranslations(paths.translationsDir, ctx.supportedLocales);
37839
38153
  for (const [acode, a] of Object.entries(actions)) {
37840
- const script = a?.script;
37841
- if (typeof script !== "string" || !script) continue;
38154
+ const declared = a?.script;
38155
+ const script = typeof declared === "string" && declared ? declared : acode;
37842
38156
  const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
37843
38157
  if (!fs15.existsSync(dslPath)) continue;
37844
38158
  let body;
@@ -37857,7 +38171,8 @@ function checkActionDsl(ctx) {
37857
38171
  executionMode: mode,
37858
38172
  actionCode: acode,
37859
38173
  moduleCode: manifest.code,
37860
- entity: entityCode && columns ? { qualified: `${manifest.code}.${entityCode}`, columns } : void 0
38174
+ entity: entityCode && columns ? { qualified: `${manifest.code}.${entityCode}`, columns } : void 0,
38175
+ messageTranslations
37861
38176
  })) {
37862
38177
  issues.push({
37863
38178
  level: issue2.level,
@@ -37877,7 +38192,7 @@ function checkFolders(ctx) {
37877
38192
  for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
37878
38193
  err(
37879
38194
  "ui/folders.json",
37880
- `folder code '${code2}' is used ${dupPaths.length} times (${dupPaths.join(", ")}). Codes must be unique across the whole tree: role rights say 'folder:${code2}' with no path, and translations key on 'folders.${code2}.label', so duplicates are ambiguous and silently collide.`
38195
+ `folder code '${code2}' is used ${dupPaths.length} times (${dupPaths.join(", ")}). Codes must be unique across the whole tree: role rights say 'folder:${code2}' with no path, so duplicates are ambiguous.`
37881
38196
  );
37882
38197
  }
37883
38198
  const folderNodes = [
@@ -38090,7 +38405,8 @@ var CHECKS = [
38090
38405
  checkFolders,
38091
38406
  checkDiagrams,
38092
38407
  checkReports,
38093
- checkTranslationCompleteness
38408
+ checkTranslationCompleteness,
38409
+ checkMessageTranslations
38094
38410
  ];
38095
38411
  function moduleValidate(args, cli) {
38096
38412
  const ctx = loadContext(args.moduleDir);
@@ -38463,6 +38779,11 @@ var resources = [
38463
38779
  "Print templates JSON schema",
38464
38780
  "JSON Schema for ui/print_templates.json \u2014 Liquid HTML print templates and reusable snippets, bound to entities for the print menu."
38465
38781
  ),
38782
+ schema(
38783
+ "translations",
38784
+ "Translations JSON schema",
38785
+ "JSON Schema for translations/<locale>.json \u2014 per-section labels (entities, fields, options, constraints, folders, views, menus, settings, roles, reports, actions) and the `messages` block that translates DSL info/warn/error/exit texts, keyed by the exact English text."
38786
+ ),
38466
38787
  schema(
38467
38788
  "settings",
38468
38789
  "Module settings JSON schema",