feat: complete OpportunityPrioritizer translation to English

- Add 40+ new translation keys for all remaining Spanish text
- Update TIER_CONFIG to use translation keys for labels and descriptions
- Translate dynamically generated whyPrioritized and nextSteps content
- Convert all hardcoded Spanish strings to i18n translation calls
- Fix malformed t() call on line 402
- Add proper translations for error messages, buttons, and methodology

All OpportunityPrioritizer content is now fully bilingual (en/es)

https://claude.ai/code/session_01GNbnkFoESkRcnPr3bLCYDg
This commit is contained in:
Claude
2026-02-08 14:22:00 +00:00
parent 38df9d6071
commit 3a6652fdce
3 changed files with 122 additions and 62 deletions

View File

@@ -58,56 +58,56 @@ interface EnrichedOpportunity extends Opportunity {
annualCost?: number;
}
// Tier configuration
// Tier configuration - labels and descriptions will be translated at usage time
const TIER_CONFIG: Record<AgenticTier, {
icon: React.ReactNode;
label: string;
labelKey: string;
color: string;
bgColor: string;
borderColor: string;
savingsRate: string;
timeline: string;
description: string;
timelineKey: string;
descriptionKey: string;
}> = {
'AUTOMATE': {
icon: <Bot size={18} />,
label: 'Automatizar',
labelKey: 'opportunityPrioritizer.tierLabels.automate',
color: 'text-emerald-700',
bgColor: 'bg-emerald-50',
borderColor: 'border-emerald-300',
savingsRate: '70%',
timeline: '3-6 meses',
description: 'Automatización completa con agentes IA'
timelineKey: 'opportunityPrioritizer.timelines.automate',
descriptionKey: 'opportunityPrioritizer.tierDescriptions.automate'
},
'ASSIST': {
icon: <Headphones size={18} />,
label: 'Asistir',
labelKey: 'opportunityPrioritizer.tierLabels.assist',
color: 'text-blue-700',
bgColor: 'bg-blue-50',
borderColor: 'border-blue-300',
savingsRate: '30%',
timeline: '6-9 meses',
description: 'Copilot IA para agentes humanos'
timelineKey: 'opportunityPrioritizer.timelines.assist',
descriptionKey: 'opportunityPrioritizer.tierDescriptions.assist'
},
'AUGMENT': {
icon: <BookOpen size={18} />,
label: 'Optimizar',
labelKey: 'opportunityPrioritizer.tierLabels.augment',
color: 'text-amber-700',
bgColor: 'bg-amber-50',
borderColor: 'border-amber-300',
savingsRate: '15%',
timeline: '9-12 meses',
description: 'Estandarización y mejora de procesos'
timelineKey: 'opportunityPrioritizer.timelines.augment',
descriptionKey: 'opportunityPrioritizer.tierDescriptions.augment'
},
'HUMAN-ONLY': {
icon: <Users size={18} />,
label: 'Humano',
labelKey: 'opportunityPrioritizer.tierLabels.human',
color: 'text-slate-600',
bgColor: 'bg-slate-50',
borderColor: 'border-slate-300',
savingsRate: '0%',
timeline: 'N/A',
description: 'Requiere intervención humana'
timelineKey: 'N/A',
descriptionKey: 'opportunityPrioritizer.tierDescriptions.humanOnly'
}
};
@@ -177,29 +177,23 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
// Timeline based on tier
const timelineMonths = tier === 'AUTOMATE' ? 4 : tier === 'ASSIST' ? 7 : 10;
// Generate "why" explanation
const whyPrioritized: string[] = [];
if (opp.savings > 50000) whyPrioritized.push(`Alto ahorro potencial (€${(opp.savings / 1000).toFixed(0)}K/año)`);
if (lookupData?.volume && lookupData.volume > 1000) whyPrioritized.push(`Alto volumen (${lookupData.volume.toLocaleString()} interacciones)`);
if (tier === 'AUTOMATE') whyPrioritized.push('Proceso altamente predecible y repetitivo');
if (cv < 60) whyPrioritized.push('Baja variabilidad en tiempos de gestión');
if (transfer < 15) whyPrioritized.push('Baja tasa de transferencias');
if (opp.feasibility >= 7) whyPrioritized.push('Alta factibilidad técnica');
// Generate "why" explanation - store keys for translation
const whyPrioritized: { key: string; params?: any }[] = [];
if (opp.savings > 50000) whyPrioritized.push({ key: 'reasons.highSavingsPotential', params: { amount: (opp.savings / 1000).toFixed(0) } });
if (lookupData?.volume && lookupData.volume > 1000) whyPrioritized.push({ key: 'reasons.highVolume', params: { volume: lookupData.volume.toLocaleString() } });
if (tier === 'AUTOMATE') whyPrioritized.push({ key: 'reasons.highlyPredictable' });
if (cv < 60) whyPrioritized.push({ key: 'reasons.lowVariability' });
if (transfer < 15) whyPrioritized.push({ key: 'reasons.lowTransferRate' });
if (opp.feasibility >= 7) whyPrioritized.push({ key: 'reasons.highFeasibility' });
// Generate next steps
// Generate next steps - store keys for translation
const nextSteps: string[] = [];
if (tier === 'AUTOMATE') {
nextSteps.push('Definir flujos conversacionales principales');
nextSteps.push('Identificar integraciones necesarias (CRM, APIs)');
nextSteps.push('Crear piloto con 10% del volumen');
nextSteps.push('steps.automate1', 'steps.automate2', 'steps.automate3');
} else if (tier === 'ASSIST') {
nextSteps.push('Mapear puntos de fricción del agente');
nextSteps.push('Diseñar sugerencias contextuales');
nextSteps.push('Piloto con equipo seleccionado');
nextSteps.push('steps.assist1', 'steps.assist2', 'steps.assist3');
} else {
nextSteps.push('Analizar causa raíz de variabilidad');
nextSteps.push('Estandarizar procesos y scripts');
nextSteps.push('Capacitar equipo en mejores prácticas');
nextSteps.push('steps.augment1', 'steps.augment2', 'steps.augment3');
}
return {
@@ -250,8 +244,8 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
return (
<div className="bg-white p-8 rounded-xl border border-slate-200 text-center">
<AlertTriangle className="mx-auto mb-4 text-amber-500" size={48} />
<h3 className="text-lg font-semibold text-slate-700">No hay oportunidades identificadas</h3>
<p className="text-slate-500 mt-2">Los datos actuales no muestran oportunidades de automatización viables.</p>
<h3 className="text-lg font-semibold text-slate-700">{t('opportunityPrioritizer.noOpportunitiesTitle')}</h3>
<p className="text-slate-500 mt-2">{t('opportunityPrioritizer.noOpportunitiesDescription')}</p>
</div>
);
}
@@ -292,7 +286,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
{summary.byTier.AUTOMATE.length}
</div>
<div className="text-xs text-emerald-600">
{(summary.byTier.AUTOMATE.reduce((s, o) => s + o.savings, 0) / 1000).toFixed(0)}K {t('opportunityPrioritizer.inMonths', { count: '3-6' })}
{(summary.byTier.AUTOMATE.reduce((s, o) => s + o.savings, 0) / 1000).toFixed(0)}K {t('opportunityPrioritizer.inMonths', { months: '3-6' })}
</div>
</div>
@@ -305,7 +299,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
{summary.byTier.ASSIST.length}
</div>
<div className="text-xs text-blue-600">
{(summary.byTier.ASSIST.reduce((s, o) => s + o.savings, 0) / 1000).toFixed(0)}K {t('opportunityPrioritizer.inMonths', { count: '6-9' })}
{(summary.byTier.ASSIST.reduce((s, o) => s + o.savings, 0) / 1000).toFixed(0)}K {t('opportunityPrioritizer.inMonths', { months: '6-9' })}
</div>
</div>
@@ -318,7 +312,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
{summary.byTier.AUGMENT.length}
</div>
<div className="text-xs text-amber-600">
{(summary.byTier.AUGMENT.reduce((s, o) => s + o.savings, 0) / 1000).toFixed(0)}K {t('opportunityPrioritizer.inMonths', { count: '9-12' })}
{(summary.byTier.AUGMENT.reduce((s, o) => s + o.savings, 0) / 1000).toFixed(0)}K {t('opportunityPrioritizer.inMonths', { months: '9-12' })}
</div>
</div>
</div>
@@ -345,7 +339,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
{topOpportunity.name.replace(/^[^\w\s]+\s*/, '')}
</h3>
<span className={`text-sm font-medium ${TIER_CONFIG[topOpportunity.tier].color}`}>
{t(`opportunityPrioritizer.tierLabels.${topOpportunity.tier.toLowerCase()}`)} {TIER_CONFIG[topOpportunity.tier].description}
{t(TIER_CONFIG[topOpportunity.tier].labelKey)} {t(TIER_CONFIG[topOpportunity.tier].descriptionKey)}
</span>
</div>
</div>
@@ -371,7 +365,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-xs text-slate-500 mb-1">Agentic Score</div>
<div className="text-xs text-slate-500 mb-1">{t('opportunityPrioritizer.agenticScore')}</div>
<div className="text-xl font-bold text-slate-700">
{topOpportunity.agenticScore.toFixed(1)}/10
</div>
@@ -382,13 +376,13 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
<div className="mb-4">
<h4 className="text-sm font-semibold text-slate-700 mb-2 flex items-center gap-2">
<Info size={14} />
¿Por qué es la prioridad #1?
{t('opportunityPrioritizer.whyPriority1')}
</h4>
<ul className="space-y-1">
{topOpportunity.whyPrioritized.slice(0, 4).map((reason, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-slate-600">
<CheckCircle2 size={14} className="text-emerald-500 flex-shrink-0" />
{reason}
{t(`opportunityPrioritizer.${reason.key}`, reason.params)}
</li>
))}
</ul>
@@ -399,7 +393,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
<div className="lg:w-80 bg-emerald-50 rounded-lg p-4 border border-emerald-200">
<h4 className="text-sm font-semibold text-emerald-800 mb-3 flex items-center gap-2">
<ArrowRight size={14} />
t("opportunityPrioritizer.nextSteps")
{t('opportunityPrioritizer.nextSteps')}
</h4>
<ol className="space-y-2">
{topOpportunity.nextSteps.map((step, i) => (
@@ -407,12 +401,12 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
<span className="bg-emerald-600 text-white w-5 h-5 rounded-full flex items-center justify-center text-xs flex-shrink-0 mt-0.5">
{i + 1}
</span>
{step}
{t(`opportunityPrioritizer.${step}`)}
</li>
))}
</ol>
<button className="mt-4 w-full bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4 rounded-lg transition-colors flex items-center justify-center gap-2">
Ver Detalle Completo
{t('opportunityPrioritizer.viewCompleteDetail')}
<ChevronRight size={16} />
</button>
</div>
@@ -462,7 +456,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
{opp.name.replace(/^[^\w\s]+\s*/, '')}
</h4>
<span className={`text-xs ${TIER_CONFIG[opp.tier].color}`}>
{t(`opportunityPrioritizer.tierLabels.${opp.tier.toLowerCase()}`)} {t(`opportunityPrioritizer.timelines.${opp.tier.toLowerCase()}`)}
{t(TIER_CONFIG[opp.tier].labelKey)} {t(TIER_CONFIG[opp.tier].timelineKey)}
</span>
</div>
@@ -496,8 +490,8 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
/>
</div>
<div className="flex justify-between text-[10px] text-slate-400 mt-0.5">
<span>Valor</span>
<span>Esfuerzo</span>
<span>{t('opportunityPrioritizer.value')}</span>
<span>{t('opportunityPrioritizer.effort')}</span>
</div>
</div>
@@ -525,12 +519,12 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Why prioritized */}
<div>
<h5 className="text-sm font-semibold text-slate-700 mb-2">¿Por qué esta posición?</h5>
<h5 className="text-sm font-semibold text-slate-700 mb-2">{t('opportunityPrioritizer.whyThisPosition')}</h5>
<ul className="space-y-1">
{opp.whyPrioritized.map((reason, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-slate-600">
<CheckCircle2 size={12} className="text-emerald-500 flex-shrink-0" />
{reason}
{t(`opportunityPrioritizer.${reason.key}`, reason.params)}
</li>
))}
</ul>
@@ -538,7 +532,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
{/* Metrics */}
<div>
<h5 className="text-sm font-semibold text-slate-700 mb-2">Métricas Clave</h5>
<h5 className="text-sm font-semibold text-slate-700 mb-2">{t('opportunityPrioritizer.keyMetrics')}</h5>
<div className="grid grid-cols-2 gap-2">
<div className="bg-white rounded p-2 border border-slate-200">
<div className="text-xs text-slate-500">CV AHT</div>
@@ -553,12 +547,12 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
<div className="font-semibold text-slate-700">{opp.fcr_rate.toFixed(1)}%</div>
</div>
<div className="bg-white rounded p-2 border border-slate-200">
<div className="text-xs text-slate-500">Riesgo</div>
<div className="text-xs text-slate-500">{t('roadmap.risk')}</div>
<div className={`font-semibold ${
opp.riskLevel === 'low' ? 'text-emerald-600' :
opp.riskLevel === 'medium' ? 'text-amber-600' : 'text-red-600'
}`}>
{opp.riskLevel === 'low' ? 'Bajo' : opp.riskLevel === 'medium' ? 'Medio' : 'Alto'}
{t(`roadmap.risk${opp.riskLevel.charAt(0).toUpperCase() + opp.riskLevel.slice(1)}`)}
</div>
</div>
</div>
@@ -571,7 +565,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
<div className="flex flex-wrap gap-2">
{opp.nextSteps.map((step, i) => (
<span key={i} className="bg-white border border-slate-200 rounded-full px-3 py-1 text-xs text-slate-600">
{i + 1}. {step}
{i + 1}. {t(`opportunityPrioritizer.${step}`)}
</span>
))}
</div>
@@ -593,12 +587,12 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
{showAllOpportunities ? (
<>
<ChevronDown size={16} className="rotate-180" />
Mostrar menos
{t('opportunityPrioritizer.showLess')}
</>
) : (
<>
<ChevronDown size={16} />
Ver {enrichedOpportunities.length - 5} oportunidades más
{t('opportunityPrioritizer.viewMore', { count: enrichedOpportunities.length - 5 })}
</>
)}
</button>
@@ -611,9 +605,7 @@ const OpportunityPrioritizer: React.FC<OpportunityPrioritizerProps> = ({
<div className="flex items-start gap-2">
<Info size={14} className="flex-shrink-0 mt-0.5" />
<div>
<strong>{t('opportunityPrioritizer.methodology')}</strong> Las oportunidades se ordenan por potencial de ahorro TCO (volumen × tasa de contención × diferencial CPI).
La clasificación de tier (AUTOMATE/ASSIST/AUGMENT) se basa en el Agentic Readiness Score considerando predictibilidad (CV AHT),
resolutividad (FCR + Transfer), volumen, calidad de datos y simplicidad del proceso.
<strong>{t('opportunityPrioritizer.methodology')}</strong> {t('opportunityPrioritizer.methodologyDescription')}
</div>
</div>
</div>