@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
@@ -11,6 +11,7 @@
11
11
 
12
12
  <script lang="ts">
13
13
  import type { Snippet } from 'svelte';
14
+ import { m } from '../../messages/index.js';
14
15
 
15
16
  interface Props {
16
17
  /** Field identifier for label association */
@@ -42,7 +43,7 @@
42
43
  {label}
43
44
  </span>
44
45
  {#if required}
45
- <span class="form-field__required" aria-label="required">*</span>
46
+ <span class="form-field__required" aria-label={m().form.field.required}>*</span>
46
47
  {/if}
47
48
  </label>
48
49
 
@@ -18,20 +18,21 @@
18
18
 
19
19
  <script lang="ts">
20
20
  import { onMount, onDestroy } from 'svelte';
21
- import { EditorView, lineNumbers, drawSelection, keymap } from '@codemirror/view';
21
+ import { EditorView, lineNumbers, drawSelection, keymap, placeholder } from '@codemirror/view';
22
22
  import { EditorState, Compartment } from '@codemirror/state';
23
23
  import { history, historyKeymap, defaultKeymap, indentWithTab } from '@codemirror/commands';
24
24
  import { highlightSpecialChars, highlightActiveLine } from '@codemirror/view';
25
25
  import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
26
26
  import { markdown } from '@codemirror/lang-markdown';
27
27
  import { oneDark } from '@codemirror/theme-one-dark';
28
+ import { m } from '../../messages/index.js';
28
29
 
29
30
  interface Props {
30
31
  /** Field identifier */
31
32
  id: string;
32
33
  /** Current value (markdown string) */
33
34
  value: string;
34
- /** Placeholder text shown when empty */
35
+ /** Placeholder text shown when empty. Falls back to `messages.form.markdown.placeholder`. */
35
36
  placeholder?: string;
36
37
  /** Whether the field is required */
37
38
  required?: boolean;
@@ -60,7 +61,7 @@
60
61
  let {
61
62
  id,
62
63
  value = '',
63
- placeholder = 'Write your markdown here...',
64
+ placeholder: placeholderProp,
64
65
  required = false,
65
66
  height = '300px',
66
67
  showToolbar = true,
@@ -97,6 +98,12 @@
97
98
  /** Theme compartment for dynamic theme switching */
98
99
  const themeCompartment = new Compartment();
99
100
 
101
+ /** Placeholder compartment so locale changes can reconfigure without rebuilding the editor */
102
+ const placeholderCompartment = new Compartment();
103
+
104
+ /** aria-label compartment so locale changes update the editor's accessible name */
105
+ const ariaLabelCompartment = new Compartment();
106
+
100
107
  // ── Toolbar actions ──────────────────────────────────────
101
108
 
102
109
  type ToolbarAction = {
@@ -165,104 +172,110 @@
165
172
  editorView.focus();
166
173
  }
167
174
 
168
- const toolbarActions: (ToolbarAction | '|')[] = [
169
- {
170
- id: 'bold',
171
- label: 'Bold',
172
- icon: 'B',
173
- shortcut: 'Mod-b',
174
- action: () => wrapSelection('**', '**')
175
- },
176
- {
177
- id: 'italic',
178
- label: 'Italic',
179
- icon: 'I',
180
- shortcut: 'Mod-i',
181
- action: () => wrapSelection('_', '_')
182
- },
183
- {
184
- id: 'strikethrough',
185
- label: 'Strikethrough',
186
- icon: 'S',
187
- action: () => wrapSelection('~~', '~~')
188
- },
189
- '|',
190
- {
191
- id: 'heading-1',
192
- label: 'Heading 1',
193
- icon: 'H1',
194
- action: () => prefixLine('# ')
195
- },
196
- {
197
- id: 'heading-2',
198
- label: 'Heading 2',
199
- icon: 'H2',
200
- action: () => prefixLine('## ')
201
- },
202
- {
203
- id: 'heading-3',
204
- label: 'Heading 3',
205
- icon: 'H3',
206
- action: () => prefixLine('### ')
207
- },
208
- '|',
209
- {
210
- id: 'quote',
211
- label: 'Quote',
212
- icon: '"',
213
- action: () => prefixLine('> ')
214
- },
215
- {
216
- id: 'unordered-list',
217
- label: 'Unordered List',
218
- icon: '',
219
- action: () => prefixLine('- ')
220
- },
221
- {
222
- id: 'ordered-list',
223
- label: 'Ordered List',
224
- icon: '1.',
225
- action: () => prefixLine('1. ')
226
- },
227
- '|',
228
- {
229
- id: 'link',
230
- label: 'Link',
231
- icon: icons.link,
232
- isSvg: true,
233
- shortcut: 'Mod-k',
234
- action: () => {
235
- if (!editorView) return;
236
- const { from, to } = editorView.state.selection.main;
237
- const selected = editorView.state.sliceDoc(from, to);
238
- const text = selected || 'link text';
239
- const replacement = `[${text}](url)`;
240
- editorView.dispatch({
241
- changes: { from, to, insert: replacement },
242
- selection: {
243
- anchor: from + text.length + 3,
244
- head: from + text.length + 6
245
- }
246
- });
247
- editorView.focus();
175
+ // Derived so `label` strings refresh whenever the consumer's i18n source
176
+ // changes locale. Action closures recreate alongside, but they're cheap —
177
+ // they only capture a stable reference to `editorView` (a state).
178
+ const toolbarActions: (ToolbarAction | '|')[] = $derived.by(() => {
179
+ const md = m().form.markdown;
180
+ return [
181
+ {
182
+ id: 'bold',
183
+ label: md.bold,
184
+ icon: 'B',
185
+ shortcut: 'Mod-b',
186
+ action: () => wrapSelection('**', '**')
187
+ },
188
+ {
189
+ id: 'italic',
190
+ label: md.italic,
191
+ icon: 'I',
192
+ shortcut: 'Mod-i',
193
+ action: () => wrapSelection('_', '_')
194
+ },
195
+ {
196
+ id: 'strikethrough',
197
+ label: md.strikethrough,
198
+ icon: 'S',
199
+ action: () => wrapSelection('~~', '~~')
200
+ },
201
+ '|',
202
+ {
203
+ id: 'heading-1',
204
+ label: md.heading1,
205
+ icon: 'H1',
206
+ action: () => prefixLine('# ')
207
+ },
208
+ {
209
+ id: 'heading-2',
210
+ label: md.heading2,
211
+ icon: 'H2',
212
+ action: () => prefixLine('## ')
213
+ },
214
+ {
215
+ id: 'heading-3',
216
+ label: md.heading3,
217
+ icon: 'H3',
218
+ action: () => prefixLine('### ')
219
+ },
220
+ '|',
221
+ {
222
+ id: 'quote',
223
+ label: md.quote,
224
+ icon: '"',
225
+ action: () => prefixLine('> ')
226
+ },
227
+ {
228
+ id: 'unordered-list',
229
+ label: md.unorderedList,
230
+ icon: '',
231
+ action: () => prefixLine('- ')
232
+ },
233
+ {
234
+ id: 'ordered-list',
235
+ label: md.orderedList,
236
+ icon: '1.',
237
+ action: () => prefixLine('1. ')
238
+ },
239
+ '|',
240
+ {
241
+ id: 'link',
242
+ label: md.link,
243
+ icon: icons.link,
244
+ isSvg: true,
245
+ shortcut: 'Mod-k',
246
+ action: () => {
247
+ if (!editorView) return;
248
+ const { from, to } = editorView.state.selection.main;
249
+ const selected = editorView.state.sliceDoc(from, to);
250
+ const text = selected || 'link text';
251
+ const replacement = `[${text}](url)`;
252
+ editorView.dispatch({
253
+ changes: { from, to, insert: replacement },
254
+ selection: {
255
+ anchor: from + text.length + 3,
256
+ head: from + text.length + 6
257
+ }
258
+ });
259
+ editorView.focus();
260
+ }
261
+ },
262
+ {
263
+ id: 'image',
264
+ label: md.image,
265
+ icon: icons.image,
266
+ isSvg: true,
267
+ action: () => insertAtCursor('![alt text](image-url)')
268
+ },
269
+ {
270
+ id: 'table',
271
+ label: md.table,
272
+ icon: icons.table,
273
+ isSvg: true,
274
+ action: () =>
275
+ insertAtCursor('\n| Header | Header |\n| ------ | ------ |\n| Cell | Cell |\n')
248
276
  }
249
- },
250
- {
251
- id: 'image',
252
- label: 'Image',
253
- icon: icons.image,
254
- isSvg: true,
255
- action: () => insertAtCursor('![alt text](image-url)')
256
- },
257
- {
258
- id: 'table',
259
- label: 'Table',
260
- icon: icons.table,
261
- isSvg: true,
262
- action: () =>
263
- insertAtCursor('\n| Header | Header |\n| ------ | ------ |\n| Cell | Cell |\n')
264
- }
265
- ];
277
+ ];
278
+ });
266
279
 
267
280
  // ── CM6 Keyboard shortcuts for toolbar actions ───────────
268
281
 
@@ -327,11 +340,14 @@
327
340
  // ── Editor setup ─────────────────────────────────────────
328
341
 
329
342
  function createExtensions() {
343
+ const placeholderText = placeholderProp ?? m().form.markdown.placeholder;
344
+
330
345
  const extensions = [
331
346
  lineNumbers(),
332
347
  highlightSpecialChars(),
333
348
  highlightActiveLine(),
334
349
  drawSelection(),
350
+ placeholderCompartment.of(placeholder(placeholderText)),
335
351
 
336
352
  // Editing features (skip when read-only)
337
353
  ...(disabled
@@ -394,10 +410,12 @@
394
410
  EditorView.lineWrapping,
395
411
 
396
412
  // Accessibility
397
- EditorView.contentAttributes.of({
398
- 'aria-label': 'Markdown editor',
399
- 'aria-multiline': 'true'
400
- })
413
+ ariaLabelCompartment.of(
414
+ EditorView.contentAttributes.of({
415
+ 'aria-label': m().form.markdown.editor,
416
+ 'aria-multiline': 'true'
417
+ })
418
+ )
401
419
  ];
402
420
 
403
421
  return extensions;
@@ -462,6 +480,27 @@
462
480
  updateStats(editorView.state.doc);
463
481
  }
464
482
  });
483
+
484
+ // Reconfigure CodeMirror-owned strings (placeholder, content aria-label)
485
+ // when the consumer's locale or `placeholder` prop changes. Toolbar and
486
+ // status-bar strings rerender via `$derived` in the template; CodeMirror
487
+ // lives outside Svelte's reactivity graph, so this effect bridges the gap.
488
+ $effect(() => {
489
+ const placeholderText = placeholderProp ?? m().form.markdown.placeholder;
490
+ const ariaLabel = m().form.markdown.editor;
491
+ if (!editorView) return;
492
+ editorView.dispatch({
493
+ effects: [
494
+ placeholderCompartment.reconfigure(placeholder(placeholderText)),
495
+ ariaLabelCompartment.reconfigure(
496
+ EditorView.contentAttributes.of({
497
+ 'aria-label': ariaLabel,
498
+ 'aria-multiline': 'true'
499
+ })
500
+ )
501
+ ]
502
+ });
503
+ });
465
504
  </script>
466
505
 
467
506
  <div
@@ -481,7 +520,11 @@
481
520
 
482
521
  <!-- Toolbar -->
483
522
  {#if showToolbar && !disabled}
484
- <div class="form-markdown-editor__toolbar" role="toolbar" aria-label="Markdown formatting">
523
+ <div
524
+ class="form-markdown-editor__toolbar"
525
+ role="toolbar"
526
+ aria-label={m().form.markdown.toolbar}
527
+ >
485
528
  {#each toolbarActions as item}
486
529
  {#if item === '|'}
487
530
  <span class="form-markdown-editor__separator"></span>
@@ -514,10 +557,11 @@
514
557
 
515
558
  <!-- Status bar -->
516
559
  {#if showStatusBar}
560
+ {@const md = m().form.markdown}
517
561
  <div class="form-markdown-editor__status">
518
- <span>words: {wordCount}</span>
519
- <span>lines: {lineCount}</span>
520
- <span>characters: {charCount}</span>
562
+ <span>{md.words}: {wordCount}</span>
563
+ <span>{md.lines}: {lineCount}</span>
564
+ <span>{md.characters}: {charCount}</span>
521
565
  </div>
522
566
  {/if}
523
567
  </div>
@@ -3,7 +3,7 @@ interface Props {
3
3
  id: string;
4
4
  /** Current value (markdown string) */
5
5
  value: string;
6
- /** Placeholder text shown when empty */
6
+ /** Placeholder text shown when empty. Falls back to `messages.form.markdown.placeholder`. */
7
7
  placeholder?: string;
8
8
  /** Whether the field is required */
9
9
  required?: boolean;
@@ -46,6 +46,7 @@
46
46
  import { createTemplateAutocomplete } from './templateAutocomplete.js';
47
47
  import { getVariableSchema } from '../../services/variableService.js';
48
48
  import { logger } from '../../utils/logger.js';
49
+ import { m } from '../../messages/index.js';
49
50
 
50
51
  interface Props {
51
52
  /** Field identifier */
@@ -506,7 +507,7 @@
506
507
  class:form-template-editor__container--dark={darkTheme}
507
508
  role="textbox"
508
509
  aria-multiline="true"
509
- aria-label="Template editor"
510
+ aria-label={m().form.template.editor}
510
511
  ></div>
511
512
 
512
513
  <!-- Loading banner (shown while fetching variables from API) -->
@@ -9,14 +9,20 @@
9
9
  -->
10
10
 
11
11
  <script lang="ts">
12
+ import { m, warnDeprecatedProp } from '../../messages/index.js';
13
+
12
14
  interface Props {
13
15
  /** Field identifier */
14
16
  id: string;
15
17
  /** Current value */
16
18
  value: boolean;
17
- /** Label shown when toggle is on */
19
+ /**
20
+ * @deprecated since v1.8 — use `messages.form.toggle.enabled`. Removed in v2.0.
21
+ */
18
22
  onLabel?: string;
19
- /** Label shown when toggle is off */
23
+ /**
24
+ * @deprecated since v1.8 — use `messages.form.toggle.disabled`. Removed in v2.0.
25
+ */
20
26
  offLabel?: string;
21
27
  /** Whether the field is disabled (read-only) */
22
28
  disabled?: boolean;
@@ -29,13 +35,25 @@
29
35
  let {
30
36
  id,
31
37
  value = false,
32
- onLabel = 'Enabled',
33
- offLabel = 'Disabled',
38
+ onLabel,
39
+ offLabel,
34
40
  disabled = false,
35
41
  ariaDescribedBy,
36
42
  onChange
37
43
  }: Props = $props();
38
44
 
45
+ // svelte-ignore state_referenced_locally — deprecation warns once per mount; later prop rebinds aren't relevant
46
+ if (onLabel !== undefined) {
47
+ warnDeprecatedProp('FormToggle', 'onLabel', 'messages.form.toggle.enabled');
48
+ }
49
+ // svelte-ignore state_referenced_locally
50
+ if (offLabel !== undefined) {
51
+ warnDeprecatedProp('FormToggle', 'offLabel', 'messages.form.toggle.disabled');
52
+ }
53
+
54
+ const resolvedOnLabel = $derived(onLabel ?? m().form.toggle.enabled);
55
+ const resolvedOffLabel = $derived(offLabel ?? m().form.toggle.disabled);
56
+
39
57
  /**
40
58
  * Handle toggle changes
41
59
  */
@@ -59,7 +77,7 @@
59
77
  <span class="form-toggle__thumb"></span>
60
78
  </span>
61
79
  <span class="form-toggle__label">
62
- {value ? onLabel : offLabel}
80
+ {value ? resolvedOnLabel : resolvedOffLabel}
63
81
  </span>
64
82
  </label>
65
83
 
@@ -3,9 +3,13 @@ interface Props {
3
3
  id: string;
4
4
  /** Current value */
5
5
  value: boolean;
6
- /** Label shown when toggle is on */
6
+ /**
7
+ * @deprecated since v1.8 — use `messages.form.toggle.enabled`. Removed in v2.0.
8
+ */
7
9
  onLabel?: string;
8
- /** Label shown when toggle is off */
10
+ /**
11
+ * @deprecated since v1.8 — use `messages.form.toggle.disabled`. Removed in v2.0.
12
+ */
9
13
  offLabel?: string;
10
14
  /** Whether the field is disabled (read-only) */
11
15
  disabled?: boolean;
@@ -10,6 +10,7 @@
10
10
  <script lang="ts">
11
11
  import Icon from '@iconify/svelte';
12
12
  import type { ChoiceConfig, InterruptChoice } from '../../types/interrupt.js';
13
+ import { m } from '../../messages/index.js';
13
14
 
14
15
  /**
15
16
  * Component props
@@ -41,6 +42,9 @@
41
42
  onSubmit
42
43
  }: Props = $props();
43
44
 
45
+ // Hoist the choice branch — counter, min, max, submit reads.
46
+ const t = $derived(m().interrupt.choice);
47
+
44
48
  /** Local state for selected values */
45
49
  let selectedValues = $state<Set<string>>(new Set());
46
50
 
@@ -165,12 +169,15 @@
165
169
  {#if isMultiple && !isResolved}
166
170
  <div class="choice-prompt__info">
167
171
  <span>
168
- {selectedValues.size} of {config.options.length} selected
172
+ {t.selectedCount({
173
+ n: selectedValues.size,
174
+ total: config.options.length
175
+ })}
169
176
  {#if minSelections > 0}
170
- (min: {minSelections})
177
+ {t.min({ n: minSelections })}
171
178
  {/if}
172
179
  {#if maxSelections < config.options.length}
173
- (max: {maxSelections})
180
+ {t.max({ n: maxSelections })}
174
181
  {/if}
175
182
  </span>
176
183
  </div>
@@ -190,7 +197,7 @@
190
197
  {:else}
191
198
  <Icon icon="mdi:check" />
192
199
  {/if}
193
- <span>Submit</span>
200
+ <span>{t.submit}</span>
194
201
  </button>
195
202
  </div>
196
203
  {/if}
@@ -200,7 +207,9 @@
200
207
  <div class="choice-prompt__resolved-badge">
201
208
  <Icon icon="mdi:check-circle" />
202
209
  <span>
203
- {resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
210
+ {resolvedByUserName
211
+ ? m().interrupt.responseSubmittedBy({ name: resolvedByUserName })
212
+ : m().interrupt.responseSubmitted}
204
213
  </span>
205
214
  </div>
206
215
  {/if}
@@ -10,6 +10,7 @@
10
10
  <script lang="ts">
11
11
  import Icon from '@iconify/svelte';
12
12
  import type { ConfirmationConfig } from '../../types/interrupt.js';
13
+ import { m } from '../../messages/index.js';
13
14
 
14
15
  /**
15
16
  * Component props
@@ -44,11 +45,11 @@
44
45
  onDecline
45
46
  }: Props = $props();
46
47
 
47
- /** Computed label for confirm button */
48
- const confirmLabel = $derived(config.confirmLabel ?? 'Yes');
48
+ /** Computed label for confirm button — config wins, falls back to messages tree. */
49
+ const confirmLabel = $derived(config.confirmLabel ?? m().interrupt.confirmation.yes);
49
50
 
50
- /** Computed label for decline/cancel button */
51
- const declineLabel = $derived(config.cancelLabel ?? 'No');
51
+ /** Computed label for decline/cancel button — config wins, falls back to messages tree. */
52
+ const declineLabel = $derived(config.cancelLabel ?? m().interrupt.confirmation.no);
52
53
  </script>
53
54
 
54
55
  <div
@@ -119,7 +120,9 @@
119
120
  <div class="confirmation-prompt__resolved-badge">
120
121
  <Icon icon="mdi:check-circle" />
121
122
  <span>
122
- {resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
123
+ {resolvedByUserName
124
+ ? m().interrupt.responseSubmittedBy({ name: resolvedByUserName })
125
+ : m().interrupt.responseSubmitted}
123
126
  </span>
124
127
  </div>
125
128
  {/if}
@@ -11,6 +11,7 @@
11
11
  import Icon from '@iconify/svelte';
12
12
  import SchemaForm from '../SchemaForm.svelte';
13
13
  import type { FormConfig } from '../../types/interrupt.js';
14
+ import { getMessages, m, mergeMessages, setMessages } from '../../messages/index.js';
14
15
 
15
16
  /**
16
17
  * Component props
@@ -49,6 +50,10 @@
49
50
  /** Display values - either resolved or current form values */
50
51
  const displayValues = $derived(isResolved ? (resolvedValue ?? {}) : formValues);
51
52
 
53
+ // Hoist the interrupt branch — six reads in the template, three of them
54
+ // inside `formatResolvedValue` which is called per `{#each schema.property}`.
55
+ const interrupt = $derived(m().interrupt);
56
+
52
57
  /**
53
58
  * Handle form value changes
54
59
  */
@@ -66,14 +71,30 @@
66
71
  }
67
72
 
68
73
  /**
69
- * Format resolved value for display
74
+ * Format resolved value for display.
75
+ * Returns localized strings via the current messages tree.
70
76
  */
71
77
  function formatResolvedValue(value: unknown): string {
72
- if (value === null || value === undefined) return '—';
73
- if (typeof value === 'boolean') return value ? 'Yes' : 'No';
78
+ if (value === null || value === undefined) return interrupt.form.empty;
79
+ if (typeof value === 'boolean') return value ? interrupt.form.yes : interrupt.form.no;
74
80
  if (typeof value === 'object') return JSON.stringify(value, null, 2);
75
81
  return String(value);
76
82
  }
83
+
84
+ // Scope a messages override for the inner SchemaForm so its Save button reads
85
+ // the interrupt-specific submit label (e.g. "Submit"), and the cancel button
86
+ // remains empty — historical behavior that effectively hid it. Avoids passing
87
+ // deprecated `saveLabel` / `cancelLabel` props on SchemaForm.
88
+ // Merges over the parent's tree so consumer-supplied overrides higher up
89
+ // (e.g. translations from <FlowDrop messages={...} />) still apply.
90
+ const parentMessages = getMessages();
91
+ const scopedMessages = $derived.by(() => {
92
+ const base = parentMessages();
93
+ return mergeMessages(base, {
94
+ form: { schema: { save: base.interrupt.form.submit, cancel: '' } }
95
+ });
96
+ });
97
+ setMessages(() => scopedMessages);
77
98
  </script>
78
99
 
79
100
  <div
@@ -101,8 +122,6 @@
101
122
  onChange={handleChange}
102
123
  onSave={handleSave}
103
124
  showActions={true}
104
- saveLabel="Submit"
105
- cancelLabel=""
106
125
  loading={isSubmitting}
107
126
  disabled={isResolved}
108
127
  />
@@ -110,7 +129,7 @@
110
129
  {:else}
111
130
  <!-- Resolved state: Show submitted values as read-only -->
112
131
  <div class="form-prompt__resolved-values">
113
- <h4 class="form-prompt__resolved-title">Submitted Values</h4>
132
+ <h4 class="form-prompt__resolved-title">{interrupt.form.submittedValuesTitle}</h4>
114
133
  <div class="form-prompt__values-list">
115
134
  {#each Object.entries(config.schema.properties ?? {}) as [key, field]}
116
135
  {@const value = displayValues[key]}
@@ -131,7 +150,9 @@
131
150
  <div class="form-prompt__resolved-badge">
132
151
  <Icon icon="mdi:check-circle" />
133
152
  <span>
134
- {resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
153
+ {resolvedByUserName
154
+ ? interrupt.responseSubmittedBy({ name: resolvedByUserName })
155
+ : interrupt.responseSubmitted}
135
156
  </span>
136
157
  </div>
137
158
  {/if}