Add dashboard and navigation translations with date-fns locale support

- Add translations for all dashboard widgets (de, es, fr)
- Add navigation menu translations for all languages
- Create useDateFnsLocale hook for localized date formatting
- Add translate="no" to prevent browser auto-translation
- Update dashboard components to use translation keys

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-17 00:49:48 -05:00
parent af001ddaeb
commit a80b35a806
13 changed files with 420 additions and 41 deletions

View File

@@ -92,7 +92,7 @@ const CapacityWidget: React.FC<CapacityWidgetProps> = ({
<div className={`flex items-center justify-between mb-3 ${isEditing ? 'pl-5' : ''}`}>
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
Capacity This Week
{t('dashboard.capacityThisWeek')}
</h3>
<div className="flex items-center gap-1.5">
<Users size={14} className="text-gray-400" />

View File

@@ -32,11 +32,11 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
newPercentage: total > 0 ? Math.round((newCustomers / total) * 100) : 0,
returningPercentage: total > 0 ? Math.round((returning / total) * 100) : 0,
chartData: [
{ name: 'New', value: newCustomers, color: '#8b5cf6' },
{ name: 'Returning', value: returning, color: '#10b981' },
{ name: t('dashboard.new'), value: newCustomers, color: '#8b5cf6' },
{ name: t('dashboard.returning'), value: returning, color: '#10b981' },
],
};
}, [customers]);
}, [customers, t]);
return (
<div className="h-full p-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
@@ -55,7 +55,7 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
)}
<h3 className={`text-base font-semibold text-gray-900 dark:text-white mb-2 ${isEditing ? 'pl-5' : ''}`}>
Customers This Month
{t('dashboard.customersThisMonth')}
</h3>
<div className="flex-1 flex items-center gap-3 min-h-0">
@@ -88,7 +88,7 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
<UserPlus size={12} className="text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">New</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('dashboard.new')}</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">
{breakdownData.new}{' '}
<span className="text-xs font-normal text-gray-400">
@@ -103,7 +103,7 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
<UserCheck size={12} className="text-green-600 dark:text-green-400" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Returning</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('dashboard.returning')}</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">
{breakdownData.returning}{' '}
<span className="text-xs font-normal text-gray-400">

View File

@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { GripVertical, X, AlertCircle, Clock, ChevronRight } from 'lucide-react';
import { Ticket } from '../../types';
import { formatDistanceToNow } from 'date-fns';
import { useDateFnsLocale } from '../../hooks/useDateFnsLocale';
interface OpenTicketsWidgetProps {
tickets: Ticket[];
@@ -17,6 +18,7 @@ const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
onRemove,
}) => {
const { t } = useTranslation();
const dateFnsLocale = useDateFnsLocale();
const openTickets = tickets.filter(ticket => ticket.status === 'open' || ticket.status === 'in_progress');
const urgentCount = openTickets.filter(t => t.priority === 'urgent' || t.isOverdue).length;
@@ -58,17 +60,17 @@ const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
<div className={`flex items-center justify-between mb-4 ${isEditing ? 'pl-5' : ''}`}>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Open Tickets
{t('dashboard.openTickets')}
</h3>
<div className="flex items-center gap-2">
{urgentCount > 0 && (
<span className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400">
<AlertCircle size={12} />
{urgentCount} urgent
{urgentCount} {t('dashboard.urgent')}
</span>
)}
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
{openTickets.length} open
{openTickets.length} {t('dashboard.open')}
</span>
</div>
</div>
@@ -93,11 +95,11 @@ const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
</p>
<div className="flex items-center gap-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
<span className={getPriorityColor(ticket.priority, ticket.isOverdue)}>
{ticket.isOverdue ? 'Overdue' : ticket.priority}
{ticket.isOverdue ? t('dashboard.overdue') : ticket.priority}
</span>
<span className="flex items-center gap-1">
<Clock size={10} />
{formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}
{formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true, locale: dateFnsLocale })}
</span>
</div>
</div>
@@ -113,7 +115,7 @@ const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
to="/dashboard/tickets"
className="mt-3 text-sm text-brand-600 dark:text-brand-400 hover:underline text-center"
>
View all {openTickets.length} tickets
{t('dashboard.viewAllTickets', { count: openTickets.length })}
</Link>
)}
</div>

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { GripVertical, X, Calendar, UserPlus, XCircle, CheckCircle, DollarSign } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { Appointment, Customer } from '../../types';
import { useDateFnsLocale } from '../../hooks/useDateFnsLocale';
interface ActivityItem {
id: string;
@@ -28,6 +29,7 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
onRemove,
}) => {
const { t } = useTranslation();
const dateFnsLocale = useDateFnsLocale();
const activities = useMemo(() => {
const items: ActivityItem[] = [];
@@ -39,8 +41,8 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
items.push({
id: `booking-${appt.id}`,
type: 'booking',
title: 'New Booking',
description: `${appt.customerName} booked an appointment`,
title: t('dashboard.newBooking'),
description: t('dashboard.customerBookedAppointment', { customerName: appt.customerName }),
timestamp,
icon: <Calendar size={14} />,
iconBg: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
@@ -49,8 +51,8 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
items.push({
id: `cancel-${appt.id}`,
type: 'cancellation',
title: 'Cancellation',
description: `${appt.customerName} cancelled their appointment`,
title: t('dashboard.cancellation'),
description: t('dashboard.customerCancelledAppointment', { customerName: appt.customerName }),
timestamp,
icon: <XCircle size={14} />,
iconBg: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
@@ -59,8 +61,8 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
items.push({
id: `complete-${appt.id}`,
type: 'completion',
title: 'Completed',
description: `${appt.customerName}'s appointment completed`,
title: t('dashboard.completed'),
description: t('dashboard.customerAppointmentCompleted', { customerName: appt.customerName }),
timestamp,
icon: <CheckCircle size={14} />,
iconBg: 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
@@ -76,8 +78,8 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
items.push({
id: `customer-${customer.id}`,
type: 'new_customer',
title: 'New Customer',
description: `${customer.name} signed up`,
title: t('dashboard.newCustomer'),
description: t('dashboard.customerSignedUp', { customerName: customer.name }),
timestamp: new Date(), // Approximate - would need createdAt field
icon: <UserPlus size={14} />,
iconBg: 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
@@ -107,7 +109,7 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
)}
<h3 className={`text-lg font-semibold text-gray-900 dark:text-white mb-4 ${isEditing ? 'pl-5' : ''}`}>
Recent Activity
{t('dashboard.recentActivity')}
</h3>
<div className="flex-1 overflow-y-auto">
@@ -131,7 +133,7 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
{activity.description}
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
{formatDistanceToNow(activity.timestamp, { addSuffix: true })}
{formatDistanceToNow(activity.timestamp, { addSuffix: true, locale: dateFnsLocale })}
</p>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { X, Plus, Check, LayoutDashboard, BarChart2, Ticket, Activity, Users, UserX, PieChart } from 'lucide-react';
import { WIDGET_DEFINITIONS, WidgetType } from './types';
import { WIDGET_DEFINITIONS, WidgetType, getWidgetTitle, getWidgetDescription } from './types';
interface WidgetConfigModalProps {
isOpen: boolean;
@@ -31,6 +32,7 @@ const WidgetConfigModal: React.FC<WidgetConfigModalProps> = ({
onToggleWidget,
onResetLayout,
}) => {
const { t } = useTranslation();
if (!isOpen) return null;
const widgets = Object.values(WIDGET_DEFINITIONS);
@@ -45,7 +47,7 @@ const WidgetConfigModal: React.FC<WidgetConfigModalProps> = ({
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Configure Dashboard Widgets
{t('dashboard.configureWidgets')}
</h2>
<button
onClick={onClose}
@@ -58,7 +60,7 @@ const WidgetConfigModal: React.FC<WidgetConfigModalProps> = ({
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Select which widgets to show on your dashboard. You can drag widgets to reposition them.
{t('dashboard.configureWidgetsDescription')}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -92,14 +94,14 @@ const WidgetConfigModal: React.FC<WidgetConfigModalProps> = ({
: 'text-gray-900 dark:text-white'
}`}
>
{widget.title}
{getWidgetTitle(widget.id, t)}
</p>
{isActive && (
<Check size={14} className="text-brand-600 dark:text-brand-400" />
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{widget.description}
{getWidgetDescription(widget.id, t)}
</p>
</div>
</button>
@@ -114,13 +116,13 @@ const WidgetConfigModal: React.FC<WidgetConfigModalProps> = ({
onClick={onResetLayout}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
Reset to Default
{t('dashboard.resetToDefault')}
</button>
<button
onClick={onClose}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Done
{t('dashboard.done')}
</button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { Layout } from 'react-grid-layout';
import { TFunction } from 'i18next';
export type WidgetType =
| 'appointments-metric'
@@ -119,6 +120,39 @@ export const WIDGET_DEFINITIONS: Record<WidgetType, WidgetConfig> = {
},
};
// Widget ID to translation key mapping
const WIDGET_TRANSLATION_KEYS: Record<WidgetType, string> = {
'appointments-metric': 'appointmentsMetric',
'customers-metric': 'customersMetric',
'services-metric': 'servicesMetric',
'resources-metric': 'resourcesMetric',
'revenue-chart': 'revenueChart',
'appointments-chart': 'appointmentsChart',
'open-tickets': 'openTickets',
'recent-activity': 'recentActivity',
'capacity-utilization': 'capacityUtilization',
'no-show-rate': 'noShowRate',
'customer-breakdown': 'customerBreakdown',
};
// Helper function to get translated widget title
export const getWidgetTitle = (widgetId: string, t: TFunction): string => {
const key = WIDGET_TRANSLATION_KEYS[widgetId as WidgetType];
if (key) {
return t(`dashboard.widgetTitles.${key}`);
}
return WIDGET_DEFINITIONS[widgetId as WidgetType]?.title || widgetId;
};
// Helper function to get translated widget description
export const getWidgetDescription = (widgetId: string, t: TFunction): string => {
const key = WIDGET_TRANSLATION_KEYS[widgetId as WidgetType];
if (key) {
return t(`dashboard.widgetDescriptions.${key}`);
}
return WIDGET_DEFINITIONS[widgetId as WidgetType]?.description || '';
};
// Default layout for new users
export const DEFAULT_LAYOUT: DashboardLayout = {
widgets: [