From 07b20f42d4aebbb872f3eee068c7b54621e4b708 Mon Sep 17 00:00:00 2001 From: Ivan Mercedes Date: Tue, 7 Jul 2026 13:43:06 -0400 Subject: [PATCH 01/19] feat(i18n): translate dashboard layout components - Added `Sidebar`, `Header`, and `ModeToggle` namespaces to English dictionary. - Refactored `sidebar.tsx` and `header.tsx` to use dynamic translations. - Updated accessibility labels for navigation and theme switcher. --- messages/en.json | 46 +++++++++++++++++++ src/components/layout/header.tsx | 43 +++++++++--------- src/components/layout/mode-toggle.tsx | 9 +++- src/components/layout/sidebar.tsx | 65 ++++++++++++++------------- 4 files changed, 110 insertions(+), 53 deletions(-) diff --git a/messages/en.json b/messages/en.json index 0987a94..b45f3f3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -13,5 +13,51 @@ "signIn": "Sign in", "noAccount": "Don't have an account?", "createAccount": "Create account" + }, + "Sidebar": { + "title": "CRM Template for WhatsApp", + "dashboard": "Dashboard", + "inbox": "Inbox", + "notifications": "Notifications", + "contacts": "Contacts", + "pipelines": "Pipelines", + "broadcasts": "Broadcasts", + "automations": "Automations", + "flows": "Flows", + "aiAgents": "AI Agents", + "settings": "Settings", + "beta": "Beta", + "unreadConversations": "{count} unread {count, plural, =1 {conversation} other {conversations}}", + "unreadNotifications": "{count} unread {count, plural, =1 {notification} other {notifications}}", + "roleOwner": "Owner", + "roleAdmin": "Admin", + "roleAgent": "Agent", + "roleViewer": "Viewer", + "closeMenu": "Close menu", + "defaultUser": "User", + "defaultAvatar": "Avatar", + "menuProfile": "Profile", + "menuSettings": "Settings", + "menuSignOut": "Sign out" + }, + "Header": { + "dashboard": "Dashboard", + "inbox": "Inbox", + "notifications": "Notifications", + "contacts": "Contacts", + "pipelines": "Pipelines", + "broadcasts": "Broadcasts", + "automations": "Automations", + "settings": "Settings", + "openMenu": "Open menu", + "openAccountMenu": "Open account menu", + "defaultUser": "User", + "defaultAvatar": "Avatar", + "menuProfile": "Profile", + "menuSettings": "Settings", + "menuSignOut": "Sign out" + }, + "ModeToggle": { + "switchMode": "Switch to {mode} mode" } } diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 45d615b..2746593 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -19,22 +19,22 @@ import { import { ModeToggle } from "@/components/layout/mode-toggle"; const pageTitles: Record = { - "/dashboard": "Dashboard", - "/inbox": "Inbox", - "/notifications": "Notifications", - "/contacts": "Contacts", - "/pipelines": "Pipelines", - "/broadcasts": "Broadcasts", - "/automations": "Automations", - "/settings": "Settings", + "/dashboard": "dashboard", + "/inbox": "inbox", + "/notifications": "notifications", + "/contacts": "contacts", + "/pipelines": "pipelines", + "/broadcasts": "broadcasts", + "/automations": "automations", + "/settings": "settings", }; -function getPageTitle(pathname: string): string { +function getPageTitleKey(pathname: string): string { if (pageTitles[pathname]) return pageTitles[pathname]; const match = Object.entries(pageTitles).find(([path]) => pathname.startsWith(path), ); - return match ? match[1] : "Dashboard"; + return match ? match[1] : "dashboard"; } interface HeaderProps { @@ -43,10 +43,13 @@ interface HeaderProps { onOpenSidebar?: () => void; } +import { useTranslations } from "next-intl"; + export function Header({ onOpenSidebar }: HeaderProps) { + const t = useTranslations("Header"); const pathname = usePathname(); const { profile, signOut } = useAuth(); - const title = getPageTitle(pathname); + const titleKey = getPageTitleKey(pathname); const initial = profile?.full_name?.charAt(0)?.toUpperCase() ?? @@ -60,13 +63,13 @@ export function Header({ onOpenSidebar }: HeaderProps) {

- {title} + {t(titleKey as any)}

@@ -76,13 +79,13 @@ export function Header({ onOpenSidebar }: HeaderProps) { {profile?.avatar_url ? ( ) : null} @@ -90,7 +93,7 @@ export function Header({ onOpenSidebar }: HeaderProps) { - {profile?.full_name ?? "User"} + {profile?.full_name ?? t("defaultUser")}

- {profile?.full_name ?? "User"} + {profile?.full_name ?? t("defaultUser")}

{profile?.email ?? ""} @@ -116,7 +119,7 @@ export function Header({ onOpenSidebar }: HeaderProps) { } > - Profile + {t("menuProfile")} - Settings + {t("menuSettings")} - Sign out + {t("menuSignOut")} diff --git a/src/components/layout/mode-toggle.tsx b/src/components/layout/mode-toggle.tsx index 273dc10..acf7bfc 100644 --- a/src/components/layout/mode-toggle.tsx +++ b/src/components/layout/mode-toggle.tsx @@ -5,6 +5,8 @@ import { Moon, Sun } from "lucide-react"; import { useTheme } from "@/hooks/use-theme"; import { cn } from "@/lib/utils"; +import { useTranslations } from "next-intl"; + /** * Light/dark mode toggle — a single icon button that flips the app * between the two modes. Sun shows in light mode (click → go dark), @@ -14,14 +16,17 @@ import { cn } from "@/lib/utils"; * 40×40 hit target to match the header's other touch controls. */ export function ModeToggle({ className }: { className?: string }) { + const t = useTranslations("ModeToggle"); const { mode, toggleMode } = useTheme(); const goingTo = mode === "dark" ? "light" : "dark"; + const switchLabel = t("switchMode", { mode: goingTo }); + return (

- CRM Template for WhatsApp + {t("title")}

- {t(titleKey as any)} + {t(titleKey as string)}

diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index a3d6d58..5db7488 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -236,7 +236,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) { )} > - {t(item.labelKey as any)} + {t(item.labelKey as string)} {item.beta && ( - {t(item.labelKey as any)} + {t(item.labelKey as string)} ); @@ -323,7 +323,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) { className={`ml-auto inline-flex shrink-0 items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider ${meta.className}`} > - {t(meta.labelKey as any)} + {t(meta.labelKey as string)} ); })() From afa136245bc21d34c7f0f78316462556f2f262b3 Mon Sep 17 00:00:00 2001 From: Ivan Mercedes Date: Tue, 7 Jul 2026 14:25:19 -0400 Subject: [PATCH 03/19] feat(i18n): translate dashboard overview components - Added `Dashboard` namespace to English dictionary. - Translated metric cards, quick actions, and activity feed. - Localized charts (conversations, pipeline donut, response time) and empty states. --- messages/en.json | 65 +++++++++++++++++++ src/app/(dashboard)/dashboard/page.tsx | 33 ++++++---- src/components/dashboard/activity-feed.tsx | 28 ++++---- .../dashboard/conversations-chart.tsx | 27 ++++---- src/components/dashboard/empty-state.tsx | 9 ++- src/components/dashboard/pipeline-donut.tsx | 18 +++-- src/components/dashboard/quick-actions.tsx | 16 +++-- .../dashboard/response-time-chart.tsx | 18 ++--- 8 files changed, 155 insertions(+), 59 deletions(-) diff --git a/messages/en.json b/messages/en.json index b45f3f3..939d20a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -59,5 +59,70 @@ }, "ModeToggle": { "switchMode": "Switch to {mode} mode" + }, + "Dashboard": { + "page": { + "title": "Dashboard", + "description": "Live analytics across conversations, contacts, deals, broadcasts, and automations.", + "activeConversations": "Active Conversations", + "newContactsToday": "New Contacts Today", + "openDealsValue": "Open Deals Value", + "messagesSentToday": "Messages Sent Today", + "newTodayVsYesterday": "new today vs yesterday", + "vsYesterday": "vs yesterday", + "openDeals": "{count} open {count, plural, =1 {deal} other {deals}}", + "noChange": "No change {suffix}" + }, + "quickActions": { + "newContact": "New Contact", + "newDeal": "New Deal", + "newBroadcast": "New Broadcast", + "newAutomation": "New Automation" + }, + "activityFeed": { + "title": "Recent Activity", + "viewAll": "View all →", + "noActivity": "No activity yet", + "noActivityHint": "Activity from messages, deals, broadcasts, and automations will appear here.", + "showingOf": "Showing {visible} of {totalLoaded}{plus}", + "show": "Show", + "timeS": "{sec}s ago", + "timeM": "{min}m ago", + "timeH": "{hr}h ago", + "timeD": "{day}d ago" + }, + "conversationsChart": { + "title": "Conversations Over Time", + "description": "Daily message volume by direction", + "days": "{count} days", + "noActivity": "No message activity in this range", + "noActivityHint": "Send or receive messages to start populating this chart.", + "incoming": "Incoming", + "outgoing": "Outgoing", + "tooltipIncoming": "{count} incoming", + "tooltipOutgoing": "{count} outgoing", + "ariaLabel": "Conversations per day" + }, + "pipelineDonut": { + "title": "Pipeline Value", + "description": "Open deals by stage", + "noOpenDeals": "No open deals yet", + "noOpenDealsHint": "Create deals in Pipelines to see stage breakdowns here.", + "dealCount": "{count} {count, plural, =1 {deal} other {deals}}", + "total": "Total", + "ariaLabel": "Pipeline value by stage" + }, + "responseTimeChart": { + "title": "Average First Response Time", + "description": "Minutes to reply to a customer's first unreplied message, by weekday", + "target": "target {minutes}m", + "thisWeek": "This week:", + "lastWeek": "Last week:", + "noReplies": "No replies recorded yet", + "noRepliesHint": "This chart fills in as you reply to customer messages." + }, + "emptyState": { + "title": "Not enough data yet" + } } } diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index fd2227e..5b8caa4 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -34,9 +34,12 @@ import { PipelineDonut } from '@/components/dashboard/pipeline-donut' import { ResponseTimeChart } from '@/components/dashboard/response-time-chart' import { ActivityFeed } from '@/components/dashboard/activity-feed' +import { useTranslations } from 'next-intl' + type RangeDays = 7 | 30 | 90 export default function DashboardPage() { + const t = useTranslations('Dashboard.page') const { defaultCurrency } = useAuth() const [metrics, setMetrics] = useState(null) const [metricsLoading, setMetricsLoading] = useState(true) @@ -122,9 +125,9 @@ export default function DashboardPage() {
{/* Header */}
-

Dashboard

+

{t('title')}

- Live analytics across conversations, contacts, deals, broadcasts, and automations. + {t('description')}

@@ -135,16 +138,20 @@ export default function DashboardPage() { ) : ( <> @@ -218,8 +227,8 @@ export default function DashboardPage() { // ------------------------------------------------------------ -function deltaLabel(delta: number, suffix: string): string { - if (delta === 0) return `No change ${suffix}` +function deltaLabel(delta: number, suffix: string, noChangeLabel: string): string { + if (delta === 0) return noChangeLabel const sign = delta > 0 ? '+' : '' return `${sign}${delta.toLocaleString()} ${suffix}` } diff --git a/src/components/dashboard/activity-feed.tsx b/src/components/dashboard/activity-feed.tsx index 346a485..a5b39e7 100644 --- a/src/components/dashboard/activity-feed.tsx +++ b/src/components/dashboard/activity-feed.tsx @@ -38,7 +38,10 @@ const KIND_THEME: Record = { automation: { icon: Zap, badge: 'bg-rose-500/10 text-rose-400' }, } +import { useTranslations } from 'next-intl' + export function ActivityFeed({ items, loading }: ActivityFeedProps) { + const t = useTranslations('Dashboard.activityFeed') // Start at 5 — a quick scan of the most recent events without // dominating vertical real estate. User expands explicitly via the // footer control when they want deeper history. @@ -56,12 +59,12 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) { return (
-

Recent Activity

+

{t('title')}

- View all → + {t('viewAll')}
@@ -75,8 +78,8 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
) : ( @@ -103,7 +106,7 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) { {it.text} - {relativeTime(it.at)} + {relativeTime(it.at, t)}
) @@ -122,11 +125,10 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {