@flowdrop/flowdrop 1.6.0 → 1.8.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.
Files changed (78) hide show
  1. package/README.md +10 -0
  2. package/dist/components/App.svelte +153 -84
  3. package/dist/components/App.svelte.d.ts +16 -1
  4. package/dist/components/ConfigModal.svelte +2 -1
  5. package/dist/components/ConfigPanel.svelte +3 -2
  6. package/dist/components/FlowDropZone.svelte +2 -1
  7. package/dist/components/LogsSidebar.svelte +3 -2
  8. package/dist/components/Navbar.svelte +10 -6
  9. package/dist/components/NodeSidebar.svelte +4 -3
  10. package/dist/components/NodeStatusOverlay.svelte +14 -7
  11. package/dist/components/NodeSwapPicker.svelte +2 -1
  12. package/dist/components/PipelineStatus.svelte +10 -7
  13. package/dist/components/ReadOnlyDetails.svelte +4 -2
  14. package/dist/components/SchemaForm.svelte +20 -9
  15. package/dist/components/SchemaForm.svelte.d.ts +2 -4
  16. package/dist/components/SettingsModal.svelte +4 -3
  17. package/dist/components/SettingsPanel.svelte +3 -2
  18. package/dist/components/SwapMappingEditor.svelte +2 -1
  19. package/dist/components/WorkflowEditor.svelte +3 -2
  20. package/dist/components/chat/AIChatPanel.svelte +22 -7
  21. package/dist/components/chat/AIChatPanel.svelte.d.ts +3 -0
  22. package/dist/components/chat/CommandPreview.svelte +10 -6
  23. package/dist/components/console/CommandConsole.svelte +4 -3
  24. package/dist/components/form/FormArray.svelte +33 -20
  25. package/dist/components/form/FormArray.svelte.d.ts +3 -1
  26. package/dist/components/form/FormAutocomplete.svelte +18 -7
  27. package/dist/components/form/FormCodeEditor.svelte +6 -4
  28. package/dist/components/form/FormFieldWrapper.svelte +2 -1
  29. package/dist/components/form/FormMarkdownEditor.svelte +152 -108
  30. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +1 -1
  31. package/dist/components/form/FormTemplateEditor.svelte +2 -1
  32. package/dist/components/form/FormToggle.svelte +23 -5
  33. package/dist/components/form/FormToggle.svelte.d.ts +6 -2
  34. package/dist/components/interrupt/ChoicePrompt.svelte +14 -5
  35. package/dist/components/interrupt/ConfirmationPrompt.svelte +8 -5
  36. package/dist/components/interrupt/FormPrompt.svelte +28 -7
  37. package/dist/components/interrupt/InterruptBubble.svelte +27 -18
  38. package/dist/components/interrupt/ReviewPrompt.svelte +32 -22
  39. package/dist/components/interrupt/TextInputPrompt.svelte +12 -5
  40. package/dist/components/layouts/MainLayout.svelte +4 -3
  41. package/dist/components/nodes/GatewayNode.svelte +8 -3
  42. package/dist/components/nodes/IdeaNode.svelte +2 -1
  43. package/dist/components/nodes/NotesNode.svelte +18 -12
  44. package/dist/components/nodes/SimpleNode.svelte +8 -8
  45. package/dist/components/nodes/WorkflowNode.svelte +8 -3
  46. package/dist/components/playground/ChatPanel.svelte +36 -24
  47. package/dist/components/playground/MessageBubble.svelte +15 -7
  48. package/dist/components/playground/Playground.svelte +2 -1
  49. package/dist/components/playground/PlaygroundModal.svelte +2 -1
  50. package/dist/components/playground/SessionManager.svelte +14 -10
  51. package/dist/core/index.d.ts +2 -0
  52. package/dist/core/index.js +9 -0
  53. package/dist/editor/index.d.ts +1 -1
  54. package/dist/editor/index.js +1 -1
  55. package/dist/messages/context.d.ts +29 -0
  56. package/dist/messages/context.js +38 -0
  57. package/dist/messages/defaults.d.ts +396 -0
  58. package/dist/messages/defaults.js +356 -0
  59. package/dist/messages/deprecation.d.ts +20 -0
  60. package/dist/messages/deprecation.js +33 -0
  61. package/dist/messages/index.d.ts +11 -0
  62. package/dist/messages/index.js +10 -0
  63. package/dist/messages/merge.d.ts +28 -0
  64. package/dist/messages/merge.js +53 -0
  65. package/dist/messages/types.d.ts +29 -0
  66. package/dist/messages/types.js +13 -0
  67. package/dist/schemas/v1/workflow.schema.json +5 -0
  68. package/dist/services/draftStorage.d.ts +13 -0
  69. package/dist/services/draftStorage.js +36 -0
  70. package/dist/stores/workflowStore.svelte.d.ts +1 -0
  71. package/dist/stores/workflowStore.svelte.js +1 -0
  72. package/dist/styles/base.css +13 -4
  73. package/dist/svelte-app.d.ts +11 -0
  74. package/dist/svelte-app.js +11 -2
  75. package/dist/types/index.d.ts +2 -0
  76. package/dist/utils/connections.d.ts +4 -0
  77. package/dist/utils/connections.js +6 -0
  78. package/package.json +1 -1
package/README.md CHANGED
@@ -131,6 +131,16 @@ const app = await mountFlowDropApp(container, {
131
131
  });
132
132
  ```
133
133
 
134
+ ## Customising messages
135
+
136
+ Every user-facing string flows through a typed `Messages` tree. Pass a callback to override any subset:
137
+
138
+ ```svelte
139
+ <FlowDrop messages={() => ({ form: { schema: { save: 'Apply' } } })} />
140
+ ```
141
+
142
+ Wire the callback to your i18n library (paraglide-js, sveltekit-i18n, etc.) — locale changes propagate automatically. See the [i18n & Custom Messages guide](https://docs.flowdrop.io/guides/i18n) for the full shape and a paraglide-js worked example.
143
+
134
144
  ## Sub-Module Exports
135
145
 
136
146
  FlowDrop provides tree-shakeable sub-module exports so you can import only what you need:
@@ -57,13 +57,16 @@
57
57
  import { getUiSettings, updateSettings } from '../stores/settingsStore.svelte.js';
58
58
  import {
59
59
  initializePortCompatibility,
60
- getPortCompatibilityChecker
60
+ getPortCompatibilityChecker,
61
+ isPortCompatibilityInitialized
61
62
  } from '../utils/connections.js';
62
63
  import { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
63
64
  import { workflowFormatRegistry } from '../registry/workflowFormatRegistry.js';
64
65
  import { logger } from '../utils/logger.js';
65
66
  import { validateWorkflowData } from '../utils/validation.js';
66
67
  import type { SettingsCategory } from '../types/settings.js';
68
+ import { defaultMessages, mergeMessages, setMessages } from '../messages/index.js';
69
+ import type { MessagesOverride } from '../messages/index.js';
67
70
 
68
71
  /**
69
72
  * Configuration props for runtime customization
@@ -121,6 +124,20 @@
121
124
  showSettingsResetButton?: boolean;
122
125
  /** Pluggable swap strategies — instance-scoped, checked in order */
123
126
  swapStrategies?: SwapStrategy[];
127
+ /** Additional JSON Schema properties to show in the Workflow Settings panel. Values are persisted in workflow.config. */
128
+ workflowSettingsSchema?: ConfigSchema;
129
+ /**
130
+ * Override user-facing strings. Pass either a partial of the `Messages`
131
+ * tree directly, or a callback that returns one. Missing keys fall through
132
+ * to English defaults.
133
+ *
134
+ * For static overrides, a value is fine: `messages={{ common: { save: 'Apply' } }}`.
135
+ * For reactive overrides driven by an i18n library (paraglide, etc.),
136
+ * either form works — Svelte 5's prop reactivity propagates locale changes.
137
+ * The callback form is useful when your translations live behind a
138
+ * function call you'd rather not invoke unless the prop is actually read.
139
+ */
140
+ messages?: MessagesOverride | (() => MessagesOverride);
124
141
  }
125
142
 
126
143
  let {
@@ -146,12 +163,75 @@
146
163
  settingsCategories,
147
164
  showSettingsSyncButton,
148
165
  showSettingsResetButton,
149
- swapStrategies
166
+ swapStrategies,
167
+ workflowSettingsSchema,
168
+ messages: messagesOverride
150
169
  }: Props = $props();
151
170
 
152
171
  // svelte-ignore state_referenced_locally — feature flags don't change at runtime
153
172
  const features = mergeFeatures(propFeatures);
154
173
 
174
+ // Messages: merge consumer overrides over defaults; expose via context as a
175
+ // getter so consumer-side reactivity (e.g. paraglide-js locale switches)
176
+ // propagates into every child without a subscription. Accepts either a
177
+ // value or a callback — normalize here so the rest of the component sees
178
+ // the merged tree directly.
179
+ let mergedMessages = $derived(
180
+ mergeMessages(
181
+ defaultMessages,
182
+ typeof messagesOverride === 'function' ? messagesOverride() : messagesOverride
183
+ )
184
+ );
185
+ // setContext must run during component init (synchronously, not in $effect)
186
+ // — Svelte enforces that. The context value is a getter that closes over
187
+ // the live $derived, so child components always read the current tree.
188
+ setMessages(() => mergedMessages);
189
+
190
+ // Default navbar primary actions — used when no `navbarActions` prop is supplied.
191
+ // Derived so the labels track locale changes.
192
+ const defaultPrimaryActions = $derived([
193
+ {
194
+ label: mergedMessages.navigation.save,
195
+ href: '#save',
196
+ icon: 'heroicons:document-arrow-down',
197
+ variant: 'primary' as const,
198
+ onclick: (e: Event) => {
199
+ e.preventDefault();
200
+ saveWorkflow();
201
+ }
202
+ },
203
+ {
204
+ label: mergedMessages.navigation.export,
205
+ href: '#export',
206
+ icon: 'heroicons:arrow-down-tray',
207
+ variant: 'outline' as const,
208
+ onclick: (e: Event) => {
209
+ e.preventDefault();
210
+ exportWorkflow();
211
+ }
212
+ },
213
+ {
214
+ label: mergedMessages.navigation.import,
215
+ href: '#import',
216
+ icon: 'heroicons:arrow-up-tray',
217
+ variant: 'outline' as const,
218
+ onclick: (e: Event) => {
219
+ e.preventDefault();
220
+ fileInputRef?.click();
221
+ }
222
+ },
223
+ {
224
+ label: mergedMessages.navigation.workflowSettings,
225
+ href: '#settings',
226
+ icon: 'heroicons:cog-6-tooth',
227
+ variant: 'outline' as const,
228
+ onclick: (e: Event) => {
229
+ e.preventDefault();
230
+ toggleWorkflowSettings();
231
+ }
232
+ }
233
+ ]);
234
+
155
235
  // Theme system — resolve named theme or custom object, inject CSS tokens from skin
156
236
  // Explicit prop wins; falls back to user's persisted theme preference from settings
157
237
  let resolvedTheme = $derived(resolveTheme(themeProp ?? getUiSettings().theme));
@@ -223,39 +303,60 @@
223
303
  let swapTargetMetadata = $state<NodeMetadata | null>(null);
224
304
  let swapInteractiveState = $state<InteractiveSwapState | null>(null);
225
305
 
306
+ // Built-in workflow settings field names — consumer schemas must not reuse these.
307
+ const WORKFLOW_SETTINGS_RESERVED = new Set(['name', 'description', 'format']);
308
+
226
309
  // Workflow configuration schema (derived to pick up dynamic format options)
227
- let workflowConfigSchema: ConfigSchema = $derived({
228
- type: 'object' as const,
229
- properties: {
230
- name: {
231
- type: 'string',
232
- title: 'Workflow Name',
233
- description: 'The name of the workflow',
234
- default: ''
235
- },
236
- description: {
237
- type: 'string',
238
- title: 'Description',
239
- description: 'A description of the workflow',
240
- format: 'multiline',
241
- default: ''
310
+ let workflowConfigSchema: ConfigSchema = $derived.by(() => {
311
+ const extraProps = Object.fromEntries(
312
+ Object.entries(workflowSettingsSchema?.properties ?? {}).filter(([k]) => {
313
+ if (WORKFLOW_SETTINGS_RESERVED.has(k)) {
314
+ logger.warn(
315
+ `workflowSettingsSchema: property "${k}" is reserved and will be ignored. Choose a different key.`
316
+ );
317
+ return false;
318
+ }
319
+ return true;
320
+ })
321
+ );
322
+ const extraRequired = (workflowSettingsSchema?.required ?? []).filter(
323
+ (k) => !WORKFLOW_SETTINGS_RESERVED.has(k)
324
+ );
325
+ return {
326
+ type: 'object' as const,
327
+ properties: {
328
+ name: {
329
+ type: 'string',
330
+ title: 'Workflow Name',
331
+ description: 'The name of the workflow',
332
+ default: ''
333
+ },
334
+ description: {
335
+ type: 'string',
336
+ title: 'Description',
337
+ description: 'A description of the workflow',
338
+ format: 'multiline',
339
+ default: ''
340
+ },
341
+ format: {
342
+ type: 'string',
343
+ title: 'Workflow Format',
344
+ description: 'The specification format for this workflow',
345
+ oneOf: workflowFormatRegistry.getOneOfOptions(),
346
+ default: 'flowdrop'
347
+ },
348
+ ...extraProps
242
349
  },
243
- format: {
244
- type: 'string',
245
- title: 'Workflow Format',
246
- description: 'The specification format for this workflow',
247
- oneOf: workflowFormatRegistry.getOneOfOptions(),
248
- default: 'flowdrop'
249
- }
250
- },
251
- required: ['name']
350
+ required: ['name', ...extraRequired]
351
+ };
252
352
  });
253
353
 
254
354
  // Workflow configuration values
255
355
  let workflowConfigValues = $derived({
256
356
  name: getWorkflowName() || '',
257
357
  description: getWorkflowStore()?.description || '',
258
- format: getWorkflowStore()?.metadata?.format || 'flowdrop'
358
+ format: getWorkflowStore()?.metadata?.format || 'flowdrop',
359
+ ...(getWorkflowStore()?.config ?? {})
259
360
  });
260
361
 
261
362
  // Get the current node from the workflow store
@@ -727,7 +828,10 @@
727
828
 
728
829
  // Ensure port compatibility checker is initialized (needed for proximity connect, etc.)
729
830
  // mountFlowDropApp initializes this before mounting, but SvelteKit routes need it here.
730
- initializePortCompatibility(DEFAULT_PORT_CONFIG);
831
+ // Only initialize with defaults if not already set — preserves custom port configs.
832
+ if (!isPortCompatibilityInitialized()) {
833
+ initializePortCompatibility(DEFAULT_PORT_CONFIG);
834
+ }
731
835
 
732
836
  await fetchNodeTypes();
733
837
 
@@ -922,50 +1026,7 @@
922
1026
  {#snippet header()}
923
1027
  <Navbar
924
1028
  title={breadcrumbTitle}
925
- primaryActions={navbarActions.length > 0
926
- ? navbarActions
927
- : [
928
- {
929
- label: 'Save',
930
- href: '#save',
931
- icon: 'heroicons:document-arrow-down',
932
- variant: 'primary',
933
- onclick: (e) => {
934
- e.preventDefault();
935
- saveWorkflow();
936
- }
937
- },
938
- {
939
- label: 'Export',
940
- href: '#export',
941
- icon: 'heroicons:arrow-down-tray',
942
- variant: 'outline',
943
- onclick: (e) => {
944
- e.preventDefault();
945
- exportWorkflow();
946
- }
947
- },
948
- {
949
- label: 'Import',
950
- href: '#import',
951
- icon: 'heroicons:arrow-up-tray',
952
- variant: 'outline',
953
- onclick: (e) => {
954
- e.preventDefault();
955
- fileInputRef?.click();
956
- }
957
- },
958
- {
959
- label: 'Workflow Settings',
960
- href: '#settings',
961
- icon: 'heroicons:cog-6-tooth',
962
- variant: 'outline',
963
- onclick: (e) => {
964
- e.preventDefault();
965
- toggleWorkflowSettings();
966
- }
967
- }
968
- ]}
1029
+ primaryActions={navbarActions.length > 0 ? navbarActions : defaultPrimaryActions}
969
1030
  showStatus={true}
970
1031
  {showSettings}
971
1032
  {settingsCategories}
@@ -1014,7 +1075,7 @@
1014
1075
  />
1015
1076
  {:else if isWorkflowSettingsOpen}
1016
1077
  <ConfigPanel
1017
- title="Workflow Settings"
1078
+ title={mergedMessages.navigation.workflowSettingsPanelTitle}
1018
1079
  id={getWorkflowStore()?.id}
1019
1080
  details={[
1020
1081
  {
@@ -1026,7 +1087,7 @@
1026
1087
  value: String(getWorkflowStore()?.edges?.length ?? 0)
1027
1088
  }
1028
1089
  ]}
1029
- configTitle="Settings"
1090
+ configTitle={mergedMessages.navigation.workflowSettingsPanelSubtitle}
1030
1091
  onClose={() => (isWorkflowSettingsOpen = false)}
1031
1092
  >
1032
1093
  <ConfigForm
@@ -1055,13 +1116,16 @@
1055
1116
  }
1056
1117
  }
1057
1118
 
1119
+ // Extract built-in fields; everything else belongs in workflow.config
1120
+ const { name, description, format: _format, ...customConfig } = config;
1058
1121
  workflowActions.batchUpdate({
1059
- name: config.name as string,
1060
- description: config.description as string | undefined,
1122
+ name: name as string,
1123
+ description: description as string | undefined,
1061
1124
  metadata: {
1062
1125
  ...wf.metadata,
1063
1126
  format: newFormat
1064
- }
1127
+ },
1128
+ ...(workflowSettingsSchema && { config: customConfig as Record<string, unknown> })
1065
1129
  });
1066
1130
  }
1067
1131
  }}
@@ -1072,7 +1136,8 @@
1072
1136
  <ConfigPanel
1073
1137
  title={currentNode.data.label}
1074
1138
  id={currentNode.id}
1075
- description={currentNode.data.metadata?.description || 'Node configuration'}
1139
+ description={currentNode.data.metadata?.description ||
1140
+ mergedMessages.navigation.nodeConfigDescription}
1076
1141
  details={[
1077
1142
  {
1078
1143
  label: 'Type',
@@ -1129,7 +1194,7 @@
1129
1194
  {/if}
1130
1195
  {/snippet}
1131
1196
 
1132
- <!-- Bottom Panel: Tabbed Console / AI Chat -->
1197
+ <!-- Bottom Panel: Tabbed Console / AI Assistant -->
1133
1198
  {#snippet bottomPanel()}
1134
1199
  <div class="bottom-panel-tabs">
1135
1200
  <div class="bottom-panel-tabs__bar">
@@ -1139,7 +1204,7 @@
1139
1204
  : ''}"
1140
1205
  onclick={() => updateSettings({ ui: { bottomPanelTab: 'console' } })}
1141
1206
  >
1142
- Console
1207
+ {mergedMessages.navigation.bottomPanel.console}
1143
1208
  </button>
1144
1209
  <button
1145
1210
  class="bottom-panel-tabs__tab {getUiSettings().bottomPanelTab === 'chat'
@@ -1147,7 +1212,7 @@
1147
1212
  : ''}"
1148
1213
  onclick={() => updateSettings({ ui: { bottomPanelTab: 'chat' } })}
1149
1214
  >
1150
- AI Chat
1215
+ {mergedMessages.navigation.bottomPanel.chat}
1151
1216
  </button>
1152
1217
  </div>
1153
1218
  <div class="bottom-panel-tabs__content">
@@ -1232,15 +1297,19 @@
1232
1297
  onclick={handleCanvasClick}
1233
1298
  onkeydown={(e) => e.key === 'Escape' && closeConfigSidebar()}
1234
1299
  role="region"
1235
- aria-label="Workflow canvas"
1300
+ aria-label={mergedMessages.layout.workflowCanvas}
1236
1301
  >
1237
1302
  <!-- Floating sidebar toggle — always visible on the canvas top-left -->
1238
1303
  {#if !disableSidebar}
1239
1304
  <button
1240
1305
  class="flowdrop-sidebar-fab"
1241
1306
  onclick={toggleSidebar}
1242
- aria-label={isSidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
1243
- title={isSidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
1307
+ aria-label={isSidebarCollapsed
1308
+ ? mergedMessages.layout.expandSidebar
1309
+ : mergedMessages.layout.collapseSidebar}
1310
+ title={isSidebarCollapsed
1311
+ ? mergedMessages.layout.expandSidebar
1312
+ : mergedMessages.layout.collapseSidebar}
1244
1313
  >
1245
1314
  <Icon icon={isSidebarCollapsed ? 'mdi:menu' : 'mdi:menu-open'} />
1246
1315
  </button>
@@ -1,10 +1,11 @@
1
- import type { NodeMetadata, Workflow } from '../types/index.js';
1
+ import type { NodeMetadata, Workflow, ConfigSchema } from '../types/index.js';
2
2
  import type { SwapStrategy } from '../utils/nodeSwap.js';
3
3
  import type { EndpointConfig } from '../config/endpoints.js';
4
4
  import type { AuthProvider } from '../types/auth.js';
5
5
  import type { FlowDropEventHandlers, FlowDropFeatures } from '../types/events.js';
6
6
  import type { FlowDropTheme, FlowDropThemeName } from '../types/theme.js';
7
7
  import type { SettingsCategory } from '../types/settings.js';
8
+ import type { MessagesOverride } from '../messages/index.js';
8
9
  /**
9
10
  * Configuration props for runtime customization
10
11
  */
@@ -61,6 +62,20 @@ interface Props {
61
62
  showSettingsResetButton?: boolean;
62
63
  /** Pluggable swap strategies — instance-scoped, checked in order */
63
64
  swapStrategies?: SwapStrategy[];
65
+ /** Additional JSON Schema properties to show in the Workflow Settings panel. Values are persisted in workflow.config. */
66
+ workflowSettingsSchema?: ConfigSchema;
67
+ /**
68
+ * Override user-facing strings. Pass either a partial of the `Messages`
69
+ * tree directly, or a callback that returns one. Missing keys fall through
70
+ * to English defaults.
71
+ *
72
+ * For static overrides, a value is fine: `messages={{ common: { save: 'Apply' } }}`.
73
+ * For reactive overrides driven by an i18n library (paraglide, etc.),
74
+ * either form works — Svelte 5's prop reactivity propagates locale changes.
75
+ * The callback form is useful when your translations live behind a
76
+ * function call you'd rather not invoke unless the prop is actually read.
77
+ */
78
+ messages?: MessagesOverride | (() => MessagesOverride);
64
79
  }
65
80
  declare const App: import("svelte").Component<Props, {}, "">;
66
81
  type App = ReturnType<typeof App>;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { ConfigSchema, ConfigValues } from '../types';
3
3
  import ConfigForm from './ConfigForm.svelte';
4
+ import { m } from '../messages/index.js';
4
5
 
5
6
  interface Props {
6
7
  isOpen: boolean;
@@ -60,7 +61,7 @@
60
61
  type="button"
61
62
  class="config-modal__close-btn"
62
63
  onclick={handleClose}
63
- aria-label="Close configuration modal"
64
+ aria-label={m().navigation.closeConfigModal}
64
65
  >
65
66
  <span aria-hidden="true">×</span>
66
67
  </button>
@@ -11,6 +11,7 @@
11
11
  import Icon from '@iconify/svelte';
12
12
  import ReadOnlyDetails from './ReadOnlyDetails.svelte';
13
13
  import { getUiSettings } from '../stores/settingsStore.svelte.js';
14
+ import { m } from '../messages/index.js';
14
15
 
15
16
  /**
16
17
  * A single detail item with label and value
@@ -70,13 +71,13 @@
70
71
  <button
71
72
  class="config-panel__action-btn"
72
73
  onclick={onSwap}
73
- aria-label="Swap node"
74
+ aria-label={m().layout.swapNode}
74
75
  title="Swap node type"
75
76
  >
76
77
  <Icon icon="heroicons:arrows-right-left" />
77
78
  </button>
78
79
  {/if}
79
- <button class="config-panel__close" onclick={onClose} aria-label="Close panel"> × </button>
80
+ <button class="config-panel__close" onclick={onClose} aria-label={m().layout.closeConfigPanel}> × </button>
80
81
  </div>
81
82
  </div>
82
83
 
@@ -7,6 +7,7 @@
7
7
  <script lang="ts">
8
8
  import { useSvelteFlow } from '@xyflow/svelte';
9
9
  import type { Snippet } from 'svelte';
10
+ import { m } from '../messages/index.js';
10
11
 
11
12
  interface Props {
12
13
  ondrop: (nodeTypeData: string, position: { x: number; y: number }) => void;
@@ -68,7 +69,7 @@
68
69
  <div
69
70
  class="flow-drop-zone"
70
71
  role="application"
71
- aria-label="Workflow canvas"
72
+ aria-label={m().layout.workflowCanvas}
72
73
  ondragover={handleDragOver}
73
74
  ondrop={handleDrop}
74
75
  >
@@ -8,6 +8,7 @@
8
8
  <script lang="ts">
9
9
  import Icon from '@iconify/svelte';
10
10
  import type { WorkflowNode as WorkflowNodeType } from '../types/index.js';
11
+ import { m } from '../messages/index.js';
11
12
 
12
13
  interface LogEntry {
13
14
  timestamp: string;
@@ -163,7 +164,7 @@
163
164
  class="logs-sidebar"
164
165
  class:logs-sidebar--open={props.isOpen}
165
166
  role="dialog"
166
- aria-label="Execution logs sidebar"
167
+ aria-label={m().layout.executionLogs}
167
168
  aria-modal="true"
168
169
  tabindex="-1"
169
170
  onkeydown={handleKeydown}
@@ -189,7 +190,7 @@
189
190
  class="logs-sidebar__close-btn"
190
191
  onclick={handleClose}
191
192
  title="Close logs sidebar (Esc)"
192
- aria-label="Close logs sidebar"
193
+ aria-label={m().layout.closeLogsSidebar}
193
194
  >
194
195
  <Icon icon="mdi:close" />
195
196
  </button>
@@ -11,6 +11,7 @@
11
11
  import Logo from './Logo.svelte';
12
12
  import SettingsModal from './SettingsModal.svelte';
13
13
  import type { SettingsCategory } from '../types/settings.js';
14
+ import { m } from '../messages/index.js';
14
15
 
15
16
  interface NavbarAction {
16
17
  label: string;
@@ -64,6 +65,9 @@
64
65
  // Settings modal state
65
66
  let isSettingsOpen = $state(false);
66
67
 
68
+ // Hoist the navigation branch — six reads in the template.
69
+ const nav = $derived(m().navigation);
70
+
67
71
  // Close dropdown when clicking outside
68
72
  function handleClickOutside(event: MouseEvent) {
69
73
  const target = event.target as HTMLElement;
@@ -90,8 +94,8 @@
90
94
  <Logo />
91
95
  </div>
92
96
  <div>
93
- <h1 class="flowdrop-text--logo flowdrop-font--bold">FlowDrop</h1>
94
- <p class="flowdrop-text--tagline flowdrop-text--gray">Visual Workflow Manager</p>
97
+ <h1 class="flowdrop-text--logo flowdrop-font--bold">{nav.appName}</h1>
98
+ <p class="flowdrop-text--tagline flowdrop-text--gray">{nav.tagline}</p>
95
99
  </div>
96
100
  </div>
97
101
  </div>
@@ -104,7 +108,7 @@
104
108
  <div class="flowdrop-navbar__status-container">
105
109
  <div class="flowdrop-navbar__status">
106
110
  <div class="flowdrop-navbar__status-indicator"></div>
107
- <span class="flowdrop-navbar__status-text">Connected</span>
111
+ <span class="flowdrop-navbar__status-text">{nav.connected}</span>
108
112
  </div>
109
113
  </div>
110
114
  {/if}
@@ -112,7 +116,7 @@
112
116
  <!-- Title or Breadcrumbs on bottom -->
113
117
  {#if breadcrumbs.length > 0}
114
118
  <div class="flowdrop-navbar__breadcrumb-container">
115
- <nav class="flowdrop-navbar__breadcrumb" aria-label="Breadcrumb">
119
+ <nav class="flowdrop-navbar__breadcrumb" aria-label={nav.breadcrumbAriaLabel}>
116
120
  <ol class="flowdrop-navbar__breadcrumb-list">
117
121
  {#each breadcrumbs as breadcrumb, index (index)}
118
122
  <li class="flowdrop-navbar__breadcrumb-item">
@@ -241,8 +245,8 @@
241
245
  <button
242
246
  class="flowdrop-navbar__settings-btn"
243
247
  onclick={() => (isSettingsOpen = true)}
244
- title="Settings"
245
- aria-label="Open settings"
248
+ title={nav.settingsTitle}
249
+ aria-label={nav.settingsAriaLabel}
246
250
  >
247
251
  <Icon icon="mdi:cog" />
248
252
  </button>
@@ -13,6 +13,7 @@
13
13
  import { getCategoryLabel } from '../stores/categoriesStore.svelte.js';
14
14
  import { getUiSettings } from '../stores/settingsStore.svelte.js';
15
15
  import { extractConfigDefaults } from '../utils/nodeIds.js';
16
+ import { m } from '../messages/index.js';
16
17
 
17
18
  interface Props {
18
19
  nodes: NodeMetadata[];
@@ -168,7 +169,7 @@
168
169
  class:flowdrop-sidebar--collapsed={isCollapsed}
169
170
  class:flowdrop-sidebar--compact={getUiSettings().compactMode}
170
171
  style:width="{isCollapsed ? 0 : getUiSettings().sidebarWidth}px"
171
- aria-label="Components sidebar"
172
+ aria-label={m().layout.componentsSidebar}
172
173
  >
173
174
  <!-- Search Section — visibility controlled by --fd-sidebar-search-display -->
174
175
  <div class="flowdrop-sidebar__search">
@@ -176,13 +177,13 @@
176
177
  <div class="flowdrop-join__item flowdrop-flex--1">
177
178
  <input
178
179
  type="text"
179
- placeholder="Search components..."
180
+ placeholder={m().layout.searchComponents}
180
181
  class="flowdrop-input flowdrop-join__item flowdrop-w--full"
181
182
  bind:value={searchInput}
182
183
  oninput={handleSearchChange}
183
184
  />
184
185
  </div>
185
- <button class="flowdrop-btn flowdrop-join__item" aria-label="Search components">
186
+ <button class="flowdrop-btn flowdrop-join__item" aria-label={m().layout.searchComponents}>
186
187
  <Icon icon="mdi:magnify" class="flowdrop-icon" />
187
188
  </button>
188
189
  </div>
@@ -17,6 +17,7 @@
17
17
  formatExecutionDuration,
18
18
  formatLastExecuted
19
19
  } from '../utils/nodeStatus.js';
20
+ import { m } from '../messages/index.js';
20
21
 
21
22
  interface Props {
22
23
  nodeId?: string;
@@ -79,6 +80,9 @@
79
80
  let shouldShow = $derived(
80
81
  executionInfo.status !== 'idle' || executionInfo.executionCount > 0 || executionInfo.isExecuting
81
82
  );
83
+
84
+ // Hoist the overlay branch — seven reads in the template.
85
+ const overlay = $derived(m().status.overlay);
82
86
  </script>
83
87
 
84
88
  {#if shouldShow}
@@ -98,9 +102,12 @@
98
102
  "
99
103
  onmouseenter={() => (isHovered = true)}
100
104
  onmouseleave={() => (isHovered = false)}
101
- title="{getStatusLabel(executionInfo.status)} - Executed {executionInfo.executionCount} times"
105
+ title={overlay.tooltip({
106
+ status: getStatusLabel(executionInfo.status),
107
+ count: executionInfo.executionCount
108
+ })}
102
109
  role="status"
103
- aria-label="Node execution status: {getStatusLabel(executionInfo.status)}"
110
+ aria-label={overlay.ariaLabel({ status: getStatusLabel(executionInfo.status) })}
104
111
  >
105
112
  <!-- Status Display: [icon] [label] -->
106
113
  <div
@@ -130,18 +137,18 @@
130
137
  {#if showDetails && isHovered}
131
138
  <div class="node-status-overlay__details">
132
139
  <div class="node-status-overlay__detail-item">
133
- <span class="node-status-overlay__detail-label">Status:</span>
140
+ <span class="node-status-overlay__detail-label">{overlay.statusLabel}</span>
134
141
  <span class="node-status-overlay__detail-value"
135
142
  >{getStatusLabel(executionInfo.status)}</span
136
143
  >
137
144
  </div>
138
145
  <div class="node-status-overlay__detail-item">
139
- <span class="node-status-overlay__detail-label">Executions:</span>
146
+ <span class="node-status-overlay__detail-label">{overlay.executionsLabel}</span>
140
147
  <span class="node-status-overlay__detail-value">{executionInfo.executionCount}</span>
141
148
  </div>
142
149
  {#if executionInfo.lastExecuted}
143
150
  <div class="node-status-overlay__detail-item">
144
- <span class="node-status-overlay__detail-label">Last Run:</span>
151
+ <span class="node-status-overlay__detail-label">{overlay.lastRunLabel}</span>
145
152
  <span class="node-status-overlay__detail-value"
146
153
  >{formatLastExecuted(executionInfo.lastExecuted)}</span
147
154
  >
@@ -149,7 +156,7 @@
149
156
  {/if}
150
157
  {#if executionInfo.lastExecutionDuration}
151
158
  <div class="node-status-overlay__detail-item">
152
- <span class="node-status-overlay__detail-label">Duration:</span>
159
+ <span class="node-status-overlay__detail-label">{overlay.durationLabel}</span>
153
160
  <span class="node-status-overlay__detail-value"
154
161
  >{formatExecutionDuration(executionInfo.lastExecutionDuration)}</span
155
162
  >
@@ -157,7 +164,7 @@
157
164
  {/if}
158
165
  {#if executionInfo.lastError}
159
166
  <div class="node-status-overlay__detail-item node-status-overlay__detail-item--error">
160
- <span class="node-status-overlay__detail-label">Error:</span>
167
+ <span class="node-status-overlay__detail-label">{overlay.errorLabel}</span>
161
168
  <span class="node-status-overlay__detail-value">{executionInfo.lastError}</span>
162
169
  </div>
163
170
  {/if}