@dforge-core/metadata 0.0.31 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -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 +88 -11
- package/dist/index.js.map +1 -1
- package/package.json +67 -66
- package/schemas/entity.schema.json +17 -13
- 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 +8 -8
- package/src/index.ts +11 -1
- package/src/manifest.ts +4 -2
- package/src/translations.ts +61 -0
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(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The messages of a script's info / warn / error / exit calls — what the server's
|
|
2
|
+
// ActionDslCompiler extracts on CompileResult.Messages, for the editor and the MCP.
|
|
3
|
+
// The two must agree: test/fixtures/dsl-messages.json is run by both test suites.
|
|
4
|
+
|
|
5
|
+
import type { Token } from "./lexer";
|
|
6
|
+
import { stringValue, templateChunkValue } from "./lexer";
|
|
7
|
+
import type { DslDocument, Span } from "./parse";
|
|
8
|
+
|
|
9
|
+
export type DslMessageKind = "literal" | "concatenated" | "dynamic";
|
|
10
|
+
|
|
11
|
+
export interface DslMessage {
|
|
12
|
+
builtin: "info" | "warn" | "error" | "exit";
|
|
13
|
+
kind: DslMessageKind;
|
|
14
|
+
/** The message text for `literal`, exactly as the runtime receives it; otherwise undefined. */
|
|
15
|
+
text?: string;
|
|
16
|
+
/** Keys of the placeholder-values object; empty without one, null when it is not an object literal. */
|
|
17
|
+
valueNames: string[] | null;
|
|
18
|
+
/** The first argument. */
|
|
19
|
+
span: Span;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Built-in → position of its placeholder-values argument.
|
|
23
|
+
const VALUES_INDEX = new Map([
|
|
24
|
+
["info", 1],
|
|
25
|
+
["warn", 1],
|
|
26
|
+
["error", 1],
|
|
27
|
+
["exit", 2],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** Message calls in `execute:` and `onBeforeStart:`, in source order. */
|
|
31
|
+
export function extractMessages(doc: DslDocument): DslMessage[] {
|
|
32
|
+
const out: DslMessage[] = [];
|
|
33
|
+
const { tokens } = doc;
|
|
34
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
35
|
+
const t = tokens[i]!;
|
|
36
|
+
const valuesIndex = VALUES_INDEX.get(t.text);
|
|
37
|
+
if (t.kind !== "ident" || valuesIndex === undefined || tokens[i + 1]!.text !== "(") continue;
|
|
38
|
+
if (tokens[i - 1]?.text === ".") continue;
|
|
39
|
+
const block = doc.blocks.find((b) => t.start >= b.body.start && t.start < b.body.end)?.kind;
|
|
40
|
+
if (block !== "execute" && block !== "onBeforeStart") continue;
|
|
41
|
+
|
|
42
|
+
const args = splitArgs(tokens, i + 1);
|
|
43
|
+
if (!args[0]?.length) continue;
|
|
44
|
+
const [kind, text] = classify(args[0]!);
|
|
45
|
+
const valuesArg = args[valuesIndex];
|
|
46
|
+
out.push({
|
|
47
|
+
builtin: t.text as DslMessage["builtin"],
|
|
48
|
+
kind,
|
|
49
|
+
text,
|
|
50
|
+
valueNames: valuesArg ? valueNames(valuesArg) : [],
|
|
51
|
+
span: { start: args[0]![0]!.start, end: args[0]![args[0]!.length - 1]!.end },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The top-level arguments of the call whose `(` is at `open`, each as its tokens. */
|
|
58
|
+
function splitArgs(tokens: Token[], open: number): Token[][] {
|
|
59
|
+
const args: Token[][] = [];
|
|
60
|
+
let current: Token[] = [];
|
|
61
|
+
let depth = 0;
|
|
62
|
+
for (let j = open + 1; j < tokens.length; j++) {
|
|
63
|
+
const t = tokens[j]!;
|
|
64
|
+
if (t.kind === "comment") continue;
|
|
65
|
+
if (depth === 0 && t.kind === "punct" && closes(t)) break;
|
|
66
|
+
if (depth === 0 && t.kind === "punct" && t.text === ",") {
|
|
67
|
+
args.push(current);
|
|
68
|
+
current = [];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
depth += depthDelta(t);
|
|
72
|
+
current.push(t);
|
|
73
|
+
}
|
|
74
|
+
if (current.length > 0) args.push(current);
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
const isWholeTemplate = (t: Token): boolean =>
|
|
80
|
+
t.kind === "template" && t.text.length > 1 && t.text.startsWith("`") && t.text.endsWith("`");
|
|
81
|
+
|
|
82
|
+
// Mirrors the compiler's classification of the parsed argument: a lone string or
|
|
83
|
+
// hole-free template is literal, a template with holes or a `+` joining text is
|
|
84
|
+
// concatenated, anything else (`'a'.trim()`, a call, a variable) is dynamic.
|
|
85
|
+
function classify(arg: Token[]): [DslMessageKind, string?] {
|
|
86
|
+
const t = unwrap(arg);
|
|
87
|
+
if (t.length === 1 && t[0]!.kind === "string") return ["literal", stringValue(t[0]!)];
|
|
88
|
+
if (t.length === 1 && isWholeTemplate(t[0]!)) return ["literal", templateChunkValue(t[0]!)];
|
|
89
|
+
return [joinsText(t) ? "concatenated" : "dynamic"];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The compiler's JoinsText: text, or a `+` whose operand joins text. The root of an
|
|
93
|
+
// expression is its last top-level operator of the loosest precedence, so
|
|
94
|
+
// `'a' + x - 1` is a subtraction and `x == 'a' + y` a comparison.
|
|
95
|
+
function joinsText(tokens: Token[]): boolean {
|
|
96
|
+
const t = unwrap(tokens);
|
|
97
|
+
if ((t.length === 1 && t[0]!.kind === "string") || isTemplate(t)) return true;
|
|
98
|
+
let depth = 0;
|
|
99
|
+
let root = -1;
|
|
100
|
+
for (let j = 0; j < t.length; j++) {
|
|
101
|
+
const tok = t[j]!;
|
|
102
|
+
if (depth === 0) {
|
|
103
|
+
if (isLooserThanPlus(t, j)) return false;
|
|
104
|
+
if (tok.kind === "punct" && (tok.text === "+" || tok.text === "-") && endsOperand(t[j - 1])) root = j;
|
|
105
|
+
}
|
|
106
|
+
depth += depthDelta(tok);
|
|
107
|
+
}
|
|
108
|
+
if (root < 0 || t[root]!.text === "-") return false;
|
|
109
|
+
return joinsText(t.slice(0, root)) || joinsText(t.slice(root + 1));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const LOOSER_THAN_PLUS = new Set([
|
|
113
|
+
"?", ":", "&&", "||", "=", "==", "!=", "===", "!==", "<", ">", "<=", ">=", "&", "|", "^", ",",
|
|
114
|
+
"=>", "+=", "-=", "*=", "/=", "%=",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
function isLooserThanPlus(t: Token[], j: number): boolean {
|
|
118
|
+
const tok = t[j]!;
|
|
119
|
+
if (tok.kind === "ident") return tok.text === "in" || tok.text === "instanceof";
|
|
120
|
+
if (tok.kind !== "punct" || !LOOSER_THAN_PLUS.has(tok.text)) return false;
|
|
121
|
+
return !(tok.text === "?" && t[j + 1]?.text === "."); // `?.` is member access
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Whether a `+` / `-` after this token is binary rather than a sign.
|
|
125
|
+
function endsOperand(prev: Token | undefined): boolean {
|
|
126
|
+
if (!prev) return false;
|
|
127
|
+
switch (prev.kind) {
|
|
128
|
+
case "number":
|
|
129
|
+
case "string":
|
|
130
|
+
case "regex":
|
|
131
|
+
return true;
|
|
132
|
+
case "ident":
|
|
133
|
+
return !["return", "typeof", "void", "delete", "new", "in", "instanceof", "case"].includes(prev.text);
|
|
134
|
+
case "template":
|
|
135
|
+
return prev.text.endsWith("`");
|
|
136
|
+
case "punct":
|
|
137
|
+
return [")", "]", "}", "++", "--"].includes(prev.text);
|
|
138
|
+
default:
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// One template literal, holes included, and nothing after it.
|
|
144
|
+
function isTemplate(tokens: Token[]): boolean {
|
|
145
|
+
if (tokens[0]?.kind !== "template" || !tokens[0].text.startsWith("`")) return false;
|
|
146
|
+
let depth = 0;
|
|
147
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
148
|
+
depth += depthDelta(tokens[j]!);
|
|
149
|
+
if (depth === 0) return j === tokens.length - 1;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Nesting: brackets, and a template chunk that opens (`a${`) or closes (`}b`) a hole.
|
|
155
|
+
const opens = (t: Token) => (t.kind === "punct" ? "([{".includes(t.text) : t.kind === "template" && t.text.endsWith("${"));
|
|
156
|
+
const closes = (t: Token) => (t.kind === "punct" ? ")]}".includes(t.text) : t.kind === "template" && t.text.startsWith("}"));
|
|
157
|
+
const depthDelta = (t: Token) => Number(opens(t)) - Number(closes(t));
|
|
158
|
+
|
|
159
|
+
// `('Done')` is the literal the parser sees.
|
|
160
|
+
function unwrap(tokens: Token[]): Token[] {
|
|
161
|
+
while (tokens[0]?.text === "(" && isGroup(tokens)) tokens = tokens.slice(1, -1);
|
|
162
|
+
return tokens;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Whether the bracket that opens `tokens` is closed by its last token.
|
|
166
|
+
function isGroup(tokens: Token[]): boolean {
|
|
167
|
+
if (tokens.length < 2 || !opens(tokens[0]!)) return false;
|
|
168
|
+
let depth = 0;
|
|
169
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
170
|
+
depth += depthDelta(tokens[j]!);
|
|
171
|
+
if (depth === 0) return j === tokens.length - 1;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The keys of an object literal, as the compiler reads them: `({ n: 1 })` is one,
|
|
177
|
+
// and a name inside a nested value or a `${…}` is not a key.
|
|
178
|
+
function valueNames(arg: Token[]): string[] | null {
|
|
179
|
+
const t = unwrap(arg);
|
|
180
|
+
if (t[0]?.text !== "{" || !isGroup(t)) return null;
|
|
181
|
+
const names: string[] = [];
|
|
182
|
+
let depth = 0;
|
|
183
|
+
for (let j = 0; j < t.length; j++) {
|
|
184
|
+
const tok = t[j]!;
|
|
185
|
+
const at = depth;
|
|
186
|
+
depth += depthDelta(tok);
|
|
187
|
+
if (at !== 1 || (tok.kind !== "ident" && tok.kind !== "string")) continue;
|
|
188
|
+
const prev = t[j - 1]!.text;
|
|
189
|
+
const next = t[j + 1]?.text;
|
|
190
|
+
if ((prev === "{" || prev === ",") && (next === ":" || next === "," || next === "}")) {
|
|
191
|
+
const name = tok.kind === "string" ? stringValue(tok) : tok.text;
|
|
192
|
+
if (name !== "links") names.push(name);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return names;
|
|
196
|
+
}
|
package/src/dsl/types.ts
CHANGED
|
@@ -95,4 +95,11 @@ export interface DslContext {
|
|
|
95
95
|
* none: pass `null`, not the fragment. See `EntityShape.columns`.
|
|
96
96
|
*/
|
|
97
97
|
currentEntity?: EntityShape | null;
|
|
98
|
+
/**
|
|
99
|
+
* Each non-English locale the manifest lists in `supportedLocales` → the
|
|
100
|
+
* `messages` block of its `translations/<locale>.json` (`{}` when the file has
|
|
101
|
+
* none). Absent, the untranslated and concatenated message rules stand down,
|
|
102
|
+
* exactly as install only reports them for a module that declares locales.
|
|
103
|
+
*/
|
|
104
|
+
messageTranslations?: Record<string, Record<string, string>>;
|
|
98
105
|
}
|
package/src/entity.ts
CHANGED
|
@@ -19,6 +19,9 @@ export type TraitCd =
|
|
|
19
19
|
| "period"
|
|
20
20
|
| "file-library";
|
|
21
21
|
|
|
22
|
+
/** Audit trail level (`auditHistory` on an entity or manifest); omitted = off. */
|
|
23
|
+
export type AuditHistoryLevel = "basic" | "fields" | "full";
|
|
24
|
+
|
|
22
25
|
/** Referential action for a FK's ON DELETE / ON UPDATE clause (omitted = noAction). */
|
|
23
26
|
export type ReferentialAction = "cascade" | "setNull" | "restrict" | "noAction";
|
|
24
27
|
|
|
@@ -159,14 +162,12 @@ export interface EntityDef {
|
|
|
159
162
|
description?: string;
|
|
160
163
|
/** Database schema name override (defaults to module schema). */
|
|
161
164
|
schema?: string;
|
|
162
|
-
/** Database table
|
|
165
|
+
/** Database table name (defaults to entity code). */
|
|
163
166
|
dbObject?: string;
|
|
164
|
-
/** When true, maps to a SQL view (read-only, no DDL). */
|
|
165
|
-
isView?: boolean;
|
|
166
|
-
/** SELECT statement for view-backed entities (`isView=true`). */
|
|
167
|
-
viewSql?: string;
|
|
168
167
|
/** Display pattern using column placeholders, e.g. "{first_name} {last_name}". */
|
|
169
168
|
toString?: string;
|
|
169
|
+
/** Audit trail level; overrides the manifest's `auditHistory`. */
|
|
170
|
+
auditHistory?: AuditHistoryLevel;
|
|
170
171
|
/** Records of this entity accept comments (composer + thread on the card). */
|
|
171
172
|
comments?: boolean;
|
|
172
173
|
/** Traits expanded at install time. */
|
|
@@ -186,9 +187,8 @@ export interface EntityDef {
|
|
|
186
187
|
params?: { restricted?: boolean } & Record<string, unknown>;
|
|
187
188
|
/**
|
|
188
189
|
* Entity views keyed by view name — the platform's column-level security.
|
|
189
|
-
* Unrelated to `
|
|
190
|
-
*
|
|
191
|
-
* "view" also names in this platform. A
|
|
190
|
+
* Unrelated to `ui/data_views.json` (grids, kanban, calendars), which the
|
|
191
|
+
* word "view" also names in this platform. A
|
|
192
192
|
* folder binds one view per entity through `ui/folders.json`
|
|
193
193
|
* (`entities.<code>.viewName`); users working in that folder see ONLY the
|
|
194
194
|
* columns the view lists. An entity without views (or a folder naming the
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// from a friendly field-type choice;
|
|
7
7
|
// • structural types — the authoring shape of every module-package file
|
|
8
8
|
// (manifest, entities, data views, reports, stored procedures, menus, folders,
|
|
9
|
-
// roles, settings, jobs, triggers, webhooks, print templates, seed data,
|
|
9
|
+
// roles, settings, jobs, triggers, webhooks, print templates, seed data, translations,
|
|
10
10
|
// actions, diagrams), mirrored from the JSON schemas under docs/schemas/.
|
|
11
11
|
//
|
|
12
12
|
// No runtime dependencies; safe to import from editors, the web app, the CLI,
|
|
@@ -69,6 +69,7 @@ export type {
|
|
|
69
69
|
SignConfig,
|
|
70
70
|
LedgerRegistryEntry,
|
|
71
71
|
TraitCd,
|
|
72
|
+
AuditHistoryLevel,
|
|
72
73
|
} from "./entity";
|
|
73
74
|
|
|
74
75
|
// Entity introspection helpers (primary keys, identity keys, reference links).
|
|
@@ -120,6 +121,15 @@ export type { MenusFile, MenuDef, MenuItemDef, MenuItemType } from "./menus";
|
|
|
120
121
|
export type { FoldersFile, FolderDef, FolderEntityBinding } from "./folders";
|
|
121
122
|
export type { DiagramFile, DiagramDef, DiagramEntity, DiagramDetail, DiagramRelation } from "./diagrams";
|
|
122
123
|
export type { RolesFile, RoleDef } from "./roles";
|
|
124
|
+
export type {
|
|
125
|
+
TranslationsFile,
|
|
126
|
+
TranslatedAction,
|
|
127
|
+
TranslatedEntity,
|
|
128
|
+
TranslatedMenu,
|
|
129
|
+
TranslatedOption,
|
|
130
|
+
TranslatedReport,
|
|
131
|
+
TranslatedWithOptions,
|
|
132
|
+
} from "./translations";
|
|
123
133
|
export type { SettingsFile, SettingDef, SettingBaseDatatype } from "./settings";
|
|
124
134
|
export type { JobsFile, JobDef } from "./jobs";
|
|
125
135
|
export type { TriggersFile, TriggerDef, EntityEvent } from "./triggers";
|
package/src/manifest.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Module manifest — authoring shape of `manifest.json`.
|
|
2
2
|
// Mirror of manifest.schema.json.
|
|
3
3
|
|
|
4
|
+
import type { AuditHistoryLevel } from "./entity";
|
|
5
|
+
|
|
4
6
|
/** Author block on a manifest. */
|
|
5
7
|
export interface ManifestAuthor {
|
|
6
8
|
name: string;
|
|
@@ -42,8 +44,8 @@ export interface ManifestDef {
|
|
|
42
44
|
license?: string;
|
|
43
45
|
/** Module code → semver range or partial-dep object. */
|
|
44
46
|
dependencies?: Record<string, ModuleDependency>;
|
|
45
|
-
/** Default audit history
|
|
46
|
-
auditHistory?:
|
|
47
|
+
/** Default audit history level for this module's entities; each entity may override. */
|
|
48
|
+
auditHistory?: AuditHistoryLevel;
|
|
47
49
|
/** Default for `comments` on this module's entities; each entity may override. */
|
|
48
50
|
comments?: boolean;
|
|
49
51
|
/** Entity code → relative path of its JSON file (dotted keys = extensions). */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Translations — authoring shape of `translations/<locale>.json`. Mirror of translations.schema.json.
|
|
2
|
+
|
|
3
|
+
/** A translated option: the label alone, or label with icon / color overrides. */
|
|
4
|
+
export type TranslatedOption = string | { label?: string; icon?: string; color?: string };
|
|
5
|
+
|
|
6
|
+
/** A label with optional option translations (fields, domains, params). */
|
|
7
|
+
export interface TranslatedWithOptions {
|
|
8
|
+
label?: string;
|
|
9
|
+
/** Option value (stored code) → translation. */
|
|
10
|
+
options?: Record<string, TranslatedOption>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TranslatedEntity {
|
|
14
|
+
label?: string;
|
|
15
|
+
desc?: string;
|
|
16
|
+
/** Column code → label and option labels. */
|
|
17
|
+
fields?: Record<string, TranslatedWithOptions>;
|
|
18
|
+
/** Card-layout section code → heading. */
|
|
19
|
+
columnGroups?: Record<string, { label?: string }>;
|
|
20
|
+
/** Constraint name → violation message. */
|
|
21
|
+
constraints?: Record<string, { message?: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TranslatedMenu {
|
|
25
|
+
label?: string;
|
|
26
|
+
/** Item code → label. Flat, even when ui/menus.json nests the items. */
|
|
27
|
+
items?: Record<string, { label?: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TranslatedReport {
|
|
31
|
+
label?: string;
|
|
32
|
+
/** Dataset code → caption. */
|
|
33
|
+
datasets?: Record<string, { caption?: string }>;
|
|
34
|
+
params?: Record<string, TranslatedWithOptions>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TranslatedAction {
|
|
38
|
+
label?: string;
|
|
39
|
+
/** Tooltip. */
|
|
40
|
+
desc?: string;
|
|
41
|
+
params?: Record<string, TranslatedWithOptions>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A `translations/<locale>.json` file. Every key is optional; a gap falls back to the base text. */
|
|
45
|
+
export interface TranslationsFile {
|
|
46
|
+
entities?: Record<string, TranslatedEntity>;
|
|
47
|
+
domains?: Record<string, TranslatedWithOptions>;
|
|
48
|
+
/** Folder code (flat, not the path) → label. */
|
|
49
|
+
folders?: Record<string, { label?: string }>;
|
|
50
|
+
views?: Record<string, { label?: string; desc?: string }>;
|
|
51
|
+
menus?: Record<string, TranslatedMenu>;
|
|
52
|
+
settings?: Record<string, { label?: string; desc?: string }>;
|
|
53
|
+
/** Module-qualified role code → label. */
|
|
54
|
+
roles?: Record<string, { label?: string }>;
|
|
55
|
+
reports?: Record<string, TranslatedReport>;
|
|
56
|
+
actions?: Record<string, TranslatedAction>;
|
|
57
|
+
/** Accepted, not applied at install yet. */
|
|
58
|
+
print_templates?: Record<string, { label?: string }>;
|
|
59
|
+
/** Exact text of a DSL info / warn / error / exit call → translation. */
|
|
60
|
+
messages?: Record<string, string>;
|
|
61
|
+
}
|