diff --git a/messages/en.json b/messages/en.json index 04405c0..d897391 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1021,6 +1021,8 @@ "deleteNode": "Delete node", "canvasView": "Canvas", "listView": "List", + "noNodeSelected": "No node selected", + "noNodeDesc": "Select a node on the canvas to edit its configuration.", "form": { "advancesTo": "Advances to", "textToCustomer": "Text sent to the customer", @@ -1127,6 +1129,34 @@ "blurb": "Ends the flow" } } + }, + "validation": { + "noIssues": "No issues. Ready to activate.", + "summary": "{errorCount} {errorCount, plural, =1 {error} other {errors}}, {warningCount} {warningCount, plural, =1 {warning} other {warnings}}", + "jumpToNode": "Jump to node {key}" + }, + "editorState": { + "saved": "Saved.", + "fixIssues": "Fix the issues below before activating.", + "statusChanged": "Flow is now {status}." + }, + "summary": { + "options": "{count} {count, plural, =1 {option} other {options}}", + "optionsAcrossSections": "{rowCount} {rowCount, plural, =1 {option} other {options}} across {sectionCount} {sectionCount, plural, =1 {section} other {sections}}", + "noFile": "{label} (no file uploaded)", + "media": "Media", + "image": "Image", + "video": "Video", + "document": "Document", + "audio": "Audio", + "hasTag": "has tag {tag}", + "opContains": "contains", + "opExists": "exists", + "opMissing": "missing", + "modeAdd": "Add", + "modeRemove": "Remove", + "tagPicked": "{mode} tag {tag}…", + "tagNone": "{mode} tag (none picked)" } }, "Settings": { diff --git a/src/components/flows/flow-builder.tsx b/src/components/flows/flow-builder.tsx index bda5a47..66f0528 100644 --- a/src/components/flows/flow-builder.tsx +++ b/src/components/flows/flow-builder.tsx @@ -403,7 +403,8 @@ function NodeCard({ const meta = NODE_META[node.node_type]; const c = nodeColors(node.node_type); const hasError = issues.some((i) => i.severity === 'error'); - const preview = summarizeNode(node); + const tSummary = useTranslations('Flows.summary'); + const preview = summarizeNode(node, tSummary); return (
(() => ({ name: initialFlow.name, @@ -347,7 +349,7 @@ export function FlowEditorProvider({ throw new Error(json.error ?? `Save failed: ${res.status}`); } setDirty(false); - toast.success("Saved."); + toast.success(t("saved")); } catch (err) { const msg = err instanceof Error ? err.message : "Save failed"; toast.error(msg); @@ -360,7 +362,7 @@ export function FlowEditorProvider({ const setStatus = useCallback( async (next: BuilderState["status"]) => { if (next === "active" && !canActivate) { - toast.error("Fix the issues below before activating."); + toast.error(t("fixIssues")); return; } setActivating(true); @@ -381,13 +383,7 @@ export function FlowEditorProvider({ throw new Error(json.error ?? `Status update failed: ${res.status}`); } setStateRaw((s) => ({ ...s, status: next })); - toast.success( - next === "active" - ? "Flow activated." - : next === "archived" - ? "Archived." - : "Saved as draft.", - ); + toast.success(t("statusChanged", { status: next })); } catch (err) { const msg = err instanceof Error ? err.message : "Status update failed"; toast.error(msg); diff --git a/src/components/flows/shared.tsx b/src/components/flows/shared.tsx index cab3f97..66a4f96 100644 --- a/src/components/flows/shared.tsx +++ b/src/components/flows/shared.tsx @@ -305,7 +305,10 @@ export function truncate(s: string, max = 80): string { return clean.slice(0, max - 1) + '…'; } -export function summarizeNode(node: BuilderNode): string | null { +export function summarizeNode( + node: BuilderNode, + t?: (key: string, values?: Record) => string +): string | null { const cfg = node.config; switch (node.node_type) { case 'start': @@ -342,11 +345,13 @@ export function summarizeNode(node: BuilderNode): string | null { }, 0); if (text.length > 0) { return rowCount > 0 - ? `${truncate(text, 50)} · ${rowCount} option${rowCount === 1 ? '' : 's'}` + ? `${truncate(text, 50)} · ${t ? t('options', { count: rowCount }) : `${rowCount} option${rowCount === 1 ? '' : 's'}`}` : truncate(text); } return rowCount > 0 - ? `${rowCount} option${rowCount === 1 ? '' : 's'} across ${sections.length} section${sections.length === 1 ? '' : 's'}` + ? t + ? t('optionsAcrossSections', { rowCount, sectionCount: sections.length }) + : `${rowCount} option${rowCount === 1 ? '' : 's'} across ${sections.length} section${sections.length === 1 ? '' : 's'}` : null; } case 'send_media': { @@ -356,9 +361,9 @@ export function summarizeNode(node: BuilderNode): string | null { const url = typeof cfg.media_url === 'string' ? cfg.media_url : ''; const caption = typeof cfg.caption === 'string' ? cfg.caption : ''; const label = mediaType - ? mediaType.charAt(0).toUpperCase() + mediaType.slice(1) - : 'Media'; - if (!url) return `${label} (no file uploaded)`; + ? t ? t(mediaType) || (mediaType.charAt(0).toUpperCase() + mediaType.slice(1)) : mediaType.charAt(0).toUpperCase() + mediaType.slice(1) + : t ? t('media') : 'Media'; + if (!url) return t ? t('noFile', { label }) : `${label} (no file uploaded)`; const name = filename || url.split('/').pop() || 'file'; return caption ? `${label}: ${truncate(name, 30)} · ${truncate(caption, 40)}` @@ -386,17 +391,17 @@ export function summarizeNode(node: BuilderNode): string | null { : 'var'; const subjectStr = subject === 'tag' - ? `has tag ${truncate(subjectKey, 24)}` + ? t ? t('hasTag', { tag: truncate(subjectKey, 24) }) : `has tag ${truncate(subjectKey, 24)}` : `${subject}.${subjectKey}`; const op = cfg.operator === 'equals' ? '==' : cfg.operator === 'contains' - ? 'contains' + ? t ? t('opContains') : 'contains' : cfg.operator === 'present' - ? 'exists' + ? t ? t('opExists') : 'exists' : cfg.operator === 'absent' - ? 'missing' + ? t ? t('opMissing') : 'missing' : ''; const value = typeof cfg.value === 'string' ? cfg.value : ''; const valStr = @@ -406,14 +411,14 @@ export function summarizeNode(node: BuilderNode): string | null { return subject === 'tag' ? subjectStr : `${subjectStr} ${op}${valStr}`; } case 'set_tag': { - const mode = cfg.mode === 'remove' ? 'Remove' : 'Add'; + const mode = cfg.mode === 'remove' ? (t ? t('modeRemove') : 'Remove') : (t ? t('modeAdd') : 'Add'); const tagId = typeof cfg.tag_id === 'string' ? cfg.tag_id : ''; // No tag name available without an async lookup here; show a // short prefix of the UUID so users can disambiguate between // multiple set_tag nodes at a glance. return tagId - ? `${mode} tag ${tagId.slice(0, 8)}…` - : `${mode} tag (none picked)`; + ? t ? t('tagPicked', { mode, tag: tagId.slice(0, 8) }) : `${mode} tag ${tagId.slice(0, 8)}…` + : t ? t('tagNone', { mode }) : `${mode} tag (none picked)`; } case 'handoff': { const note = typeof cfg.note === 'string' ? cfg.note : ''; diff --git a/src/components/flows/validation-panel.tsx b/src/components/flows/validation-panel.tsx index 75812ac..f10c8f1 100644 --- a/src/components/flows/validation-panel.tsx +++ b/src/components/flows/validation-panel.tsx @@ -18,12 +18,14 @@ */ import { CircleAlert, CircleCheck } from "lucide-react"; +import { useTranslations } from "next-intl"; import { cn } from "@/lib/utils"; import type { ValidationIssue } from "@/lib/flows/validate"; import { useFlowEditor } from "./flow-editor-state"; export function ValidationPanel() { const { issues, requestFlash } = useFlowEditor(); + const t = useTranslations("Flows.validation"); if (issues.length === 0) { // Slate-950 base + emerald accents so the panel stays readable when @@ -32,7 +34,7 @@ export function ValidationPanel() { return (
- No issues. Ready to activate. + {t("noIssues")}
); } @@ -51,12 +53,11 @@ export function ValidationPanel() { ) : ( )} - {errors.length} error{errors.length === 1 ? "" : "s"},{" "} - {warnings.length} warning{warnings.length === 1 ? "" : "s"} + {t("summary", { errorCount: errors.length, warningCount: warnings.length })}
{issues.map((i, ix) => ( - + ))}
@@ -72,9 +73,11 @@ export function ValidationPanel() { export function IssueLine({ issue, onJump, + t, }: { issue: ValidationIssue; onJump?: (key: string) => void; + t?: ReturnType; }) { const tone = issue.severity === "error" ? "text-red-300" : "text-amber-300"; @@ -106,7 +109,7 @@ export function IssueLine({ "flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs transition-colors hover:bg-muted/60", tone, )} - aria-label={`Jump to node ${issue.node_key}`} + aria-label={t ? t("jumpToNode", { key: issue.node_key! }) : `Jump to node ${issue.node_key}`} > {body}