Skip to content

Commit e401bc3

Browse files
i18n[frontend](soar): translate flow canvas UI in all 7 locales
1 parent 318b56d commit e401bc3

11 files changed

Lines changed: 339 additions & 48 deletions

File tree

‎frontend/src/features/soar/components/AgentPicker.tsx‎

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useMemo, useState } from 'react'
2+
import { Trans, useTranslation } from 'react-i18next'
23
import { Check, ChevronDown, Plus, X } from 'lucide-react'
34
import { cn } from '@/shared/lib/utils'
45
import { Input } from '@/shared/components/ui/input'
@@ -38,6 +39,7 @@ function scopeFrom(agent?: string, excluded?: string[]): Scope {
3839
* Platform is a UI-only filter (limits the agent list); the runtime uses
3940
* `agent` + `excludedAgents`. */
4041
export function AgentPicker({ platform, agent, excludedAgents, shell, readOnly, onChange }: Props) {
42+
const { t } = useTranslation()
4143
const [agents, setAgents] = useState<AgentOption[]>([])
4244
const [scope, setScope] = useState<Scope>(() => scopeFrom(agent, excludedAgents))
4345

@@ -100,25 +102,25 @@ export function AgentPicker({ platform, agent, excludedAgents, shell, readOnly,
100102
return (
101103
<div className="space-y-2">
102104
<div className="grid grid-cols-2 gap-2">
103-
<Field label="Platform">
105+
<Field label={t('soar.editor.canvas.platform')}>
104106
<select value={platform ?? ''} disabled={readOnly} onChange={(e) => onPlatformChange(e.target.value)} className={SELECT + ' w-full'}>
105-
<option value="">any</option>
107+
<option value="">{t('soar.editor.canvas.any')}</option>
106108
{platforms.map((p) => (
107109
<option key={p} value={p}>{p}</option>
108110
))}
109111
</select>
110112
</Field>
111-
<Field label="Shell">
113+
<Field label={t('soar.editor.canvas.shell')}>
112114
<select value={shell ?? ''} disabled={readOnly} onChange={(e) => onChange({ shell: e.target.value || undefined })} className={SELECT + ' w-full'}>
113-
<option value="">auto</option>
115+
<option value="">{t('soar.editor.canvas.auto')}</option>
114116
{shells.map((s) => (
115117
<option key={s} value={s}>{s}</option>
116118
))}
117119
</select>
118120
</Field>
119121
</div>
120122

121-
<Field label="Run on">
123+
<Field label={t('soar.editor.canvas.runOn')}>
122124
<div className="inline-flex w-full rounded-md border border-border p-0.5">
123125
{(['auto', 'specific', 'except'] as const).map((s) => (
124126
<button
@@ -132,22 +134,28 @@ export function AgentPicker({ platform, agent, excludedAgents, shell, readOnly,
132134
)}
133135
title={
134136
s === 'auto'
135-
? 'Runs on the host that raised the alert'
137+
? t('soar.editor.canvas.runAutoTitle')
136138
: s === 'specific'
137-
? 'Pin to one host (or a template like $(alert.foo))'
138-
: 'Runs on the host that raised the alert, but skips ones in the list'
139+
? t('soar.editor.canvas.runSpecificTitle')
140+
: t('soar.editor.canvas.runExceptTitle')
139141
}
140142
>
141-
{s === 'auto' ? 'alert source' : s === 'specific' ? 'one host' : 'all except'}
143+
{s === 'auto'
144+
? t('soar.editor.canvas.runAutoLabel')
145+
: s === 'specific'
146+
? t('soar.editor.canvas.runSpecificLabel')
147+
: t('soar.editor.canvas.runExceptLabel')}
142148
</button>
143149
))}
144150
</div>
145151
</Field>
146152

147153
{scope === 'auto' && (
148154
<p className="rounded-md bg-muted/40 px-2 py-1.5 text-[10px] text-muted-foreground">
149-
Runs on <code className="rounded bg-background px-1 font-mono">$(alert.dataSource)</code> — the host that
150-
raised the matched alert.
155+
<Trans
156+
i18nKey="soar.editor.canvas.runAutoHint"
157+
components={{ code: <code className="rounded bg-background px-1 font-mono" /> }}
158+
/>
151159
</p>
152160
)}
153161

@@ -159,15 +167,17 @@ export function AgentPicker({ platform, agent, excludedAgents, shell, readOnly,
159167
onChange={(e) => onChange({ agent: e.target.value || undefined, excludedAgents: undefined })}
160168
className={SELECT + ' w-full'}
161169
>
162-
<option value="">pick a host…</option>
170+
<option value="">{t('soar.editor.canvas.pickHost')}</option>
163171
{options.map((name) => (
164172
<option key={name} value={name}>{name}</option>
165173
))}
166174
{agent && !options.includes(agent) && <option value={agent}>{agent}</option>}
167175
</select>
168176
<p className="text-[10px] text-muted-foreground">
169-
Or type a template — <code className="rounded bg-muted px-1 font-mono">$(alert.field)</code> resolves per
170-
execution.
177+
<Trans
178+
i18nKey="soar.editor.canvas.orTemplateHint"
179+
components={{ code: <code className="rounded bg-muted px-1 font-mono" /> }}
180+
/>
171181
</p>
172182
<Input
173183
value={agent ?? ''}
@@ -214,6 +224,7 @@ function AgentMultiSelect({
214224
readOnly?: boolean
215225
onChange: (v: string[]) => void
216226
}) {
227+
const { t } = useTranslation()
217228
const [open, setOpen] = useState(false)
218229
const [q, setQ] = useState('')
219230

@@ -235,7 +246,7 @@ function AgentMultiSelect({
235246
))}
236247
{!readOnly && (
237248
<button type="button" onClick={() => setOpen((o) => !o)} className="inline-flex items-center gap-1 px-1 text-[11px] text-muted-foreground hover:text-foreground">
238-
<Plus size={11} /> {values.length ? '' : 'add exclusion'}
249+
<Plus size={11} /> {values.length ? '' : t('soar.editor.canvas.addExclusion')}
239250
<ChevronDown size={10} className="opacity-60" />
240251
</button>
241252
)}
@@ -247,12 +258,12 @@ function AgentMultiSelect({
247258
value={q}
248259
onChange={(e) => setQ(e.target.value)}
249260
autoFocus
250-
placeholder="search agents…"
261+
placeholder={t('soar.editor.canvas.searchAgents')}
251262
className="h-7 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
252263
/>
253264
</div>
254265
<div className="max-h-52 overflow-y-auto">
255-
{filtered.length === 0 && <div className="px-3 py-1.5 text-xs text-muted-foreground">no agents</div>}
266+
{filtered.length === 0 && <div className="px-3 py-1.5 text-xs text-muted-foreground">{t('soar.editor.canvas.noAgents')}</div>}
256267
{filtered.map((o) => {
257268
const on = values.includes(o)
258269
return (

‎frontend/src/features/soar/components/FlowCanvas.tsx‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
23
import {
34
Background,
45
Controls,
@@ -47,6 +48,7 @@ export function FlowCanvas(props: Props) {
4748
}
4849

4950
function FlowCanvasInner({ roots, nodes, readOnly, onChange }: Props) {
51+
const { t } = useTranslation()
5052
const wrapperRef = useRef<HTMLDivElement>(null)
5153
const { screenToFlowPosition } = useReactFlow()
5254
const { theme } = useTheme()
@@ -339,13 +341,13 @@ function FlowCanvasInner({ roots, nodes, readOnly, onChange }: Props) {
339341
type="button"
340342
onClick={() => setPaletteOpen(false)}
341343
className="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
342-
title="Hide palette"
344+
title={t('soar.editor.canvas.hidePalette')}
343345
>
344346
<ChevronLeft size={13} />
345347
</button>
346348
</div>
347349
) : (
348-
<CollapsedRail side="left" label="Palette" onClick={() => setPaletteOpen(true)} />
350+
<CollapsedRail side="left" label={t('soar.editor.canvas.paletteLabel')} onClick={() => setPaletteOpen(true)} />
349351
)}
350352

351353
<div ref={wrapperRef} className="relative flex-1" onDragOver={onDragOver} onDrop={onDrop}>
@@ -397,20 +399,21 @@ function FlowCanvasInner({ roots, nodes, readOnly, onChange }: Props) {
397399
type="button"
398400
onClick={() => setInspectorOpen(false)}
399401
className="absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
400-
title="Hide inspector"
402+
title={t('soar.editor.canvas.hideInspector')}
401403
>
402404
<ChevronRight size={13} />
403405
</button>
404406
</div>
405407
) : (
406-
<CollapsedRail side="right" label="Node" onClick={() => setInspectorOpen(true)} />
408+
<CollapsedRail side="right" label={t('soar.editor.canvas.nodeShort')} onClick={() => setInspectorOpen(true)} />
407409
)
408410
) : null}
409411
</div>
410412
)
411413
}
412414

413415
function CollapsedRail({ side, label, onClick }: { side: 'left' | 'right'; label: string; onClick: () => void }) {
416+
const { t } = useTranslation()
414417
const Icon = side === 'left' ? PanelLeft : PanelRight
415418
return (
416419
<button
@@ -420,7 +423,7 @@ function CollapsedRail({ side, label, onClick }: { side: 'left' | 'right'; label
420423
'flex w-8 shrink-0 flex-col items-center gap-2 border-border bg-card py-2 text-muted-foreground hover:bg-muted hover:text-foreground',
421424
side === 'left' ? 'border-r' : 'border-l',
422425
)}
423-
title={`Show ${label.toLowerCase()}`}
426+
title={t('soar.editor.canvas.showPanel', { name: label })}
424427
>
425428
<Icon size={13} />
426429
<span

‎frontend/src/features/soar/components/InsertFieldMenu.tsx‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useRef, useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
23
import { ChevronDown } from 'lucide-react'
34
import { Tooltip, TooltipContent, TooltipTrigger } from '@/shared/components/ui/tooltip'
45
import { ALERT_FIELDS } from '../lib/alert-fields'
@@ -21,6 +22,7 @@ interface Props {
2122
* item per field; the rest offer a generic `$(id.field)` placeholder the
2223
* user renames after clicking. */
2324
export function InsertFieldMenu({ nodes, currentNodeId, onInsert }: Props) {
25+
const { t } = useTranslation()
2426
const [open, setOpen] = useState(false)
2527
const ref = useRef<HTMLDivElement>(null)
2628

@@ -47,11 +49,11 @@ export function InsertFieldMenu({ nodes, currentNodeId, onInsert }: Props) {
4749
onClick={() => setOpen((o) => !o)}
4850
className="inline-flex items-center gap-1 rounded-md border border-border bg-card px-2 py-1 text-[10px] hover:bg-muted"
4951
>
50-
Insert field <ChevronDown size={10} className="opacity-60" />
52+
{t('soar.editor.canvas.insertField')} <ChevronDown size={10} className="opacity-60" />
5153
</button>
5254
{open && (
5355
<div className="absolute left-0 top-full z-30 mt-1 max-h-72 w-72 overflow-y-auto rounded-md border border-border bg-popover py-1 shadow-lg">
54-
<SectionHeader>Alert</SectionHeader>
56+
<SectionHeader>{t('soar.editor.canvas.alert')}</SectionHeader>
5557
{ALERT_FIELDS.map((af) => (
5658
<MenuItem
5759
key={`alert:${af.field}`}
@@ -82,7 +84,7 @@ export function InsertFieldMenu({ nodes, currentNodeId, onInsert }: Props) {
8284
))
8385
) : (
8486
<MenuItem
85-
label="whole output (edit the path)"
87+
label={t('soar.editor.canvas.wholeOutput')}
8688
path={`${a.nodeId}.*`}
8789
token={`$(${a.nodeId}.)`}
8890
onClick={() => pick(`$(${a.nodeId}.)`)}
@@ -93,7 +95,7 @@ export function InsertFieldMenu({ nodes, currentNodeId, onInsert }: Props) {
9395

9496
{ancestors.length === 0 && (
9597
<div className="mt-1 border-t border-border px-3 pt-2 text-[10px] italic text-muted-foreground">
96-
No enrichment ancestors yet — add an enrichment node upstream to expose its output here.
98+
{t('soar.editor.canvas.noAncestorsHint')}
9799
</div>
98100
)}
99101
</div>
@@ -115,6 +117,7 @@ function SectionDivider() {
115117
}
116118

117119
function MenuItem({ label, path, token, onClick }: { label: string; path: string; token: string; onClick: () => void }) {
120+
const { t } = useTranslation()
118121
return (
119122
<Tooltip delayDuration={200}>
120123
<TooltipTrigger asChild>
@@ -129,7 +132,7 @@ function MenuItem({ label, path, token, onClick }: { label: string; path: string
129132
</TooltipTrigger>
130133
<TooltipContent side="left" className="max-w-xs">
131134
<div className="text-[11px]">{label}</div>
132-
<div className="mt-0.5 font-mono text-[10px] opacity-80">inserts {token}</div>
135+
<div className="mt-0.5 font-mono text-[10px] opacity-80">{t('soar.editor.canvas.inserts')} {token}</div>
133136
</TooltipContent>
134137
</Tooltip>
135138
)

‎frontend/src/features/soar/components/NodeInspector.tsx‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useRef, useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
23
import { ChevronDown, Trash2, Zap } from 'lucide-react'
34
import { Button } from '@/shared/components/ui/button'
45
import { Input } from '@/shared/components/ui/input'
@@ -25,6 +26,7 @@ interface Props {
2526
* command/params (schema depends on executor), on_success/on_error left
2627
* implicit (drawn on the canvas). */
2728
export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChange, onDelete }: Props) {
29+
const { t } = useTranslation()
2830
const [localId, setLocalId] = useState(nodeId)
2931
const [paramsText, setParamsText] = useState(() => (node.params ? JSON.stringify(node.params, null, 2) : ''))
3032
const [paramsError, setParamsError] = useState<string | null>(null)
@@ -80,7 +82,7 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang
8082
onChange({ params: parsed })
8183
setParamsError(null)
8284
} catch (e) {
83-
setParamsError(e instanceof Error ? e.message : 'invalid JSON')
85+
setParamsError(e instanceof Error ? e.message : t('soar.editor.canvas.invalidJson'))
8486
}
8587
}
8688

@@ -144,18 +146,18 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang
144146
<div
145147
onMouseDown={startResize}
146148
className="absolute left-0 top-0 z-10 h-full w-1 cursor-col-resize hover:bg-primary/40"
147-
title="Drag to resize"
149+
title={t('soar.editor.canvas.dragToResize')}
148150
/>
149151
<div className="flex items-center justify-between border-b border-border px-3 py-2">
150-
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Node</div>
152+
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">{t('soar.editor.canvas.node')}</div>
151153
{!readOnly && (
152-
<button onClick={onDelete} className="rounded p-1 text-muted-foreground hover:text-red-500" title="Delete node">
154+
<button onClick={onDelete} className="rounded p-1 text-muted-foreground hover:text-red-500" title={t('soar.editor.canvas.deleteNode')}>
153155
<Trash2 size={13} />
154156
</button>
155157
)}
156158
</div>
157159
<div className="flex-1 space-y-3 overflow-y-auto p-3 text-xs">
158-
<Field label="ID">
160+
<Field label={t('soar.editor.canvas.id')}>
159161
<Input
160162
value={localId}
161163
readOnly={readOnly}
@@ -164,7 +166,7 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang
164166
className="h-8 font-mono"
165167
/>
166168
</Field>
167-
<Field label="Kind">
169+
<Field label={t('soar.editor.canvas.kind')}>
168170
<select
169171
value={node.kind}
170172
disabled={readOnly}
@@ -183,7 +185,7 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang
183185
))}
184186
</select>
185187
</Field>
186-
<Field label="Executor">
188+
<Field label={t('soar.editor.canvas.executor')}>
187189
<select value={node.executor} disabled={readOnly} onChange={(e) => onChange({ executor: e.target.value })} className={SELECT}>
188190
{validExecutors.map((m) => (
189191
<option key={m.type} value={m.type}>
@@ -195,7 +197,7 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang
195197

196198
{node.executor === 'shell' && (
197199
<>
198-
<Field label="Command">
200+
<Field label={t('soar.editor.canvas.commandLabel')}>
199201
{!readOnly && (
200202
<div className="mb-1 flex flex-wrap items-center gap-1.5">
201203
<TemplatesPopover
@@ -229,7 +231,7 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang
229231
)}
230232

231233
{node.executor !== 'shell' && (
232-
<Field label="Params (JSON)">
234+
<Field label={t('soar.editor.canvas.paramsJson')}>
233235
{!readOnly && (
234236
<div className="mb-1 flex flex-wrap items-center gap-1.5">
235237
<InsertFieldMenu nodes={nodes} currentNodeId={nodeId} onInsert={(token) => insertIntoParams(token)} />
@@ -250,14 +252,12 @@ export function NodeInspector({ nodeId, node, nodes, readOnly, onRename, onChang
250252
)}
251253

252254
<div className="rounded-md bg-muted/40 p-2 text-[10px] text-muted-foreground">
253-
Drag from the bottom-left <span className="text-emerald-500">green</span> handle for on_success, the
254-
bottom-right <span className="text-red-500">red</span> handle for on_error. Multiple wires = parallel.
255-
Multiple wires into one node = AND-join.
255+
{t('soar.editor.canvas.handlesHint')}
256256
</div>
257257
</div>
258258
<div className="border-t border-border p-2">
259259
<Button size="sm" variant="outline" className="w-full" onClick={onDelete} disabled={readOnly}>
260-
Delete node
260+
{t('soar.editor.canvas.deleteNode')}
261261
</Button>
262262
</div>
263263
</aside>
@@ -284,6 +284,7 @@ function TemplatesPopover({
284284
onPick: (command: string) => void
285285
shellKind: ReturnType<typeof shellKindFor>
286286
}) {
287+
const { t } = useTranslation()
287288
const ref = useRef<HTMLDivElement>(null)
288289
useEffect(() => {
289290
if (!open) return
@@ -300,7 +301,7 @@ function TemplatesPopover({
300301
onClick={() => onOpenChange(!open)}
301302
className="inline-flex items-center gap-1 rounded-md border border-border bg-card px-2 py-1 text-[10px] hover:bg-muted"
302303
>
303-
<Zap size={11} /> Templates <ChevronDown size={10} className="opacity-60" />
304+
<Zap size={11} /> {t('soar.editor.canvas.templatesLabel')} <ChevronDown size={10} className="opacity-60" />
304305
</button>
305306
{open && (
306307
<div className="absolute left-0 top-full z-30 mt-1 max-h-64 w-64 overflow-y-auto rounded-md border border-border bg-popover py-1 shadow-lg">

0 commit comments

Comments
 (0)