@revenexx/studio-integrations 0.6.0 → 0.7.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 (110) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +31 -0
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +29 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +16 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +15 -14
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
  18. package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
  20. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
  21. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
  22. package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
  23. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  24. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  25. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  26. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  27. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  28. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  29. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  30. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  31. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  32. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  33. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  34. package/dist/runtime/components/Integrations/StickyNoteView.vue +37 -23
  35. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  36. package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
  37. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
  38. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  39. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
  40. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  41. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  42. package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
  43. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
  44. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  45. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
  46. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
  47. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
  48. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  49. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
  50. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
  51. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  52. package/dist/runtime/composables/integrationsApi.js +4 -2
  53. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  54. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
  55. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  56. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  57. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  58. package/dist/runtime/composables/useIntegrationsNodes.js +3 -1
  59. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  60. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  61. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  62. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  63. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  64. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  65. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  66. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  67. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  68. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  69. package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
  70. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
  71. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  72. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  73. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  74. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  75. package/dist/runtime/data/integrationTemplates.js +7 -1
  76. package/dist/runtime/data/payloadForm.js +2 -1
  77. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  78. package/dist/runtime/i18n/messages/de.json +1207 -0
  79. package/dist/runtime/i18n/messages/en.json +1207 -0
  80. package/dist/runtime/i18n/translate.d.ts +83 -0
  81. package/dist/runtime/i18n/translate.js +41 -0
  82. package/dist/runtime/pages/integrations/audit.vue +52 -34
  83. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  84. package/dist/runtime/pages/integrations/credentials.vue +70 -52
  85. package/dist/runtime/pages/integrations/index.vue +50 -56
  86. package/dist/runtime/pages/integrations/runs.vue +33 -21
  87. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  88. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
  90. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
  92. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  93. package/dist/runtime/plugins/locale.d.ts +10 -0
  94. package/dist/runtime/plugins/locale.js +6 -0
  95. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  96. package/dist/runtime/plugins/realtime.client.js +4 -0
  97. package/dist/runtime/utils/credentialState.d.ts +24 -1
  98. package/dist/runtime/utils/credentialState.js +26 -11
  99. package/dist/runtime/utils/localize.d.ts +18 -5
  100. package/dist/runtime/utils/localize.js +32 -9
  101. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  102. package/dist/runtime/utils/pollBudget.js +17 -0
  103. package/dist/runtime/utils/schedule.d.ts +0 -25
  104. package/dist/runtime/utils/schedule.js +37 -25
  105. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  106. package/dist/runtime/utils/statusStyle.js +9 -1
  107. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  108. package/dist/runtime/utils/validationFindings.js +4 -3
  109. package/dist/types.d.mts +2 -6
  110. package/package.json +3 -2
@@ -29,6 +29,7 @@ const props = defineProps({
29
29
  tagSuggestions: { type: Array, required: false }
30
30
  });
31
31
  const emit = defineEmits(["update:open", "change-mode", "save-meta"]);
32
+ const t = useIntegrationsT();
32
33
  const nameDraft = ref("");
33
34
  const descriptionDraft = ref("");
34
35
  const tagsDraft = ref([]);
@@ -54,15 +55,15 @@ function formatDateTime(iso) {
54
55
  <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
55
56
  <DialogContent class="sm:max-w-lg">
56
57
  <DialogHeader>
57
- <DialogTitle>Workflow settings</DialogTitle>
58
+ <DialogTitle>{{ t("integration.workflows.settings.dialog.title") }}</DialogTitle>
58
59
  <DialogDescription class="text-sm">
59
- Execution behavior and metadata for “{{ workflow.name }}”.
60
+ {{ t("integration.workflows.settings.dialog.description", { name: workflow.name }) }}
60
61
  </DialogDescription>
61
62
  </DialogHeader>
62
63
 
63
64
  <div class="space-y-4 text-sm">
64
65
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3">
65
- <div class="text-mono text-muted-foreground">Execution</div>
66
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.execution") }}</div>
66
67
  <div class="flex items-center gap-2">
67
68
  <Select
68
69
  :model-value="modeDraft"
@@ -71,8 +72,8 @@ function formatDateTime(iso) {
71
72
  >
72
73
  <!-- Named explicitly: the section caption above says "Execution",
73
74
  which is not a name the control itself carries. -->
74
- <SelectTrigger aria-label="Execution mode" class="h-8 w-full bg-white text-sm">
75
- <SelectValue placeholder="Pick a mode" />
75
+ <SelectTrigger :aria-label="t('integration.workflows.settings.field.executionMode.aria')" class="h-8 w-full bg-white text-sm">
76
+ <SelectValue :placeholder="t('integration.workflows.settings.field.executionMode.placeholder')" />
76
77
  </SelectTrigger>
77
78
  <SelectContent>
78
79
  <SelectItem v-for="m in modes" :key="m.value" :value="m.value">
@@ -88,24 +89,24 @@ function formatDateTime(iso) {
88
89
  </div>
89
90
 
90
91
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3">
91
- <div class="text-mono text-muted-foreground">Appearance</div>
92
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.appearance") }}</div>
92
93
  <!-- Real labels, not styled captions: each one names its own control
93
94
  (`for`/`id`), otherwise a screen reader reads four unlabelled
94
95
  fields in a row. -->
95
96
  <div class="space-y-1.5">
96
- <Label for="wf-set-name" class="text-sm font-normal text-muted-foreground">Name</Label>
97
- <Input id="wf-set-name" v-model="nameDraft" class="bg-white" placeholder="Workflow name" />
97
+ <Label for="wf-set-name" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.name.label") }}</Label>
98
+ <Input id="wf-set-name" v-model="nameDraft" class="bg-white" :placeholder="t('integration.workflows.settings.field.name.placeholder')" />
98
99
  </div>
99
100
  <div class="space-y-1.5">
100
- <Label for="wf-set-desc" class="text-sm font-normal text-muted-foreground">Description</Label>
101
- <Textarea id="wf-set-desc" v-model="descriptionDraft" rows="2" class="bg-white" placeholder="What this workflow does and why it exists." />
101
+ <Label for="wf-set-desc" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.description.label") }}</Label>
102
+ <Textarea id="wf-set-desc" v-model="descriptionDraft" rows="2" class="bg-white" :placeholder="t('integration.workflows.settings.field.description.placeholder')" />
102
103
  </div>
103
104
  <div class="space-y-1.5">
104
- <Label for="wf-set-tags" class="text-sm font-normal text-muted-foreground">Tags</Label>
105
+ <Label for="wf-set-tags" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.tags.label") }}</Label>
105
106
  <WorkflowTagsInput v-model="tagsDraft" input-id="wf-set-tags" :suggestions="tagSuggestions ?? []" />
106
107
  </div>
107
108
  <div class="space-y-1.5">
108
- <Label id="wf-set-icon-label" class="text-sm font-normal text-muted-foreground">Icon</Label>
109
+ <Label id="wf-set-icon-label" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.icon.label") }}</Label>
109
110
  <WorkflowIconPicker v-model="iconDraft" trigger-labelledby="wf-set-icon-label" />
110
111
  </div>
111
112
  <div class="flex justify-end">
@@ -116,23 +117,23 @@ function formatDateTime(iso) {
116
117
  @click="emit('save-meta', { name: nameDraft.trim(), description: descriptionDraft.trim(), tags: tagsDraft, icon: iconDraft })"
117
118
  >
118
119
  <Loader2 v-if="savingMeta" :size="12" class="animate-spin" />
119
- Save changes
120
+ {{ t("integration.workflows.settings.action.save") }}
120
121
  </Button>
121
122
  </div>
122
123
  </div>
123
124
 
124
125
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-2">
125
- <div class="text-mono text-muted-foreground">Metadata</div>
126
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.metadata") }}</div>
126
127
  <div class="grid grid-cols-[110px_1fr] gap-x-3 gap-y-2">
127
- <span class="text-muted-foreground">ID</span>
128
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.id") }}</span>
128
129
  <span class="font-mono">{{ workflow.id }}</span>
129
- <span class="text-muted-foreground">Definition</span>
130
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.definition") }}</span>
130
131
  <span class="font-mono">{{ workflow.blob_definition_version }}</span>
131
- <span class="text-muted-foreground">Revision</span>
132
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.revision") }}</span>
132
133
  <span class="font-mono">r{{ workflow.revision }}</span>
133
- <span class="text-muted-foreground">Created</span>
134
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.created") }}</span>
134
135
  <span>{{ formatDateTime(workflow.created_at) }}</span>
135
- <span class="text-muted-foreground">Updated</span>
136
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.updated") }}</span>
136
137
  <span>{{ formatDateTime(workflow.updated_at) }}</span>
137
138
  </div>
138
139
  </div>
@@ -5,6 +5,7 @@ const props = defineProps({
5
5
  suggestions: { type: Array, required: false, default: () => [] },
6
6
  inputId: { type: String, required: false }
7
7
  });
8
+ const t = useIntegrationsT();
8
9
  const draft = ref("");
9
10
  const inputRef = ref(null);
10
11
  const listId = useId();
@@ -15,7 +16,7 @@ function commit() {
15
16
  if (!model.value.includes(tag)) model.value = [...model.value, tag];
16
17
  }
17
18
  function remove(tag) {
18
- model.value = model.value.filter((t) => t !== tag);
19
+ model.value = model.value.filter((t2) => t2 !== tag);
19
20
  }
20
21
  function onKeydown(e) {
21
22
  if (e.key === "Enter" || e.key === ",") {
@@ -33,17 +34,17 @@ function onKeydown(e) {
33
34
  @click="inputRef?.focus()"
34
35
  >
35
36
  <span
36
- v-for="t in model"
37
- :key="t"
37
+ v-for="tag in model"
38
+ :key="tag"
38
39
  class="inline-flex items-center gap-1 rounded-md border border-black/[0.06] bg-muted/50 px-1.5 py-0.5 text-xs text-foreground"
39
40
  >
40
- {{ t }}
41
+ {{ tag }}
41
42
  <button
42
43
  type="button"
43
44
  class="text-muted-foreground transition hover:text-foreground"
44
- :title="`Remove ${t}`"
45
- :aria-label="`Remove ${t}`"
46
- @click.stop="remove(t)"
45
+ :title="t('integration.workflows.tagsInput.action.remove', { tag })"
46
+ :aria-label="t('integration.workflows.tagsInput.action.remove', { tag })"
47
+ @click.stop="remove(tag)"
47
48
  >
48
49
  ×
49
50
  </button>
@@ -57,7 +58,7 @@ function onKeydown(e) {
57
58
  type="text"
58
59
  :list="listId"
59
60
  class="h-6 min-w-24 flex-1 bg-transparent text-sm focus:outline-none"
60
- :placeholder="model.length ? 'Add tag\u2026' : 'Add tags \u2014 Enter confirms\u2026'"
61
+ :placeholder="model.length ? t('integration.workflows.tagsInput.placeholder.add') : t('integration.workflows.tagsInput.placeholder.addFirst')"
61
62
  @keydown="onKeydown"
62
63
  @blur="commit()"
63
64
  >
@@ -2,8 +2,9 @@
2
2
  import { computed, ref } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
- import { ClockCircle as Clock, Play, TrashBinMinimalistic as Trash2, LinkRound as Webhook, Bolt } from "#solar-icons";
5
+ import { ClockCircle as Clock, Play, TrashBinMinimalistic as TrashBin, LinkRound as Webhook, Bolt } from "#solar-icons";
6
6
  import { resolveDisplayName } from "../../utils/localize";
7
+ import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
7
8
  import { describeCron } from "../../utils/schedule";
8
9
  const props = defineProps({
9
10
  id: { type: String, required: true },
@@ -12,19 +13,20 @@ const props = defineProps({
12
13
  });
13
14
  defineOptions({ inheritAttrs: false });
14
15
  defineEmits(["updateNodeInternals"]);
16
+ const t = useIntegrationsT();
15
17
  const icon = computed(() => {
16
18
  return props.data.type === "schedule" ? Clock : props.data.type === "webhook" ? Webhook : props.data.type === "event" ? Bolt : Play;
17
19
  });
18
20
  const subtitle = computed(() => {
19
21
  const c = props.data.config;
20
- if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? "no schedule set";
22
+ if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? t("integration.canvas.trigger.subtitle.noSchedule");
21
23
  if (props.data.type === "webhook") return `${c.method ?? "POST"} ${c.path ?? "/"}`;
22
- if (props.data.type === "event") return c.event_type ? `on ${c.event_type}` : "no event set";
23
- return "On demand";
24
+ if (props.data.type === "event") return c.event_type ? t("integration.canvas.trigger.subtitle.onEvent", { event: c.event_type }) : t("integration.canvas.trigger.subtitle.noEvent");
25
+ return t("integration.canvas.trigger.subtitle.onDemand");
24
26
  });
25
27
  const title = computed(() => {
26
28
  const n = props.data.name;
27
- if (!n) return props.data.type.charAt(0).toUpperCase() + props.data.type.slice(1);
29
+ if (!n) return triggerTypeLabel(props.data.type);
28
30
  return resolveDisplayName(n);
29
31
  });
30
32
  const hovered = ref(false);
@@ -54,10 +56,11 @@ function sideToPosition(s) {
54
56
  <button
55
57
  type="button"
56
58
  class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
57
- title="Delete"
59
+ :title="t('integration.common.action.delete')"
60
+ :aria-label="t('integration.canvas.trigger.action.deleteAria')"
58
61
  @click="remove"
59
62
  >
60
- <Trash2 :size="11" />
63
+ <TrashBin :size="11" />
61
64
  </button>
62
65
  </NodeToolbar>
63
66
 
@@ -86,8 +89,8 @@ function sideToPosition(s) {
86
89
  </div>
87
90
  </div>
88
91
  <div class="flex items-center justify-between border-t border-emerald-500/15 bg-emerald-500/[0.04] rounded-b-md px-3 py-1 text-mono text-emerald-700/80 font-semibold">
89
- <span>Trigger · {{ data.type }}</span>
90
- <span v-if="data.active === false" class="text-amber-600">Inactive</span>
92
+ <span>{{ t("integration.canvas.badge.trigger") }} · {{ data.type }}</span>
93
+ <span v-if="data.active === false" class="text-amber-600">{{ t("integration.canvas.trigger.badge.inactive") }}</span>
91
94
  </div>
92
95
  </div>
93
96
 
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, reactive, ref, watch } from "vue";
3
- import { AddCircle as Plus, Copy, TrashBinMinimalistic as Trash2 } from "#solar-icons";
3
+ import { AddCircle as Plus, Copy, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import {
5
5
  Button,
6
6
  Dialog,
@@ -24,6 +24,7 @@ const props = defineProps({
24
24
  variables: { type: Object, required: true }
25
25
  });
26
26
  const emit = defineEmits(["update:open", "update:variables"]);
27
+ const t = useIntegrationsT();
27
28
  const KEY_PATTERN = /^[a-z_][a-z0-9_]*$/i;
28
29
  const TYPES = ["string", "number", "boolean", "object", "array"];
29
30
  const jsonDrafts = reactive({});
@@ -80,13 +81,13 @@ function applyJsonDraft(key, type) {
80
81
  const parsed = JSON.parse(raw);
81
82
  const ok = type === "array" ? Array.isArray(parsed) : parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
82
83
  if (!ok) {
83
- jsonErrors[key] = type === "array" ? "Must be a JSON array." : "Must be a JSON object.";
84
+ jsonErrors[key] = type === "array" ? t("integration.workflows.variables.error.mustBeArray") : t("integration.workflows.variables.error.mustBeObject");
84
85
  return;
85
86
  }
86
87
  jsonErrors[key] = null;
87
88
  patchVar(key, { default: parsed });
88
89
  } catch {
89
- jsonErrors[key] = "Not valid JSON.";
90
+ jsonErrors[key] = t("integration.workflows.variables.error.invalidJson");
90
91
  }
91
92
  }
92
93
  function descriptionText(v) {
@@ -107,8 +108,8 @@ const newKey = ref("");
107
108
  const newKeyError = computed(() => {
108
109
  const k = newKey.value.trim();
109
110
  if (!k) return null;
110
- if (!KEY_PATTERN.test(k)) return "Letters, digits and _ only; must not start with a digit.";
111
- if (props.variables[k]) return "Key already exists.";
111
+ if (!KEY_PATTERN.test(k)) return t("integration.workflows.variables.error.invalidKey");
112
+ if (props.variables[k]) return t("integration.workflows.variables.error.keyExists");
112
113
  return null;
113
114
  });
114
115
  function addVariable() {
@@ -120,9 +121,9 @@ function addVariable() {
120
121
  async function copyUsage(key) {
121
122
  try {
122
123
  await navigator.clipboard.writeText(`\${{ vars.${key} }}`);
123
- toast.success("Expression copied");
124
+ toast.success(t("integration.workflows.variables.toast.copied"));
124
125
  } catch {
125
- toast.error("Could not copy");
126
+ toast.error(t("integration.workflows.variables.toast.copyFailed"));
126
127
  }
127
128
  }
128
129
  </script>
@@ -131,24 +132,24 @@ async function copyUsage(key) {
131
132
  <Dialog :open="open" @update:open="(v2) => emit('update:open', v2)">
132
133
  <DialogContent class="max-w-[94vw] sm:max-w-3xl">
133
134
  <DialogHeader>
134
- <DialogTitle>Workflow variables</DialogTitle>
135
+ <DialogTitle>{{ t("integration.workflows.variables.dialog.title") }}</DialogTitle>
135
136
  <DialogDescription class="text-sm">
136
- Reusable values for this workflow — reference them in any config field as
137
+ {{ t("integration.workflows.variables.dialog.description.before") }}
137
138
  <code v-pre class="rounded bg-black/[0.05] px-1 py-0.5 font-mono text-xs">${{ vars.key }}</code>.
138
- Changes persist with the workflow Save.
139
+ {{ t("integration.workflows.variables.dialog.description.after") }}
139
140
  </DialogDescription>
140
141
  </DialogHeader>
141
142
 
142
143
  <div class="max-h-[62vh] space-y-3 overflow-y-auto pr-1">
143
144
  <p v-if="!entries.length" class="rounded-lg border border-dashed border-black/[0.12] px-4 py-6 text-center text-sm text-muted-foreground">
144
- No variables yet — add the first one below.
145
+ {{ t("integration.workflows.variables.state.empty") }}
145
146
  </p>
146
147
 
147
148
  <div
148
149
  v-for="[key, v] in entries"
149
150
  :key="key"
150
151
  role="group"
151
- :aria-label="`Variable ${key}`"
152
+ :aria-label="t('integration.workflows.variables.row.aria', { key })"
152
153
  class="rounded-lg border border-black/[0.06] bg-white p-3"
153
154
  >
154
155
  <div class="flex items-center gap-2">
@@ -156,7 +157,7 @@ async function copyUsage(key) {
156
157
  <button
157
158
  type="button"
158
159
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
159
- :title="`Copy \${{ vars.${key} }}`"
160
+ :title="t('integration.workflows.variables.action.copyUsage.title', { key })"
160
161
  @click="copyUsage(key)"
161
162
  >
162
163
  <Copy :size="12" />
@@ -165,10 +166,10 @@ async function copyUsage(key) {
165
166
  <button
166
167
  type="button"
167
168
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
168
- title="Delete variable"
169
+ :title="t('integration.workflows.variables.action.delete')"
169
170
  @click="removeVar(key)"
170
171
  >
171
- <Trash2 :size="13" />
172
+ <TrashBin :size="13" />
172
173
  </button>
173
174
  </div>
174
175
 
@@ -176,12 +177,12 @@ async function copyUsage(key) {
176
177
  <!-- One row per variable, so every id carries its key — otherwise
177
178
  the labels of the second row point at the first row's fields. -->
178
179
  <div class="space-y-1">
179
- <Label :for="`var-${key}-type`" class="text-mono text-muted-foreground">Type</Label>
180
- <Select :model-value="v.type" @update:model-value="(t) => changeType(key, t)">
180
+ <Label :for="`var-${key}-type`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.type.label") }}</Label>
181
+ <Select :model-value="v.type" @update:model-value="(vt) => changeType(key, vt)">
181
182
  <!-- Both: the `id` lets the visible Label's `for` resolve, and the
182
183
  `aria-label` names the trigger without relying on an engine
183
184
  naming a button from a <label> — see the sweep's docblock. -->
184
- <SelectTrigger :id="`var-${key}-type`" aria-label="Type" class="h-8 bg-white text-sm">
185
+ <SelectTrigger :id="`var-${key}-type`" :aria-label="t('integration.workflows.variables.field.type.label')" class="h-8 bg-white text-sm">
185
186
  <SelectValue />
186
187
  </SelectTrigger>
187
188
  <SelectContent>
@@ -193,7 +194,7 @@ async function copyUsage(key) {
193
194
  <div class="space-y-1">
194
195
  <!-- "Value", not "Default": PO-206. The `for`/`id` still name the
195
196
  blob field, which is called `default` and stays that way. -->
196
- <Label :for="`var-${key}-default`" class="text-mono text-muted-foreground">Value</Label>
197
+ <Label :for="`var-${key}-default`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.value.label") }}</Label>
197
198
 
198
199
  <!-- Only one of these renders, so they share the one `id` the Label
199
200
  points at. The labelable ones take their name from that Label —
@@ -221,7 +222,7 @@ async function copyUsage(key) {
221
222
  <Switch
222
223
  :id="`var-${key}-default`"
223
224
  :model-value="v.default === true"
224
- aria-label="Value"
225
+ :aria-label="t('integration.workflows.variables.field.value.label')"
225
226
  @update:model-value="(val) => patchVar(key, { default: val })"
226
227
  />
227
228
  </div>
@@ -233,7 +234,7 @@ async function copyUsage(key) {
233
234
  spellcheck="false"
234
235
  class="w-full rounded-md border bg-white p-2 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-primary/40"
235
236
  :class="jsonErrors[key] ? 'border-rose-400' : 'border-black/[0.08]'"
236
- :placeholder="v.type === 'array' ? 'JSON array, e.g. [1, 2]' : 'JSON object, e.g. { \u2026 }'"
237
+ :placeholder="v.type === 'array' ? t('integration.workflows.variables.field.value.placeholder.array') : t('integration.workflows.variables.field.value.placeholder.object')"
237
238
  @input="(e) => jsonDrafts[key] = e.target.value"
238
239
  @blur="applyJsonDraft(key, v.type)"
239
240
  />
@@ -243,12 +244,12 @@ async function copyUsage(key) {
243
244
  </div>
244
245
 
245
246
  <div class="mt-2 space-y-1">
246
- <Label :for="`var-${key}-desc`" class="text-mono text-muted-foreground">Description</Label>
247
+ <Label :for="`var-${key}-desc`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.description.label") }}</Label>
247
248
  <Input
248
249
  :id="`var-${key}-desc`"
249
250
  :model-value="descriptionText(v)"
250
251
  class="h-8 bg-white text-sm"
251
- placeholder="What this variable is for"
252
+ :placeholder="t('integration.workflows.variables.field.description.placeholder')"
252
253
  @update:model-value="(val) => writeDescription(key, String(val))"
253
254
  />
254
255
  </div>
@@ -256,18 +257,18 @@ async function copyUsage(key) {
256
257
 
257
258
  <!-- Add row -->
258
259
  <div class="rounded-lg border border-dashed border-black/[0.12] p-3">
259
- <Label for="var-new-key" class="text-mono text-muted-foreground">New variable</Label>
260
+ <Label for="var-new-key" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.newKey.label") }}</Label>
260
261
  <div class="mt-1 flex items-center gap-2">
261
262
  <Input
262
263
  id="var-new-key"
263
264
  v-model="newKey"
264
265
  class="h-8 flex-1 bg-white font-mono text-sm"
265
- placeholder="e.g. base_url"
266
+ :placeholder="t('integration.workflows.variables.field.newKey.placeholder')"
266
267
  @keydown.enter.prevent="addVariable"
267
268
  />
268
269
  <Button size="sm" class="h-8" :disabled="!newKey.trim() || !!newKeyError" @click="addVariable">
269
270
  <Plus :size="14" class="mr-1" />
270
- Add
271
+ {{ t("integration.workflows.variables.action.add") }}
271
272
  </Button>
272
273
  </div>
273
274
  <p v-if="newKeyError" class="mt-1 text-xs text-rose-600">{{ newKeyError }}</p>