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:
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user