diff --git a/messages/en.json b/messages/en.json index 77497cd..44be474 100644 --- a/messages/en.json +++ b/messages/en.json @@ -739,5 +739,182 @@ "toastFailedPreview": "Failed to load preview data" } } + }, + "Automations": { + "list": { + "title": "Automations", + "subtitle": "Build workflows that react to WhatsApp® events automatically.", + "create": "Create Automation", + "retry": "Retry", + "templatesTitle": "Quick-start templates", + "emptyTitle": "No automations yet", + "emptyDesc": "Pick a template above or create one from scratch.", + "runs": "{count} run", + "runsPlural": "{count} runs", + "lastRun": "last {time}", + "activate": "Activate", + "deactivate": "Deactivate", + "edit": "Edit", + "duplicate": "Duplicate", + "viewLogs": "View Logs", + "delete": "Delete", + "cancel": "Cancel", + "deleteTitle": "Delete automation", + "deleteDesc": "This permanently removes {name} and its execution history. This cannot be undone.", + "toasts": { + "activated": "Automation activated", + "paused": "Automation paused", + "duplicated": "Automation duplicated", + "deleted": "Automation deleted", + "updateError": "Failed to update", + "duplicateError": "Failed to duplicate", + "deleteError": "Failed to delete" + } + }, + "edit": { + "loading": "Loading...", + "loadError": "Failed to load ({status})", + "back": "Back to Automations" + }, + "logs": { + "title": "Execution logs", + "loadError": "Failed to load logs", + "emptyTitle": "No executions yet", + "emptyDesc": "Trigger this automation to see runs here.", + "unknownContact": "Unknown contact", + "step": "{count} step", + "stepPlural": "{count} steps", + "noSteps": "No steps recorded.", + "status": { + "success": "success", + "partial": "partial", + "failed": "failed" + }, + "back": "Back", + "backAria": "Back" + }, + "builder": { + "steps": { + "send_message": "Send Message", + "send_template": "Send Template", + "add_tag": "Add Tag", + "remove_tag": "Remove Tag", + "assign_conversation": "Assign Conversation", + "update_contact_field": "Update Contact Field", + "create_deal": "Create Deal", + "wait": "Wait", + "condition": "Condition (If/Else)", + "send_webhook": "Send Webhook", + "close_conversation": "Close Conversation" + }, + "triggers": { + "new_message_received": { "label": "New Message Received", "hint": "Any incoming message" }, + "first_inbound_message": { "label": "First Message from Contact", "hint": "First time this contact ever messages you (works for manually-added contacts too)" }, + "keyword_match": { "label": "Keyword Match", "hint": "Message contains specific keyword(s)" }, + "new_contact_created": { "label": "New Contact Created", "hint": "When a contact is auto-created from an incoming message" }, + "conversation_assigned": { "label": "Conversation Assigned", "hint": "When assigned to an agent" }, + "tag_added": { "label": "Tag Added", "hint": "When a tag is added to a contact" }, + "time_based": { "label": "Time-Based", "hint": "On a recurring schedule" } + }, + "trigger": "Trigger", + "triggerType": "Trigger type", + "keywords": "Keywords", + "keywordsHint": "Comma-separated list (e.g. hello, help, buy)", + "schedule": "Schedule", + "scheduleHint": "Cron expression (e.g. 0 9 * * 1-5)", + "active": "Active", + "activeAria": "Active", + "save": "Save", + "saveDraft": "Save Draft", + "untitled": "Untitled automation", + "backToAutomations": "Back to automations", + "toasts": { + "saved": "Automation saved", + "created": "Automation created", + "saveFailed": "Save failed" + }, + "tags": { + "select": "Select a tag…", + "placeholder": "Tag id", + "unknown": "{id} (unknown tag)" + }, + "fields": { + "name": "Name", + "email": "Email", + "company": "Company", + "customFields": "Custom fields", + "unknown": "{id} (unknown field)" + }, + "agents": { + "select": "Select an agent…", + "placeholder": "Agent id", + "unknown": "{id} (unknown agent)" + }, + "pipelines": { + "pipelineLabel": "Pipeline", + "pipelineIdLabel": "Pipeline id", + "stageLabel": "Stage", + "stageIdLabel": "Stage id", + "selectPipeline": "Select a pipeline…", + "selectStage": "Select a stage…", + "selectPipelineFirst": "Select a pipeline first…", + "unknownPipeline": "{id} (unknown pipeline)", + "unknownStage": "{id} (unknown stage)" + }, + "templates": { + "templateLabel": "Template", + "templateNameLabel": "Template name", + "languageLabel": "Language", + "select": "Select a template…", + "unknown": "{name} ({lang}) — not in approved list", + "unknownLang": "unknown" + }, + "branches": { + "yes": "Yes", + "no": "No" + }, + "addStep": "Add step", + "config": { + "messageText": "Message text", + "placeholderMessageText": "Hi! Thanks for reaching out…", + "tagLabel": "Tag", + "modeLabel": "Mode", + "modes": { + "round_robin": "Round-robin", + "specific": "Specific agent" + }, + "agentLabel": "Agent", + "fieldLabel": "Field", + "valueLabel": "Value", + "placeholderValue": "Text or {{ vars.x }} / {{ message.text }}", + "titleLabel": "Title", + "amountLabel": "Amount", + "unitLabel": "Unit", + "units": { + "minutes": "Minutes", + "hours": "Hours", + "days": "Days" + }, + "subjectLabel": "Subject", + "subjects": { + "tag_presence": "Tag presence", + "contact_field": "Contact field", + "message_content": "Message content", + "time_of_day": "Time of day" + }, + "operandLabel": "Operand", + "placeholderTime": "HH:mm-HH:mm", + "placeholderContact": "name / email / company", + "placeholderTag": "tag id", + "urlLabel": "URL", + "headersLabel": "Headers", + "placeholderHeaders": "{\"Authorization\": \"Bearer ...\"}", + "bodyTemplateLabel": "Body template", + "placeholderBody": "{\"id\": \"{{ contact.id }}\"}", + "matchType": "Match type", + "matchContains": "Contains", + "matchExact": "Exact" + } + } } } diff --git a/src/app/(dashboard)/automations/[id]/edit/page.tsx b/src/app/(dashboard)/automations/[id]/edit/page.tsx index 7c40c79..eefda5d 100644 --- a/src/app/(dashboard)/automations/[id]/edit/page.tsx +++ b/src/app/(dashboard)/automations/[id]/edit/page.tsx @@ -3,6 +3,7 @@ import { use, useEffect, useState } from "react" import { useRouter } from "next/navigation" import { Loader2 } from "lucide-react" +import { useTranslations } from "next-intl" import { AutomationBuilder, @@ -19,6 +20,7 @@ export default function EditAutomationPage({ }) { const { id } = use(params) const router = useRouter() + const t = useTranslations("Automations.edit") const [initial, setInitial] = useState(null) const [error, setError] = useState(null) @@ -27,7 +29,7 @@ export default function EditAutomationPage({ async function load() { const res = await fetch(`/api/automations/${id}`) if (!res.ok) { - if (!cancelled) setError(`Failed to load (${res.status})`) + if (!cancelled) setError(t("loadError", { status: res.status })) return } const body = await res.json() @@ -56,7 +58,7 @@ export default function EditAutomationPage({ onClick={() => router.push("/automations")} className="text-sm text-primary hover:text-primary/80" > - Back to Automations + {t("back")} ) diff --git a/src/app/(dashboard)/automations/[id]/logs/page.tsx b/src/app/(dashboard)/automations/[id]/logs/page.tsx index bf13be2..6f5c5fb 100644 --- a/src/app/(dashboard)/automations/[id]/logs/page.tsx +++ b/src/app/(dashboard)/automations/[id]/logs/page.tsx @@ -10,6 +10,7 @@ import { ChevronDown, ChevronRight, } from "lucide-react" +import { useTranslations } from "next-intl" import { createClient } from "@/lib/supabase/client" import type { @@ -28,6 +29,7 @@ export default function AutomationLogsPage({ }) { const { id } = use(params) const router = useRouter() + const t = useTranslations("Automations.logs") const [automation, setAutomation] = useState(null) const [logs, setLogs] = useState(null) @@ -56,7 +58,7 @@ export default function AutomationLogsPage({ setAutomation(autRes.data as Automation | null) setLogs((logRes.data ?? []) as AutomationLog[]) } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load logs") + setError(err instanceof Error ? err.message : t("loadError")) } } load() @@ -67,7 +69,7 @@ export default function AutomationLogsPage({

{error}

) @@ -88,21 +90,21 @@ export default function AutomationLogsPage({ type="button" onClick={() => router.push("/automations")} className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" - aria-label="Back" + aria-label={t("backAria")} >

{automation.name}

-

Execution logs

+

{t("title")}

{logs.length === 0 ? (
-

No executions yet

+

{t("emptyTitle")}

- Trigger this automation to see runs here. + {t("emptyDesc")}

) : ( @@ -124,14 +126,14 @@ export default function AutomationLogsPage({ ) : ( )} - +
- {log.contact?.name ?? log.contact?.phone ?? "Unknown contact"} + {log.contact?.name ?? log.contact?.phone ?? t("unknownContact")}
- {log.trigger_event} · {log.steps_executed?.length ?? 0} step - {log.steps_executed?.length === 1 ? "" : "s"} + {log.trigger_event} · {log.steps_executed?.length ?? 0}{" "} + {log.steps_executed?.length === 1 ? t("step", { count: 1 }).replace("1 ", "") : t("stepPlural", { count: log.steps_executed?.length ?? 0 }).replace(/^[0-9]+ /, "")}
@@ -150,7 +152,7 @@ export default function AutomationLogsPage({ ))} {(log.steps_executed ?? []).length === 0 && ( -
  • No steps recorded.
  • +
  • {t("noSteps")}
  • )}
    @@ -164,7 +166,7 @@ export default function AutomationLogsPage({ ) } -function StatusBadge({ status }: { status: AutomationLog["status"] }) { +function StatusBadge({ status, t }: { status: AutomationLog["status"], t: ReturnType }) { const classes = status === "success" ? "border-primary/30 bg-primary/10 text-primary" @@ -178,7 +180,7 @@ function StatusBadge({ status }: { status: AutomationLog["status"] }) { classes, )} > - {status} + {t(`status.${status}`)} ) } diff --git a/src/app/(dashboard)/automations/page.tsx b/src/app/(dashboard)/automations/page.tsx index 28760ae..4a87a88 100644 --- a/src/app/(dashboard)/automations/page.tsx +++ b/src/app/(dashboard)/automations/page.tsx @@ -20,6 +20,7 @@ import { import { createClient } from "@/lib/supabase/client" import { useCan } from "@/hooks/use-can" +import { useTranslations } from "next-intl" import type { Automation } from "@/types" import { Button } from "@/components/ui/button" import { GatedButton } from "@/components/ui/gated-button" @@ -60,6 +61,7 @@ const TEMPLATE_ICON: Record = { export default function AutomationsPage() { const router = useRouter() const canCreate = useCan("send-messages") + const t = useTranslations("Automations.list") const [automations, setAutomations] = useState(null) const [error, setError] = useState(null) const [pendingDelete, setPendingDelete] = useState(null) @@ -99,20 +101,20 @@ export default function AutomationsPage() { prev?.map((x) => (x.id === a.id ? { ...x, is_active: !next } : x)) ?? prev, ) const body = await res.json().catch(() => ({})) - toast.error(body?.error ?? "Failed to update") + toast.error(body?.error ?? t("toasts.updateError")) return } - toast.success(next ? "Automation activated" : "Automation paused") + toast.success(next ? t("toasts.activated") : t("toasts.paused")) } async function duplicate(a: Automation) { const res = await fetch(`/api/automations/${a.id}/duplicate`, { method: "POST" }) if (!res.ok) { const body = await res.json().catch(() => ({})) - toast.error(body?.error ?? "Failed to duplicate") + toast.error(body?.error ?? t("toasts.duplicateError")) return } - toast.success("Automation duplicated") + toast.success(t("toasts.duplicated")) load() } @@ -123,10 +125,10 @@ export default function AutomationsPage() { setDeleting(false) if (!res.ok) { const body = await res.json().catch(() => ({})) - toast.error(body?.error ?? "Failed to delete") + toast.error(body?.error ?? t("toasts.deleteError")) return } - toast.success("Automation deleted") + toast.success(t("toasts.deleted")) setPendingDelete(null) load() } @@ -140,7 +142,7 @@ export default function AutomationsPage() {

    {error}

    ) @@ -160,9 +162,9 @@ export default function AutomationsPage() {
    -

    Automations

    +

    {t("title")}

    - Build workflows that react to WhatsApp® events automatically. + {t("subtitle")}

    - Create Automation + {t("create")}
    {showTemplates && (
    -

    Quick-start templates

    +

    {t("templatesTitle")}

    {TEMPLATE_ORDER.map((slug) => { const t = AUTOMATION_TEMPLATES[slug] @@ -206,9 +208,9 @@ export default function AutomationsPage() {
    -

    No automations yet

    +

    {t("emptyTitle")}

    - Pick a template above or create one from scratch. + {t("emptyDesc")}

    ) : ( @@ -222,6 +224,7 @@ export default function AutomationsPage() { onDuplicate={() => duplicate(a)} onLogs={() => router.push(`/automations/${a.id}/logs`)} onDelete={() => setPendingDelete(a)} + t={t} /> ))} @@ -230,11 +233,9 @@ export default function AutomationsPage() { !v && setPendingDelete(null)}> - Delete automation + {t("deleteTitle")} - This permanently removes{" "} - {pendingDelete?.name} and its execution - history. This cannot be undone. + {t("deleteDesc", { name: pendingDelete?.name ?? "" })} @@ -243,7 +244,7 @@ export default function AutomationsPage() { onClick={() => setPendingDelete(null)} disabled={deleting} > - Cancel + {t("cancel")} @@ -267,6 +268,7 @@ function AutomationCard({ onDuplicate, onLogs, onDelete, + t, }: { automation: Automation onToggle: (next: boolean) => void @@ -274,6 +276,7 @@ function AutomationCard({ onDuplicate: () => void onLogs: () => void onDelete: () => void + t: ReturnType }) { const meta = triggerMeta(automation.trigger_type) return ( @@ -315,10 +318,12 @@ function AutomationCard({ {meta.label} - {automation.execution_count} run{automation.execution_count === 1 ? "" : "s"} + {automation.execution_count === 1 + ? t("runs", { count: automation.execution_count }) + : t("runsPlural", { count: automation.execution_count })} · - last {formatRelative(automation.last_executed_at)} + {t("lastRun", { time: formatRelative(automation.last_executed_at) })}
    @@ -326,7 +331,7 @@ function AutomationCard({ onToggle(!!v)} - aria-label={automation.is_active ? "Deactivate" : "Activate"} + aria-label={automation.is_active ? t("deactivate") : t("activate")} /> @@ -339,20 +344,20 @@ function AutomationCard({ - Edit + {t("edit")} - Duplicate + {t("duplicate")} - View Logs + {t("viewLogs")} - Delete + {t("delete")} diff --git a/src/components/automations/automation-builder.tsx b/src/components/automations/automation-builder.tsx index 7632166..d887b35 100644 --- a/src/components/automations/automation-builder.tsx +++ b/src/components/automations/automation-builder.tsx @@ -8,6 +8,7 @@ import { type ReactNode, } from "react" import { useRouter } from "next/navigation" +import { useTranslations } from "next-intl" import { toast } from "sonner" import { ArrowLeft, @@ -88,17 +89,17 @@ interface StepMeta { } const STEP_META: Record = { - send_message: { label: "Send Message", icon: MessageSquare, border: "border-l-primary" }, - send_template: { label: "Send Template", icon: FileText, border: "border-l-primary" }, - add_tag: { label: "Add Tag", icon: Tag, border: "border-l-primary" }, - remove_tag: { label: "Remove Tag", icon: TagIcon, border: "border-l-primary" }, - assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-primary" }, - update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-primary" }, - create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-primary" }, - wait: { label: "Wait", icon: Hourglass, border: "border-l-border" }, - condition: { label: "Condition (If/Else)", icon: GitBranch, border: "border-l-amber-500" }, - send_webhook: { label: "Send Webhook", icon: Webhook, border: "border-l-primary" }, - close_conversation: { label: "Close Conversation", icon: CircleSlash, border: "border-l-primary" }, + send_message: { label: "send_message", icon: MessageSquare, border: "border-l-primary" }, + send_template: { label: "send_template", icon: FileText, border: "border-l-primary" }, + add_tag: { label: "add_tag", icon: Tag, border: "border-l-primary" }, + remove_tag: { label: "remove_tag", icon: TagIcon, border: "border-l-primary" }, + assign_conversation: { label: "assign_conversation", icon: UserCheck, border: "border-l-primary" }, + update_contact_field: { label: "update_contact_field", icon: PencilLine, border: "border-l-primary" }, + create_deal: { label: "create_deal", icon: Briefcase, border: "border-l-primary" }, + wait: { label: "wait", icon: Hourglass, border: "border-l-border" }, + condition: { label: "condition", icon: GitBranch, border: "border-l-amber-500" }, + send_webhook: { label: "send_webhook", icon: Webhook, border: "border-l-primary" }, + close_conversation: { label: "close_conversation", icon: CircleSlash, border: "border-l-primary" }, } const ADDABLE_STEPS: AutomationStepType[] = [ @@ -115,18 +116,14 @@ const ADDABLE_STEPS: AutomationStepType[] = [ "close_conversation", ] -const TRIGGER_OPTIONS: { value: AutomationTriggerType; label: string; hint: string }[] = [ - { value: "new_message_received", label: "New Message Received", hint: "Any incoming message" }, - { - value: "first_inbound_message", - label: "First Message from Contact", - hint: "First time this contact ever messages you (works for manually-added contacts too)", - }, - { value: "keyword_match", label: "Keyword Match", hint: "Message contains specific keyword(s)" }, - { value: "new_contact_created", label: "New Contact Created", hint: "When a contact is auto-created from an incoming message" }, - { value: "conversation_assigned", label: "Conversation Assigned", hint: "When assigned to an agent" }, - { value: "tag_added", label: "Tag Added", hint: "When a tag is added to a contact" }, - { value: "time_based", label: "Time-Based", hint: "On a recurring schedule" }, +const TRIGGER_OPTIONS: { value: AutomationTriggerType }[] = [ + { value: "new_message_received" }, + { value: "first_inbound_message" }, + { value: "keyword_match" }, + { value: "new_contact_created" }, + { value: "conversation_assigned" }, + { value: "tag_added" }, + { value: "time_based" }, ] function cid(): string { @@ -286,15 +283,17 @@ const SELECT_CLASS = function TagSelect({ value, onChange, + t, }: { value: string onChange: (v: string) => void + t: ReturnType }) { const { tags } = useResources() if (tags.length === 0) { return ( onChange(e.target.value)} className="bg-muted text-foreground" @@ -314,16 +313,16 @@ function TagSelect({ onChange={(e) => onChange(e.target.value)} className={SELECT_CLASS} > - - {tags.map((t) => ( - + {tags.map((tg) => ( + ))} {/* Preserve a saved tag that's since been deleted so editing an existing automation doesn't silently drop it. */} {value && !selected && ( - + )} @@ -337,9 +336,11 @@ function TagSelect({ function ContactFieldSelect({ value, onChange, + t, }: { value: string onChange: (v: string) => void + t: ReturnType }) { const { customFields } = useResources() const customValue = value.startsWith("custom:") ? value : "" @@ -351,11 +352,11 @@ function ContactFieldSelect({ onChange={(e) => onChange(e.target.value)} className={SELECT_CLASS} > - - - + + + {customFields.length > 0 && ( - + {customFields.map((f) => ( )} {customValue && !knownCustom && ( - + )} ) @@ -375,15 +376,17 @@ function ContactFieldSelect({ function AgentSelect({ value, onChange, + t, }: { value: string onChange: (v: string) => void + t: ReturnType }) { const { members } = useResources() if (members.length === 0) { return ( onChange(e.target.value)} className="bg-muted text-foreground" @@ -397,14 +400,14 @@ function AgentSelect({ onChange={(e) => onChange(e.target.value)} className={SELECT_CLASS} > - + {members.map((m) => ( ))} {value && !selected && ( - + )} ) @@ -416,17 +419,19 @@ function DealPipelineFields({ pipelineId, stageId, onChange, + t, }: { pipelineId: string stageId: string onChange: (patch: { pipeline_id: string; stage_id: string }) => void + t: ReturnType }) { const { pipelines, stages } = useResources() if (pipelines.length === 0) { return ( <> - + @@ -435,7 +440,7 @@ function DealPipelineFields({ className="bg-muted text-foreground" /> - + @@ -454,7 +459,7 @@ function DealPipelineFields({ return ( <> - + - + @@ -513,17 +518,19 @@ function SendTemplateFields({ templateName, language, onChange, + t, }: { templateName: string language: string onChange: (patch: { template_name: string; language: string }) => void + t: ReturnType }) { const { templates } = useResources() if (templates.length === 0) { return ( <> - + @@ -532,7 +539,7 @@ function SendTemplateFields({ className="bg-muted text-foreground" /> - + @@ -554,7 +561,7 @@ function SendTemplateFields({ ) return ( - + @@ -588,6 +595,7 @@ function SendTemplateFields({ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) { const router = useRouter() + const t = useTranslations("Automations.builder") const isEditing = !!initial.id const [state, setState] = useState(initial) const [saving, setSaving] = useState(false) @@ -658,11 +666,11 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) { description: firstIssue.path ? `at ${firstIssue.path}` : undefined, }) } else { - toast.error(body?.error ?? "Save failed") + toast.error(body?.error ?? t("toasts.saveFailed")) } return } - toast.success(isEditing ? "Automation saved" : "Automation created") + toast.success(isEditing ? t("toasts.saved") : t("toasts.created")) if (!isEditing && body?.automation?.id) { router.replace(`/automations/${body.automation.id}/edit`) } @@ -681,22 +689,22 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) { type="button" onClick={() => router.push("/automations")} className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" - aria-label="Back to automations" + aria-label={t("backToAutomations")} > patchTop("name", e.target.value)} - placeholder="Untitled automation" + placeholder={t("untitled")} className="min-w-0 flex-1 rounded-md bg-transparent px-2 py-1 text-sm font-semibold text-foreground placeholder:text-muted-foreground focus:bg-muted focus:outline-none sm:text-base" />
    - Active + {t("active")} patchTop("is_active", !!v)} - aria-label="Active" + aria-label={t("activeAria")} />
    @@ -717,8 +725,9 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) { patchTop("trigger_type", t)} + onTypeChange={(tVal) => patchTop("trigger_type", tVal)} onConfigChange={(c) => patchTop("trigger_config", c)} + t={t} /> onTypeChange: (t: AutomationTriggerType) => void onConfigChange: (c: Record) => void + t: ReturnType }) { const [open, setOpen] = useState(false) return ( @@ -767,9 +778,9 @@ function TriggerCard({
    -
    Trigger
    +
    {t("trigger")}
    - {TRIGGER_OPTIONS.find((o) => o.value === type)?.label ?? type} + {t(`triggers.${type}.label`)}

    - {TRIGGER_OPTIONS.find((o) => o.value === type)?.hint} + {t(`triggers.${type}.hint`)}

    {type === "keyword_match" && ( )} {type === "tag_added" && ( @@ -811,18 +823,27 @@ function TriggerCard({ onConfigChange({ ...config, tag_id: v })} + t={t} /> )} {type === "time_based" && ( - - onConfigChange({ ...config, schedule: e.target.value }) - } - className="bg-muted text-foreground" - /> +
    + + + onConfigChange({ ...config, schedule: e.target.value }) + } + className="bg-muted text-foreground" + /> +

    + {t("scheduleHint")} +

    +
    )} )} @@ -834,9 +855,11 @@ function TriggerCard({ function KeywordMatchConfig({ config, onChange, + t, }: { config: KeywordMatchTriggerConfig onChange: (c: Record) => void + t: ReturnType }) { const keywords = config?.keywords ?? [] // Keep a local draft string so the comma and trailing space aren't @@ -872,7 +895,7 @@ function KeywordMatchConfig({
    @@ -972,6 +995,7 @@ function StepRenderer({ parentScope: ParentScope parentPath: StepPath } & Omit) { + const t = useTranslations("Automations.builder") const path: StepPath = [ ...parentPath, parentScope.kind === "root" @@ -1011,7 +1035,7 @@ function StepRenderer({
    {isCondition ? "Condition" : step.step_type === "wait" ? "Wait" : "Action"}
    -
    {meta.label}
    +
    {t(`steps.${meta.label}`)}
    {previewFor(step)}
    props.deleteStepAt(path)} > - Delete + {t("delete", { defaultValue: "Delete" })} @@ -1083,6 +1107,7 @@ function ConditionBranches({ step: BuilderStep parentPath: StepPath } & Omit) { + const t = useTranslations("Automations.builder") const yes = step.branches?.yes ?? [] const no = step.branches?.no ?? [] // Build the child scope by appending a branch marker. The scope the @@ -1101,10 +1126,10 @@ function ConditionBranches({ // cram each branch to ~170px which is too narrow for the nested // cards. Two-column grid returns on sm+.
    - + - +
    @@ -1129,13 +1154,14 @@ function BranchColumn({ } function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) { + const t = useTranslations("Automations.builder") return (
    @@ -1143,12 +1169,12 @@ function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) { align="start" className="max-h-80 min-w-56 overflow-y-auto border-border bg-popover" > - {ADDABLE_STEPS.map((t) => { - const Icon = STEP_META[t].icon + {ADDABLE_STEPS.map((tp) => { + const Icon = STEP_META[tp].icon return ( - onPick(t)}> + onPick(tp)}> - {STEP_META[t].label} + {t(`steps.${STEP_META[tp].label}`)} ) })} @@ -1170,6 +1196,7 @@ function StepEditor({ step: BuilderStep onChange: (s: BuilderStep) => void }) { + const t = useTranslations("Automations.builder") const cfg = step.step_config const set = (patch: Record) => onChange({ ...step, step_config: { ...cfg, ...patch } }) @@ -1177,11 +1204,11 @@ function StepEditor({ switch (step.step_type) { case "send_message": return ( - +