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:
poduck
2025-12-03 21:40:54 -05:00
parent 902582f4ba
commit c7f241b30a
34 changed files with 1313 additions and 592 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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) => (

View File

@@ -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">