@flowdrop/flowdrop 1.7.0 → 1.8.1

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.
Files changed (73) hide show
  1. package/README.md +10 -0
  2. package/dist/chat/responseParser.js +7 -0
  3. package/dist/commands/parser.js +12 -0
  4. package/dist/components/App.svelte +92 -54
  5. package/dist/components/App.svelte.d.ts +13 -0
  6. package/dist/components/ConfigModal.svelte +2 -1
  7. package/dist/components/ConfigPanel.svelte +3 -2
  8. package/dist/components/FlowDropZone.svelte +2 -1
  9. package/dist/components/LogsSidebar.svelte +3 -2
  10. package/dist/components/Navbar.svelte +10 -6
  11. package/dist/components/NodeSidebar.svelte +4 -3
  12. package/dist/components/NodeStatusOverlay.svelte +14 -7
  13. package/dist/components/NodeSwapPicker.svelte +2 -1
  14. package/dist/components/PipelineStatus.svelte +10 -7
  15. package/dist/components/ReadOnlyDetails.svelte +4 -2
  16. package/dist/components/SchemaForm.svelte +20 -9
  17. package/dist/components/SchemaForm.svelte.d.ts +2 -4
  18. package/dist/components/SettingsModal.svelte +4 -3
  19. package/dist/components/SettingsPanel.svelte +3 -2
  20. package/dist/components/SwapMappingEditor.svelte +2 -1
  21. package/dist/components/WorkflowEditor.svelte +3 -2
  22. package/dist/components/chat/AIChatPanel.svelte +33 -8
  23. package/dist/components/chat/AIChatPanel.svelte.d.ts +3 -0
  24. package/dist/components/chat/CommandPreview.svelte +10 -6
  25. package/dist/components/console/CommandConsole.svelte +4 -3
  26. package/dist/components/form/FormArray.svelte +33 -20
  27. package/dist/components/form/FormArray.svelte.d.ts +3 -1
  28. package/dist/components/form/FormAutocomplete.svelte +18 -7
  29. package/dist/components/form/FormCodeEditor.svelte +2 -1
  30. package/dist/components/form/FormFieldWrapper.svelte +2 -1
  31. package/dist/components/form/FormMarkdownEditor.svelte +152 -108
  32. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +1 -1
  33. package/dist/components/form/FormTemplateEditor.svelte +2 -1
  34. package/dist/components/form/FormToggle.svelte +23 -5
  35. package/dist/components/form/FormToggle.svelte.d.ts +6 -2
  36. package/dist/components/interrupt/ChoicePrompt.svelte +14 -5
  37. package/dist/components/interrupt/ConfirmationPrompt.svelte +8 -5
  38. package/dist/components/interrupt/FormPrompt.svelte +28 -7
  39. package/dist/components/interrupt/InterruptBubble.svelte +27 -18
  40. package/dist/components/interrupt/ReviewPrompt.svelte +32 -22
  41. package/dist/components/interrupt/TextInputPrompt.svelte +12 -5
  42. package/dist/components/layouts/MainLayout.svelte +4 -3
  43. package/dist/components/nodes/GatewayNode.svelte +8 -3
  44. package/dist/components/nodes/IdeaNode.svelte +2 -1
  45. package/dist/components/nodes/NotesNode.svelte +18 -12
  46. package/dist/components/nodes/WorkflowNode.svelte +8 -3
  47. package/dist/components/playground/ChatPanel.svelte +36 -24
  48. package/dist/components/playground/MessageBubble.svelte +15 -7
  49. package/dist/components/playground/Playground.svelte +2 -1
  50. package/dist/components/playground/PlaygroundModal.svelte +2 -1
  51. package/dist/components/playground/SessionManager.svelte +14 -10
  52. package/dist/core/index.d.ts +2 -0
  53. package/dist/core/index.js +9 -0
  54. package/dist/editor/index.d.ts +1 -1
  55. package/dist/editor/index.js +1 -1
  56. package/dist/messages/context.d.ts +29 -0
  57. package/dist/messages/context.js +38 -0
  58. package/dist/messages/defaults.d.ts +396 -0
  59. package/dist/messages/defaults.js +356 -0
  60. package/dist/messages/deprecation.d.ts +20 -0
  61. package/dist/messages/deprecation.js +33 -0
  62. package/dist/messages/index.d.ts +11 -0
  63. package/dist/messages/index.js +10 -0
  64. package/dist/messages/merge.d.ts +28 -0
  65. package/dist/messages/merge.js +53 -0
  66. package/dist/messages/types.d.ts +29 -0
  67. package/dist/messages/types.js +13 -0
  68. package/dist/services/draftStorage.d.ts +13 -0
  69. package/dist/services/draftStorage.js +36 -0
  70. package/dist/styles/base.css +13 -4
  71. package/dist/svelte-app.d.ts +11 -0
  72. package/dist/svelte-app.js +11 -2
  73. package/package.json +1 -1
@@ -37,6 +37,7 @@
37
37
  } from '../../stores/interruptStore.svelte.js';
38
38
  import { interruptService } from '../../services/interruptService.js';
39
39
  import { logger } from '../../utils/logger.js';
40
+ import { m } from '../../messages/index.js';
40
41
 
41
42
  /**
42
43
  * Component props
@@ -105,41 +106,46 @@
105
106
  }
106
107
  }
107
108
 
109
+ // Hoist the bubble branch — five reads inside the header alone.
110
+ const t = $derived(m().interrupt.bubble);
111
+
108
112
  /**
109
113
  * Get the label for the interrupt type
110
114
  */
111
115
  function getTypeLabel(type: InterruptType): string {
116
+ const required = t.required;
112
117
  switch (type) {
113
118
  case 'confirmation':
114
- return 'Confirmation Required';
119
+ return required.confirmation;
115
120
  case 'choice':
116
- return 'Selection Required';
121
+ return required.selection;
117
122
  case 'text':
118
- return 'Input Required';
123
+ return required.input;
119
124
  case 'form':
120
- return 'Form Required';
125
+ return required.form;
121
126
  case 'review':
122
- return 'Review Required';
127
+ return required.review;
123
128
  default:
124
- return 'Action Required';
129
+ return required.default;
125
130
  }
126
131
  }
127
132
 
128
133
  /** Get resolved label for the header when resolved */
129
134
  function getResolvedLabel(type: InterruptType): string {
135
+ const submitted = t.submitted;
130
136
  switch (type) {
131
137
  case 'confirmation':
132
- return 'Confirmation Submitted';
138
+ return submitted.confirmation;
133
139
  case 'choice':
134
- return 'Selection Made';
140
+ return submitted.selection;
135
141
  case 'text':
136
- return 'Input Submitted';
142
+ return submitted.input;
137
143
  case 'form':
138
- return 'Form Submitted';
144
+ return submitted.form;
139
145
  case 'review':
140
- return 'Review Submitted';
146
+ return submitted.review;
141
147
  default:
142
- return 'Response Submitted';
148
+ return submitted.default;
143
149
  }
144
150
  }
145
151
 
@@ -257,10 +263,10 @@
257
263
  <Icon icon={getTypeIcon(currentInterrupt.type)} />
258
264
  {#if isResolved}
259
265
  {currentInterrupt.machineState.status === 'cancelled'
260
- ? 'Cancelled'
266
+ ? t.cancelled
261
267
  : getResolvedLabel(currentInterrupt.type)}
262
268
  {:else if currentInterrupt.machineState.status === 'error'}
263
- Error - Click to Retry
269
+ {t.errorRetry}
264
270
  {:else}
265
271
  {getTypeLabel(currentInterrupt.type)}
266
272
  {/if}
@@ -279,7 +285,7 @@
279
285
  <span>{error}</span>
280
286
  <button type="button" class="interrupt-bubble__retry-btn" onclick={handleRetry}>
281
287
  <Icon icon="mdi:refresh" />
282
- Retry
288
+ {t.retry}
283
289
  </button>
284
290
  </div>
285
291
  {/if}
@@ -344,9 +350,12 @@
344
350
  {#if currentInterrupt.nodeId || (currentInterrupt.allowCancel && !isResolved && currentInterrupt.type !== 'confirmation')}
345
351
  <div class="interrupt-bubble__footer">
346
352
  {#if currentInterrupt.nodeId}
347
- <span class="interrupt-bubble__node" title="Node ID: {currentInterrupt.nodeId}">
353
+ <span
354
+ class="interrupt-bubble__node"
355
+ title={t.nodeIdTooltip({ id: currentInterrupt.nodeId })}
356
+ >
348
357
  <Icon icon="mdi:graph" />
349
- <span>From workflow node</span>
358
+ <span>{t.fromWorkflow}</span>
350
359
  </span>
351
360
  {/if}
352
361
  {#if currentInterrupt.allowCancel && !isResolved && currentInterrupt.type !== 'confirmation'}
@@ -357,7 +366,7 @@
357
366
  disabled={isSubmitting}
358
367
  >
359
368
  <Icon icon="mdi:close" />
360
- <span>Cancel</span>
369
+ <span>{t.cancel}</span>
361
370
  </button>
362
371
  {/if}
363
372
  </div>
@@ -18,6 +18,7 @@
18
18
  ReviewResolution,
19
19
  ReviewFieldDecision
20
20
  } from '../../types/interrupt.js';
21
+ import { m } from '../../messages/index.js';
21
22
 
22
23
  /**
23
24
  * Component props
@@ -70,10 +71,14 @@
70
71
  /** Total number of changes */
71
72
  const totalCount = $derived(config.changes.length);
72
73
 
73
- /** Button labels with defaults */
74
- const acceptAllLabel = $derived(config.acceptAllLabel ?? 'Accept All');
75
- const rejectAllLabel = $derived(config.rejectAllLabel ?? 'Reject All');
76
- const submitLabel = $derived(config.submitLabel ?? 'Submit Review');
74
+ // Hoist the review branch — 18 reads inside the template, plus three label
75
+ // resolvers below. One getter walk per render is enough.
76
+ const t = $derived(m().interrupt.review);
77
+
78
+ /** Button labels — config wins, falls back to messages tree. */
79
+ const acceptAllLabel = $derived(config.acceptAllLabel ?? t.acceptAll);
80
+ const rejectAllLabel = $derived(config.rejectAllLabel ?? t.rejectAll);
81
+ const submitLabel = $derived(config.submitLabel ?? t.submit);
77
82
 
78
83
  /**
79
84
  * Set a specific field's decision
@@ -166,9 +171,9 @@
166
171
  * Format a value for display
167
172
  */
168
173
  function formatValue(value: unknown): string {
169
- if (value === null || value === undefined) return '(empty)';
174
+ if (value === null || value === undefined) return t.empty;
170
175
  if (typeof value === 'string') return value;
171
- if (typeof value === 'boolean') return value ? 'Yes' : 'No';
176
+ if (typeof value === 'boolean') return value ? t.yes : t.no;
172
177
  if (typeof value === 'object') return JSON.stringify(value, null, 2);
173
178
  return String(value);
174
179
  }
@@ -258,7 +263,7 @@
258
263
  </button>
259
264
  </div>
260
265
  <span class="review-prompt__counter">
261
- {acceptedCount} of {totalCount} accepted
266
+ {t.counter({ accepted: acceptedCount, total: totalCount })}
262
267
  </span>
263
268
  </div>
264
269
  {/if}
@@ -288,11 +293,11 @@
288
293
  class:review-prompt__toggle-btn--active={isAccepted}
289
294
  onclick={() => setFieldDecision(change.field, true)}
290
295
  disabled={isSubmitting}
291
- aria-label="Accept {change.label}"
292
- title="Accept"
296
+ aria-label={t.acceptItem({ label: change.label })}
297
+ title={t.accept}
293
298
  >
294
299
  <Icon icon="mdi:check" />
295
- <span>Accept</span>
300
+ <span>{t.accept}</span>
296
301
  </button>
297
302
  <button
298
303
  type="button"
@@ -300,11 +305,11 @@
300
305
  class:review-prompt__toggle-btn--active={!isAccepted}
301
306
  onclick={() => setFieldDecision(change.field, false)}
302
307
  disabled={isSubmitting}
303
- aria-label="Reject {change.label}"
304
- title="Reject"
308
+ aria-label={t.rejectItem({ label: change.label })}
309
+ title={t.reject}
305
310
  >
306
311
  <Icon icon="mdi:close" />
307
- <span>Reject</span>
312
+ <span>{t.reject}</span>
308
313
  </button>
309
314
  </div>
310
315
  {:else}
@@ -315,10 +320,10 @@
315
320
  >
316
321
  {#if isAccepted}
317
322
  <Icon icon="mdi:check-circle" />
318
- <span>Accepted</span>
323
+ <span>{t.accepted}</span>
319
324
  {:else}
320
325
  <Icon icon="mdi:close-circle" />
321
- <span>Rejected</span>
326
+ <span>{t.rejected}</span>
322
327
  {/if}
323
328
  </span>
324
329
  {/if}
@@ -334,12 +339,12 @@
334
339
  onclick={() => toggleHtmlView(change.field)}
335
340
  >
336
341
  <Icon icon={isRawView ? 'mdi:eye' : 'mdi:code-tags'} />
337
- <span>{isRawView ? 'Rendered' : 'Raw HTML'}</span>
342
+ <span>{isRawView ? t.rendered : t.rawHtml}</span>
338
343
  </button>
339
344
  </div>
340
345
  {/if}
341
346
  <div class="review-prompt__diff-row">
342
- <span class="review-prompt__diff-label">Original:</span>
347
+ <span class="review-prompt__diff-label">{t.original}</span>
343
348
  {#if isHtml && !isRawView}
344
349
  <span class="review-prompt__diff-value review-prompt__html-content"
345
350
  >{@html sanitizeHtml(String(change.original))}</span
@@ -355,7 +360,7 @@
355
360
  {/if}
356
361
  </div>
357
362
  <div class="review-prompt__diff-row">
358
- <span class="review-prompt__diff-label">Proposed:</span>
363
+ <span class="review-prompt__diff-label">{t.proposed}</span>
359
364
  {#if isHtml && !isRawView}
360
365
  <span
361
366
  class="review-prompt__diff-value review-prompt__diff-value--proposed review-prompt__html-content"
@@ -374,7 +379,7 @@
374
379
  </div>
375
380
  {#if diff}
376
381
  <div class="review-prompt__diff-row">
377
- <span class="review-prompt__diff-label">Diff:</span>
382
+ <span class="review-prompt__diff-label">{t.diff}</span>
378
383
  {#if isMultiLineDiff(diff)}
379
384
  <pre
380
385
  class="review-prompt__diff-value review-prompt__diff-block">{#each diff as part}{#if part.added}<span
@@ -425,8 +430,11 @@
425
430
  {#if isResolved && resolvedValue}
426
431
  <div class="review-prompt__summary">
427
432
  <span class="review-prompt__summary-text">
428
- {resolvedValue.summary.accepted} accepted, {resolvedValue.summary.rejected} rejected out of {resolvedValue
429
- .summary.total} changes
433
+ {t.summary({
434
+ accepted: resolvedValue.summary.accepted,
435
+ rejected: resolvedValue.summary.rejected,
436
+ total: resolvedValue.summary.total
437
+ })}
430
438
  </span>
431
439
  </div>
432
440
  {/if}
@@ -436,7 +444,9 @@
436
444
  <div class="review-prompt__resolved-badge">
437
445
  <Icon icon="mdi:check-circle" />
438
446
  <span>
439
- {resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
447
+ {resolvedByUserName
448
+ ? m().interrupt.responseSubmittedBy({ name: resolvedByUserName })
449
+ : m().interrupt.responseSubmitted}
440
450
  </span>
441
451
  </div>
442
452
  {/if}
@@ -10,6 +10,7 @@
10
10
  <script lang="ts">
11
11
  import Icon from '@iconify/svelte';
12
12
  import type { TextConfig } from '../../types/interrupt.js';
13
+ import { m } from '../../messages/index.js';
13
14
 
14
15
  /**
15
16
  * Component props
@@ -41,6 +42,10 @@
41
42
  onSubmit
42
43
  }: Props = $props();
43
44
 
45
+ // Hoist the text branch — placeholder/min/submit reads, including duplicate
46
+ // placeholder in single- vs. multiline branches.
47
+ const t = $derived(m().interrupt.text);
48
+
44
49
  /** Local state for input value */
45
50
  // svelte-ignore state_referenced_locally — initial default, user edits the input
46
51
  let inputValue = $state(config.defaultValue ?? '');
@@ -112,7 +117,7 @@
112
117
  class="text-prompt__textarea"
113
118
  class:text-prompt__textarea--resolved={isResolved}
114
119
  value={displayValue}
115
- placeholder={config.placeholder ?? 'Enter your response...'}
120
+ placeholder={config.placeholder ?? t.placeholder}
116
121
  disabled={isResolved || isSubmitting}
117
122
  oninput={handleInput}
118
123
  onkeydown={handleKeyDown}
@@ -126,7 +131,7 @@
126
131
  class="text-prompt__input"
127
132
  class:text-prompt__input--resolved={isResolved}
128
133
  value={displayValue}
129
- placeholder={config.placeholder ?? 'Enter your response...'}
134
+ placeholder={config.placeholder ?? t.placeholder}
130
135
  disabled={isResolved || isSubmitting}
131
136
  oninput={handleInput}
132
137
  onkeydown={handleKeyDown}
@@ -148,7 +153,7 @@
148
153
  / {config.maxLength}
149
154
  {/if}
150
155
  {#if config.minLength !== undefined}
151
- (min: {config.minLength})
156
+ {t.min({ n: config.minLength })}
152
157
  {/if}
153
158
  </span>
154
159
  </div>
@@ -168,7 +173,7 @@
168
173
  {:else}
169
174
  <Icon icon="mdi:send" />
170
175
  {/if}
171
- <span>Submit</span>
176
+ <span>{t.submit}</span>
172
177
  </button>
173
178
  </div>
174
179
  {/if}
@@ -178,7 +183,9 @@
178
183
  <div class="text-prompt__resolved-badge">
179
184
  <Icon icon="mdi:check-circle" />
180
185
  <span>
181
- {resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
186
+ {resolvedByUserName
187
+ ? m().interrupt.responseSubmittedBy({ name: resolvedByUserName })
188
+ : m().interrupt.responseSubmitted}
182
189
  </span>
183
190
  </div>
184
191
  {/if}
@@ -11,6 +11,7 @@
11
11
 
12
12
  <script lang="ts">
13
13
  import { onMount } from 'svelte';
14
+ import { m } from '../../messages/index.js';
14
15
 
15
16
  /**
16
17
  * Configuration props for the MainLayout component
@@ -342,7 +343,7 @@
342
343
  aria-valuenow={leftSidebarWidth}
343
344
  aria-valuemin={leftSidebarMinWidth}
344
345
  aria-valuemax={leftSidebarMaxWidth}
345
- aria-label="Resize left sidebar"
346
+ aria-label={m().layout.resizeLeftSidebar}
346
347
  tabindex="0"
347
348
  >
348
349
  <div class="flowdrop-main-layout__divider-handle"></div>
@@ -377,7 +378,7 @@
377
378
  aria-valuenow={bottomPanelHeightState}
378
379
  aria-valuemin={bottomPanelMinHeight}
379
380
  aria-valuemax={bottomPanelMaxHeight}
380
- aria-label="Resize bottom panel"
381
+ aria-label={m().layout.resizeBottomPanel}
381
382
  tabindex="0"
382
383
  >
383
384
  <div
@@ -408,7 +409,7 @@
408
409
  aria-valuenow={rightSidebarWidth}
409
410
  aria-valuemin={rightSidebarMinWidth}
410
411
  aria-valuemax={rightSidebarMaxWidth}
411
- aria-label="Resize right sidebar"
412
+ aria-label={m().layout.resizeRightSidebar}
412
413
  tabindex="0"
413
414
  >
414
415
  <div class="flowdrop-main-layout__divider-handle"></div>
@@ -19,6 +19,7 @@
19
19
  getPortBackgroundColor
20
20
  } from '../../utils/colors.js';
21
21
  import { getConnectedHandles } from '../../stores/workflowStore.svelte.js';
22
+ import { m } from '../../messages/index.js';
22
23
 
23
24
  interface Props {
24
25
  data: WorkflowNode['data'] & {
@@ -30,6 +31,10 @@
30
31
 
31
32
  let props: Props = $props();
32
33
 
34
+ // Hoist the graph branch — three reads in the template, two inside
35
+ // {#each port} / {#each branch} loops. One getter walk per render.
36
+ const graph = $derived(m().nodes.graph);
37
+
33
38
  /**
34
39
  * Instance-specific title override from config.
35
40
  * Falls back to the original label if not set.
@@ -159,7 +164,7 @@
159
164
  onkeydown={handleKeydown}
160
165
  role="button"
161
166
  tabindex="0"
162
- aria-label="Gateway node: {displayTitle}"
167
+ aria-label={graph.gatewayNode({ title: displayTitle })}
163
168
  aria-describedby="node-description-{props.data.nodeId || 'unknown'}"
164
169
  >
165
170
  <!-- Node Header: expands in multiples of 10 (title row 40px + gap 10px + description 20px per line) -->
@@ -207,7 +212,7 @@
207
212
  )}; --fd-handle-border-color: var(--fd-handle-border);"
208
213
  role="button"
209
214
  tabindex={0}
210
- aria-label="Connect to {port.name} input port"
215
+ aria-label={graph.connectInputPort({ name: port.name })}
211
216
  />
212
217
 
213
218
  <!-- Port Info: padding lives here so handle position is simple -->
@@ -295,7 +300,7 @@
295
300
  )}; --fd-handle-border-color: var(--fd-handle-border);"
296
301
  role="button"
297
302
  tabindex={0}
298
- aria-label="Connect from {branch.name} branch"
303
+ aria-label={graph.connectBranch({ name: branch.name })}
299
304
  />
300
305
  </div>
301
306
  {/each}
@@ -11,6 +11,7 @@
11
11
  import type { ConfigValues, NodeMetadata } from '../../types/index.js';
12
12
  import Icon from '@iconify/svelte';
13
13
  import { getDataTypeColor } from '../../utils/colors.js';
14
+ import { m } from '../../messages/index.js';
14
15
 
15
16
  /**
16
17
  * IdeaNode component props
@@ -142,7 +143,7 @@
142
143
  onkeydown={handleKeydown}
143
144
  role="button"
144
145
  tabindex="0"
145
- aria-label="Idea node: {displayTitle}"
146
+ aria-label={m().nodes.graph.ideaNode({ title: displayTitle })}
146
147
  >
147
148
  <!-- Left Port (Target/Input): center at top 40px (multiple of 10), 20px connection area -->
148
149
  {#if enableLeftPort}
@@ -2,6 +2,7 @@
2
2
  import type { ConfigValues, NodeMetadata } from '../../types/index.js';
3
3
  import Icon from '@iconify/svelte';
4
4
  import MarkdownDisplay from '../MarkdownDisplay.svelte';
5
+ import { m } from '../../messages/index.js';
5
6
 
6
7
  /**
7
8
  * NotesNode component props
@@ -24,40 +25,45 @@
24
25
  isError?: boolean;
25
26
  }>();
26
27
 
28
+ // Hoist the notes branch — read for placeholder, every type name, processing,
29
+ // error, and configure tooltip.
30
+ const notes = $derived(m().nodes.notes);
31
+
27
32
  /** Note content derived from config */
28
- const noteContent = $derived((props.data.config?.content as string) || 'Add your notes here...');
33
+ const noteContent = $derived((props.data.config?.content as string) || notes.placeholder);
29
34
 
30
35
  /** Note type derived from config */
31
36
  const noteType = $derived((props.data.config?.noteType as string) || 'info');
32
37
 
33
- /** Note type configuration with styling for each type */
34
- const noteTypes = {
38
+ /** Note type configuration with styling for each type. Type names track the
39
+ * messages tree so locale changes flow through. */
40
+ const noteTypes = $derived({
35
41
  info: {
36
- name: 'Info',
42
+ name: notes.types.info,
37
43
  typeClass: 'flowdrop-notes-node--info',
38
44
  icon: 'mdi:information'
39
45
  },
40
46
  warning: {
41
- name: 'Warning',
47
+ name: notes.types.warning,
42
48
  typeClass: 'flowdrop-notes-node--warning',
43
49
  icon: 'mdi:alert'
44
50
  },
45
51
  success: {
46
- name: 'Success',
52
+ name: notes.types.success,
47
53
  typeClass: 'flowdrop-notes-node--success',
48
54
  icon: 'mdi:check-circle'
49
55
  },
50
56
  error: {
51
- name: 'Error',
57
+ name: notes.types.error,
52
58
  typeClass: 'flowdrop-notes-node--error',
53
59
  icon: 'mdi:close-circle'
54
60
  },
55
61
  note: {
56
- name: 'Note',
62
+ name: notes.types.default,
57
63
  typeClass: 'flowdrop-notes-node--note',
58
64
  icon: 'mdi:note-text'
59
65
  }
60
- };
66
+ });
61
67
 
62
68
  /** Current note type configuration based on selected type */
63
69
  const currentType = $derived(noteTypes[noteType as keyof typeof noteTypes] || noteTypes.info);
@@ -126,7 +132,7 @@
126
132
  {#if props.isProcessing}
127
133
  <div class="flowdrop-notes-node__processing">
128
134
  <div class="flowdrop-notes-node__spinner"></div>
129
- <span>Processing...</span>
135
+ <span>{notes.processing}</span>
130
136
  </div>
131
137
  {/if}
132
138
 
@@ -134,7 +140,7 @@
134
140
  {#if props.isError}
135
141
  <div class="flowdrop-notes-node__error-indicator">
136
142
  <Icon icon="mdi:alert-circle" class="flowdrop-notes-node__error-icon" />
137
- <span>Error occurred</span>
143
+ <span>{notes.errorOccurred}</span>
138
144
  </div>
139
145
  {/if}
140
146
  </div>
@@ -143,7 +149,7 @@
143
149
  <button
144
150
  class="flowdrop-notes-node__config-btn"
145
151
  onclick={openConfigSidebar}
146
- title="Configure note"
152
+ title={notes.configure}
147
153
  >
148
154
  <Icon icon="mdi:cog" />
149
155
  </button>
@@ -24,6 +24,7 @@
24
24
  } from '../../utils/colors.js';
25
25
  import { getConnectedHandles } from '../../stores/workflowStore.svelte.js';
26
26
  import { applyPortOrder } from '../../utils/portUtils.js';
27
+ import { m } from '../../messages/index.js';
27
28
 
28
29
  interface Props {
29
30
  data: WorkflowNode['data'] & {
@@ -36,6 +37,10 @@
36
37
  let props: Props = $props();
37
38
  let isHandleInteraction = $state(false);
38
39
 
40
+ // Hoist the graph branch — three reads in the template, two of them inside
41
+ // {#each port} loops where N×M reads add up. One getter walk per render.
42
+ const graph = $derived(m().nodes.graph);
43
+
39
44
  /**
40
45
  * Instance-specific title override from config.
41
46
  * Falls back to the original label if not set.
@@ -205,7 +210,7 @@
205
210
  handleDoubleClick();
206
211
  }
207
212
  }}
208
- aria-label="Workflow node: {props.data.metadata.name}"
213
+ aria-label={graph.workflowNode({ name: props.data.metadata.name })}
209
214
  aria-describedby="node-description-{props.data.nodeId || 'unknown'}"
210
215
  >
211
216
  <!-- Default Node Header: expands in multiples of 10 (title row 40px + gap 10px + description 20px per line) -->
@@ -261,7 +266,7 @@
261
266
  )}); --fd-handle-border-color: var(--fd-handle-border);"
262
267
  role="button"
263
268
  tabindex={0}
264
- aria-label="Connect to {port.name} input port"
269
+ aria-label={graph.connectInputPort({ name: port.name })}
265
270
  />
266
271
 
267
272
  <!-- Port Info: padding lives here so handle position is simple -->
@@ -339,7 +344,7 @@
339
344
  )}); --fd-handle-border-color: var(--fd-handle-border);"
340
345
  role="button"
341
346
  tabindex={0}
342
- aria-label="Connect from {port.name} output port"
347
+ aria-label={graph.connectOutputPort({ name: port.name })}
343
348
  />
344
349
  </div>
345
350
  {/each}