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({
action,
onApprove,
@@ -102,25 +126,25 @@ function ActionItemCard({
// Translate i18n fields (or fallback to deprecated text fields or reasoning_data for alerts)
const reasoning = useMemo(() => {
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) {
const formatted = formatPOAction(action.reasoning_data);
return formatted.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(() => {
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) {
const formatted = formatPOAction(action.reasoning_data);
return formatted.consequence;
}
return '';
}, [action.consequence_i18n, action.reasoning_data, tDashboard, formatPOAction]);
}, [action.consequence_i18n, action.reasoning_data, tDashboard, tReasoning, formatPOAction]);
return (
<div
@@ -136,7 +160,7 @@ function ActionItemCard({
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<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>
<span
className="px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0"
@@ -149,7 +173,7 @@ function ActionItemCard({
</span>
</div>
<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>
</div>
</div>
@@ -493,7 +517,7 @@ function ActionItemCard({
{button.action === 'reject' && <X className="w-4 h-4" />}
{button.action === 'view_details' && <Eye 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>
);
})}

View File

@@ -50,8 +50,46 @@ const iconMap = {
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) {
const { t, i18n } = useTranslation('reasoning');
const { t, i18n } = useTranslation(['dashboard', 'reasoning', 'production']);
// Get date-fns locale based on current language
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">
<h2 className="text-xl md:text-2xl font-bold mb-2" style={{ color: config.textColor }}>
{typeof healthStatus.headline === 'object' && healthStatus.headline?.key
? t(healthStatus.headline.key, healthStatus.headline.params || {})
: healthStatus.headline || t(`jtbd.health_status.${status}`)}
? translateKey(healthStatus.headline.key, healthStatus.headline.params || {}, t)
: healthStatus.headline || t(`jtbd.health_status.${status}`, { ns: 'reasoning' })}
</h2>
{/* Last Update */}
<div className="flex items-center gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
<Clock className="w-4 h-4" />
<span>
{t('jtbd.health_status.last_updated')}:{' '}
{t('jtbd.health_status.last_updated', { ns: 'reasoning' })}:{' '}
{healthStatus.lastOrchestrationRun
? formatDistanceToNow(new Date(healthStatus.lastOrchestrationRun), {
addSuffix: true,
locale: dateLocale,
})
: t('jtbd.health_status.never')}
: t('jtbd.health_status.never', { ns: 'reasoning' })}
</span>
</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)' }}>
<RefreshCw className="w-4 h-4" />
<span>
{t('jtbd.health_status.next_check')}:{' '}
{t('jtbd.health_status.next_check', { ns: 'reasoning' })}:{' '}
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true, locale: dateLocale })}
</span>
</div>
@@ -126,7 +164,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
// Translate using textKey if available, otherwise use text
const displayText = item.textKey
? t(item.textKey.replace('.', ':'), item.textParams || {})
? translateKey(item.textKey, item.textParams || {}, t)
: item.text || '';
return (
@@ -156,7 +194,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4" style={{ color: 'var(--color-error-600)' }} />
<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>
</div>
)}
@@ -164,7 +202,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4" style={{ color: 'var(--color-warning-600)' }} />
<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>
</div>
)}

View File

@@ -38,7 +38,7 @@ function TimelineItemCard({
}) {
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'var(--text-tertiary)';
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)
// Memoize to prevent undefined values from being created on each render
@@ -102,7 +102,13 @@ function TimelineItemCard({
<div className="mb-3">
<div className="flex items-center justify-between mb-1">
<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>
{item.status === 'IN_PROGRESS' && (
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>{item.progress || 0}%</span>