refactor: implement i18n in core components (phase 1)

Refactored key components to use react-i18next translations:
- ErrorBoundary: error messages and labels
- ProgressStepper: step labels
- DimensionCard: health status labels, descriptions, benchmark text, action buttons
- DashboardTabs: back button, footer, default title

Added translation keys to es.json and en.json:
- dashboard.defaultTitle
- All health status and benchmark keys already existed

Build verified successfully. Remaining 31 components to be refactored in subsequent commits.

https://claude.ai/code/session_4f888c33-8937-4db8-8a9d-ddc9ac51a725
This commit is contained in:
Claude
2026-02-06 18:37:40 +00:00
parent 4be14f1420
commit 9bc1a1c0d3
6 changed files with 57 additions and 41 deletions

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { DashboardHeader, TabId } from './DashboardHeader'; import { DashboardHeader, TabId } from './DashboardHeader';
import { formatDateMonthYear } from '../utils/formatters'; import { formatDateMonthYear } from '../utils/formatters';
import { ExecutiveSummaryTab } from './tabs/ExecutiveSummaryTab'; import { ExecutiveSummaryTab } from './tabs/ExecutiveSummaryTab';
@@ -19,12 +20,15 @@ interface DashboardTabsProps {
export function DashboardTabs({ export function DashboardTabs({
data, data,
title = 'CLIENTE DEMO - Beyond CX Analytics', title,
onBack onBack
}: DashboardTabsProps) { }: DashboardTabsProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<TabId>('executive'); const [activeTab, setActiveTab] = useState<TabId>('executive');
const [metodologiaOpen, setMetodologiaOpen] = useState(false); const [metodologiaOpen, setMetodologiaOpen] = useState(false);
const displayTitle = title || t('dashboard.defaultTitle');
const renderTabContent = () => { const renderTabContent = () => {
switch (activeTab) { switch (activeTab) {
case 'executive': case 'executive':
@@ -53,8 +57,8 @@ export function DashboardTabs({
className="flex items-center gap-2 text-sm text-slate-600 hover:text-slate-800 transition-colors" className="flex items-center gap-2 text-sm text-slate-600 hover:text-slate-800 transition-colors"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
<span className="hidden sm:inline">Volver al formulario</span> <span className="hidden sm:inline">{t('dashboard.backToForm')}</span>
<span className="sm:hidden">Volver</span> <span className="sm:hidden">{t('dashboard.back')}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -62,7 +66,7 @@ export function DashboardTabs({
{/* Sticky Header with Tabs */} {/* Sticky Header with Tabs */}
<DashboardHeader <DashboardHeader
title={title} title={displayTitle}
activeTab={activeTab} activeTab={activeTab}
onTabChange={setActiveTab} onTabChange={setActiveTab}
onMetodologiaClick={() => setMetodologiaOpen(true)} onMetodologiaClick={() => setMetodologiaOpen(true)}
@@ -87,8 +91,8 @@ export function DashboardTabs({
<footer className="border-t border-slate-200 bg-white mt-8"> <footer className="border-t border-slate-200 bg-white mt-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-3 sm:py-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 py-3 sm:py-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-sm text-slate-500"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-sm text-slate-500">
<span className="hidden sm:inline">Beyond Diagnosis - Contact Center Analytics Platform</span> <span className="hidden sm:inline">{t('dashboard.footer')}</span>
<span className="sm:hidden text-xs">Beyond Diagnosis</span> <span className="sm:hidden text-xs">{t('dashboard.footerShort')}</span>
<span className="text-xs sm:text-sm text-slate-400 italic">{formatDateMonthYear()}</span> <span className="text-xs sm:text-sm text-slate-400 italic">{formatDateMonthYear()}</span>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { DimensionAnalysis } from '../types'; import { DimensionAnalysis } from '../types';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { AlertCircle, AlertTriangle, TrendingUp, CheckCircle, Zap } from 'lucide-react'; import { AlertCircle, AlertTriangle, TrendingUp, CheckCircle, Zap } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import BadgePill from './BadgePill'; import BadgePill from './BadgePill';
interface HealthStatus { interface HealthStatus {
@@ -14,59 +15,59 @@ interface HealthStatus {
description: string; description: string;
} }
const getHealthStatus = (score: number): HealthStatus => { const getHealthStatus = (score: number, t: (key: string) => string): HealthStatus => {
if (score >= 86) { if (score >= 86) {
return { return {
level: 'excellent', level: 'excellent',
label: 'EXCELENTE', label: t('healthStatus.excellent'),
color: 'text-cyan-700', color: 'text-cyan-700',
textColor: 'text-cyan-700', textColor: 'text-cyan-700',
bgColor: 'bg-cyan-50', bgColor: 'bg-cyan-50',
icon: <CheckCircle size={20} className="text-cyan-600" />, icon: <CheckCircle size={20} className="text-cyan-600" />,
description: 'Top quartile, modelo a seguir' description: t('healthStatus.excellentDesc')
}; };
} }
if (score >= 71) { if (score >= 71) {
return { return {
level: 'good', level: 'good',
label: 'BUENO', label: t('healthStatus.good'),
color: 'text-emerald-700', color: 'text-emerald-700',
textColor: 'text-emerald-700', textColor: 'text-emerald-700',
bgColor: 'bg-emerald-50', bgColor: 'bg-emerald-50',
icon: <TrendingUp size={20} className="text-emerald-600" />, icon: <TrendingUp size={20} className="text-emerald-600" />,
description: 'Por encima de benchmarks, desempeño sólido' description: t('healthStatus.goodDesc')
}; };
} }
if (score >= 51) { if (score >= 51) {
return { return {
level: 'medium', level: 'medium',
label: 'MEDIO', label: t('healthStatus.medium'),
color: 'text-amber-700', color: 'text-amber-700',
textColor: 'text-amber-700', textColor: 'text-amber-700',
bgColor: 'bg-amber-50', bgColor: 'bg-amber-50',
icon: <AlertTriangle size={20} className="text-amber-600" />, icon: <AlertTriangle size={20} className="text-amber-600" />,
description: 'Oportunidad de mejora identificada' description: t('healthStatus.mediumDesc')
}; };
} }
if (score >= 31) { if (score >= 31) {
return { return {
level: 'low', level: 'low',
label: 'BAJO', label: t('healthStatus.low'),
color: 'text-orange-700', color: 'text-orange-700',
textColor: 'text-orange-700', textColor: 'text-orange-700',
bgColor: 'bg-orange-50', bgColor: 'bg-orange-50',
icon: <AlertTriangle size={20} className="text-orange-600" />, icon: <AlertTriangle size={20} className="text-orange-600" />,
description: 'Requiere mejora, por debajo de benchmarks' description: t('healthStatus.lowDesc')
}; };
} }
return { return {
level: 'critical', level: 'critical',
label: 'CRÍTICO', label: t('healthStatus.critical'),
color: 'text-red-700', color: 'text-red-700',
textColor: 'text-red-700', textColor: 'text-red-700',
bgColor: 'bg-red-50', bgColor: 'bg-red-50',
icon: <AlertCircle size={20} className="text-red-600" />, icon: <AlertCircle size={20} className="text-red-600" />,
description: 'Requiere acción inmediata' description: t('healthStatus.criticalDesc')
}; };
}; };
@@ -79,7 +80,8 @@ const getProgressBarColor = (score: number): string => {
}; };
const ScoreIndicator: React.FC<{ score: number; benchmark?: number }> = ({ score, benchmark }) => { const ScoreIndicator: React.FC<{ score: number; benchmark?: number }> = ({ score, benchmark }) => {
const healthStatus = getHealthStatus(score); const { t } = useTranslation();
const healthStatus = getHealthStatus(score, t);
return ( return (
<div className="space-y-3"> <div className="space-y-3">
@@ -119,21 +121,21 @@ const ScoreIndicator: React.FC<{ score: number; benchmark?: number }> = ({ score
{benchmark !== undefined && ( {benchmark !== undefined && (
<div className="bg-slate-50 rounded-lg p-3 text-sm"> <div className="bg-slate-50 rounded-lg p-3 text-sm">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-slate-600">Benchmark Industria (P50)</span> <span className="text-slate-600">{t('benchmark.title')}</span>
<span className="font-bold text-slate-900">{benchmark}/100</span> <span className="font-bold text-slate-900">{benchmark}/100</span>
</div> </div>
<div className="text-xs text-slate-500"> <div className="text-xs text-slate-500">
{score > benchmark ? ( {score > benchmark ? (
<span className="text-emerald-600 font-semibold"> <span className="text-emerald-600 font-semibold">
{score - benchmark} puntos por encima del promedio {t('benchmark.aboveBenchmark', { points: score - benchmark })}
</span> </span>
) : score === benchmark ? ( ) : score === benchmark ? (
<span className="text-amber-600 font-semibold"> <span className="text-amber-600 font-semibold">
= Alineado con promedio de industria {t('benchmark.atBenchmark')}
</span> </span>
) : ( ) : (
<span className="text-orange-600 font-semibold"> <span className="text-orange-600 font-semibold">
{benchmark - score} puntos por debajo del promedio {t('benchmark.belowBenchmark', { points: benchmark - score })}
</span> </span>
)} )}
</div> </div>
@@ -154,7 +156,8 @@ const ScoreIndicator: React.FC<{ score: number; benchmark?: number }> = ({ score
}; };
const DimensionCard: React.FC<{ dimension: DimensionAnalysis }> = ({ dimension }) => { const DimensionCard: React.FC<{ dimension: DimensionAnalysis }> = ({ dimension }) => {
const healthStatus = getHealthStatus(dimension.score); const { t } = useTranslation();
const healthStatus = getHealthStatus(dimension.score, t);
return ( return (
<motion.div <motion.div
@@ -226,10 +229,10 @@ const DimensionCard: React.FC<{ dimension: DimensionAnalysis }> = ({ dimension }
> >
<Zap size={16} /> <Zap size={16} />
{dimension.score < 51 {dimension.score < 51
? 'Ver Acciones Críticas' ? t('dimensionCard.viewCriticalActions')
: dimension.score < 71 : dimension.score < 71
? 'Explorar Mejoras' ? t('dimensionCard.exploreImprovements')
: 'En buen estado'} : t('dimensionCard.inGoodState')}
</motion.button> </motion.button>
</motion.div> </motion.div>
); );

View File

@@ -1,7 +1,8 @@
import React, { Component, ErrorInfo, ReactNode } from 'react'; import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle } from 'lucide-react';
import { withTranslation, WithTranslation } from 'react-i18next';
interface Props { interface Props extends WithTranslation {
children: ReactNode; children: ReactNode;
fallback?: ReactNode; fallback?: ReactNode;
componentName?: string; componentName?: string;
@@ -45,28 +46,31 @@ class ErrorBoundary extends Component<Props, State> {
return this.props.fallback; return this.props.fallback;
} }
const { t } = this.props;
return ( return (
<div className="bg-amber-50 border-2 border-amber-200 rounded-lg p-6"> <div className="bg-amber-50 border-2 border-amber-200 rounded-lg p-6">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<AlertTriangle className="text-amber-600 flex-shrink-0 mt-1" size={24} /> <AlertTriangle className="text-amber-600 flex-shrink-0 mt-1" size={24} />
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold text-amber-900 mb-2"> <h3 className="text-lg font-semibold text-amber-900 mb-2">
{this.props.componentName ? `Error en ${this.props.componentName}` : 'Error de Renderizado'} {this.props.componentName
? t('errors.errorInComponent', { componentName: this.props.componentName })
: t('errors.renderError')}
</h3> </h3>
<p className="text-amber-800 mb-3"> <p className="text-amber-800 mb-3">
Este componente encontró un error y no pudo renderizarse correctamente. {t('errors.componentError')}
El resto del dashboard sigue funcionando normalmente.
</p> </p>
<details className="text-sm"> <details className="text-sm">
<summary className="cursor-pointer text-amber-700 font-medium mb-2"> <summary className="cursor-pointer text-amber-700 font-medium mb-2">
Ver detalles técnicos {t('errors.viewTechnicalDetails')}
</summary> </summary>
<div className="bg-white rounded p-3 mt-2 font-mono text-xs overflow-auto max-h-40"> <div className="bg-white rounded p-3 mt-2 font-mono text-xs overflow-auto max-h-40">
<p className="text-red-600 font-semibold mb-1">Error:</p> <p className="text-red-600 font-semibold mb-1">{t('errors.errorLabel')}</p>
<p className="text-slate-700 mb-3">{this.state.error?.toString()}</p> <p className="text-slate-700 mb-3">{this.state.error?.toString()}</p>
{this.state.errorInfo && ( {this.state.errorInfo && (
<> <>
<p className="text-red-600 font-semibold mb-1">Stack:</p> <p className="text-red-600 font-semibold mb-1">{t('errors.stackLabel')}</p>
<pre className="text-slate-600 whitespace-pre-wrap"> <pre className="text-slate-600 whitespace-pre-wrap">
{this.state.errorInfo.componentStack} {this.state.errorInfo.componentStack}
</pre> </pre>
@@ -78,7 +82,7 @@ class ErrorBoundary extends Component<Props, State> {
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-amber-600 text-white rounded hover:bg-amber-700 transition-colors" className="mt-4 px-4 py-2 bg-amber-600 text-white rounded hover:bg-amber-700 transition-colors"
> >
Recargar Página {t('dashboard.reloadPage')}
</button> </button>
</div> </div>
</div> </div>
@@ -90,4 +94,4 @@ class ErrorBoundary extends Component<Props, State> {
} }
} }
export default ErrorBoundary; export default withTranslation()(ErrorBoundary);

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Check, Package, Upload, BarChart3 } from 'lucide-react'; import { Check, Package, Upload, BarChart3 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'; import clsx from 'clsx';
interface Step { interface Step {
@@ -13,13 +14,15 @@ interface ProgressStepperProps {
currentStep: number; currentStep: number;
} }
const steps: Step[] = [
{ id: 1, label: 'Seleccionar Tier', icon: Package },
{ id: 2, label: 'Subir Datos', icon: Upload },
{ id: 3, label: 'Ver Resultados', icon: BarChart3 },
];
const ProgressStepper: React.FC<ProgressStepperProps> = ({ currentStep }) => { const ProgressStepper: React.FC<ProgressStepperProps> = ({ currentStep }) => {
const { t } = useTranslation();
const steps: Step[] = [
{ id: 1, label: t('stepper.selectTier'), icon: Package },
{ id: 2, label: t('stepper.uploadData'), icon: Upload },
{ id: 3, label: t('stepper.viewResults'), icon: BarChart3 },
];
return ( return (
<div className="w-full max-w-3xl mx-auto mb-8"> <div className="w-full max-w-3xl mx-auto mb-8">
<div className="relative flex items-center justify-between"> <div className="relative flex items-center justify-between">

View File

@@ -131,6 +131,7 @@
}, },
"dashboard": { "dashboard": {
"title": "Diagnostic Dashboard", "title": "Diagnostic Dashboard",
"defaultTitle": "DEMO CLIENT - Beyond CX Analytics",
"viewDashboard": "View Diagnostic Dashboard", "viewDashboard": "View Diagnostic Dashboard",
"generateAnalysis": "Generate Analysis", "generateAnalysis": "Generate Analysis",
"analyzing": "Analyzing...", "analyzing": "Analyzing...",

View File

@@ -131,6 +131,7 @@
}, },
"dashboard": { "dashboard": {
"title": "Dashboard de Diagnóstico", "title": "Dashboard de Diagnóstico",
"defaultTitle": "CLIENTE DEMO - Beyond CX Analytics",
"viewDashboard": "Ver Dashboard de Diagnóstico", "viewDashboard": "Ver Dashboard de Diagnóstico",
"generateAnalysis": "Generar Análisis", "generateAnalysis": "Generar Análisis",
"analyzing": "Analizando...", "analyzing": "Analizando...",