@formicoidea/labre-framework-bpmn 0.40.0 → 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/actions.d.ts CHANGED
@@ -79,16 +79,16 @@ export declare function bpmnPoolsForLaneEdit(std: BlockStdScope): BpmnPoolElemen
79
79
  * Before it, a single lane was indistinguishable from no lane at all, and the
80
80
  * gesture looked broken until the second click.
81
81
  *
82
- * ## `Lane N` is DOCUMENT DATA, not vocabulary
82
+ * ## `Lane N` is a SEED, resolved once at placement
83
83
  *
84
84
  * The default name is a plain persisted string, exactly like the pool's own
85
85
  * `'Pool'` default: it is written into the document by this action and is the
86
- * user's to rewrite from that moment on. It is deliberately NOT a `labelKey`
87
- * through the translation seam — a host that ships a French catalogue must not
88
- * silently retitle a lane an author named, and a name that changed language
89
- * when the reader's locale did would be a document that says different things
90
- * to different people. `N` is the count AFTER this lane, so the first is
91
- * `Lane 1`.
86
+ * user's to rewrite from that moment on. Since the i18n pass (#278) it goes
87
+ * through the translation seam exactly as `nodeLabelKey` does for a node's
88
+ * caption — resolved HERE, once, so a lane added in a translated host starts
89
+ * in that language, and never re-resolved afterwards: a lane the author has
90
+ * since renamed is document content, and nothing here revisits it. `{{n}}` is
91
+ * the count AFTER this lane, so the first is `Lane 1`.
92
92
  */
93
93
  export declare function addBpmnLane(std: BlockStdScope): void;
94
94
  /**
package/dist/actions.js CHANGED
@@ -5,8 +5,8 @@ import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/sha
5
5
  import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
6
6
  import { Bound } from '@formicoidea/labre-core/global/gfx';
7
7
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
8
- import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_LABEL, nodeLabelKey, NODE_SIZE, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
9
- import { BPMN_FORMAT_ID } from './export.js';
8
+ import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, LANE_NAME_FALLBACK, LANE_NAME_KEY, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_LABEL, nodeLabelKey, NODE_SIZE, POOL_NAME_FALLBACK, POOL_NAME_KEY, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
9
+ import { BPMN_FORMAT_ID, exportBpmnXmlWithWarnings, } from './export.js';
10
10
  import { BPMN_SVG_IMPORT, BPMN_XML_EXPORT, BPMN_XML_FORMAT, BPMN_XML_IMPORT, bpmnBoardFrom, bpmnSafeFilename, } from './interchange.js';
11
11
  import { bpmnNodeProps, bpmnPoolProps } from './presets.js';
12
12
  import { BPMN_ROLE } from './roles.js';
@@ -60,7 +60,16 @@ export function createBpmnPool(std) {
60
60
  // and for the same reason: the shipped cards build their participants from it
61
61
  // too, so a pool laid out by a template and one dropped from the toolbox
62
62
  // cannot say different things. The gesture owns the BOX and nothing else.
63
- const id = surface.addElement(bpmnPoolProps({ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize() }));
63
+ //
64
+ // The NAME is the one exception, and it is written HERE rather than left to
65
+ // the model's default (`BpmnPoolElementModel.name = 'Pool'`, a red-zone
66
+ // field: a document created before this key existed keeps its literal
67
+ // 'Pool' verbatim). Resolved at placement and never again — content from
68
+ // the moment it lands, exactly like a node's caption.
69
+ const id = surface.addElement(bpmnPoolProps({
70
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
71
+ name: translateKey(std, POOL_NAME_KEY, POOL_NAME_FALLBACK),
72
+ }));
64
73
  finish(gfx, id);
65
74
  }
66
75
  /**
@@ -217,16 +226,16 @@ function writeLanes(std, model, lanes) {
217
226
  * Before it, a single lane was indistinguishable from no lane at all, and the
218
227
  * gesture looked broken until the second click.
219
228
  *
220
- * ## `Lane N` is DOCUMENT DATA, not vocabulary
229
+ * ## `Lane N` is a SEED, resolved once at placement
221
230
  *
222
231
  * The default name is a plain persisted string, exactly like the pool's own
223
232
  * `'Pool'` default: it is written into the document by this action and is the
224
- * user's to rewrite from that moment on. It is deliberately NOT a `labelKey`
225
- * through the translation seam — a host that ships a French catalogue must not
226
- * silently retitle a lane an author named, and a name that changed language
227
- * when the reader's locale did would be a document that says different things
228
- * to different people. `N` is the count AFTER this lane, so the first is
229
- * `Lane 1`.
233
+ * user's to rewrite from that moment on. Since the i18n pass (#278) it goes
234
+ * through the translation seam exactly as `nodeLabelKey` does for a node's
235
+ * caption — resolved HERE, once, so a lane added in a translated host starts
236
+ * in that language, and never re-resolved afterwards: a lane the author has
237
+ * since renamed is document content, and nothing here revisits it. `{{n}}` is
238
+ * the count AFTER this lane, so the first is `Lane 1`.
230
239
  */
231
240
  export function addBpmnLane(std) {
232
241
  const pools = bpmnPoolsForLaneEdit(std);
@@ -245,9 +254,14 @@ export function addBpmnLane(std) {
245
254
  const size = lanes.length
246
255
  ? lanes.reduce((sum, lane) => sum + lane.size, 0) / lanes.length
247
256
  : 1;
257
+ const n = lanes.length + 1;
248
258
  writeLanes(std, model, [
249
259
  ...lanes,
250
- { id: generateElementId(), name: `Lane ${lanes.length + 1}`, size },
260
+ {
261
+ id: generateElementId(),
262
+ name: translateKey(std, LANE_NAME_KEY, LANE_NAME_FALLBACK, { n }),
263
+ size,
264
+ },
251
265
  ]);
252
266
  }
253
267
  }
@@ -335,9 +349,8 @@ export function bpmnExportFilename(std) {
335
349
  */
336
350
  export function exportBpmnXmlFile(std) {
337
351
  const elements = gfxOf(std).surface?.elementModels ?? [];
338
- const { text, filename, mime, warnings } = BPMN_XML_EXPORT.run(elements, {
339
- name: bpmnExportFilename(std),
340
- });
352
+ const name = bpmnExportFilename(std);
353
+ const { text, filename, mime } = BPMN_XML_EXPORT.run(elements, { name });
341
354
  // The charset is the browser's business, not the format's: `mime` is what
342
355
  // `.bpmn` IS, and this is how a blob is told to carry it.
343
356
  downloadBlob(new Blob([text], { type: `${mime};charset=utf-8` }), filename);
@@ -347,11 +360,25 @@ export function exportBpmnXmlFile(std) {
347
360
  // Export is the one person entitled to be told" false in the only place a
348
361
  // user stands. A warning is never an error: the file downloaded, and it is
349
362
  // valid; what it could not say is what this names.
350
- if (!warnings || warnings.length === 0)
363
+ //
364
+ // Read straight from the PURE exporter rather than through
365
+ // `BPMN_XML_EXPORT.run` above: the interchange capability's `warnings` is
366
+ // `readonly string[]` (a seam shared with every other format, resolved to
367
+ // its English fallback with no `std` to ask), while the STRUCTURED
368
+ // `BpmnExportWarning[]` — key, fallback, params — is what `translateKey`
369
+ // needs. Calling the exporter a second time costs nothing beyond CPU (it is
370
+ // pure and deterministic on the same `elements`/`name`), and keeps the
371
+ // downloaded bytes on the one door `BPMN_XML_EXPORT.run` already is.
372
+ const { warnings } = exportBpmnXmlWithWarnings(bpmnBoardFrom(elements), {
373
+ name,
374
+ });
375
+ if (warnings.length === 0)
351
376
  return;
352
377
  notifyBpmn(std, {
353
378
  title: translateKey(std, EXPORT_WARNINGS_KEY, EXPORT_WARNINGS_FALLBACK),
354
- message: warnings.join('\n'),
379
+ message: warnings
380
+ .map(w => translateKey(std, w.key, w.fallback, w.params))
381
+ .join('\n'),
355
382
  accent: 'warning',
356
383
  });
357
384
  }
package/dist/consts.d.ts CHANGED
@@ -135,6 +135,26 @@ export declare const NODE_LABEL: Record<BpmnNodeKind, string>;
135
135
  * no word to translate, and BPMN puts an event's name outside its glyph anyway.
136
136
  */
137
137
  export declare const nodeLabelKey: (kind: BpmnNodeKind) => string;
138
+ /**
139
+ * The pool's own default name, at creation — a plain seed like {@link
140
+ * NODE_LABEL}, and never restated: `packages/affine/model/src/elements/bpmn/pool.ts`
141
+ * still defaults `name` to the English literal `'Pool'` (a document created
142
+ * before this key existed, or without a host catalogue, keeps that exact
143
+ * text), but `createBpmnPool` writes the resolved value explicitly so a pool
144
+ * dropped in a translated host starts in that language.
145
+ *
146
+ * "Pool" is the PO's glossary term for BPMN: the French proposal IS the
147
+ * English word.
148
+ */
149
+ export declare const POOL_NAME_KEY = "com.labre.bpmn.seed.pool";
150
+ export declare const POOL_NAME_FALLBACK = "Pool";
151
+ /**
152
+ * A fresh lane's name — `Lane 1`, `Lane 2`… — resolved AT PLACEMENT
153
+ * (`addBpmnLane`) like every other seed here. `{{n}}` is the count AFTER the
154
+ * lane being added, so the first is `Lane 1`.
155
+ */
156
+ export declare const LANE_NAME_KEY = "com.labre.bpmn.seed.lane";
157
+ export declare const LANE_NAME_FALLBACK = "Lane {{n}}";
138
158
  /**
139
159
  * Pool (background container) defaults — read by the `BPMN_POOL_BACKGROUND`
140
160
  * declaration (`background.ts`), which is the only thing that draws a pool.
package/dist/consts.js CHANGED
@@ -172,6 +172,26 @@ export const NODE_LABEL = {
172
172
  * no word to translate, and BPMN puts an event's name outside its glyph anyway.
173
173
  */
174
174
  export const nodeLabelKey = (kind) => `com.labre.bpmn.seed.${kind}`;
175
+ /**
176
+ * The pool's own default name, at creation — a plain seed like {@link
177
+ * NODE_LABEL}, and never restated: `packages/affine/model/src/elements/bpmn/pool.ts`
178
+ * still defaults `name` to the English literal `'Pool'` (a document created
179
+ * before this key existed, or without a host catalogue, keeps that exact
180
+ * text), but `createBpmnPool` writes the resolved value explicitly so a pool
181
+ * dropped in a translated host starts in that language.
182
+ *
183
+ * "Pool" is the PO's glossary term for BPMN: the French proposal IS the
184
+ * English word.
185
+ */
186
+ export const POOL_NAME_KEY = 'com.labre.bpmn.seed.pool';
187
+ export const POOL_NAME_FALLBACK = 'Pool';
188
+ /**
189
+ * A fresh lane's name — `Lane 1`, `Lane 2`… — resolved AT PLACEMENT
190
+ * (`addBpmnLane`) like every other seed here. `{{n}}` is the count AFTER the
191
+ * lane being added, so the first is `Lane 1`.
192
+ */
193
+ export const LANE_NAME_KEY = 'com.labre.bpmn.seed.lane';
194
+ export const LANE_NAME_FALLBACK = 'Lane {{n}}';
175
195
  /**
176
196
  * Pool (background container) defaults — read by the `BPMN_POOL_BACKGROUND`
177
197
  * declaration (`background.ts`), which is the only thing that draws a pool.
package/dist/export.d.ts CHANGED
@@ -222,6 +222,23 @@ export interface BpmnExportOptions {
222
222
  /** Names the `collaboration` / lone `process` and the `BPMNDiagram`. */
223
223
  name?: string;
224
224
  }
225
+ /**
226
+ * One warning, KEYED rather than a resolved string — see
227
+ * {@link BpmnExportOutcome}. `exportBpmnXmlWithWarnings` is pure and has no
228
+ * `std` to ask a host's catalogue, so it hands back the key, the English
229
+ * fallback (with `{{name}}` placeholders) and the params to fill them; the
230
+ * caller that DOES have `std` (`exportBpmnXmlFile`, `actions.ts`) is the one
231
+ * that calls `translateKey`.
232
+ */
233
+ export interface BpmnExportWarning {
234
+ key: string;
235
+ /** The English default, `{{name}}` placeholders included. */
236
+ fallback: string;
237
+ /** Values for the fallback's placeholders — a count, a list of names. */
238
+ params?: Record<string, string | number>;
239
+ }
240
+ /** All nine, for `translations.ts`'s manifest contribution. */
241
+ export declare const BPMN_EXPORT_WARNING_KEYS: readonly (readonly [string, string])[];
225
242
  /**
226
243
  * The document, plus what writing it could not say.
227
244
  *
@@ -235,7 +252,7 @@ export interface BpmnExportOptions {
235
252
  export interface BpmnExportOutcome {
236
253
  text: string;
237
254
  /** Empty when the board came out whole, which is the usual case. */
238
- warnings: string[];
255
+ warnings: BpmnExportWarning[];
239
256
  }
240
257
  /**
241
258
  * Serialize a board as a BPMN 2.0 XML interchange document.
package/dist/export.js CHANGED
@@ -763,6 +763,67 @@ class IdMinter {
763
763
  return unique;
764
764
  }
765
765
  }
766
+ /**
767
+ * The nine export-warning keys, each paired with its English fallback
768
+ * (`{{name}}` placeholders included) — declared ONCE, here, and read both by
769
+ * the `if` blocks below (which add the live `params`) and by
770
+ * `translations.ts`'s manifest (which needs the pair with no board to run
771
+ * against).
772
+ */
773
+ // Each fallback below is ONE unbroken literal rather than the `+`-joined
774
+ // pieces an earlier draft used: `manifest.unit.spec.ts`'s drift check reads a
775
+ // key/fallback PAIR as two adjacent literals in one argument list, and a
776
+ // concatenation left it seeing only the first piece — a drift the check
777
+ // itself then (correctly) flagged. A long line is the trade for a fallback
778
+ // the check can actually confirm against the manifest.
779
+ const UNDRAWN_ARTEFACTS_WARNING = [
780
+ 'com.labre.bpmn.export.warning.undrawn',
781
+ '{{count}} artefact(s) are drawn outside every pool. They are in the file, but most BPMN tools will not draw them: only a pool has a shape to hold them. Draw them inside a pool to make them visible.',
782
+ ];
783
+ const DROPPED_MESSAGE_FLOWS_WARNING = [
784
+ 'com.labre.bpmn.export.warning.dropped-message-flows',
785
+ '{{count}} message flow(s) were left out: a message flow runs between participants, and this board has no pool. Draw the pools it runs between, or say "is followed by" instead.',
786
+ ];
787
+ const SUBSTITUTED_IDS_WARNING = [
788
+ 'com.labre.bpmn.export.warning.substituted-ids',
789
+ '{{count}} element(s) imported from a BPMN file could not keep their original id: it was already taken in this file. A new id was written instead; nothing else changed.',
790
+ ];
791
+ const DISPLACED_SHAPES_WARNING = [
792
+ 'com.labre.bpmn.export.warning.displaced-shapes',
793
+ '{{count}} shape(s) imported from a BPMN file are kept exactly as the file drew them, and the rest of this drawing has moved since. They will open beside the process rather than inside it. Nothing was lost: they are in the file, at the position the original gave them.',
794
+ ];
795
+ const CONTRADICTING_DECLARATIONS_WARNING = [
796
+ 'com.labre.bpmn.export.warning.contradicting-declarations',
797
+ "This board came from a BPMN file that used {{names}} for something other than what BPMN means by them. Labre writes its own, so the file's declarations were left out rather than allowed to redefine the diagram's own namespaces. They are still in the document. Anything the file wrote under those prefixes is still in the export and will now be read under Labre's meaning of them, which is not the meaning the original had.",
798
+ ];
799
+ const DISAGREEING_DECLARATIONS_WARNING = [
800
+ 'com.labre.bpmn.export.warning.disagreeing-declarations',
801
+ "Two pools on this board disagree about {{names}}: they came from BPMN files that gave the same name two different values. The last was written and the other left out, so matter carried from the first file is now read under the second's meaning. Both are still in the document.",
802
+ ];
803
+ const CONFLICTING_IDS_WARNING = [
804
+ 'com.labre.bpmn.export.warning.conflicting-ids',
805
+ '{{count}} element(s) imported from a BPMN file could not be written back: {{names}} name(s) an id another imported element had already claimed, and a BPMN id must be unique across a document. The first was kept. The other(s) are still in the document; they are not in this file.',
806
+ ];
807
+ const REFUSED_ATTRIBUTE_NAMES_WARNING = [
808
+ 'com.labre.bpmn.export.warning.refused-attribute-names',
809
+ '{{count}} carried attribute(s) could not be written back, because their name(s) are not a valid XML name. They are still in the document. This is a sign the payload was edited by something other than a BPMN import.',
810
+ ];
811
+ const UNWRITABLE_EDGES_WARNING = [
812
+ 'com.labre.bpmn.export.warning.unwritable-edges',
813
+ '{{count}} arrow(s) were left out: BPMN requires both ends of a flow to be named, and they have an end that is loose or attached to something that is not a BPMN artefact.',
814
+ ];
815
+ /** All nine, for `translations.ts`'s manifest contribution. */
816
+ export const BPMN_EXPORT_WARNING_KEYS = [
817
+ UNDRAWN_ARTEFACTS_WARNING,
818
+ DROPPED_MESSAGE_FLOWS_WARNING,
819
+ SUBSTITUTED_IDS_WARNING,
820
+ DISPLACED_SHAPES_WARNING,
821
+ CONTRADICTING_DECLARATIONS_WARNING,
822
+ DISAGREEING_DECLARATIONS_WARNING,
823
+ CONFLICTING_IDS_WARNING,
824
+ REFUSED_ATTRIBUTE_NAMES_WARNING,
825
+ UNWRITABLE_EDGES_WARNING,
826
+ ];
766
827
  /* ── The plan ─────────────────────────────────────────────────────────── */
767
828
  /**
768
829
  * Which element an artefact is written INSIDE.
@@ -1465,6 +1526,14 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1465
1526
  ...slot(document, 'relationship'),
1466
1527
  ]);
1467
1528
  /* ── What the format refused to carry ────────────────────────────── */
1529
+ //
1530
+ // Each warning is a KEY plus its English fallback and params — see
1531
+ // `BpmnExportWarning`. This function stays `std`-free (like every other
1532
+ // pure exporter in the pack), so it cannot ask a host's catalogue itself;
1533
+ // `exportBpmnXmlFile` (`actions.ts`) resolves every one of these through
1534
+ // `translateKey` before showing them. The fallback is deliberately NEUTRAL
1535
+ // on number ("{{count}} element(s)") rather than branching singular/plural
1536
+ // English by hand: pluralisation is the host's, off `count`.
1468
1537
  const warnings = [];
1469
1538
  // Flow objects drawn beside the pools. They are in the file and correct for
1470
1539
  // any tool that reads the MODEL, and a collaboration plane has no shape to
@@ -1478,18 +1547,12 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1478
1547
  ? planned.filter(node => node.scope === orphanProcess).length
1479
1548
  : 0;
1480
1549
  if (undrawn > 0) {
1481
- warnings.push(`${undrawn} ${undrawn === 1 ? 'artefact is' : 'artefacts are'} drawn ` +
1482
- `outside every pool. ${undrawn === 1 ? 'It is' : 'They are'} in the ` +
1483
- `file, but most BPMN tools will not draw ${undrawn === 1 ? 'it' : 'them'}: ` +
1484
- `only a pool has a shape to hold ${undrawn === 1 ? 'it' : 'them'}. ` +
1485
- `Draw ${undrawn === 1 ? 'it' : 'them'} inside a pool to make ` +
1486
- `${undrawn === 1 ? 'it' : 'them'} visible.`);
1550
+ const [key, fallback] = UNDRAWN_ARTEFACTS_WARNING;
1551
+ warnings.push({ key, fallback, params: { count: undrawn } });
1487
1552
  }
1488
1553
  if (droppedMessageFlows > 0) {
1489
- warnings.push(`${droppedMessageFlows} message ${droppedMessageFlows === 1 ? 'flow was' : 'flows were'} ` +
1490
- `left out: a message flow runs between participants, and this board ` +
1491
- `has no pool. Draw the pools it runs between, or say "is followed by" ` +
1492
- `instead.`);
1554
+ const [key, fallback] = DROPPED_MESSAGE_FLOWS_WARNING;
1555
+ warnings.push({ key, fallback, params: { count: droppedMessageFlows } });
1493
1556
  }
1494
1557
  // An id a file gave us that we could not give back (ADR 0012 D3). It happens
1495
1558
  // when two elements carry the same recorded id — a merged document, an
@@ -1497,11 +1560,8 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1497
1560
  // file is correct either way; what the author loses is the continuity of one
1498
1561
  // name between the document they imported and the one they are exporting.
1499
1562
  if (minter.substituted > 0) {
1500
- warnings.push(`${minter.substituted} ${minter.substituted === 1 ? 'element' : 'elements'} ` +
1501
- `imported from a BPMN file could not keep ` +
1502
- `${minter.substituted === 1 ? 'its' : 'their'} original id: ` +
1503
- `${minter.substituted === 1 ? 'it was' : 'they were'} already taken in ` +
1504
- `this file. A new id was written instead; nothing else changed.`);
1563
+ const [key, fallback] = SUBSTITUTED_IDS_WARNING;
1564
+ warnings.push({ key, fallback, params: { count: minter.substituted } });
1505
1565
  }
1506
1566
  // A carried diagram element keeps the source file's own coordinates, and the
1507
1567
  // rest of the drawing has just been translated to the plane origin (§12.3).
@@ -1510,44 +1570,28 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1510
1570
  // once anything has been dragged. Nothing is lost; something is displaced,
1511
1571
  // and the person who clicked Export is the one entitled to hear about it.
1512
1572
  if (carriedPlane.length > 0 && (dx !== 0 || dy !== 0)) {
1513
- const count = carriedPlane.length;
1514
- warnings.push(`${count} ${count === 1 ? 'shape' : 'shapes'} imported from a BPMN file ` +
1515
- `${count === 1 ? 'is' : 'are'} kept exactly as the file drew ` +
1516
- `${count === 1 ? 'it' : 'them'}, and the rest of this drawing has ` +
1517
- `moved since. ${count === 1 ? 'It' : 'They'} will open beside the ` +
1518
- `process rather than inside it. Nothing was lost: ` +
1519
- `${count === 1 ? 'it is' : 'they are'} in the file, at the position ` +
1520
- `the original gave ${count === 1 ? 'it' : 'them'}.`);
1573
+ const [key, fallback] = DISPLACED_SHAPES_WARNING;
1574
+ warnings.push({ key, fallback, params: { count: carriedPlane.length } });
1521
1575
  }
1522
1576
  if (contradictingDeclarations.length > 0) {
1523
- const one = contradictingDeclarations.length === 1;
1524
- warnings.push(`This board came from a BPMN file that used ` +
1525
- `${contradictingDeclarations.join(' and ')} for something other than ` +
1526
- `what BPMN means by ${one ? 'it' : 'them'}. ` +
1527
- `Labre writes its own, so the file's ` +
1528
- `${one ? 'declaration was' : 'declarations were'} ` +
1529
- `left out rather than allowed to redefine the diagram's own namespaces. ` +
1530
- `${one ? 'It is' : 'They are'} still in the document. ` +
1531
- // The half a reader would otherwise have to work out: the DECLARATION
1532
- // is what was dropped, and the matter written under it was not — so it
1533
- // is now read under Labre's binding of the same prefix, which means
1534
- // something else. That is the larger of the two changes and it was the
1535
- // silent one.
1536
- `Anything the file wrote under ` +
1537
- `${one ? 'that prefix' : 'those prefixes'} is still in the export and ` +
1538
- `will now be read under Labre's meaning of ` +
1539
- `${one ? 'it' : 'them'}, which is not the meaning the original had.`);
1577
+ const [key, fallback] = CONTRADICTING_DECLARATIONS_WARNING;
1578
+ warnings.push({
1579
+ key,
1580
+ fallback,
1581
+ params: { names: contradictingDeclarations.join(' and ') },
1582
+ });
1540
1583
  }
1541
1584
  // Two pools, two source files, one prefix bound two ways. Last one wins —
1542
1585
  // there is one attribute of each name on `definitions` and something has to
1543
1586
  // be written — but a rebinding nobody was told about is how a fragment comes
1544
1587
  // to mean something else with no trace of when.
1545
1588
  if (disagreeingDeclarations.length > 0) {
1546
- warnings.push(`Two pools on this board disagree about ` +
1547
- `${disagreeingDeclarations.join(' and ')}: they came from BPMN files ` +
1548
- `that gave the same name two different values. The last was written ` +
1549
- `and the other left out, so matter carried from the first file is now ` +
1550
- `read under the second's meaning. Both are still in the document.`);
1589
+ const [key, fallback] = DISAGREEING_DECLARATIONS_WARNING;
1590
+ warnings.push({
1591
+ key,
1592
+ fallback,
1593
+ params: { names: disagreeingDeclarations.join(' and ') },
1594
+ });
1551
1595
  }
1552
1596
  // A carried element claiming an id another has already written back. The
1553
1597
  // duplicate-by-paste case is silent on purpose — one thing carried twice is
@@ -1555,32 +1599,27 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1555
1599
  // fragments claiming one id, which is a file that cannot hold both.
1556
1600
  const conflicting = [...new Set(carried.conflictingIds)];
1557
1601
  if (conflicting.length > 0) {
1558
- const one = conflicting.length === 1;
1559
- warnings.push(`${conflicting.length} ${one ? 'element' : 'elements'} imported from a ` +
1560
- `BPMN file could not be written back: ` +
1561
- `${conflicting.map(id => `"${id}"`).join(', ')} ` +
1562
- `${one ? 'names an id' : 'name ids'} another imported element had ` +
1563
- `already claimed, and a BPMN id must be unique across a document. The ` +
1564
- `first was kept. ${one ? 'The other is' : 'The others are'} still in ` +
1565
- `the document; ${one ? 'it is' : 'they are'} not in this file.`);
1602
+ const [key, fallback] = CONFLICTING_IDS_WARNING;
1603
+ warnings.push({
1604
+ key,
1605
+ fallback,
1606
+ params: {
1607
+ count: conflicting.length,
1608
+ names: conflicting.map(id => `"${id}"`).join(', '),
1609
+ },
1610
+ });
1566
1611
  }
1567
1612
  // An attribute NAME that is not a name. It cannot be written without
1568
1613
  // unbalancing the document — the serializer interpolates a name and escapes
1569
1614
  // only a value — so it is dropped rather than allowed to corrupt the file.
1570
1615
  const refusedNames = [...new Set(carried.refusedNames)];
1571
1616
  if (refusedNames.length > 0) {
1572
- const one = refusedNames.length === 1;
1573
- warnings.push(`${refusedNames.length} carried ${one ? 'attribute' : 'attributes'} ` +
1574
- `could not be written back, because ` +
1575
- `${one ? 'its name is not' : 'their names are not'} a valid XML name. ` +
1576
- `${one ? 'It is' : 'They are'} still in the document. This is a sign ` +
1577
- `the payload was edited by something other than a BPMN import.`);
1617
+ const [key, fallback] = REFUSED_ATTRIBUTE_NAMES_WARNING;
1618
+ warnings.push({ key, fallback, params: { count: refusedNames.length } });
1578
1619
  }
1579
1620
  if (unwritableEdges > 0) {
1580
- warnings.push(`${unwritableEdges} ${unwritableEdges === 1 ? 'arrow was' : 'arrows were'} ` +
1581
- `left out: BPMN requires both ends of a flow to be named, and ` +
1582
- `${unwritableEdges === 1 ? 'this one has' : 'these have'} an end that ` +
1583
- `is loose or attached to something that is not a BPMN artefact.`);
1621
+ const [key, fallback] = UNWRITABLE_EDGES_WARNING;
1622
+ warnings.push({ key, fallback, params: { count: unwritableEdges } });
1584
1623
  }
1585
1624
  return {
1586
1625
  text: `<?xml version="1.0" encoding="UTF-8"?>\n${serializeElement(definitions, '')}\n`,
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
  *