Flow not found.
+{t("notFound")}
Flow not found.
+{tEdit("notFound")}
- The 50 most recent times this flow ran. Expand a row to see the engine's - per-step log. + {t("description")}
{runs.length === 0 ? (
- at {run.current_node_key}
+ {t("atNode", { node: run.current_node_key })}
)}
{JSON.stringify(run.vars, null, 2)}
@@ -279,7 +296,7 @@ function RunCard({
{events.length === 0 ? (
- No events recorded for this run.
+ {t("noEvents")}
) : (
events.map((ev, ix) => )
diff --git a/src/app/(dashboard)/flows/page.tsx b/src/app/(dashboard)/flows/page.tsx
index 418acb2..0753f5a 100644
--- a/src/app/(dashboard)/flows/page.tsx
+++ b/src/app/(dashboard)/flows/page.tsx
@@ -18,6 +18,7 @@ import {
FileText,
} from "lucide-react";
+import { useTranslations } from "next-intl";
import { useCan } from "@/hooks/use-can";
import { Button } from "@/components/ui/button";
import { GatedButton } from "@/components/ui/gated-button";
@@ -54,11 +55,11 @@ interface FlowRow {
updated_at: string;
}
-const STATUS_LABELS: Record = {
- draft: "Draft",
- active: "Active",
- archived: "Archived",
-};
+const STATUS_LABELS = (t: ReturnType): Record => ({
+ draft: t("statusDraft"),
+ active: t("statusActive"),
+ archived: t("statusArchived"),
+});
const STATUS_COLORS: Record = {
draft: "border-border bg-muted text-muted-foreground",
@@ -84,6 +85,7 @@ const TEMPLATE_ICONS = {
export default function FlowsPage() {
const router = useRouter();
const canCreate = useCan("send-messages");
+ const t = useTranslations("Flows.list");
const [flows, setFlows] = useState([]);
const [loading, setLoading] = useState(true);
const [createOpen, setCreateOpen] = useState(false);
@@ -115,7 +117,7 @@ export default function FlowsPage() {
} catch (err) {
if (!cancelled) {
console.error(err);
- toast.error("Couldn't load flows.");
+ toast.error(t("loadError"));
}
} finally {
if (!cancelled) setLoading(false);
@@ -146,7 +148,7 @@ export default function FlowsPage() {
router.push(`/flows/${json.flow.id}`);
} catch (err) {
console.error(err);
- toast.error("Couldn't create flow.");
+ toast.error(t("createError"));
} finally {
setCreating(false);
}
@@ -168,7 +170,7 @@ export default function FlowsPage() {
setCreateOpen(false);
router.push(`/flows/${json.flow.id}`);
} catch (err) {
- const msg = err instanceof Error ? err.message : "Clone failed";
+ const msg = err instanceof Error ? err.message : t("cloneError");
toast.error(msg);
} finally {
setCreating(false);
@@ -176,18 +178,16 @@ export default function FlowsPage() {
}
async function handleDelete(flow: FlowRow) {
- const yes = window.confirm(
- `Delete "${flow.name}"? Any active runs will end immediately.`,
- );
+ const yes = window.confirm(t("deleteConfirm", { name: flow.name }));
if (!yes) return;
try {
const res = await fetch(`/api/flows/${flow.id}`, { method: "DELETE" });
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
setFlows((prev) => prev.filter((f) => f.id !== flow.id));
- toast.success("Flow deleted.");
+ toast.success(t("deleteSuccess"));
} catch (err) {
console.error(err);
- toast.error("Couldn't delete flow.");
+ toast.error(t("deleteError"));
}
}
@@ -204,14 +204,13 @@ export default function FlowsPage() {
- Flows
+ {t("title")}
- Beta
+ {t("beta")}
- Build branching, button-driven WhatsApp conversations. Useful for
- menus, FAQs, and triage before a human steps in.
+ {t("description")}
setCreateOpen(true)}
>
- New flow
+ {t("newFlow")}
@@ -228,6 +227,7 @@ export default function FlowsPage() {
setCreateOpen(true)}
canCreate={canCreate}
+ t={t}
/>
) : (
@@ -237,6 +237,7 @@ export default function FlowsPage() {
flow={flow}
onEdit={() => router.push(`/flows/${flow.id}`)}
onDelete={() => handleDelete(flow)}
+ t={t}
/>
))}
@@ -249,37 +250,37 @@ export default function FlowsPage() {
sm-scoped 384px wins at every real desktop breakpoint. */}
- Create a new flow
+ {t("createTitle")}
- Start from a template or build from scratch.
+ {t("createDesc")}
{templates.length > 0 && (
- Start from a template
+ {t("startTemplate")}
- {templates.map((t) => {
- const Icon = TEMPLATE_ICONS[t.icon] ?? FileText;
+ {templates.map((template) => {
+ const Icon = TEMPLATE_ICONS[template.icon] ?? FileText;
return (
);
@@ -290,12 +291,12 @@ export default function FlowsPage() {
- Or start blank
+ {t("startBlank")}
setNewName(e.target.value)}
- placeholder="e.g. Welcome menu"
+ placeholder={t("placeholderName")}
className="bg-muted"
onKeyDown={(e) => {
if (e.key === "Enter") handleCreate();
@@ -309,11 +310,11 @@ export default function FlowsPage() {
onClick={() => setCreateOpen(false)}
disabled={creating}
>
- Cancel
+ {t("cancel")}
@@ -325,9 +326,11 @@ export default function FlowsPage() {
function EmptyState({
onCreate,
canCreate,
+ t,
}: {
onCreate: () => void;
canCreate: boolean;
+ t: ReturnType;
}) {
return (
@@ -335,12 +338,10 @@ function EmptyState({
- No flows yet
+ {t("emptyTitle")}
- Build your first conversation — a welcome menu, an order lookup, an FAQ
- bot. Customers tap buttons; the bot routes them to the right answer (or
- the right agent).
+ {t("emptyDesc")}
- Create your first flow
+ {t("createFirst")}
);
@@ -359,12 +360,14 @@ function FlowCard({
flow,
onEdit,
onDelete,
+ t,
}: {
flow: FlowRow;
onEdit: () => void;
onDelete: () => void;
+ t: ReturnType;
}) {
- const triggerSummary = describeTrigger(flow);
+ const triggerSummary = describeTrigger(flow, t);
const StatusIcon =
flow.status === "active"
? PlayCircle
@@ -388,7 +391,7 @@ function FlowCard({
)}
>
- {STATUS_LABELS[flow.status]}
+ {STATUS_LABELS(t)[flow.status]}
@@ -399,14 +402,14 @@ function FlowCard({
- {flow.execution_count} {flow.execution_count === 1 ? "run" : "runs"}
+ {t("runCount", { count: flow.execution_count })}
);
}
-function describeTrigger(flow: FlowRow): string {
+function describeTrigger(flow: FlowRow, t: ReturnType): string {
if (flow.trigger_type === "keyword") {
const keywords = Array.isArray(flow.trigger_config.keywords)
? (flow.trigger_config.keywords as string[])
: [];
- if (keywords.length === 0) return "Triggers on keyword (none set)";
- return `Triggers on: ${keywords.join(", ")}`;
+ if (keywords.length === 0) return t("triggerKeywordNone");
+ return t("triggerKeyword", { keywords: keywords.join(", ") });
}
if (flow.trigger_type === "first_inbound_message") {
- return "Triggers on a contact's first-ever inbound message";
+ return t("triggerFirstInbound");
}
- return "Manual trigger";
+ return t("triggerManual");
}
diff --git a/src/components/flows/flow-builder.tsx b/src/components/flows/flow-builder.tsx
index ae459da..bda5a47 100644
--- a/src/components/flows/flow-builder.tsx
+++ b/src/components/flows/flow-builder.tsx
@@ -18,6 +18,7 @@
*/
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useTranslations } from 'next-intl';
import {
CircleAlert,
Plus,
@@ -73,6 +74,7 @@ import { useFlowEditor, type BuilderState } from './flow-editor-state';
// ============================================================
export function FlowBuilder() {
+ const t = useTranslations('Flows.builder');
const {
state,
setState,
@@ -158,24 +160,21 @@ export function FlowBuilder() {
state={state}
setState={setState}
triggerIssues={issues.filter((i) => i.scope === 'trigger')}
+ t={t}
/>
-
+
- Nodes ({state.nodes.length})
+ {t('nodesTitle', { count: state.nodes.length })}
-
+
{state.nodes.length === 0 ? (
-
- Add a Start node, then a{' '}
- Send buttons node, then a Handoff{' '}
- — that's the welcome-menu shape from the brief.
-
+
) : (
state.nodes.map((node) => (
setState((s) => ({ ...s, entry_node_id: node.node_key }))
}
+ t={t}
/>
))
)}
@@ -221,9 +221,11 @@ export function FlowBuilder() {
function KeywordsInput({
keywords,
onChange,
+ t,
}: {
keywords: string[];
onChange: (keywords: string[]) => void;
+ t: ReturnType;
}) {
const [draft, setDraft] = useState(keywords.join(', '));
@@ -247,7 +249,7 @@ function KeywordsInput({
commit();
}
}}
- placeholder="support, help, hi"
+ placeholder={t('keywordsPlaceholder')}
className="bg-muted"
/>
);
@@ -261,18 +263,20 @@ function TriggerPanel({
state,
setState,
triggerIssues,
+ t,
}: {
state: BuilderState;
setState: React.Dispatch>;
triggerIssues: ValidationIssue[];
+ t: ReturnType;
}) {
return (
- Trigger
+ {t('triggerTitle')}
@@ -304,7 +308,7 @@ function TriggerPanel({
{state.trigger_type === 'keyword' && (
)}
@@ -340,20 +345,22 @@ function TriggerPanel({
function EntryPicker({
state,
setState,
+ t,
}: {
state: BuilderState;
setState: React.Dispatch>;
+ t: ReturnType;
}) {
if (state.nodes.length === 0) return null;
return (
- Entry node:
+ {t('entryNodeTitle')}
setState((s) => ({ ...s, entry_node_id: key }))}
- placeholder="Pick the first node…"
+ placeholder={t('entryNodePlaceholder')}
className="max-w-xs flex-1"
/>
@@ -377,6 +384,7 @@ function NodeCard({
onUpdateConfig,
onRemove,
onSetEntry,
+ t,
}: {
node: BuilderNode;
allNodes: BuilderNode[];
@@ -390,6 +398,7 @@ function NodeCard({
onUpdateConfig: (patch: Record) => void;
onRemove: () => void;
onSetEntry: () => void;
+ t: ReturnType;
}) {
const meta = NODE_META[node.node_type];
const c = nodeColors(node.node_type);
@@ -425,7 +434,7 @@ function NodeCard({
className="truncate text-[11px] font-semibold tracking-wider uppercase"
style={{ color: c.text }}
>
- {meta.label}
+ {t(`nodes.${node.node_type}.label`)}
{node.node_key}
@@ -435,7 +444,7 @@ function NodeCard({
variant="outline"
className="border-primary/40 bg-primary/10 text-primary text-[10px]"
>
- Entry
+ {t('badgeEntry')}
)}
@@ -461,12 +470,13 @@ function NodeCard({
allNodes={allNodes}
onUpdate={onUpdate}
onUpdateConfig={onUpdateConfig}
+ t={t}
/>
{!isEntry && (
)}
@@ -477,7 +487,7 @@ function NodeCard({
className="text-red-400 hover:bg-red-500/10 hover:text-red-300"
>
- Remove node
+ {t('removeNode')}
{issues.length > 0 && (
@@ -504,11 +514,13 @@ function NodeConfigWithAdvanced({
allNodes,
onUpdate,
onUpdateConfig,
+ t,
}: {
node: BuilderNode;
allNodes: BuilderNode[];
onUpdate: (patch: Partial) => void;
onUpdateConfig: (patch: Record) => void;
+ t: ReturnType;
}) {
const [showAdvanced, setShowAdvanced] = useState(false);
const hasReplyIds =
@@ -532,13 +544,13 @@ function NodeConfigWithAdvanced({
) : (
)}
- {showAdvanced ? 'Hide' : 'Show'} advanced
+ {showAdvanced ? t('hideAdvanced') : t('showAdvanced')}
{showAdvanced && (
{hasReplyIds && (
- Reply IDs for each option are shown inline above. They're
- returned by WhatsApp when a customer taps; you usually
- don't need to touch them.
+ {t('replyIdsHint')}
)}
@@ -566,7 +576,7 @@ function NodeConfigWithAdvanced({
// Add-node menu
// ============================================================
-function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
+function AddNodeButton({ onAdd, t }: { onAdd: (type: NodeType) => void; t: ReturnType }) {
const types: NodeType[] = [
'start',
'send_buttons',
@@ -583,10 +593,10 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
- Add node
+ {t('addNode')}
{groupNodeTypesByCategory(types).map((group, i) => (
@@ -598,14 +608,14 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
{i > 0 && }
- {group.label}
+ {t(`categories.${group.id}`)}
- {group.types.map((t) => {
- const meta = NODE_META[t];
+ {group.types.map((t_type) => {
+ const meta = NODE_META[t_type];
return (
- onAdd(t)}>
+ onAdd(t_type)}>
- {meta.label}
+ {t(`nodes.${t_type}.label`)}
);
})}
diff --git a/src/components/flows/flow-canvas.tsx b/src/components/flows/flow-canvas.tsx
index 10e668b..9747e3f 100644
--- a/src/components/flows/flow-canvas.tsx
+++ b/src/components/flows/flow-canvas.tsx
@@ -59,6 +59,8 @@ import {
import '@xyflow/react/dist/style.css';
import { Plus, Trash2 } from 'lucide-react';
+import { useTranslations } from 'next-intl';
+
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
@@ -132,6 +134,7 @@ function slotColor(nodeType: NodeType, slotId: string, fallback: string) {
}
function FlowNodeCard({ data, selected }: NodeProps) {
+ const t = useTranslations('Flows.builder');
const { node, isEntry, isFlashed } = data as NodeData;
const meta = NODE_META[node.node_type];
const c = nodeColors(node.node_type);
@@ -191,11 +194,11 @@ function FlowNodeCard({ data, selected }: NodeProps) {
className="truncate text-[10.5px] font-semibold tracking-wider uppercase"
style={{ color: c.text }}
>
- {meta.label}
+ {t(`nodes.${node.node_type}.label`)}
{isEntry && (
- Entry
+ {t('badgeEntry')}
)}
@@ -270,6 +273,7 @@ export function FlowCanvas() {
}
function FlowCanvasInner() {
+ const t = useTranslations('Flows.builder');
const {
state,
setState,
@@ -508,8 +512,8 @@ function FlowCanvasInner() {
if (rfNodes.length === 0) {
return (
- No nodes yet.
-
+ {t('noNodesYet')}
+
);
}
@@ -565,7 +569,7 @@ function FlowCanvasInner() {
className="!border-border !bg-card !rounded-xl !border !shadow-[0_6px_20px_-8px_rgba(0,0,0,0.5)]"
/>
-
+
@@ -578,6 +582,7 @@ function FlowCanvasInner() {
onUpdateConfig={onSelectedUpdateConfig}
onDelete={handleDeleteSelected}
onSetEntry={handleSetEntry}
+ t={t}
/>
>
);
@@ -597,6 +602,7 @@ function NodeEditSheet({
onUpdateConfig,
onDelete,
onSetEntry,
+ t,
}: {
node: BuilderNode | null;
isEntry: boolean;
@@ -605,6 +611,7 @@ function NodeEditSheet({
onUpdateConfig: (patch: Record) => void;
onDelete: () => void;
onSetEntry: () => void;
+ t: ReturnType;
}) {
// Sheet is controlled — opens when a node is selected, closes via
// Esc / overlay / close button (all delegated to onClose).
@@ -628,15 +635,15 @@ function NodeEditSheet({
- {meta.label}
+ {t(`nodes.${node.node_type}.label`)}
{isEntry && (
- Entry
+ {t('badgeEntry')}
)}
- {meta.blurb}
+ {t(`nodes.${node.node_type}.blurb`)}
@@ -656,7 +663,7 @@ function NodeEditSheet({
{!isEntry ? (
) : (
@@ -668,7 +675,7 @@ function NodeEditSheet({
className="text-red-400 hover:bg-red-500/10 hover:text-red-300"
>
- Delete node
+ {t('deleteNode')}
@@ -696,7 +703,7 @@ const ADD_NODE_TYPES: NodeType[] = [
'end',
];
-function CanvasAddNodeButton() {
+function CanvasAddNodeButton({ t }: { t: ReturnType }) {
const reactFlow = useReactFlow();
const { addNode, updateNodePosition } = useFlowEditor();
@@ -728,10 +735,10 @@ function CanvasAddNodeButton() {
- Add node
+ {t('addNode')}
0 && }
- {group.label}
+ {t(`categories.${group.id}`)}
- {group.types.map((t) => {
- const meta = NODE_META[t];
+ {group.types.map((t_type) => {
+ const meta = NODE_META[t_type];
return (
handleAdd(t)}
+ key={t_type}
+ onClick={() => handleAdd(t_type)}
className="gap-3 py-2"
>
- {meta.label}
+ {t(`nodes.${t_type}.label`)}
- {meta.blurb}
+ {t(`nodes.${t_type}.blurb`)}
diff --git a/src/components/flows/flow-editor-shell.tsx b/src/components/flows/flow-editor-shell.tsx
index 73a005f..384417d 100644
--- a/src/components/flows/flow-editor-shell.tsx
+++ b/src/components/flows/flow-editor-shell.tsx
@@ -35,6 +35,7 @@ import { ValidationPanel } from "./validation-panel";
import { NODE_META, nodeColors, type NodeType } from "./shared";
import { cn } from "@/lib/utils";
import type { FlowRow, FlowNodeRow } from "@/lib/flows/types";
+import { useTranslations } from "next-intl";
/**
* Below this viewport width we force list view and hide the toggle.
@@ -59,6 +60,8 @@ interface Props {
}
export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
+ const t = useTranslations("Flows.builder");
+
// Read the persisted choice in the useState initializer. Safe even
// though this is a client component because the parent page only
// mounts us AFTER a client-side fetch resolves — there's no SSR
@@ -110,26 +113,26 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
active={effectiveView === "canvas"}
onClick={() => choose("canvas")}
icon={ }
- label="Canvas"
+ label={t("canvasView")}
/>
choose("list")}
icon={
}
- label="List"
+ label={t("listView")}
/>
- {LEGEND_TYPES.map((t) => (
+ {LEGEND_TYPES.map((t_type) => (
- {NODE_META[t].label}
+ {t(`nodes.${t_type}.label`)}
))}
diff --git a/src/components/flows/forms/fields.tsx b/src/components/flows/forms/fields.tsx
index f404185..f43294c 100644
--- a/src/components/flows/forms/fields.tsx
+++ b/src/components/flows/forms/fields.tsx
@@ -28,6 +28,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
+import { useTranslations } from "next-intl";
import { NODE_META, type BuilderNode } from "../shared";
export function TextRow({
@@ -83,7 +84,7 @@ export function NextNodeRow({
nodes={allNodes}
excludeKey={currentKey}
onChange={(v) => onChange(v ?? "")}
- placeholder="Pick a next node…"
+ placeholder={useTranslations("Flows.builder.form")("pickNextNode")}
/>
);
@@ -104,6 +105,7 @@ export function NodeKeySelect({
placeholder?: string;
className?: string;
}) {
+ const t = useTranslations("Flows.builder.form");
const options = nodes.filter((n) => n.node_key !== excludeKey);
return (
- — None —
+ {t("none")}
{options.map((n) => {
const Icon = NODE_META[n.node_type].icon;
return (
diff --git a/src/components/flows/forms/node-config-form.tsx b/src/components/flows/forms/node-config-form.tsx
index 7ad15ef..7ae1db6 100644
--- a/src/components/flows/forms/node-config-form.tsx
+++ b/src/components/flows/forms/node-config-form.tsx
@@ -34,6 +34,7 @@ import {
X,
} from "lucide-react";
import { toast } from "sonner";
+import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -62,6 +63,7 @@ export function NodeConfigForm({
showAdvanced,
onUpdateConfig,
}: NodeConfigFormProps) {
+ const t = useTranslations("Flows.builder.form");
const cfg = node.config;
switch (node.node_type) {
case "start":
@@ -71,7 +73,7 @@ export function NodeConfigForm({
allNodes={allNodes}
currentKey={node.node_key}
onChange={(v) => onUpdateConfig({ next_node_key: v })}
- label="Advances to"
+ label={t("advancesTo")}
/>
);
@@ -79,7 +81,7 @@ export function NodeConfigForm({
return (
<>
onUpdateConfig({ text: v })}
/>
@@ -88,7 +90,7 @@ export function NodeConfigForm({
allNodes={allNodes}
currentKey={node.node_key}
onChange={(v) => onUpdateConfig({ next_node_key: v })}
- label="Advances to"
+ label={t("advancesTo")}
/>
>
);
@@ -101,6 +103,7 @@ export function NodeConfigForm({
currentKey={node.node_key}
onUpdateConfig={onUpdateConfig}
showAdvanced={showAdvanced}
+ t={t}
/>
);
@@ -112,6 +115,7 @@ export function NodeConfigForm({
currentKey={node.node_key}
onUpdateConfig={onUpdateConfig}
showAdvanced={showAdvanced}
+ t={t}
/>
);
@@ -122,6 +126,7 @@ export function NodeConfigForm({
allNodes={allNodes}
currentKey={node.node_key}
onUpdateConfig={onUpdateConfig}
+ t={t}
/>
);
@@ -129,14 +134,14 @@ export function NodeConfigForm({
return (
<>
onUpdateConfig({ prompt_text: v })}
rows={2}
/>
- Interpolate in downstream prompts and handoff notes with{" "}
+ {t("varKeyHelp")}{" "}
{"{{vars."}
{(cfg as { var_key?: string }).var_key || "name"}
@@ -163,7 +168,7 @@ export function NodeConfigForm({
allNodes={allNodes}
currentKey={node.node_key}
onChange={(v) => onUpdateConfig({ next_node_key: v })}
- label="After capturing, advance to"
+ label={t("advanceAfterCapture")}
/>
>
);
@@ -175,6 +180,7 @@ export function NodeConfigForm({
allNodes={allNodes}
currentKey={node.node_key}
onUpdateConfig={onUpdateConfig}
+ t={t}
/>
);
@@ -185,13 +191,14 @@ export function NodeConfigForm({
allNodes={allNodes}
currentKey={node.node_key}
onUpdateConfig={onUpdateConfig}
+ t={t}
/>
);
case "handoff":
return (
onUpdateConfig({ note: v })}
rows={2}
@@ -201,8 +208,7 @@ export function NodeConfigForm({
case "end":
return (
- Terminal node. When the runner reaches this node the run is marked
- complete. No config needed.
+ {t("endNodeHelp")}
);
}
@@ -224,12 +230,14 @@ function SendButtonsForm({
currentKey,
onUpdateConfig,
showAdvanced,
+ t,
}: {
cfg: SendButtonsCfg;
allNodes: BuilderNode[];
currentKey: string;
onUpdateConfig: (patch: Record) => void;
showAdvanced: boolean;
+ t: ReturnType;
}) {
const buttons = cfg.buttons ?? [];
const updateButton = (
@@ -257,20 +265,20 @@ function SendButtonsForm({
return (
<>
onUpdateConfig({ text: v })}
rows={3}
/>
onUpdateConfig({ footer_text: v })}
/>
@@ -299,7 +307,7 @@ function SendButtonsForm({
updateButton(i, { title: e.target.value })}
- placeholder="Visible title (≤20 chars)"
+ placeholder={t("optionTitlePlaceholder")}
className="bg-muted"
maxLength={20}
/>
@@ -308,7 +316,7 @@ function SendButtonsForm({
nodes={allNodes}
excludeKey={currentKey}
onChange={(v) => updateButton(i, { next_node_key: v ?? "" })}
- placeholder="Next node…"
+ placeholder={t("nextNodePlaceholder")}
/>
)}
@@ -362,12 +370,14 @@ function SendListForm({
currentKey,
onUpdateConfig,
showAdvanced,
+ t,
}: {
cfg: SendListCfg;
allNodes: BuilderNode[];
currentKey: string;
onUpdateConfig: (patch: Record) => void;
showAdvanced: boolean;
+ t: ReturnType;
}) {
const sections = cfg.sections ?? [];
const totalRows = sections.reduce((sum, s) => sum + s.rows.length, 0);
@@ -453,12 +463,12 @@ function SendListForm({
/>
onUpdateConfig({ button_label: v })}
/>
onUpdateConfig({ footer_text: v })}
/>
@@ -466,7 +476,7 @@ function SendListForm({
{sections.map((section, sIdx) => (
updateSection(sIdx, { title: e.target.value })
}
- placeholder={`Section ${sIdx + 1} title (optional)`}
+ placeholder={t("sectionTitlePlaceholder", { count: sIdx + 1 })}
className="bg-muted text-xs"
/>
{sections.length > 1 && (
@@ -524,7 +534,7 @@ function SendListForm({
onChange={(e) =>
updateRow(sIdx, rIdx, { title: e.target.value })
}
- placeholder="Row title (≤24)"
+ placeholder={t("rowTitlePlaceholder")}
className="bg-muted"
maxLength={24}
/>
@@ -535,7 +545,7 @@ function SendListForm({
onChange={(v) =>
updateRow(sIdx, rIdx, { next_node_key: v ?? "" })
}
- placeholder="Next node…"
+ placeholder={t("nextNodePlaceholder")}
/>
)}
@@ -566,7 +576,7 @@ function SendListForm({
{sections.length < 10 && (
)}
@@ -598,11 +608,13 @@ function ConditionForm({
allNodes,
currentKey,
onUpdateConfig,
+ t,
}: {
cfg: ConditionCfg;
allNodes: BuilderNode[];
currentKey: string;
onUpdateConfig: (patch: Record) => void;
+ t: ReturnType;
}) {
const tags = useUserTags();
@@ -614,7 +626,7 @@ function ConditionForm({
<>
-
+
{subject === "tag" && tags.length > 0 ? (