Fix dashboard translation issues and template variable interpolation

This commit resolves three critical translation/localization issues in the bakery dashboard:

1. **Health Status Translation Keys**: Fixed HealthStatusCard's translateKey function to properly handle `dashboard.health.*` keys by correctly stripping the `dashboard.` prefix while preserving the `health.` namespace path. This ensures checklist items like "production_on_schedule" and "all_ingredients_in_stock" display correctly in Spanish.

2. **Reasoning Translation Keys**: Updated backend dashboard_service.py to use the correct i18n key prefixes:
   - Purchase orders now use `reasoning.purchaseOrder.*` instead of `reasoning.types.*`
   - Production batches now use `reasoning.productionBatch.*`
   - Added context parameter to `_get_reasoning_type_i18n_key()` method for proper namespace routing

3. **Template Variable Interpolation**: Fixed template variable replacement in action cards:
   - Added array preprocessing logic in both backend and frontend to convert `product_names` arrays to `product_names_joined` strings
   - Updated ActionQueueCard's translateKey to preprocess array parameters before i18n interpolation
   - Fixed ProductionTimelineCard to properly handle reasoning namespace prefix removal

These fixes ensure that:
- Health status indicators show translated text instead of raw keys (e.g., "Producción a tiempo" vs "dashboard.health.production_on_schedule")
- Purchase order reasoning displays with proper product names and stockout days instead of literal template variables (e.g., "Stock bajo para Harina. El stock se agotará en 7 días" vs "Stock bajo para {{product_name}}")
- All dashboard components consistently handle i18n key namespaces and parameter interpolation

Affected files:
- frontend/src/components/dashboard/HealthStatusCard.tsx
- frontend/src/components/dashboard/ActionQueueCard.tsx
- frontend/src/components/dashboard/ProductionTimelineCard.tsx
- services/orchestrator/app/services/dashboard_service.py
This commit is contained in:
Claude
2025-11-20 19:03:39 +00:00
parent 5a2b0b0757
commit 8aa1b0d859
4 changed files with 80 additions and 22 deletions

View File

@@ -77,18 +77,31 @@ function translateKey(
tDashboard: any, tDashboard: any,
tReasoning: any tReasoning: any
): string { ): string {
// Preprocess parameters - join arrays into strings
const processedParams = { ...params };
// Convert product_names array to product_names_joined string
if ('product_names' in processedParams && Array.isArray(processedParams.product_names)) {
processedParams.product_names_joined = processedParams.product_names.join(', ');
}
// Convert affected_products array to affected_products_joined string
if ('affected_products' in processedParams && Array.isArray(processedParams.affected_products)) {
processedParams.affected_products_joined = processedParams.affected_products.join(', ');
}
// Determine namespace based on key prefix // Determine namespace based on key prefix
if (key.startsWith('reasoning.')) { if (key.startsWith('reasoning.')) {
// Remove 'reasoning.' prefix and use reasoning namespace // Remove 'reasoning.' prefix and use reasoning namespace
const translationKey = key.substring('reasoning.'.length); const translationKey = key.substring('reasoning.'.length);
return tReasoning(translationKey, { ...params, defaultValue: key }); return tReasoning(translationKey, { ...processedParams, defaultValue: key });
} else if (key.startsWith('action_queue.') || key.startsWith('dashboard.') || key.startsWith('health.')) { } else if (key.startsWith('action_queue.') || key.startsWith('dashboard.') || key.startsWith('health.')) {
// Use dashboard namespace // Use dashboard namespace
return tDashboard(key, { ...params, defaultValue: key }); return tDashboard(key, { ...processedParams, defaultValue: key });
} }
// Default to dashboard // Default to dashboard
return tDashboard(key, { ...params, defaultValue: key }); return tDashboard(key, { ...processedParams, defaultValue: key });
} }
function ActionItemCard({ function ActionItemCard({

View File

@@ -62,6 +62,7 @@ function translateKey(
// Map key prefixes to their correct namespaces // Map key prefixes to their correct namespaces
const namespaceMap: Record<string, string> = { const namespaceMap: Record<string, string> = {
'health.': 'dashboard', 'health.': 'dashboard',
'dashboard.health.': 'dashboard',
'dashboard.': 'dashboard', 'dashboard.': 'dashboard',
'reasoning.': 'reasoning', 'reasoning.': 'reasoning',
'production.': 'production', 'production.': 'production',
@@ -76,10 +77,16 @@ function translateKey(
if (key.startsWith(prefix)) { if (key.startsWith(prefix)) {
namespace = ns; namespace = ns;
// Remove the first segment if it matches the namespace // Remove the first segment if it matches the namespace
// e.g., "health.headline_yellow" stays as is for dashboard:health.headline_yellow // e.g., "dashboard.health.production_on_schedule" -> "health.production_on_schedule"
// e.g., "reasoning.types.xyz" -> keep as "types.xyz" for reasoning:types.xyz // e.g., "reasoning.types.xyz" -> "types.xyz" for reasoning namespace
if (prefix === 'reasoning.') { if (prefix === 'reasoning.') {
translationKey = key.substring(prefix.length); translationKey = key.substring(prefix.length);
} else if (prefix === 'dashboard.health.') {
// Keep "health." prefix but remove "dashboard."
translationKey = key.substring('dashboard.'.length);
} else if (prefix === 'dashboard.' && !key.startsWith('dashboard.health.')) {
// For other dashboard keys, remove "dashboard." prefix
translationKey = key.substring('dashboard.'.length);
} }
break; break;
} }

View File

@@ -45,8 +45,17 @@ function TimelineItemCard({
const { reasoning } = useMemo(() => { const { reasoning } = useMemo(() => {
if (item.reasoning_i18n) { if (item.reasoning_i18n) {
// Use new i18n structure if available // Use new i18n structure if available
const { key, params } = item.reasoning_i18n; const { key, params = {} } = item.reasoning_i18n;
return { reasoning: t(key, params, { defaultValue: item.reasoning || '' }) }; // Handle namespace - remove "reasoning." prefix for reasoning namespace
let translationKey = key;
let namespace = 'reasoning';
if (key.startsWith('reasoning.')) {
translationKey = key.substring('reasoning.'.length);
} else if (key.startsWith('production.')) {
translationKey = key.substring('production.'.length);
namespace = 'production';
}
return { reasoning: t(translationKey, { ...params, ns: namespace, defaultValue: item.reasoning || '' }) };
} else if (item.reasoning_data) { } else if (item.reasoning_data) {
return formatBatchAction(item.reasoning_data); return formatBatchAction(item.reasoning_data);
} }

View File

@@ -402,7 +402,13 @@ class DashboardService:
# Get reasoning type and convert to i18n key # Get reasoning type and convert to i18n key
reasoning_type = reasoning_data.get('type', 'inventory_replenishment') reasoning_type = reasoning_data.get('type', 'inventory_replenishment')
reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type) reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type, context="purchaseOrder")
# Preprocess parameters for i18n
params = reasoning_data.get('parameters', {})
# Convert product_names array to product_names_joined string
if 'product_names' in params and isinstance(params['product_names'], list):
params['product_names_joined'] = ', '.join(params['product_names'])
actions.append({ actions.append({
"id": po["id"], "id": po["id"],
@@ -418,7 +424,7 @@ class DashboardService:
}, },
"reasoning_i18n": { "reasoning_i18n": {
"key": reasoning_type_i18n_key, "key": reasoning_type_i18n_key,
"params": reasoning_data.get('parameters', {}) "params": params
}, },
"consequence_i18n": { "consequence_i18n": {
"key": "action_queue.consequences.delayed_delivery", "key": "action_queue.consequences.delayed_delivery",
@@ -465,18 +471,41 @@ class DashboardService:
return actions return actions
def _get_reasoning_type_i18n_key(self, reasoning_type: str) -> str: def _get_reasoning_type_i18n_key(self, reasoning_type: str, context: str = "purchaseOrder") -> str:
"""Map reasoning type identifiers to i18n keys""" """Map reasoning type identifiers to i18n keys
Args:
reasoning_type: The type of reasoning (e.g., "low_stock_detection")
context: The context (either "purchaseOrder" or "productionBatch")
Returns:
Full i18n key with namespace and context prefix
"""
if context == "productionBatch":
reasoning_type_map = { reasoning_type_map = {
"low_stock_detection": "reasoning.types.low_stock_detection", "forecast_demand": "reasoning.productionBatch.forecast_demand",
"stockout_prevention": "reasoning.types.stockout_prevention", "customer_order": "reasoning.productionBatch.customer_order",
"forecast_demand": "reasoning.types.forecast_demand", "stock_replenishment": "reasoning.productionBatch.stock_replenishment",
"customer_orders": "reasoning.types.customer_orders", "seasonal_preparation": "reasoning.productionBatch.seasonal_preparation",
"seasonal_demand": "reasoning.types.seasonal_demand", "promotion_event": "reasoning.productionBatch.promotion_event",
"inventory_replenishment": "reasoning.types.inventory_replenishment", "urgent_order": "reasoning.productionBatch.urgent_order",
"production_schedule": "reasoning.types.production_schedule", "regular_schedule": "reasoning.productionBatch.regular_schedule",
} }
return reasoning_type_map.get(reasoning_type, "reasoning.types.other") else: # purchaseOrder context
reasoning_type_map = {
"low_stock_detection": "reasoning.purchaseOrder.low_stock_detection",
"stockout_prevention": "reasoning.purchaseOrder.low_stock_detection",
"forecast_demand": "reasoning.purchaseOrder.forecast_demand",
"customer_orders": "reasoning.purchaseOrder.forecast_demand",
"seasonal_demand": "reasoning.purchaseOrder.seasonal_demand",
"inventory_replenishment": "reasoning.purchaseOrder.safety_stock_replenishment",
"production_schedule": "reasoning.purchaseOrder.production_requirement",
"supplier_contract": "reasoning.purchaseOrder.supplier_contract",
"safety_stock_replenishment": "reasoning.purchaseOrder.safety_stock_replenishment",
"production_requirement": "reasoning.purchaseOrder.production_requirement",
"manual_request": "reasoning.purchaseOrder.manual_request",
}
return reasoning_type_map.get(reasoning_type, f"reasoning.{context}.forecast_demand")
def _calculate_po_urgency(self, po: Dict[str, Any]) -> str: def _calculate_po_urgency(self, po: Dict[str, Any]) -> str:
"""Calculate urgency of PO approval based on delivery date""" """Calculate urgency of PO approval based on delivery date"""
@@ -579,7 +608,7 @@ class DashboardService:
# Get reasoning type and convert to i18n key # Get reasoning type and convert to i18n key
reasoning_type = reasoning_data.get('type', 'forecast_demand') reasoning_type = reasoning_data.get('type', 'forecast_demand')
reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type) reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type, context="productionBatch")
timeline.append({ timeline.append({
"id": batch["id"], "id": batch["id"],