@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
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:
@@ -82,6 +82,13 @@ export function extractCommands(llmResponse) {
82
82
  currentExplanation.push(line);
83
83
  }
84
84
  }
85
+ // Flush dangling multiline buffer (LLM never closed """ and the response
86
+ // ended before any closing fence). Surface as a command so the parser
87
+ // produces a visible error rather than silently dropping the content.
88
+ if (multilineBuffer !== null) {
89
+ commands.push(multilineBuffer.join('\n'));
90
+ multilineBuffer = null;
91
+ }
85
92
  // Flush remaining explanation text
86
93
  if (currentExplanation.length > 0) {
87
94
  explanationParts.push(currentExplanation.join('\n'));
@@ -259,6 +259,18 @@ export function parseCommand(input) {
259
259
  if (!trimmed) {
260
260
  return { ok: false, error: 'Empty command', input };
261
261
  }
262
+ // Detect an unclosed multiline """ block — common when a low-quality LLM
263
+ // omits the closing """ on its own line. The opener pattern is `"""\n`
264
+ // (triple-quote followed by a newline), and a well-formed value must end
265
+ // with `"""`. If we see the opener but not the closer, surface a clear
266
+ // error instead of falling through to a generic "Invalid syntax".
267
+ if (trimmed.includes('"""\n') && !trimmed.endsWith('"""')) {
268
+ return {
269
+ ok: false,
270
+ error: 'Unclosed """ block — missing closing """ on its own line',
271
+ input
272
+ };
273
+ }
262
274
  for (const rule of rules) {
263
275
  const match = trimmed.match(rule.pattern);
264
276
  if (match) {
@@ -65,6 +65,8 @@
65
65
  import { logger } from '../utils/logger.js';
66
66
  import { validateWorkflowData } from '../utils/validation.js';
67
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';
68
70
 
69
71
  /**
70
72
  * Configuration props for runtime customization
@@ -124,6 +126,18 @@
124
126
  swapStrategies?: SwapStrategy[];
125
127
  /** Additional JSON Schema properties to show in the Workflow Settings panel. Values are persisted in workflow.config. */
126
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);
127
141
  }
128
142
 
129
143
  let {
@@ -150,12 +164,74 @@
150
164
  showSettingsSyncButton,
151
165
  showSettingsResetButton,
152
166
  swapStrategies,
153
- workflowSettingsSchema
167
+ workflowSettingsSchema,
168
+ messages: messagesOverride
154
169
  }: Props = $props();
155
170
 
156
171
  // svelte-ignore state_referenced_locally — feature flags don't change at runtime
157
172
  const features = mergeFeatures(propFeatures);
158
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
+
159
235
  // Theme system — resolve named theme or custom object, inject CSS tokens from skin
160
236
  // Explicit prop wins; falls back to user's persisted theme preference from settings
161
237
  let resolvedTheme = $derived(resolveTheme(themeProp ?? getUiSettings().theme));
@@ -950,50 +1026,7 @@
950
1026
  {#snippet header()}
951
1027
  <Navbar
952
1028
  title={breadcrumbTitle}
953
- primaryActions={navbarActions.length > 0
954
- ? navbarActions
955
- : [
956
- {
957
- label: 'Save',
958
- href: '#save',
959
- icon: 'heroicons:document-arrow-down',
960
- variant: 'primary',
961
- onclick: (e) => {
962
- e.preventDefault();
963
- saveWorkflow();
964
- }
965
- },
966
- {
967
- label: 'Export',
968
- href: '#export',
969
- icon: 'heroicons:arrow-down-tray',
970
- variant: 'outline',
971
- onclick: (e) => {
972
- e.preventDefault();
973
- exportWorkflow();
974
- }
975
- },
976
- {
977
- label: 'Import',
978
- href: '#import',
979
- icon: 'heroicons:arrow-up-tray',
980
- variant: 'outline',
981
- onclick: (e) => {
982
- e.preventDefault();
983
- fileInputRef?.click();
984
- }
985
- },
986
- {
987
- label: 'Workflow Settings',
988
- href: '#settings',
989
- icon: 'heroicons:cog-6-tooth',
990
- variant: 'outline',
991
- onclick: (e) => {
992
- e.preventDefault();
993
- toggleWorkflowSettings();
994
- }
995
- }
996
- ]}
1029
+ primaryActions={navbarActions.length > 0 ? navbarActions : defaultPrimaryActions}
997
1030
  showStatus={true}
998
1031
  {showSettings}
999
1032
  {settingsCategories}
@@ -1042,7 +1075,7 @@
1042
1075
  />
1043
1076
  {:else if isWorkflowSettingsOpen}
1044
1077
  <ConfigPanel
1045
- title="Workflow Settings"
1078
+ title={mergedMessages.navigation.workflowSettingsPanelTitle}
1046
1079
  id={getWorkflowStore()?.id}
1047
1080
  details={[
1048
1081
  {
@@ -1054,7 +1087,7 @@
1054
1087
  value: String(getWorkflowStore()?.edges?.length ?? 0)
1055
1088
  }
1056
1089
  ]}
1057
- configTitle="Settings"
1090
+ configTitle={mergedMessages.navigation.workflowSettingsPanelSubtitle}
1058
1091
  onClose={() => (isWorkflowSettingsOpen = false)}
1059
1092
  >
1060
1093
  <ConfigForm
@@ -1103,7 +1136,8 @@
1103
1136
  <ConfigPanel
1104
1137
  title={currentNode.data.label}
1105
1138
  id={currentNode.id}
1106
- description={currentNode.data.metadata?.description || 'Node configuration'}
1139
+ description={currentNode.data.metadata?.description ||
1140
+ mergedMessages.navigation.nodeConfigDescription}
1107
1141
  details={[
1108
1142
  {
1109
1143
  label: 'Type',
@@ -1160,7 +1194,7 @@
1160
1194
  {/if}
1161
1195
  {/snippet}
1162
1196
 
1163
- <!-- Bottom Panel: Tabbed Console / AI Chat -->
1197
+ <!-- Bottom Panel: Tabbed Console / AI Assistant -->
1164
1198
  {#snippet bottomPanel()}
1165
1199
  <div class="bottom-panel-tabs">
1166
1200
  <div class="bottom-panel-tabs__bar">
@@ -1170,7 +1204,7 @@
1170
1204
  : ''}"
1171
1205
  onclick={() => updateSettings({ ui: { bottomPanelTab: 'console' } })}
1172
1206
  >
1173
- Console
1207
+ {mergedMessages.navigation.bottomPanel.console}
1174
1208
  </button>
1175
1209
  <button
1176
1210
  class="bottom-panel-tabs__tab {getUiSettings().bottomPanelTab === 'chat'
@@ -1178,7 +1212,7 @@
1178
1212
  : ''}"
1179
1213
  onclick={() => updateSettings({ ui: { bottomPanelTab: 'chat' } })}
1180
1214
  >
1181
- AI Chat
1215
+ {mergedMessages.navigation.bottomPanel.chat}
1182
1216
  </button>
1183
1217
  </div>
1184
1218
  <div class="bottom-panel-tabs__content">
@@ -1263,15 +1297,19 @@
1263
1297
  onclick={handleCanvasClick}
1264
1298
  onkeydown={(e) => e.key === 'Escape' && closeConfigSidebar()}
1265
1299
  role="region"
1266
- aria-label="Workflow canvas"
1300
+ aria-label={mergedMessages.layout.workflowCanvas}
1267
1301
  >
1268
1302
  <!-- Floating sidebar toggle — always visible on the canvas top-left -->
1269
1303
  {#if !disableSidebar}
1270
1304
  <button
1271
1305
  class="flowdrop-sidebar-fab"
1272
1306
  onclick={toggleSidebar}
1273
- aria-label={isSidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
1274
- 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}
1275
1313
  >
1276
1314
  <Icon icon={isSidebarCollapsed ? 'mdi:menu' : 'mdi:menu-open'} />
1277
1315
  </button>
@@ -5,6 +5,7 @@ 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
  */
@@ -63,6 +64,18 @@ interface Props {
63
64
  swapStrategies?: SwapStrategy[];
64
65
  /** Additional JSON Schema properties to show in the Workflow Settings panel. Values are persisted in workflow.config. */
65
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);
66
79
  }
67
80
  declare const App: import("svelte").Component<Props, {}, "">;
68
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}
@@ -10,6 +10,7 @@
10
10
  import Icon from '@iconify/svelte';
11
11
  import { getNodeIcon, getCategoryIcon } from '../utils/icons.js';
12
12
  import { getCategoryColorToken } from '../utils/colors.js';
13
+ import { m } from '../messages/index.js';
13
14
  import { getCategoryLabel } from '../stores/categoriesStore.svelte.js';
14
15
  import { getVersionUpgrade } from '../utils/nodeSwap.js';
15
16
 
@@ -80,7 +81,7 @@
80
81
  <div class="swap-picker">
81
82
  <!-- Header -->
82
83
  <div class="swap-picker__header">
83
- <button class="swap-picker__back" onclick={onCancel} aria-label="Back to configuration">
84
+ <button class="swap-picker__back" onclick={onCancel} aria-label={m().layout.backToConfiguration}>
84
85
  <Icon icon="heroicons:arrow-left" />
85
86
  </button>
86
87
  <h2 class="swap-picker__title">Swap Node</h2>
@@ -13,6 +13,7 @@
13
13
  import type { Workflow } from '../types/index.js';
14
14
  import type { EndpointConfig } from '../config/endpoints.js';
15
15
  import { logger } from '../utils/logger.js';
16
+ import { m } from '../messages/index.js';
16
17
 
17
18
  interface Props {
18
19
  pipelineId: string;
@@ -166,9 +167,10 @@
166
167
  * Get pipeline actions for the parent navbar
167
168
  */
168
169
  function getPipelineActions() {
170
+ const sp = m().status.pipeline;
169
171
  return [
170
172
  {
171
- label: isLoadingJobStatus ? 'Refreshing...' : 'Refresh Status',
173
+ label: isLoadingJobStatus ? sp.refreshing : sp.refresh,
172
174
  href: '#refresh',
173
175
  icon: isLoadingJobStatus ? 'mdi:loading' : 'mdi:refresh',
174
176
  variant: 'outline' as const,
@@ -178,7 +180,7 @@
178
180
  }
179
181
  },
180
182
  {
181
- label: 'View Logs',
183
+ label: sp.viewLogs,
182
184
  href: '#logs',
183
185
  icon: 'mdi:file-document-outline',
184
186
  variant: 'outline' as const,
@@ -214,29 +216,30 @@
214
216
  // Send pipeline breadcrumbs to layout when they change
215
217
  $effect(() => {
216
218
  if (pipelineStatus && pipelineId && workflow) {
219
+ const sp = m().status.pipeline;
217
220
  const breadcrumbs = [
218
221
  {
219
- label: 'Home',
222
+ label: sp.home,
220
223
  href: '/',
221
224
  icon: 'mdi:home'
222
225
  },
223
226
  {
224
- label: 'Workflows',
227
+ label: sp.workflows,
225
228
  href: '/',
226
229
  icon: 'mdi:view-list'
227
230
  },
228
231
  {
229
- label: workflow.name || 'Workflow',
232
+ label: workflow.name || sp.workflow,
230
233
  href: `/workflow/${workflow.id}/edit`,
231
234
  icon: 'mdi:workflow'
232
235
  },
233
236
  {
234
- label: 'Pipelines',
237
+ label: sp.pipelines,
235
238
  href: `/workflow/${workflow.id}/pipelines`,
236
239
  icon: 'mdi:source-branch'
237
240
  },
238
241
  {
239
- label: `Pipeline ${pipelineId} - ${pipelineStatus}`,
242
+ label: sp.pipelineCrumb({ id: pipelineId, status: pipelineStatus }),
240
243
  icon: 'mdi:play-circle'
241
244
  }
242
245
  ];
@@ -5,6 +5,8 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
+ import { m } from '../messages/index.js';
9
+
8
10
  /**
9
11
  * A single detail item with label and value
10
12
  */
@@ -52,8 +54,8 @@
52
54
  <button
53
55
  class="readonly-details__copy-btn"
54
56
  onclick={copyId}
55
- title="Copy ID"
56
- aria-label="Copy ID to clipboard"
57
+ title={m().navigation.copyId}
58
+ aria-label={m().navigation.copyId}
57
59
  >
58
60
  <svg
59
61
  xmlns="http://www.w3.org/2000/svg"