mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
feat(i18n): complete flows module internationalization
This commit is contained in:
parent
7cb019081b
commit
a91b8a7ba7
@ -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": {
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
ref={cardRef}
|
||||
|
||||
@ -138,7 +138,8 @@ function FlowNodeCard({ data, selected }: NodeProps) {
|
||||
const { node, isEntry, isFlashed } = data as NodeData;
|
||||
const meta = NODE_META[node.node_type];
|
||||
const c = nodeColors(node.node_type);
|
||||
const summary = summarizeNode(node);
|
||||
const tSummary = useTranslations('Flows.summary');
|
||||
const summary = summarizeNode(node, tSummary);
|
||||
const slots = outgoingSlots(node);
|
||||
// Start nodes are entry-only; nothing ever targets them, so they
|
||||
// don't need an incoming Handle. Every other node type accepts
|
||||
|
||||
@ -49,6 +49,7 @@ import {
|
||||
validateFlowForActivation,
|
||||
type ValidationIssue,
|
||||
} from "@/lib/flows/validate";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { unlinkNodeReferences } from "@/lib/flows/edges";
|
||||
import type { FlowNodeRow, FlowRow } from "@/lib/flows/types";
|
||||
import { NODE_META, slugify, type BuilderNode, type NodeType } from "./shared";
|
||||
@ -237,6 +238,7 @@ export function FlowEditorProvider({
|
||||
children,
|
||||
}: ProviderProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("Flows.editorState");
|
||||
|
||||
const [state, setStateRaw] = useState<BuilderState>(() => ({
|
||||
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);
|
||||
|
||||
@ -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 | number>) => 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 : '';
|
||||
|
||||
@ -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 (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-emerald-600/50 bg-background p-3 text-sm font-medium text-emerald-300">
|
||||
<CircleCheck className="h-4 w-4 shrink-0" />
|
||||
No issues. Ready to activate.
|
||||
{t("noIssues")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -51,12 +53,11 @@ export function ValidationPanel() {
|
||||
) : (
|
||||
<CircleAlert className="h-4 w-4 text-amber-400" />
|
||||
)}
|
||||
{errors.length} error{errors.length === 1 ? "" : "s"},{" "}
|
||||
{warnings.length} warning{warnings.length === 1 ? "" : "s"}
|
||||
{t("summary", { errorCount: errors.length, warningCount: warnings.length })}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{issues.map((i, ix) => (
|
||||
<IssueLine key={ix} issue={i} onJump={requestFlash} />
|
||||
<IssueLine key={ix} issue={i} onJump={requestFlash} t={t} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -72,9 +73,11 @@ export function ValidationPanel() {
|
||||
export function IssueLine({
|
||||
issue,
|
||||
onJump,
|
||||
t,
|
||||
}: {
|
||||
issue: ValidationIssue;
|
||||
onJump?: (key: string) => void;
|
||||
t?: ReturnType<typeof useTranslations>;
|
||||
}) {
|
||||
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}
|
||||
</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user