From 8b1d75456f673d35655eff9abd99d556a8caf74e Mon Sep 17 00:00:00 2001 From: Ivan Mercedes Date: Tue, 7 Jul 2026 17:14:48 -0400 Subject: [PATCH] feat(i18n): localize pipelines module and components - Added Pipelines namespace to messages/en.json encompassing page, board, card, form, settings, and analytics. - Migrated PipelinesPage empty states, dropdowns, and pipeline creation dialogues. - Localized pipeline-board and deal-card UI strings and badges (Won/Lost). - Translated deal-form inputs, dynamic tooltips, select options, and error toasts. - Localized pipeline-settings including the sortable stage management rows and delete confirmations. - Migrated pipeline-analytics metrics and detailed statistical tooltips. - Resolved typecheck dependencies to ensure the t function is correctly passed to nested sortable components. --- messages/en.json | 113 ++++++++++++++++++ src/app/(dashboard)/pipelines/page.tsx | 40 ++++--- src/components/pipelines/deal-card.tsx | 8 +- src/components/pipelines/deal-form.tsx | 70 +++++------ .../pipelines/pipeline-analytics.tsx | 36 ++++-- src/components/pipelines/pipeline-board.tsx | 6 +- .../pipelines/pipeline-settings.tsx | 54 +++++---- 7 files changed, 232 insertions(+), 95 deletions(-) diff --git a/messages/en.json b/messages/en.json index ad35618..85fee41 100644 --- a/messages/en.json +++ b/messages/en.json @@ -445,5 +445,118 @@ "toastDeleteFailed": "Could not delete field.", "toastDeleted": "Deleted \"{name}\"." } + }, + "Pipelines": { + "page": { + "selectPipeline": "Select Pipeline", + "noPipelinesYet": "No pipelines yet", + "managePipelines": "Manage Pipelines", + "addPipeline": "Add Pipeline", + "addDeal": "Add Deal", + "createToStartTracking": "Create a pipeline to start tracking deals", + "createPipeline": "Create Pipeline", + "newPipeline": "New Pipeline", + "pipelineName": "Pipeline Name", + "pipelineNamePlaceholder": "e.g., Enterprise Sales", + "defaultStagesDesc": "Default stages (New Lead → Won) will be created automatically.", + "cancel": "Cancel", + "creating": "Creating...", + "createPipelineBtn": "Create Pipeline", + "toastFailedLoadPipelines": "Failed to load pipelines", + "toastFailedMoveDeal": "Failed to move deal", + "toastFailedCreatePipeline": "Failed to create pipeline", + "toastPipelineCreated": "Pipeline created", + "toastNotLinkedToAccount": "Your profile is not linked to an account." + }, + "board": { + "dropDealHere": "Drop a deal here", + "addDeal": "Add Deal" + }, + "card": { + "noContact": "No contact", + "won": "Won", + "lost": "Lost" + }, + "form": { + "editDeal": "Edit Deal", + "newDeal": "New Deal", + "title": "Title", + "titlePlaceholder": "Deal title", + "contact": "Contact", + "selectContact": "Select a contact", + "linkToConversation": "Link to Conversation", + "value": "Value", + "currency": "Currency", + "expectedCloseDate": "Expected Close Date", + "stage": "Stage", + "assignedTo": "Assigned To", + "unassigned": "Unassigned", + "notes": "Notes", + "notesPlaceholder": "Add notes...", + "status": "Status", + "markAsWon": "Mark as Won", + "markAsLost": "Mark as Lost", + "reopenDeal": "Reopen deal", + "cancel": "Cancel", + "saving": "Saving...", + "saveChanges": "Save Changes", + "createDeal": "Create Deal", + "deletePrompt": "Delete this deal?", + "confirm": "Confirm", + "deleting": "Deleting...", + "deleteDeal": "Delete Deal", + "toastRequired": "Title, contact, and stage are required", + "toastFailedSave": "Failed to save deal", + "toastNotSignedIn": "Not signed in", + "toastNotLinked": "Your profile is not linked to an account.", + "toastFailedCreate": "Failed to create deal", + "toastUpdated": "Deal updated", + "toastCreated": "Deal created", + "toastFailedStatus": "Failed to update deal status", + "toastMarkedWon": "Marked as won", + "toastMarkedLost": "Marked as lost", + "toastReopened": "Deal reopened", + "toastFailedDelete": "Failed to delete deal", + "toastDeleted": "Deal deleted" + }, + "settings": { + "managePipeline": "Manage Pipeline", + "deletePipeline": "Delete Pipeline", + "deletePipelineDesc": "This will archive all deals in this pipeline. This cannot be undone.", + "cancel": "Cancel", + "deleting": "Deleting...", + "deletePipelineBtn": "Delete Pipeline", + "pipelineName": "Pipeline Name", + "stages": "Stages", + "newStageNamePlaceholder": "New stage name", + "add": "Add", + "createNewPipeline": "Create a new pipeline", + "saving": "Saving...", + "saveChanges": "Save Changes", + "dragToReorder": "Drag to reorder", + "changeColor": "Change color", + "toastFailedSave": "Failed to save pipeline", + "toastSaved": "Pipeline saved", + "toastFailedAddStage": "Failed to add stage", + "toastMoveOrDeleteDeals": "Move or delete deals in this stage first", + "toastFailedDeleteStage": "Failed to delete stage", + "toastFailedDeletePipeline": "Failed to delete pipeline", + "toastDeleted": "Pipeline deleted" + }, + "analytics": { + "totalDeals": "Total Deals", + "totalDealsTooltip": "Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included.", + "pipelineValue": "Pipeline Value", + "pipelineValueTooltip": "Sum of the dollar values of all deals in this pipeline, excluding deals marked as Lost.", + "avgDealSize": "Avg Deal Size", + "avgDealSizeTooltip": "Pipeline Value divided by Total Deals — the average value of a single non-lost deal.", + "weightedValue": "Weighted Value", + "weightedValueTooltip": "Expected revenue: each open deal's value × its stage probability. First stage ≈ 10%, stages progress up to 90%, Won = 100%. Lost deals are excluded.", + "wonThisMonth": "Won This Month", + "wonThisMonthTooltip": "Deals marked as Won since the first day of the current month.", + "lostThisMonth": "Lost This Month", + "lostThisMonthTooltip": "Deals marked as Lost since the first day of the current month.", + "howCalculated": "How {label} is calculated" + } } } diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx index b7292df..fff6f98 100644 --- a/src/app/(dashboard)/pipelines/page.tsx +++ b/src/app/(dashboard)/pipelines/page.tsx @@ -29,6 +29,7 @@ import { toast } from "sonner"; import { useCan } from "@/hooks/use-can"; import { useAuth } from "@/hooks/use-auth"; import { GatedButton } from "@/components/ui/gated-button"; +import { useTranslations } from "next-intl"; // Pipeline creation is admin-class (settings-tier write under // the new RLS); deal creation is operational and only requires @@ -45,6 +46,7 @@ const SPEC_DEFAULT_STAGES = [ ]; export default function PipelinesPage() { + const t = useTranslations("Pipelines.page"); const supabase = createClient(); const canEditSettings = useCan("edit-settings"); const canCreateDeals = useCan("send-messages"); @@ -223,11 +225,11 @@ export default function PipelinesPage() { .update({ stage_id: newStageId }) .eq("id", dealId); if (error) { - toast.error("Failed to move deal"); + toast.error(t("toastFailedMoveDeal")); refreshDeals(); } }, - [supabase, refreshDeals], + [supabase, refreshDeals, t], ); const handleAddDeal = useCallback( @@ -260,7 +262,7 @@ export default function PipelinesPage() { } // pipelines.account_id is NOT NULL post-017 with no DB default. if (!accountId) { - toast.error("Your profile is not linked to an account."); + toast.error(t("toastNotLinkedToAccount")); setCreating(false); return; } @@ -272,7 +274,7 @@ export default function PipelinesPage() { .single(); if (error || !pipeline) { - toast.error("Failed to create pipeline"); + toast.error(t("toastFailedCreatePipeline")); setCreating(false); return; } @@ -290,7 +292,7 @@ export default function PipelinesPage() { setSelectedPipelineId(pipeline.id); await refreshPipelines(); setCreating(false); - toast.success("Pipeline created"); + toast.success(t("toastPipelineCreated")); } const selectedPipeline = pipelines.find((p) => p.id === selectedPipelineId); @@ -323,7 +325,7 @@ export default function PipelinesPage() { > - {selectedPipeline?.name ?? "Select Pipeline"} + {selectedPipeline?.name ?? t("selectPipeline")} @@ -333,7 +335,7 @@ export default function PipelinesPage() { > {pipelines.length === 0 && ( - No pipelines yet + {t("noPipelinesYet")} )} {pipelines.map((p) => ( @@ -357,7 +359,7 @@ export default function PipelinesPage() { className="text-popover-foreground" > - Manage Pipelines + {t("managePipelines")} )} @@ -373,7 +375,7 @@ export default function PipelinesPage() { className="border-border bg-card text-foreground hover:bg-muted" > - Add Pipeline + {t("addPipeline")} - Add Deal + {t("addDeal")} @@ -393,10 +395,10 @@ export default function PipelinesPage() {

- No pipelines yet + {t("noPipelinesYet")}

- Create a pipeline to start tracking deals + {t("createToStartTracking")}

- Create Pipeline + {t("createPipeline")}
) : ( @@ -425,21 +427,21 @@ export default function PipelinesPage() { - New Pipeline + {t("newPipeline")}
- + setNewPipelineName(e.target.value)} - placeholder="e.g., Enterprise Sales" + placeholder={t("pipelineNamePlaceholder")} className="mt-2 bg-muted border-border text-foreground" onKeyDown={(e) => { if (e.key === "Enter") handleCreatePipeline(); }} />

- Default stages (New Lead → Won) will be created automatically. + {t("defaultStagesDesc")}

@@ -448,14 +450,14 @@ export default function PipelinesPage() { onClick={() => setNewPipelineOpen(false)} className="border-border text-muted-foreground hover:bg-muted" > - Cancel + {t("cancel")}
diff --git a/src/components/pipelines/deal-card.tsx b/src/components/pipelines/deal-card.tsx index 2f1c271..1dd8335 100644 --- a/src/components/pipelines/deal-card.tsx +++ b/src/components/pipelines/deal-card.tsx @@ -3,6 +3,7 @@ import type { Deal, PipelineStage } from "@/types"; import { Calendar, Check, X } from "lucide-react"; import { formatCurrency } from "@/lib/currency"; +import { useTranslations } from "next-intl"; interface DealCardProps { deal: Deal; @@ -26,7 +27,8 @@ function initials(name?: string, fallback?: string) { } export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) { - const contactLabel = deal.contact?.name || deal.contact?.phone || "No contact"; + const t = useTranslations("Pipelines.card"); + const contactLabel = deal.contact?.name || deal.contact?.phone || t("noContact"); const assigneeLabel = deal.assignee?.full_name || null; return ( @@ -59,13 +61,13 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) { {deal.status === "won" && ( - Won + {t("won")} )} {deal.status === "lost" && ( - Lost + {t("lost")} )} diff --git a/src/components/pipelines/deal-form.tsx b/src/components/pipelines/deal-form.tsx index 01c0bfe..c0c25a0 100644 --- a/src/components/pipelines/deal-form.tsx +++ b/src/components/pipelines/deal-form.tsx @@ -32,6 +32,7 @@ import { Loader2, } from "lucide-react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; interface DealFormProps { open: boolean; @@ -52,6 +53,7 @@ export function DealForm({ defaultStageId, onSaved, }: DealFormProps) { + const t = useTranslations("Pipelines.form"); const supabase = createClient(); const { accountId, defaultCurrency } = useAuth(); @@ -151,7 +153,7 @@ export function DealForm({ async function handleSave() { if (!title.trim() || !contactId || !stageId) { - toast.error("Title, contact, and stage are required"); + toast.error(t("toastRequired")); return; } setSaving(true); @@ -174,7 +176,7 @@ export function DealForm({ .update(payload) .eq("id", deal.id); if (error) { - toast.error("Failed to save deal"); + toast.error(t("toastFailedSave")); setSaving(false); return; } @@ -184,12 +186,12 @@ export function DealForm({ } = await supabase.auth.getSession(); const user = session?.user; if (!user) { - toast.error("Not signed in"); + toast.error(t("toastNotSignedIn")); setSaving(false); return; } if (!accountId) { - toast.error("Your profile is not linked to an account."); + toast.error(t("toastNotLinked")); setSaving(false); return; } @@ -197,14 +199,14 @@ export function DealForm({ .from("deals") .insert({ ...payload, user_id: user.id, account_id: accountId, status: "open" }); if (error) { - toast.error("Failed to create deal"); + toast.error(t("toastFailedCreate")); setSaving(false); return; } } setSaving(false); - toast.success(deal ? "Deal updated" : "Deal created"); + toast.success(deal ? t("toastUpdated") : t("toastCreated")); onOpenChange(false); onSaved(); } @@ -218,11 +220,11 @@ export function DealForm({ .eq("id", deal.id); setStatusAction(null); if (error) { - toast.error("Failed to update deal status"); + toast.error(t("toastFailedStatus")); return; } toast.success( - status === "won" ? "Marked as won" : status === "lost" ? "Marked as lost" : "Deal reopened", + status === "won" ? t("toastMarkedWon") : status === "lost" ? t("toastMarkedLost") : t("toastReopened"), ); onOpenChange(false); onSaved(); @@ -234,10 +236,10 @@ export function DealForm({ const { error } = await supabase.from("deals").delete().eq("id", deal.id); setDeleting(false); if (error) { - toast.error("Failed to delete deal"); + toast.error(t("toastFailedDelete")); return; } - toast.success("Deal deleted"); + toast.success(t("toastDeleted")); setConfirmDelete(false); onOpenChange(false); onSaved(); @@ -252,29 +254,29 @@ export function DealForm({
- {deal ? "Edit Deal" : "New Deal"} + {deal ? t("editDeal") : t("newDeal")}
- + setTitle(e.target.value)} - placeholder="Deal title" + placeholder={t("titlePlaceholder")} className="border-border bg-muted text-foreground" />
- +
- +
- + setAssignedTo(e.target.value)} className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary" > - + {profiles.map((p) => (
- +