Fix translation namespace issues in dashboard components

Fixed translation keys not being resolved properly in the Spanish dashboard by:

1. HealthStatusCard:
   - Added support for multiple namespaces (dashboard, reasoning, production)
   - Created translateKey helper function to map backend keys to correct i18next namespaces
   - Fixed translation of health.headline_yellow_approvals and other dynamic keys

2. ActionQueueCard:
   - Added translateKey helper to properly handle reasoning.types.* keys
   - Fixed namespace resolution for purchase order reasoning and consequences
   - Updated all translation calls to use proper namespace mapping

3. ProductionTimelineCard:
   - Added 'production' namespace to support production.status.pending translations
   - Fixed status translation to detect and use correct namespace

All untranslated keys like "health.headline_yellow_approvals",
"reasoning.types.low_stock_detection", "reasoning.types.forecast_demand",
and "production.status.pending" should now display correctly in Spanish.
This commit is contained in:
Claude
2025-11-20 18:43:34 +00:00
parent 6648c5a799
commit 925dbfb47c
3 changed files with 86 additions and 18 deletions

View File

@@ -67,6 +67,30 @@ const urgencyConfig = {
}, },
}; };
/**
* Helper function to translate keys with proper namespace handling
* Maps backend key formats to correct i18next namespaces
*/
function translateKey(
key: string,
params: Record<string, any>,
tDashboard: any,
tReasoning: any
): string {
// Determine namespace based on key prefix
if (key.startsWith('reasoning.')) {
// Remove 'reasoning.' prefix and use reasoning namespace
const translationKey = key.substring('reasoning.'.length);
return tReasoning(translationKey, { ...params, defaultValue: key });
} else if (key.startsWith('action_queue.') || key.startsWith('dashboard.') || key.startsWith('health.')) {
// Use dashboard namespace
return tDashboard(key, { ...params, defaultValue: key });
}
// Default to dashboard
return tDashboard(key, { ...params, defaultValue: key });
}
function ActionItemCard({ function ActionItemCard({
action, action,
onApprove, onApprove,
@@ -102,25 +126,25 @@ function ActionItemCard({
// Translate i18n fields (or fallback to deprecated text fields or reasoning_data for alerts) // Translate i18n fields (or fallback to deprecated text fields or reasoning_data for alerts)
const reasoning = useMemo(() => { const reasoning = useMemo(() => {
if (action.reasoning_i18n) { if (action.reasoning_i18n) {
return tDashboard(action.reasoning_i18n.key, action.reasoning_i18n.params); return translateKey(action.reasoning_i18n.key, action.reasoning_i18n.params, tDashboard, tReasoning);
} }
if (action.reasoning_data) { if (action.reasoning_data) {
const formatted = formatPOAction(action.reasoning_data); const formatted = formatPOAction(action.reasoning_data);
return formatted.reasoning; return formatted.reasoning;
} }
return action.reasoning || ''; return action.reasoning || '';
}, [action.reasoning_i18n, action.reasoning_data, action.reasoning, tDashboard, formatPOAction]); }, [action.reasoning_i18n, action.reasoning_data, action.reasoning, tDashboard, tReasoning, formatPOAction]);
const consequence = useMemo(() => { const consequence = useMemo(() => {
if (action.consequence_i18n) { if (action.consequence_i18n) {
return tDashboard(action.consequence_i18n.key, action.consequence_i18n.params); return translateKey(action.consequence_i18n.key, action.consequence_i18n.params, tDashboard, tReasoning);
} }
if (action.reasoning_data) { if (action.reasoning_data) {
const formatted = formatPOAction(action.reasoning_data); const formatted = formatPOAction(action.reasoning_data);
return formatted.consequence; return formatted.consequence;
} }
return ''; return '';
}, [action.consequence_i18n, action.reasoning_data, tDashboard, formatPOAction]); }, [action.consequence_i18n, action.reasoning_data, tDashboard, tReasoning, formatPOAction]);
return ( return (
<div <div
@@ -136,7 +160,7 @@ function ActionItemCard({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1"> <div className="flex items-start justify-between gap-2 mb-1">
<h3 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}> <h3 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}>
{action.title_i18n ? tDashboard(action.title_i18n.key, action.title_i18n.params) : (action.title || 'Action Required')} {action.title_i18n ? translateKey(action.title_i18n.key, action.title_i18n.params, tDashboard, tReasoning) : (action.title || 'Action Required')}
</h3> </h3>
<span <span
className="px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0" className="px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0"
@@ -149,7 +173,7 @@ function ActionItemCard({
</span> </span>
</div> </div>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}> <p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{action.subtitle_i18n ? tDashboard(action.subtitle_i18n.key, action.subtitle_i18n.params) : (action.subtitle || '')} {action.subtitle_i18n ? translateKey(action.subtitle_i18n.key, action.subtitle_i18n.params, tDashboard, tReasoning) : (action.subtitle || '')}
</p> </p>
</div> </div>
</div> </div>
@@ -493,7 +517,7 @@ function ActionItemCard({
{button.action === 'reject' && <X className="w-4 h-4" />} {button.action === 'reject' && <X className="w-4 h-4" />}
{button.action === 'view_details' && <Eye className="w-4 h-4" />} {button.action === 'view_details' && <Eye className="w-4 h-4" />}
{button.action === 'modify' && <Edit className="w-4 h-4" />} {button.action === 'modify' && <Edit className="w-4 h-4" />}
{tDashboard(button.label_i18n.key, button.label_i18n.params)} {translateKey(button.label_i18n.key, button.label_i18n.params, tDashboard, tReasoning)}
</button> </button>
); );
})} })}

View File

@@ -50,8 +50,46 @@ const iconMap = {
alert: AlertCircle, alert: AlertCircle,
}; };
/**
* Helper function to translate keys with proper namespace handling
* Maps backend key formats to correct i18next namespaces
*/
function translateKey(
key: string,
params: Record<string, any>,
t: any
): string {
// Map key prefixes to their correct namespaces
const namespaceMap: Record<string, string> = {
'health.': 'dashboard',
'dashboard.': 'dashboard',
'reasoning.': 'reasoning',
'production.': 'production',
'jtbd.': 'reasoning',
};
// Find the matching namespace
let namespace = 'common';
let translationKey = key;
for (const [prefix, ns] of Object.entries(namespaceMap)) {
if (key.startsWith(prefix)) {
namespace = ns;
// Remove the first segment if it matches the namespace
// e.g., "health.headline_yellow" stays as is for dashboard:health.headline_yellow
// e.g., "reasoning.types.xyz" -> keep as "types.xyz" for reasoning:types.xyz
if (prefix === 'reasoning.') {
translationKey = key.substring(prefix.length);
}
break;
}
}
return t(translationKey, { ...params, ns: namespace, defaultValue: key });
}
export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) { export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) {
const { t, i18n } = useTranslation('reasoning'); const { t, i18n } = useTranslation(['dashboard', 'reasoning', 'production']);
// Get date-fns locale based on current language // Get date-fns locale based on current language
const dateLocale = i18n.language === 'es' ? es : i18n.language === 'eu' ? eu : enUS; const dateLocale = i18n.language === 'es' ? es : i18n.language === 'eu' ? eu : enUS;
@@ -85,21 +123,21 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h2 className="text-xl md:text-2xl font-bold mb-2" style={{ color: config.textColor }}> <h2 className="text-xl md:text-2xl font-bold mb-2" style={{ color: config.textColor }}>
{typeof healthStatus.headline === 'object' && healthStatus.headline?.key {typeof healthStatus.headline === 'object' && healthStatus.headline?.key
? t(healthStatus.headline.key, healthStatus.headline.params || {}) ? translateKey(healthStatus.headline.key, healthStatus.headline.params || {}, t)
: healthStatus.headline || t(`jtbd.health_status.${status}`)} : healthStatus.headline || t(`jtbd.health_status.${status}`, { ns: 'reasoning' })}
</h2> </h2>
{/* Last Update */} {/* Last Update */}
<div className="flex items-center gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}> <div className="flex items-center gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
<span> <span>
{t('jtbd.health_status.last_updated')}:{' '} {t('jtbd.health_status.last_updated', { ns: 'reasoning' })}:{' '}
{healthStatus.lastOrchestrationRun {healthStatus.lastOrchestrationRun
? formatDistanceToNow(new Date(healthStatus.lastOrchestrationRun), { ? formatDistanceToNow(new Date(healthStatus.lastOrchestrationRun), {
addSuffix: true, addSuffix: true,
locale: dateLocale, locale: dateLocale,
}) })
: t('jtbd.health_status.never')} : t('jtbd.health_status.never', { ns: 'reasoning' })}
</span> </span>
</div> </div>
@@ -108,7 +146,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2 text-sm mt-1" style={{ color: 'var(--text-secondary)' }}> <div className="flex items-center gap-2 text-sm mt-1" style={{ color: 'var(--text-secondary)' }}>
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
<span> <span>
{t('jtbd.health_status.next_check')}:{' '} {t('jtbd.health_status.next_check', { ns: 'reasoning' })}:{' '}
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true, locale: dateLocale })} {formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true, locale: dateLocale })}
</span> </span>
</div> </div>
@@ -126,7 +164,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
// Translate using textKey if available, otherwise use text // Translate using textKey if available, otherwise use text
const displayText = item.textKey const displayText = item.textKey
? t(item.textKey.replace('.', ':'), item.textParams || {}) ? translateKey(item.textKey, item.textParams || {}, t)
: item.text || ''; : item.text || '';
return ( return (
@@ -156,7 +194,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4" style={{ color: 'var(--color-error-600)' }} /> <AlertCircle className="w-4 h-4" style={{ color: 'var(--color-error-600)' }} />
<span className="font-semibold" style={{ color: 'var(--color-error-800)' }}> <span className="font-semibold" style={{ color: 'var(--color-error-800)' }}>
{t('jtbd.health_status.critical_issues', { count: healthStatus.criticalIssues })} {t('jtbd.health_status.critical_issues', { count: healthStatus.criticalIssues, ns: 'reasoning' })}
</span> </span>
</div> </div>
)} )}
@@ -164,7 +202,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4" style={{ color: 'var(--color-warning-600)' }} /> <AlertTriangle className="w-4 h-4" style={{ color: 'var(--color-warning-600)' }} />
<span className="font-semibold" style={{ color: 'var(--color-warning-800)' }}> <span className="font-semibold" style={{ color: 'var(--color-warning-800)' }}>
{t('jtbd.health_status.actions_needed', { count: healthStatus.pendingActions })} {t('jtbd.health_status.actions_needed', { count: healthStatus.pendingActions, ns: 'reasoning' })}
</span> </span>
</div> </div>
)} )}

View File

@@ -38,7 +38,7 @@ function TimelineItemCard({
}) { }) {
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'var(--text-tertiary)'; const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'var(--text-tertiary)';
const { formatBatchAction } = useReasoningFormatter(); const { formatBatchAction } = useReasoningFormatter();
const { t } = useTranslation(['reasoning', 'dashboard']); const { t } = useTranslation(['reasoning', 'dashboard', 'production']);
// Translate reasoning_data (or use new reasoning_i18n or fallback to deprecated text field) // Translate reasoning_data (or use new reasoning_i18n or fallback to deprecated text field)
// Memoize to prevent undefined values from being created on each render // Memoize to prevent undefined values from being created on each render
@@ -102,7 +102,13 @@ function TimelineItemCard({
<div className="mb-3"> <div className="mb-3">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> <span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
{item.status_i18n ? t(item.status_i18n.key, item.status_i18n.params) : item.statusText || 'Status'} {item.status_i18n
? t(item.status_i18n.key, {
...item.status_i18n.params,
ns: item.status_i18n.key.startsWith('production.') ? 'production' : 'reasoning',
defaultValue: item.statusText || 'Status'
})
: item.statusText || 'Status'}
</span> </span>
{item.status === 'IN_PROGRESS' && ( {item.status === 'IN_PROGRESS' && (
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>{item.progress || 0}%</span> <span className="text-sm" style={{ color: 'var(--text-secondary)' }}>{item.progress || 0}%</span>