feat(i18n): Comprehensive internationalization of frontend components and pages
Translate all hardcoded English strings to use i18n translation keys: Components: - TransactionDetailModal: payment details, refunds, technical info - ConnectOnboarding/ConnectOnboardingEmbed: Stripe Connect setup - StripeApiKeysForm: API key management - DomainPurchase: domain registration flow - Sidebar: navigation labels - Schedule/Sidebar, PendingSidebar: scheduler UI - MasqueradeBanner: masquerade status - Dashboard widgets: metrics, capacity, customers, tickets - Marketing: PricingTable, PluginShowcase, BenefitsSection - ConfirmationModal, ServiceList: common UI Pages: - Staff: invitation flow, role management - Customers: form placeholders - Payments: transactions, payouts, billing - BookingSettings: URL and redirect configuration - TrialExpired: upgrade prompts and features - PlatformSettings, PlatformBusinesses: admin UI - HelpApiDocs: API documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GripVertical, X, Users, User } from 'lucide-react';
|
||||
import { Appointment, Resource } from '../../types';
|
||||
import { startOfWeek, endOfWeek, isWithinInterval } from 'date-fns';
|
||||
@@ -16,6 +17,7 @@ const CapacityWidget: React.FC<CapacityWidgetProps> = ({
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const capacityData = useMemo(() => {
|
||||
const now = new Date();
|
||||
const weekStart = startOfWeek(now, { weekStartsOn: 1 });
|
||||
@@ -103,7 +105,7 @@ const CapacityWidget: React.FC<CapacityWidgetProps> = ({
|
||||
{capacityData.resources.length === 0 ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
<Users size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No resources configured</p>
|
||||
<p className="text-sm">{t('dashboard.noResourcesConfigured')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 grid grid-cols-2 gap-2 auto-rows-min">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GripVertical, X, Users, UserPlus, UserCheck } from 'lucide-react';
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { Customer } from '../../types';
|
||||
@@ -14,6 +15,7 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const breakdownData = useMemo(() => {
|
||||
// Customers with lastVisit are returning, without are new
|
||||
const returning = customers.filter((c) => c.lastVisit !== null).length;
|
||||
@@ -122,7 +124,7 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
||||
<Users size={12} />
|
||||
<span>Total Customers</span>
|
||||
<span>{t('dashboard.totalCustomers')}</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-white">{breakdownData.total}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { TrendingUp, TrendingDown, Minus, GripVertical, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface GrowthData {
|
||||
weekly: { value: number; change: number };
|
||||
@@ -23,6 +24,7 @@ const MetricWidget: React.FC<MetricWidgetProps> = ({
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const formatChange = (change: number) => {
|
||||
if (change === 0) return '0%';
|
||||
return change > 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
|
||||
@@ -68,14 +70,14 @@ const MetricWidget: React.FC<MetricWidgetProps> = ({
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Week:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">{t('dashboard.weekLabel')}</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.weekly.change)}`}>
|
||||
{getTrendIcon(growth.weekly.change)}
|
||||
{formatChange(growth.weekly.change)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Month:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">{t('dashboard.monthLabel')}</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.monthly.change)}`}>
|
||||
{getTrendIcon(growth.monthly.change)}
|
||||
{formatChange(growth.monthly.change)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GripVertical, X, UserX, TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
||||
import { Appointment } from '../../types';
|
||||
import { subDays, subMonths, isAfter } from 'date-fns';
|
||||
@@ -14,6 +15,8 @@ const NoShowRateWidget: React.FC<NoShowRateWidgetProps> = ({
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const noShowData = useMemo(() => {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = subDays(now, 7);
|
||||
@@ -108,7 +111,7 @@ const NoShowRateWidget: React.FC<NoShowRateWidgetProps> = ({
|
||||
<div className={isEditing ? 'pl-5' : ''}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<UserX size={18} className="text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">No-Show Rate</p>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{t('dashboard.noShowRate')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
@@ -116,20 +119,20 @@ const NoShowRateWidget: React.FC<NoShowRateWidgetProps> = ({
|
||||
{noShowData.currentRate.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({noShowData.noShowCount} this month)
|
||||
({noShowData.noShowCount} {t('dashboard.thisMonth')})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs mt-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Week:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">{t('dashboard.week')}:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(noShowData.weeklyChange)}`}>
|
||||
{getTrendIcon(noShowData.weeklyChange)}
|
||||
{formatChange(noShowData.weeklyChange)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Month:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">{t('dashboard.month')}:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(noShowData.monthlyChange)}`}>
|
||||
{getTrendIcon(noShowData.monthlyChange)}
|
||||
{formatChange(noShowData.monthlyChange)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GripVertical, X, AlertCircle, Clock, ChevronRight } from 'lucide-react';
|
||||
import { Ticket } from '../../types';
|
||||
@@ -15,7 +16,8 @@ const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const openTickets = tickets.filter(t => t.status === 'open' || t.status === 'in_progress');
|
||||
const { t } = useTranslation();
|
||||
const openTickets = tickets.filter(ticket => ticket.status === 'open' || ticket.status === 'in_progress');
|
||||
const urgentCount = openTickets.filter(t => t.priority === 'urgent' || t.isOverdue).length;
|
||||
|
||||
const getPriorityColor = (priority: string, isOverdue?: boolean) => {
|
||||
@@ -75,7 +77,7 @@ const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
|
||||
{openTickets.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
|
||||
<AlertCircle size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No open tickets</p>
|
||||
<p className="text-sm">{t('dashboard.noOpenTickets')}</p>
|
||||
</div>
|
||||
) : (
|
||||
openTickets.slice(0, 5).map((ticket) => (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
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';
|
||||
@@ -26,6 +27,7 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const activities = useMemo(() => {
|
||||
const items: ActivityItem[] = [];
|
||||
|
||||
@@ -112,7 +114,7 @@ const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
|
||||
{activities.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
|
||||
<Calendar size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No recent activity</p>
|
||||
<p className="text-sm">{t('dashboard.noRecentActivity')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user