@formicoidea/labre-framework-bpmn 0.39.3 → 0.41.0

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/import.d.ts CHANGED
@@ -1,22 +1,34 @@
1
- import type { InterchangeImportContext, InterchangeImportResult } from '@formicoidea/labre-core/blocks/surface';
1
+ import { type InterchangeImportContext, type InterchangeImportResult } from '@formicoidea/labre-core/blocks/surface';
2
2
  import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
3
3
  import { BPMN_SCOPE } from './export.js';
4
4
  /**
5
- * The remarks whose wording is FIXED, as `[key, English]` pairs.
5
+ * The remarks whose wording is FIXED, as `[key, English]` pairs — one entry
6
+ * per SHAPE of sentence, never one per exact string: a remark that names
7
+ * something out of the file (`<boundaryEvent>`, an id, a count of lanes)
8
+ * carries `{{name}}` / `{{count}}` holes in the fallback here, filled at the
9
+ * call site by `messageParams` (`InterchangeNote.messageParams`) — never by
10
+ * minting a new key for every value the file happens to carry.
6
11
  *
7
12
  * A reader is a pure function of text (`docs/adr/0012`, P3) and has no `std`,
8
- * so it cannot ask the host's catalogue for anything: it declares the key on
9
- * the note and `reportInterchangeImport` resolves it when it draws the report
10
- * (`InterchangeNote.messageKey`). The English string stays here and stays the
11
- * fallback, so the console table and a playground with no catalogue read
12
- * exactly what they read before.
13
+ * so it cannot ask the host's catalogue for anything: it declares the key
14
+ * (and, where the sentence has a hole, the params) on the note, and
15
+ * `reportInterchangeImport` resolves both when it draws the report
16
+ * (`InterchangeNote.messageKey` / `.messageParams`). The English string stays
17
+ * here and stays the fallback, so the console table and a playground with no
18
+ * catalogue read exactly what they read before: `translateKey` fills a
19
+ * `{{…}}` hole from `messageParams` even with no `TranslationProvider`
20
+ * registered (`fillPlaceholders`), so the ENGLISH sentence a call site builds
21
+ * is unchanged by giving it a key.
13
22
  *
14
- * These three and no others, and the line is not arbitrary: every remaining
15
- * remark NAMES something out of the file — `<boundaryEvent>`, an id, a count of
16
- * lanes — and the seam has neither interpolation nor pluralisation. A key for
17
- * one of those would be a sentence with holes in it that a translator cannot
18
- * see the shape of, which is the same refusal the interchange count labels
19
- * already make (`interchange-import.ts`).
23
+ * Plural wording is the host's: where a count can be 0, 1 or many, the
24
+ * fallback stays grammatically neutral (`'{{count}} lane(s)…'`) rather than
25
+ * picking English's own singular/plural — the same rule
26
+ * `interchange-import.ts`'s own count labels follow.
27
+ *
28
+ * `BPMN_QUARANTINE_REASON` (declared further down, with its own key pairing)
29
+ * is the one exception: its plain string is ALSO written verbatim into
30
+ * `ForeignInterchange.quarantined[].reason` (data, never translated), so it
31
+ * cannot become a `[key, english]` pair without breaking that contract.
20
32
  *
21
33
  * They are contributed to the manifest by `./translations.ts`, with the
22
34
  * framework, because they ship in the BPMN bundle rather than in core.
@@ -25,6 +37,32 @@ export declare const BPMN_IMPORT_REMARKS: {
25
37
  readonly inventedPool: readonly ["com.labre.bpmn.import.remark.invented-pool", "This file names no participant, so its process was drawn in a pool of Labre's own. The pool is not the file's: exporting writes the process back without one."];
26
38
  readonly laneGap: readonly ["com.labre.bpmn.import.remark.lane-gap", "The lanes of this pool are drawn with a gap or an overlap between them. Labre lays its bands end to end, so their heights were kept in proportion and the space between them was closed."];
27
39
  readonly mustUnderstand: readonly ["com.labre.bpmn.import.remark.must-understand", "The file declares an extension that it says MUST be understood to read the model correctly. Labre does not understand it: the import went ahead, and this reading of the process may be wrong."];
40
+ readonly duplicateId: readonly ["com.labre.bpmn.import.remark.duplicate-id", "Two elements in this file share the id \"{{id}}\", which BPMN requires to be unique across a document. Both were imported; the second will be written back under an id Labre mints."];
41
+ readonly carriedChild: readonly ["com.labre.bpmn.import.remark.carried-child", "<{{tag}}> has no Labre artefact, so it is kept verbatim on the nearest element that has one. It is not drawn, and no validation rule sees it."];
42
+ readonly inventedPoolLayout: readonly ["com.labre.bpmn.import.remark.invented-pool-layout", "The participant \"{{name}}\" arrived with no diagram, so Labre placed its pool beside the drawing."];
43
+ readonly inventedLaneLayout: readonly ["com.labre.bpmn.import.remark.invented-lane-layout", "{{count}} lane(s) arrived with no diagram, so Labre split the pool into equal bands. The proportions are Labre's and not the file's."];
44
+ readonly undrawnTrigger: readonly ["com.labre.bpmn.import.remark.undrawn-trigger", "<{{tag}}> names its trigger by reference to \"{{ref}}\", which Labre does not draw. The event was kept whole rather than drawn as a plain one."];
45
+ readonly inventedNodeLayout: readonly ["com.labre.bpmn.import.remark.invented-node-layout", "<{{tag}}> arrived with no diagram, so Labre placed it beside the drawing. Its position is Labre's and not the file's."];
46
+ readonly singleEndedFlow: readonly ["com.labre.bpmn.import.remark.single-ended-flow", "<{{tag}}> names only one of its two ends, so there is no arrow to draw between them. It was left out."];
47
+ readonly danglingFlowEnd: readonly ["com.labre.bpmn.import.remark.dangling-flow-end", "<{{tag}}> runs to {{ends}}, not drawn on this canvas. The flow is kept whole beside them rather than drawn with a loose end."];
48
+ readonly undeclaredShape: readonly ["com.labre.bpmn.import.remark.undeclared-shape", "The diagram draws \"{{id}}\", which the file does not declare. The shape is kept, and nothing is drawn for it."];
49
+ readonly noProcessResidue: readonly ["com.labre.bpmn.import.remark.no-process-residue", "This file declares {{count}} root element(s) and no process to draw, so there was no artefact for them to be kept on. Nothing was imported."];
50
+ readonly laneDrift: readonly ["com.labre.bpmn.import.remark.lane-drift", "The file lists this artefact in the lane \"{{listedLane}}\" and draws it in \"{{drawnLane}}\". Labre reads the drawing: a lane holds what is drawn inside it."];
51
+ readonly inventedRouting: readonly ["com.labre.bpmn.import.remark.invented-routing", "{{count}} flow(s) carry an explicit routing in the file. Labre routes a flow between its two ends and re-routes it whenever they move, so their bend points are not kept."];
52
+ };
53
+ /**
54
+ * The reader's own refusals, as `[key, English]` pairs — thrown via
55
+ * {@link InterchangeImportError}, never returned, because none of these say
56
+ * "this is a file I can partly read" (see `parseDefinitions` and the
57
+ * choreography refusal, below). Same shape and the same reason as
58
+ * {@link BPMN_IMPORT_REMARKS}: one entry per shape of refusal, `{{name}}`
59
+ * holes filled by `InterchangeImportError.messageParams`.
60
+ */
61
+ export declare const BPMN_IMPORT_ERRORS: {
62
+ readonly malformedXml: readonly ["com.labre.bpmn.import.error.malformed-xml", "This file is not well-formed XML, so no BPMN can be read out of it: {{detail}}"];
63
+ readonly notDefinitions: readonly ["com.labre.bpmn.import.error.not-definitions", "A BPMN file opens on <definitions>; this one opens on <{{tag}}>."];
64
+ readonly wrongNamespace: readonly ["com.labre.bpmn.import.error.wrong-namespace", "This <definitions> is in \"{{namespace}}\", not in BPMN 2.0's (\"{{bpmnNamespace}}\"). A DMN decision model and a BPMN process open on the same element name and are not the same file."];
65
+ readonly declinedRootKind: readonly ["com.labre.bpmn.import.error.declined-root-kind", "This file is a BPMN {{kind}}, which Labre does not draw. Only a process or a collaboration can be imported."];
28
66
  };
29
67
  /**
30
68
  * `startEvent` + `messageEventDefinition` → `startEventMessage`, and the
@@ -60,6 +98,20 @@ export declare const BPMN_QUARANTINE_REASON: {
60
98
  readonly nestedLanes: string;
61
99
  readonly imported: string;
62
100
  };
101
+ /**
102
+ * {@link BPMN_QUARANTINE_REASON}'s own i18n keys, one per reason.
103
+ *
104
+ * A SEPARATE table rather than folding the key into each reason string
105
+ * (`BPMN_IMPORT_REMARKS`'s `[key, english]` shape): `BPMN_QUARANTINE_REASON`'s
106
+ * plain strings are ALSO written verbatim into
107
+ * `ForeignInterchange.quarantined[].reason` — a DATA field the reader hands
108
+ * the writer, never re-emitted, never shown, and pinned in
109
+ * `import.unit.spec.ts` as the literal English text — so it cannot become a
110
+ * tuple without breaking that contract. Only the REPORT note (`messageKey`
111
+ * below) is resolved through the host's catalogue; the carried `reason` data
112
+ * stays English, exactly as it always has.
113
+ */
114
+ export declare const BPMN_QUARANTINE_REASON_KEY: Record<keyof typeof BPMN_QUARANTINE_REASON, string>;
63
115
  /**
64
116
  * Read a BPMN 2.0 interchange document as element props plus a report.
65
117
  *
package/dist/import.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { InterchangeImportError, } from '@formicoidea/labre-core/blocks/surface';
1
2
  import { ConnectorMode, PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
2
3
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
4
  import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_SIZE, POOL_BAND_WIDTH, POOL_REF_HEIGHT, POOL_REF_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
@@ -5,21 +6,33 @@ import { BPMN_FORMAT_ID, BPMN_NS, BPMN_OWN_DECLARATIONS, BPMN_SCOPE, BPMN_XML_OF
5
6
  import { bpmnNodeProps } from './presets.js';
6
7
  import { BPMN_ROLE } from './roles.js';
7
8
  /**
8
- * The remarks whose wording is FIXED, as `[key, English]` pairs.
9
+ * The remarks whose wording is FIXED, as `[key, English]` pairs — one entry
10
+ * per SHAPE of sentence, never one per exact string: a remark that names
11
+ * something out of the file (`<boundaryEvent>`, an id, a count of lanes)
12
+ * carries `{{name}}` / `{{count}}` holes in the fallback here, filled at the
13
+ * call site by `messageParams` (`InterchangeNote.messageParams`) — never by
14
+ * minting a new key for every value the file happens to carry.
9
15
  *
10
16
  * A reader is a pure function of text (`docs/adr/0012`, P3) and has no `std`,
11
- * so it cannot ask the host's catalogue for anything: it declares the key on
12
- * the note and `reportInterchangeImport` resolves it when it draws the report
13
- * (`InterchangeNote.messageKey`). The English string stays here and stays the
14
- * fallback, so the console table and a playground with no catalogue read
15
- * exactly what they read before.
17
+ * so it cannot ask the host's catalogue for anything: it declares the key
18
+ * (and, where the sentence has a hole, the params) on the note, and
19
+ * `reportInterchangeImport` resolves both when it draws the report
20
+ * (`InterchangeNote.messageKey` / `.messageParams`). The English string stays
21
+ * here and stays the fallback, so the console table and a playground with no
22
+ * catalogue read exactly what they read before: `translateKey` fills a
23
+ * `{{…}}` hole from `messageParams` even with no `TranslationProvider`
24
+ * registered (`fillPlaceholders`), so the ENGLISH sentence a call site builds
25
+ * is unchanged by giving it a key.
16
26
  *
17
- * These three and no others, and the line is not arbitrary: every remaining
18
- * remark NAMES something out of the file — `<boundaryEvent>`, an id, a count of
19
- * lanes — and the seam has neither interpolation nor pluralisation. A key for
20
- * one of those would be a sentence with holes in it that a translator cannot
21
- * see the shape of, which is the same refusal the interchange count labels
22
- * already make (`interchange-import.ts`).
27
+ * Plural wording is the host's: where a count can be 0, 1 or many, the
28
+ * fallback stays grammatically neutral (`'{{count}} lane(s)…'`) rather than
29
+ * picking English's own singular/plural — the same rule
30
+ * `interchange-import.ts`'s own count labels follow.
31
+ *
32
+ * `BPMN_QUARANTINE_REASON` (declared further down, with its own key pairing)
33
+ * is the one exception: its plain string is ALSO written verbatim into
34
+ * `ForeignInterchange.quarantined[].reason` (data, never translated), so it
35
+ * cannot become a `[key, english]` pair without breaking that contract.
23
36
  *
24
37
  * They are contributed to the manifest by `./translations.ts`, with the
25
38
  * framework, because they ship in the BPMN bundle rather than in core.
@@ -37,6 +50,80 @@ export const BPMN_IMPORT_REMARKS = {
37
50
  'com.labre.bpmn.import.remark.must-understand',
38
51
  'The file declares an extension that it says MUST be understood to read the model correctly. Labre does not understand it: the import went ahead, and this reading of the process may be wrong.',
39
52
  ],
53
+ duplicateId: [
54
+ 'com.labre.bpmn.import.remark.duplicate-id',
55
+ 'Two elements in this file share the id "{{id}}", which BPMN requires to be unique across a document. Both were imported; the second will be written back under an id Labre mints.',
56
+ ],
57
+ carriedChild: [
58
+ 'com.labre.bpmn.import.remark.carried-child',
59
+ '<{{tag}}> has no Labre artefact, so it is kept verbatim on the nearest element that has one. It is not drawn, and no validation rule sees it.',
60
+ ],
61
+ inventedPoolLayout: [
62
+ 'com.labre.bpmn.import.remark.invented-pool-layout',
63
+ 'The participant "{{name}}" arrived with no diagram, so Labre placed its pool beside the drawing.',
64
+ ],
65
+ inventedLaneLayout: [
66
+ 'com.labre.bpmn.import.remark.invented-lane-layout',
67
+ "{{count}} lane(s) arrived with no diagram, so Labre split the pool into equal bands. The proportions are Labre's and not the file's.",
68
+ ],
69
+ undrawnTrigger: [
70
+ 'com.labre.bpmn.import.remark.undrawn-trigger',
71
+ '<{{tag}}> names its trigger by reference to "{{ref}}", which Labre does not draw. The event was kept whole rather than drawn as a plain one.',
72
+ ],
73
+ inventedNodeLayout: [
74
+ 'com.labre.bpmn.import.remark.invented-node-layout',
75
+ "<{{tag}}> arrived with no diagram, so Labre placed it beside the drawing. Its position is Labre's and not the file's.",
76
+ ],
77
+ singleEndedFlow: [
78
+ 'com.labre.bpmn.import.remark.single-ended-flow',
79
+ '<{{tag}}> names only one of its two ends, so there is no arrow to draw between them. It was left out.',
80
+ ],
81
+ danglingFlowEnd: [
82
+ 'com.labre.bpmn.import.remark.dangling-flow-end',
83
+ '<{{tag}}> runs to {{ends}}, not drawn on this canvas. The flow is kept whole beside them rather than drawn with a loose end.',
84
+ ],
85
+ undeclaredShape: [
86
+ 'com.labre.bpmn.import.remark.undeclared-shape',
87
+ 'The diagram draws "{{id}}", which the file does not declare. The shape is kept, and nothing is drawn for it.',
88
+ ],
89
+ noProcessResidue: [
90
+ 'com.labre.bpmn.import.remark.no-process-residue',
91
+ 'This file declares {{count}} root element(s) and no process to draw, so there was no artefact for them to be kept on. Nothing was imported.',
92
+ ],
93
+ laneDrift: [
94
+ 'com.labre.bpmn.import.remark.lane-drift',
95
+ 'The file lists this artefact in the lane "{{listedLane}}" and draws it in "{{drawnLane}}". Labre reads the drawing: a lane holds what is drawn inside it.',
96
+ ],
97
+ inventedRouting: [
98
+ 'com.labre.bpmn.import.remark.invented-routing',
99
+ '{{count}} flow(s) carry an explicit routing in the file. Labre routes a flow between its two ends and re-routes it whenever they move, so their bend points are not kept.',
100
+ ],
101
+ };
102
+ /**
103
+ * The reader's own refusals, as `[key, English]` pairs — thrown via
104
+ * {@link InterchangeImportError}, never returned, because none of these say
105
+ * "this is a file I can partly read" (see `parseDefinitions` and the
106
+ * choreography refusal, below). Same shape and the same reason as
107
+ * {@link BPMN_IMPORT_REMARKS}: one entry per shape of refusal, `{{name}}`
108
+ * holes filled by `InterchangeImportError.messageParams`.
109
+ */
110
+ export const BPMN_IMPORT_ERRORS = {
111
+ malformedXml: [
112
+ 'com.labre.bpmn.import.error.malformed-xml',
113
+ 'This file is not well-formed XML, so no BPMN can be read out of it: {{detail}}',
114
+ ],
115
+ notDefinitions: [
116
+ 'com.labre.bpmn.import.error.not-definitions',
117
+ 'A BPMN file opens on <definitions>; this one opens on <{{tag}}>.',
118
+ ],
119
+ wrongNamespace: [
120
+ 'com.labre.bpmn.import.error.wrong-namespace',
121
+ 'This <definitions> is in "{{namespace}}", not in BPMN 2.0\'s ("{{bpmnNamespace}}"). A DMN decision model and a BPMN process open on the same element name and are not the same file.',
122
+ ],
123
+ declinedRootKind: [
124
+ 'com.labre.bpmn.import.error.declined-root-kind',
125
+ 'This file is a BPMN {{kind}}, which Labre does not draw. Only a process or a collaboration can be imported.',
126
+ ],
40
127
  };
41
128
  /**
42
129
  * A BPMN 2.0 interchange document, read as a board — the inverse of `export.ts`
@@ -223,6 +310,25 @@ export const BPMN_QUARANTINE_REASON = {
223
310
  'self-contained set), so writing this back would claim a resolution of ' +
224
311
  'another document that never happened.',
225
312
  };
313
+ /**
314
+ * {@link BPMN_QUARANTINE_REASON}'s own i18n keys, one per reason.
315
+ *
316
+ * A SEPARATE table rather than folding the key into each reason string
317
+ * (`BPMN_IMPORT_REMARKS`'s `[key, english]` shape): `BPMN_QUARANTINE_REASON`'s
318
+ * plain strings are ALSO written verbatim into
319
+ * `ForeignInterchange.quarantined[].reason` — a DATA field the reader hands
320
+ * the writer, never re-emitted, never shown, and pinned in
321
+ * `import.unit.spec.ts` as the literal English text — so it cannot become a
322
+ * tuple without breaking that contract. Only the REPORT note (`messageKey`
323
+ * below) is resolved through the host's catalogue; the carried `reason` data
324
+ * stays English, exactly as it always has.
325
+ */
326
+ export const BPMN_QUARANTINE_REASON_KEY = {
327
+ colour: 'com.labre.bpmn.import.quarantine.colour',
328
+ expanded: 'com.labre.bpmn.import.quarantine.expanded',
329
+ nestedLanes: 'com.labre.bpmn.import.quarantine.nested-lanes',
330
+ imported: 'com.labre.bpmn.import.quarantine.imported',
331
+ };
226
332
  /**
227
333
  * The children of an activity that are NOT its body.
228
334
  *
@@ -344,21 +450,32 @@ function parseDefinitions(source) {
344
450
  const doc = new DOMParser().parseFromString(source, 'application/xml');
345
451
  const error = doc.querySelector('parsererror');
346
452
  if (error) {
347
- throw new Error(`This file is not well-formed XML, so no BPMN can be read out of it: ` +
348
- `${(error.textContent ?? '').trim().slice(0, 200)}`);
453
+ const detail = (error.textContent ?? '').trim().slice(0, 200);
454
+ throw new InterchangeImportError(`This file is not well-formed XML, so no BPMN can be read out of it: ${detail}`, {
455
+ messageKey: BPMN_IMPORT_ERRORS.malformedXml[0],
456
+ messageParams: { detail },
457
+ });
349
458
  }
350
459
  const root = doc.documentElement;
351
460
  if (!root || root.localName !== 'definitions') {
352
- throw new Error(`A BPMN file opens on <definitions>; this one opens on <${root?.localName ?? 'nothing'}>.`);
461
+ const tag = root?.localName ?? 'nothing';
462
+ throw new InterchangeImportError(`A BPMN file opens on <definitions>; this one opens on <${tag}>.`, {
463
+ messageKey: BPMN_IMPORT_ERRORS.notDefinitions[0],
464
+ messageParams: { tag },
465
+ });
353
466
  }
354
467
  // The NAMESPACE, not the element name — `<definitions>` is also the root of a
355
468
  // DMN decision model, and of anything else built on the same OMG scaffolding.
356
469
  // Without this a `.dmn` imports as an empty board, which is exactly the
357
470
  // "three zeroes claiming an empty process" this reader refuses to return.
358
471
  if (root.namespaceURI !== BPMN_NS.model) {
359
- throw new Error(`This <definitions> is in "${root.namespaceURI ?? 'no namespace'}", not ` +
472
+ const namespace = root.namespaceURI ?? 'no namespace';
473
+ throw new InterchangeImportError(`This <definitions> is in "${namespace}", not ` +
360
474
  `in BPMN 2.0's ("${BPMN_NS.model}"). A DMN decision model and a BPMN ` +
361
- `process open on the same element name and are not the same file.`);
475
+ `process open on the same element name and are not the same file.`, {
476
+ messageKey: BPMN_IMPORT_ERRORS.wrongNamespace[0],
477
+ messageParams: { namespace, bpmnNamespace: BPMN_NS.model },
478
+ });
362
479
  }
363
480
  return root;
364
481
  }
@@ -612,8 +729,12 @@ export function importBpmnXml(source, context = {}) {
612
729
  if (processes.length === 0 && collaborations.length === 0) {
613
730
  const declined = roots.find(root => ['choreography', 'globalChoreographyTask', 'conversation'].includes(root.localName));
614
731
  if (declined) {
615
- throw new Error(`This file is a BPMN ${declined.localName}, which Labre does not draw. ` +
616
- `Only a process or a collaboration can be imported.`);
732
+ const kind = declined.localName;
733
+ throw new InterchangeImportError(`This file is a BPMN ${kind}, which Labre does not draw. ` +
734
+ `Only a process or a collaboration can be imported.`, {
735
+ messageKey: BPMN_IMPORT_ERRORS.declinedRootKind[0],
736
+ messageParams: { kind },
737
+ });
617
738
  }
618
739
  }
619
740
  /** `categoryValue` id → the label a `group` pointing at it wears. */
@@ -637,9 +758,9 @@ export function importBpmnXml(source, context = {}) {
637
758
  kind: 'substituted-id',
638
759
  sourceId,
639
760
  element,
640
- message: `Two elements in this file share the id "${sourceId}", which BPMN ` +
641
- `requires to be unique across a document. Both were imported; the ` +
642
- `second will be written back under an id Labre mints.`,
761
+ messageKey: BPMN_IMPORT_REMARKS.duplicateId[0],
762
+ message: `Two elements in this file share the id "${sourceId}", which BPMN requires to be unique across a document. Both were imported; the second will be written back under an id Labre mints.`,
763
+ messageParams: { id: sourceId },
643
764
  });
644
765
  }
645
766
  seenSourceIds.add(sourceId);
@@ -671,9 +792,9 @@ export function importBpmnXml(source, context = {}) {
671
792
  kind: 'carried',
672
793
  element: child.nodeName,
673
794
  sourceId,
674
- message: `<${child.nodeName}> has no Labre artefact, so it is kept verbatim on ` +
675
- `the nearest element that has one. It is not drawn, and no validation ` +
676
- `rule sees it.`,
795
+ messageKey: BPMN_IMPORT_REMARKS.carriedChild[0],
796
+ message: `<${child.nodeName}> has no Labre artefact, so it is kept verbatim on the nearest element that has one. It is not drawn, and no validation rule sees it.`,
797
+ messageParams: { tag: child.nodeName },
677
798
  });
678
799
  };
679
800
  /** One diagram fragment, kept under the scope of what it DRAWS. */
@@ -684,13 +805,23 @@ export function importBpmnXml(source, context = {}) {
684
805
  };
685
806
  carried++;
686
807
  };
687
- const quarantine = (payload, fragment, reason, entry) => {
808
+ const quarantine = (payload, fragment, reason, entry,
809
+ // The reason's OWN key ({@link BPMN_QUARANTINE_REASON_KEY}) — separate
810
+ // from `reason` because that string is also written into
811
+ // `payload.quarantined[].reason` (data, never translated); only the
812
+ // REPORT note below carries the key.
813
+ reasonKey) => {
688
814
  payload.quarantined = [
689
815
  ...(payload.quarantined ?? []),
690
816
  { fragment, reason },
691
817
  ];
692
818
  quarantined++;
693
- note({ kind: 'quarantined', ...entry, message: reason });
819
+ note({
820
+ kind: 'quarantined',
821
+ ...entry,
822
+ message: reason,
823
+ ...(reasonKey ? { messageKey: reasonKey } : {}),
824
+ });
694
825
  };
695
826
  /** Every attribute the reader does not model — colours quarantined (D5). */
696
827
  const sortAttributes = (element, payload, scope, sourceId, understood = READ_ATTRS[element.localName] ??
@@ -701,7 +832,7 @@ export function importBpmnXml(source, context = {}) {
701
832
  if (understood.includes(attr.name))
702
833
  continue;
703
834
  if (isColourAttr(attr)) {
704
- quarantine(payload, `${attr.name}="${escapeAttr(attr.value)}"`, BPMN_QUARANTINE_REASON.colour, { sourceId, element: attr.name });
835
+ quarantine(payload, `${attr.name}="${escapeAttr(attr.value)}"`, BPMN_QUARANTINE_REASON.colour, { sourceId, element: attr.name }, BPMN_QUARANTINE_REASON_KEY.colour);
705
836
  continue;
706
837
  }
707
838
  carryAttr(payload, scope, attr.name, attr.value);
@@ -792,12 +923,14 @@ export function importBpmnXml(source, context = {}) {
792
923
  if (process)
793
924
  poolOfProcess.set(process, draft);
794
925
  if (!bounds) {
926
+ const name = draft.props.name || 'unnamed';
795
927
  note({
796
928
  kind: 'invented-layout',
797
929
  sourceId,
798
930
  element: 'participant',
799
- message: `The participant "${draft.props.name || 'unnamed'}" arrived with no ` +
800
- `diagram, so Labre placed its pool beside the drawing.`,
931
+ messageKey: BPMN_IMPORT_REMARKS.inventedPoolLayout[0],
932
+ message: `The participant "${name}" arrived with no diagram, so Labre placed its pool beside the drawing.`,
933
+ messageParams: { name },
801
934
  });
802
935
  }
803
936
  }
@@ -887,7 +1020,7 @@ export function importBpmnXml(source, context = {}) {
887
1020
  const name = attrOf(lane, 'name') ?? '';
888
1021
  const nested = modelChild(lane, 'childLaneSet');
889
1022
  if (nested) {
890
- quarantine(pool.payload, fragmentOf(nested), BPMN_QUARANTINE_REASON.nestedLanes, { sourceId, element: 'childLaneSet' });
1023
+ quarantine(pool.payload, fragmentOf(nested), BPMN_QUARANTINE_REASON.nestedLanes, { sourceId, element: 'childLaneSet' }, BPMN_QUARANTINE_REASON_KEY.nestedLanes);
891
1024
  // A lane holding a child set is not a band Labre paints — its LEAVES
892
1025
  // are — so the shape drawing it describes a subdivision the flat pool
893
1026
  // does not have. Accounted for by the quarantine, and kept out of the
@@ -959,9 +1092,9 @@ export function importBpmnXml(source, context = {}) {
959
1092
  kind: 'invented-layout',
960
1093
  sourceId: attrOf(laneSet, 'id'),
961
1094
  element: 'laneSet',
962
- message: `${bands.length === 1 ? 'This lane' : `Some of these ${bands.length} lanes`} ` +
963
- `arrived with no diagram, so Labre split the pool into equal bands. ` +
964
- `The proportions are Labre's and not the file's.`,
1095
+ messageKey: BPMN_IMPORT_REMARKS.inventedLaneLayout[0],
1096
+ message: `${bands.length} lane(s) arrived with no diagram, so Labre split the pool into equal bands. The proportions are Labre's and not the file's.`,
1097
+ messageParams: { count: bands.length },
965
1098
  });
966
1099
  }
967
1100
  // Bands that do not tile the pool are still only WEIGHTS here — Labre lays
@@ -1035,13 +1168,14 @@ export function importBpmnXml(source, context = {}) {
1035
1168
  // whole into the carried branch below rather than onto the canvas claiming
1036
1169
  // something the file did not say.
1037
1170
  if (kind === undefined && triggerRef !== undefined) {
1171
+ const ref = triggerRef.textContent?.trim() ?? '';
1038
1172
  note({
1039
1173
  kind: 'warning',
1040
1174
  sourceId,
1041
1175
  element: local,
1042
- message: `<${local}> names its trigger by reference to ` +
1043
- `"${triggerRef.textContent?.trim() ?? ''}", which Labre does not ` +
1044
- `draw. The event was kept whole rather than drawn as a plain one.`,
1176
+ messageKey: BPMN_IMPORT_REMARKS.undrawnTrigger[0],
1177
+ message: `<${local}> names its trigger by reference to "${ref}", which Labre does not draw. The event was kept whole rather than drawn as a plain one.`,
1178
+ messageParams: { tag: local, ref },
1045
1179
  });
1046
1180
  }
1047
1181
  if (kind === undefined) {
@@ -1100,7 +1234,7 @@ export function importBpmnXml(source, context = {}) {
1100
1234
  continue;
1101
1235
  }
1102
1236
  if (expanded && isModel(child) && !NOT_A_BODY.has(child.localName)) {
1103
- quarantine(payload, fragmentOf(child), BPMN_QUARANTINE_REASON.expanded, { sourceId, element: child.nodeName });
1237
+ quarantine(payload, fragmentOf(child), BPMN_QUARANTINE_REASON.expanded, { sourceId, element: child.nodeName }, BPMN_QUARANTINE_REASON_KEY.expanded);
1104
1238
  // The body's diagram goes with the body, all the way down. A shape left
1105
1239
  // behind here is an ORPHAN — nothing declares what it draws any more —
1106
1240
  // so the residue sweep at the end of this reader would pick it up and
@@ -1152,8 +1286,9 @@ export function importBpmnXml(source, context = {}) {
1152
1286
  kind: 'invented-layout',
1153
1287
  sourceId,
1154
1288
  element: local,
1155
- message: `<${local}> arrived with no diagram, so Labre placed it beside the ` +
1156
- `drawing. Its position is Labre's and not the file's.`,
1289
+ messageKey: BPMN_IMPORT_REMARKS.inventedNodeLayout[0],
1290
+ message: `<${local}> arrived with no diagram, so Labre placed it beside the drawing. Its position is Labre's and not the file's.`,
1291
+ messageParams: { tag: local },
1157
1292
  });
1158
1293
  }
1159
1294
  };
@@ -1188,8 +1323,9 @@ export function importBpmnXml(source, context = {}) {
1188
1323
  kind: 'warning',
1189
1324
  sourceId,
1190
1325
  element: local,
1191
- message: `<${local}> names only one of its two ends, so there is no arrow ` +
1192
- `to draw between them. It was left out.`,
1326
+ messageKey: BPMN_IMPORT_REMARKS.singleEndedFlow[0],
1327
+ message: `<${local}> names only one of its two ends, so there is no arrow to draw between them. It was left out.`,
1328
+ messageParams: { tag: local },
1193
1329
  });
1194
1330
  continue;
1195
1331
  }
@@ -1208,16 +1344,17 @@ export function importBpmnXml(source, context = {}) {
1208
1344
  if (edgeDi) {
1209
1345
  carryDi(host.payload, sourceId ?? hostScope, fragmentOf(edgeDi.element));
1210
1346
  }
1211
- note({
1212
- kind: 'warning',
1213
- sourceId,
1214
- element: local,
1215
- message: `<${local}> runs to ${dangling.map(end => `"${end}"`).join(' and ')}, ` +
1216
- `which ${dangling.length === 1 ? 'is' : 'are'} not drawn on this ` +
1217
- `canvas. The flow is kept whole beside ` +
1218
- `${dangling.length === 1 ? 'it' : 'them'} rather than drawn with a ` +
1219
- `loose end.`,
1220
- });
1347
+ {
1348
+ const ends = dangling.map(end => `"${end}"`).join(' and ');
1349
+ note({
1350
+ kind: 'warning',
1351
+ sourceId,
1352
+ element: local,
1353
+ messageKey: BPMN_IMPORT_REMARKS.danglingFlowEnd[0],
1354
+ message: `<${local}> runs to ${ends}, not drawn on this canvas. The flow is kept whole beside them rather than drawn with a loose end.`,
1355
+ messageParams: { tag: local, ends },
1356
+ });
1357
+ }
1221
1358
  continue;
1222
1359
  }
1223
1360
  claim(sourceId, local);
@@ -1340,7 +1477,7 @@ export function importBpmnXml(source, context = {}) {
1340
1477
  // file, so writing an `<import>` back would claim a resolution we never
1341
1478
  // made.
1342
1479
  if (isModel(root) && root.localName === 'import') {
1343
- quarantine(host.payload, fragmentOf(root), BPMN_QUARANTINE_REASON.imported, { element: 'import' });
1480
+ quarantine(host.payload, fragmentOf(root), BPMN_QUARANTINE_REASON.imported, { element: 'import' }, BPMN_QUARANTINE_REASON_KEY.imported);
1344
1481
  continue;
1345
1482
  }
1346
1483
  // Not quarantine: the file telling us our reading of it may be wrong,
@@ -1376,8 +1513,9 @@ export function importBpmnXml(source, context = {}) {
1376
1513
  kind: 'warning',
1377
1514
  sourceId: target,
1378
1515
  element: shape.localName,
1379
- message: `The diagram draws "${target}", which the file does not declare. ` +
1380
- `The shape is kept, and nothing is drawn for it.`,
1516
+ messageKey: BPMN_IMPORT_REMARKS.undeclaredShape[0],
1517
+ message: `The diagram draws "${target}", which the file does not declare. The shape is kept, and nothing is drawn for it.`,
1518
+ messageParams: { id: target },
1381
1519
  });
1382
1520
  }
1383
1521
  }
@@ -1387,11 +1525,9 @@ export function importBpmnXml(source, context = {}) {
1387
1525
  // process and no participant has none.
1388
1526
  note({
1389
1527
  kind: 'warning',
1390
- message: `This file declares ${residue.length} root ` +
1391
- `${residue.length === 1 ? 'element' : 'elements'} and no process to ` +
1392
- `draw, so there was no artefact for ` +
1393
- `${residue.length === 1 ? 'it' : 'them'} to be kept on. Nothing was ` +
1394
- `imported.`,
1528
+ messageKey: BPMN_IMPORT_REMARKS.noProcessResidue[0],
1529
+ message: `This file declares ${residue.length} root element(s) and no process to draw, so there was no artefact for them to be kept on. Nothing was imported.`,
1530
+ messageParams: { count: residue.length },
1395
1531
  });
1396
1532
  }
1397
1533
  /* ── Lane membership: checked against the drawing, never stored (D3) ─ */
@@ -1413,9 +1549,12 @@ export function importBpmnXml(source, context = {}) {
1413
1549
  kind: 'warning',
1414
1550
  sourceId: ref,
1415
1551
  element: 'flowNodeRef',
1416
- message: `The file lists this artefact in the lane "${band.lane.name}" and ` +
1417
- `draws it in "${drawnIn.lane.name}". Labre reads the drawing: a ` +
1418
- `lane holds what is drawn inside it.`,
1552
+ messageKey: BPMN_IMPORT_REMARKS.laneDrift[0],
1553
+ message: `The file lists this artefact in the lane "${band.lane.name ?? ''}" and draws it in "${drawnIn.lane.name ?? ''}". Labre reads the drawing: a lane holds what is drawn inside it.`,
1554
+ messageParams: {
1555
+ listedLane: band.lane.name ?? '',
1556
+ drawnLane: drawnIn.lane.name ?? '',
1557
+ },
1419
1558
  });
1420
1559
  }
1421
1560
  }
@@ -1425,11 +1564,9 @@ export function importBpmnXml(source, context = {}) {
1425
1564
  if (explicitRoutes > 0) {
1426
1565
  note({
1427
1566
  kind: 'invented-layout',
1428
- message: `${explicitRoutes} ${explicitRoutes === 1 ? 'flow carries' : 'flows carry'} ` +
1429
- `an explicit routing in the file. Labre routes a flow between its two ` +
1430
- `ends and re-routes it whenever they move, so ` +
1431
- `${explicitRoutes === 1 ? 'its bend points are' : 'their bend points are'} ` +
1432
- `not kept.`,
1567
+ messageKey: BPMN_IMPORT_REMARKS.inventedRouting[0],
1568
+ message: `${explicitRoutes} flow(s) carry an explicit routing in the file. Labre routes a flow between its two ends and re-routes it whenever they move, so their bend points are not kept.`,
1569
+ messageParams: { count: explicitRoutes },
1433
1570
  });
1434
1571
  }
1435
1572
  /* ── Out ───────────────────────────────────────────────────────────── */
@@ -1,5 +1,6 @@
1
1
  import { interchangeCapabilityId, parseSvgSketch, SVG_SKETCH_EXTENSION, SVG_SKETCH_FORMAT_ID, SVG_SKETCH_MIME, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { BpmnNodeElementModel, BpmnPoolElementModel, ConnectorElementModel, } from '@formicoidea/labre-core/model';
3
+ import { fillPlaceholders } from '@formicoidea/labre-core/shared/services';
3
4
  import { BPMN_FORMAT_ID, exportBpmnXmlWithWarnings, } from './export.js';
4
5
  import { importBpmnXml } from './import.js';
5
6
  /**
@@ -115,7 +116,18 @@ const runBpmnXmlExport = (elements, context) => {
115
116
  text,
116
117
  filename: `${name}${BPMN_XML_EXTENSION}`,
117
118
  mime: BPMN_XML_MIME,
118
- ...(warnings.length > 0 ? { warnings } : {}),
119
+ // `InterchangeExportResult.warnings` is `readonly string[]` — a seam this
120
+ // capability shares with every other format and does not own — so each
121
+ // KEYED warning (`BpmnExportWarning`, `export.ts`) is resolved to its
122
+ // English fallback here. This adapter is `std`-free like the exporter it
123
+ // wraps (P3), so it cannot ask a host's catalogue either; the translated
124
+ // command path is `exportBpmnXmlFile` (`actions.ts`), which reads the
125
+ // structured warnings directly instead of going through this capability.
126
+ ...(warnings.length > 0
127
+ ? {
128
+ warnings: warnings.map(w => fillPlaceholders(w.fallback, w.params)),
129
+ }
130
+ : {}),
119
131
  };
120
132
  };
121
133
  /** `bpmn:bpmn:export` — the registry's first capability. */