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:
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user