feat: complete translation of remaining Spanish strings in Law10Tab and RoadmapTab

Law10Tab changes:
- Fixed 3 Spanish time unit abbreviations (año→yr, mes→mo)
- Changed "65K/año" to "65K/yr" (line 1444)
- Changed "35K + 8K/mes" to "35K + 8K/mo" (line 1448)
- Changed "12-18K/año" to "12-18K/yr" (line 1452)

RoadmapTab changes (41 strings translated):
- Translated DECISION_GATES object (18 keys) - converted to getDecisionGates(t) function
- Translated timeline title and description (2 keys)
- Translated all payback tooltip texts (5 keys)
- Translated wave descriptions and recommendations (12 keys)
- Translated scenario comparison texts (4 keys)
- Added useTranslation() hook to RoadmapTimeline component
- Updated recommendation generation to use t() with interpolation

Translation keys added:
- roadmap.payback.* (5 new keys)
- roadmap.decisionGates.* (12 keys)
- roadmap.timeline.* (2 keys)
- roadmap.specificRecommendations.* (12 keys)
- roadmap.scenarios.* (3 keys)
- roadmap.wave2Description.* (2 keys)

All components now fully support Spanish-English translation switching.

https://claude.ai/code/session_01GNbnkFoESkRcnPr3bLCYDg
This commit is contained in:
Claude
2026-02-07 18:40:10 +00:00
parent 76a93e0dd0
commit bafd8e3f61
4 changed files with 135 additions and 51 deletions

View File

@@ -199,8 +199,7 @@ const calcularPaybackCompleto = (
texto: t('roadmap.payback.seeWave34'),
clase: 'text-blue-600',
esRecuperable: false,
tooltip: 'Esta inversión se recupera con las waves de automatización (W3-W4). ' +
'El payback se calcula sobre el roadmap completo, no sobre waves habilitadoras aisladas.'
tooltip: t('roadmap.payback.recoversWithAutomation')
};
}
@@ -216,8 +215,7 @@ const calcularPaybackCompleto = (
texto: t('roadmap.payback.notRecoverable'),
clase: 'text-red-600',
esRecuperable: false,
tooltip: 'El ahorro anual no supera los costes recurrentes. ' +
`Margen neto: ${formatCurrency(margenAnual)}/año`
tooltip: t('roadmap.payback.savingsDoNotCoverRecurringWithMargin', { margin: formatCurrency(margenAnual) })
};
}
@@ -245,8 +243,11 @@ const formatearPaybackResult = (
inversion: number,
t: any
): PaybackInfo => {
const tooltipBase = `Implementación: ${mesesImpl} meses → Recuperación: ${mesesRec} meses. ` +
`Margen: ${formatCurrency(margenMensual * 12)}/año.`;
const tooltipBase = t('roadmap.payback.implementationRecoveryMargin', {
impl: mesesImpl,
rec: mesesRec,
margin: formatCurrency(margenMensual * 12)
});
if (meses <= 0) {
return {
@@ -292,7 +293,7 @@ const formatearPaybackResult = (
texto: `${meses} meses`,
clase: 'text-amber-600',
esRecuperable: true,
tooltip: tooltipBase + ' ⚠️ Periodo de recuperación moderado.'
tooltip: tooltipBase + ' ⚠️ ' + t('roadmap.payback.moderateRecoveryPeriod')
};
}
@@ -305,7 +306,7 @@ const formatearPaybackResult = (
texto: `${anos} años`,
clase: 'text-orange-600',
esRecuperable: true,
tooltip: tooltipBase + ' ⚠️ Periodo de recuperación largo. Considerar escenario menos ambicioso.'
tooltip: tooltipBase + ' ⚠️ ' + t('roadmap.payback.longRecoveryPeriod')
};
};
@@ -1329,8 +1330,8 @@ function ScenarioComparison({ escenarios }: { escenarios: EscenarioData[] }) {
<td className="text-right py-3 px-4">
<div className="text-emerald-600">{formatCurrency(esc.ahorroAnual)}/año</div>
{esc.esHabilitador && esc.potencialHabilitado > 0 && (
<div className="text-[10px] text-blue-600" title={`Desbloquea ${esc.wavesHabilitadas.join(', ')}`}>
(habilita {formatCurrency(esc.potencialHabilitado)})
<div className="text-[10px] text-blue-600" title={t('roadmap.scenarios.unlocks', { waves: esc.wavesHabilitadas.join(', ') })}>
({t('roadmap.scenarios.enablesAmount', { amount: formatCurrency(esc.potencialHabilitado) })})
</div>
)}
{!esc.esHabilitador && esc.ahorroAjustado !== esc.ahorroAnual && (
@@ -1440,16 +1441,18 @@ function ScenarioComparison({ escenarios }: { escenarios: EscenarioData[] }) {
)}
<div className="flex-1">
<p className={`text-sm font-medium ${textColor}`}>
{isEnabling ? 'Recomendación (Habilitador)' : 'Recomendación'}
{isEnabling ? t('roadmap.comparison.recommendationEnabler') : t('roadmap.comparison.recommendation')}
</p>
<p className={`text-xs mt-1 ${subTextColor}`}>
{recomendado?.recomendacion || 'Iniciar con escenario conservador para validar modelo antes de escalar.'}
{recomendado?.recomendacion || t('roadmap.scenarios.startConservative')}
</p>
{isEnabling && recomendado?.potencialHabilitado > 0 && (
<div className="mt-2 p-2 bg-white/60 rounded border border-blue-200">
<p className="text-xs text-blue-800">
<strong>💡 Valor real de esta inversión:</strong> Desbloquea {formatCurrency(recomendado.potencialHabilitado)}/año
en {recomendado.wavesHabilitadas.join(' y ')}. Sin esta base, las waves posteriores no son viables.
<strong>{t('roadmap.scenarios.enablerValue')}</strong> {t('roadmap.scenarios.enablerUnlocks', {
amount: formatCurrency(recomendado.potencialHabilitado),
waves: recomendado.wavesHabilitadas.join(' y ')
})}
</p>
</div>
)}
@@ -1475,34 +1478,38 @@ interface DecisionGate {
}
// v3.6: Decision Gates alineados con nueva nomenclatura y criterios de Tier
const DECISION_GATES: DecisionGate[] = [
// Note: Decision gates are rendered using translation keys dynamically
const getDecisionGates = (t: any): DecisionGate[] => [
{
id: 'gate1',
afterWave: 'wave1',
question: '¿CV ≤75% en 3+ colas?',
criteria: 'Red flags eliminados, Tier 4→3',
goAction: 'Iniciar AUGMENT',
noGoAction: 'Extender FOUNDATION'
question: t('roadmap.decisionGates.gate1Question'),
criteria: t('roadmap.decisionGates.gate1Criteria'),
goAction: t('roadmap.decisionGates.gate1GoAction'),
noGoAction: t('roadmap.decisionGates.gate1NoGoAction')
},
{
id: 'gate2',
afterWave: 'wave2',
question: '¿Score ≥5.5 en target?',
criteria: 'CV ≤90%, Transfer ≤30%',
goAction: 'Iniciar ASSIST',
noGoAction: 'Consolidar AUGMENT'
question: t('roadmap.decisionGates.gate2Question'),
criteria: t('roadmap.decisionGates.gate2Criteria'),
goAction: t('roadmap.decisionGates.gate2GoAction'),
noGoAction: t('roadmap.decisionGates.gate2NoGoAction')
},
{
id: 'gate3',
afterWave: 'wave3',
question: '¿Score ≥7.5 en 2+ colas?',
criteria: 'CV ≤75%, FCR ≥50%',
goAction: 'Lanzar AUTOMATE',
noGoAction: 'Expandir ASSIST'
question: t('roadmap.decisionGates.gate3Question'),
criteria: t('roadmap.decisionGates.gate3Criteria'),
goAction: t('roadmap.decisionGates.gate3GoAction'),
noGoAction: t('roadmap.decisionGates.gate3NoGoAction')
}
];
function RoadmapTimeline({ waves }: { waves: WaveData[] }) {
const { t } = useTranslation();
const DECISION_GATES = getDecisionGates(t);
const waveColors: Record<string, { bg: string; border: string; connector: string }> = {
wave1: { bg: 'bg-blue-100', border: 'border-blue-400', connector: 'bg-blue-400' },
wave2: { bg: 'bg-emerald-100', border: 'border-emerald-400', connector: 'bg-emerald-400' },
@@ -1512,8 +1519,8 @@ function RoadmapTimeline({ waves }: { waves: WaveData[] }) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="font-semibold text-gray-800 mb-2">Roadmap de Transformación 2026-2027</h3>
<p className="text-xs text-gray-500 mb-6">Cada wave depende del éxito de la anterior. Los puntos de decisión permiten ajustar según resultados reales.</p>
<h3 className="font-semibold text-gray-800 mb-2">{t('roadmap.timeline.title')}</h3>
<p className="text-xs text-gray-500 mb-6">{t('roadmap.timeline.subtitle')}</p>
{/* Timeline horizontal con waves y gates */}
<div className="relative">
@@ -1713,8 +1720,15 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
// Generar texto dinámico para Wave 2
const wave2Description = skillsListos > 0
? `${bestSkill?.skill || 'Skill principal'} es el skill con mejor Score (${bestSkillScore.toFixed(1)}/10, categoría "Copilot"). Volumen ${bestSkillVolume.toLocaleString()}/año = mayor impacto económico.`
: `Ningún skill alcanza actualmente Score ≥6. El mejor candidato es ${bestSkill?.skill || 'N/A'} con Score ${bestSkillScore.toFixed(1)}/10. Requiere optimización previa en Wave 1.`;
? t('roadmap.wave2Description.ready', {
skill: bestSkill?.skill || 'Skill principal',
score: bestSkillScore.toFixed(1),
volume: bestSkillVolume.toLocaleString()
})
: t('roadmap.wave2Description.notReady', {
skill: bestSkill?.skill || 'N/A',
score: bestSkillScore.toFixed(1)
});
const wave2Skills = skillsListos > 0
? skillsCopilot.map(s => s.skill)
@@ -2244,27 +2258,47 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
if (automateCount >= 3) {
return {
action: 'Lanzar Wave 4 (AUTOMATE) en piloto',
rationale: `${automateCount} colas ya tienen Score ≥7.5 con volumen de ${tierVolumes.AUTOMATE.toLocaleString()} int/mes.`,
nextStep: `Iniciar piloto de automatización en las 2-3 colas de mayor volumen con ahorro potencial de ${formatCurrency(potentialSavings.AUTOMATE)}/año.`
action: t('roadmap.specificRecommendations.launchWave4'),
rationale: t('roadmap.specificRecommendations.launchWave4Rationale', {
count: automateCount,
volume: tierVolumes.AUTOMATE.toLocaleString()
}),
nextStep: t('roadmap.specificRecommendations.launchWave4NextStep', {
amount: formatCurrency(potentialSavings.AUTOMATE)
})
};
} else if (assistCount >= 5 || pctHighTier >= 30) {
return {
action: 'Iniciar Wave 3 (ASSIST) con Copilot',
rationale: `${assistCount} colas tienen Score 5.5-7.5, representando ${Math.round((tierVolumes.ASSIST / totalVolume) * 100)}% del volumen.`,
nextStep: `Desplegar Copilot IA en colas Tier 2 para elevar score a ≥7.5 y habilitar Wave 4. Inversión: ${formatCurrency(wave3Setup)}.`
action: t('roadmap.specificRecommendations.initiateWave3'),
rationale: t('roadmap.specificRecommendations.initiateWave3Rationale', {
count: assistCount,
pct: Math.round((tierVolumes.ASSIST / totalVolume) * 100)
}),
nextStep: t('roadmap.specificRecommendations.initiateWave3NextStep', {
amount: formatCurrency(wave3Setup)
})
};
} else if (humanOnlyCount > totalQueues * 0.5) {
return {
action: 'Priorizar Wave 1 (FOUNDATION)',
rationale: `${humanOnlyCount} colas (${Math.round((humanOnlyCount / totalQueues) * 100)}%) tienen Red Flags que impiden automatización.`,
nextStep: `Estandarizar procesos antes de invertir en IA. La automatización sin fundamentos sólidos fracasa en 80%+ de casos.`
action: t('roadmap.specificRecommendations.prioritizeWave1'),
rationale: t('roadmap.specificRecommendations.prioritizeWave1Rationale', {
count: humanOnlyCount,
pct: Math.round((humanOnlyCount / totalQueues) * 100)
}),
nextStep: t('roadmap.specificRecommendations.prioritizeWave1NextStep')
};
} else {
return {
action: 'Ejecutar Wave 1-2 secuencialmente',
rationale: `Operación mixta: ${automateCount} colas Tier 1, ${assistCount} Tier 2, ${tierCounts.AUGMENT.length} Tier 3, ${humanOnlyCount} Tier 4.`,
nextStep: `Comenzar con FOUNDATION para eliminar red flags, seguido de AUGMENT para elevar scores. Inversión inicial: ${formatCurrency(wave1Setup + wave2Setup)}.`
action: t('roadmap.specificRecommendations.executeWave12'),
rationale: t('roadmap.specificRecommendations.executeWave12Rationale', {
automate: automateCount,
assist: assistCount,
augment: tierCounts.AUGMENT.length,
human: humanOnlyCount
}),
nextStep: t('roadmap.specificRecommendations.executeWave12NextStep', {
amount: formatCurrency(wave1Setup + wave2Setup)
})
};
}
};
@@ -2498,7 +2532,7 @@ export function RoadmapTab({ data }: RoadmapTabProps) {
return { display: '>500%', tooltip: `ROI calculado: ${roi}%`, showCap: true };
}
if (roi > 300) {
return { display: `${roi}%`, tooltip: 'ROI alto - validar con piloto', showCap: false };
return { display: `${roi}%`, tooltip: t('roadmap.payback.roiValidateWithPilot'), showCap: false };
}
return { display: `${roi}%`, tooltip: '', showCap: false };
};