From 8aa1b0d859d8cc9076e0fb27d3d196b29ae6deba Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 19:03:39 +0000 Subject: [PATCH] Fix dashboard translation issues and template variable interpolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/dashboard/ActionQueueCard.tsx | 19 +++++- .../components/dashboard/HealthStatusCard.tsx | 11 +++- .../dashboard/ProductionTimelineCard.tsx | 13 +++- .../app/services/dashboard_service.py | 59 ++++++++++++++----- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/dashboard/ActionQueueCard.tsx b/frontend/src/components/dashboard/ActionQueueCard.tsx index 25d56ae9..d4a8ea9c 100644 --- a/frontend/src/components/dashboard/ActionQueueCard.tsx +++ b/frontend/src/components/dashboard/ActionQueueCard.tsx @@ -77,18 +77,31 @@ function translateKey( tDashboard: any, tReasoning: any ): 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 if (key.startsWith('reasoning.')) { // Remove 'reasoning.' prefix and use reasoning namespace 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.')) { // Use dashboard namespace - return tDashboard(key, { ...params, defaultValue: key }); + return tDashboard(key, { ...processedParams, defaultValue: key }); } // Default to dashboard - return tDashboard(key, { ...params, defaultValue: key }); + return tDashboard(key, { ...processedParams, defaultValue: key }); } function ActionItemCard({ diff --git a/frontend/src/components/dashboard/HealthStatusCard.tsx b/frontend/src/components/dashboard/HealthStatusCard.tsx index a8ea5d37..4ab80b9a 100644 --- a/frontend/src/components/dashboard/HealthStatusCard.tsx +++ b/frontend/src/components/dashboard/HealthStatusCard.tsx @@ -62,6 +62,7 @@ function translateKey( // Map key prefixes to their correct namespaces const namespaceMap: Record = { 'health.': 'dashboard', + 'dashboard.health.': 'dashboard', 'dashboard.': 'dashboard', 'reasoning.': 'reasoning', 'production.': 'production', @@ -76,10 +77,16 @@ function translateKey( 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 + // e.g., "dashboard.health.production_on_schedule" -> "health.production_on_schedule" + // e.g., "reasoning.types.xyz" -> "types.xyz" for reasoning namespace if (prefix === 'reasoning.') { 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; } diff --git a/frontend/src/components/dashboard/ProductionTimelineCard.tsx b/frontend/src/components/dashboard/ProductionTimelineCard.tsx index f2779c59..e09fae77 100644 --- a/frontend/src/components/dashboard/ProductionTimelineCard.tsx +++ b/frontend/src/components/dashboard/ProductionTimelineCard.tsx @@ -45,8 +45,17 @@ function TimelineItemCard({ const { reasoning } = useMemo(() => { if (item.reasoning_i18n) { // Use new i18n structure if available - const { key, params } = item.reasoning_i18n; - return { reasoning: t(key, params, { defaultValue: item.reasoning || '' }) }; + const { key, params = {} } = item.reasoning_i18n; + // 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) { return formatBatchAction(item.reasoning_data); } diff --git a/services/orchestrator/app/services/dashboard_service.py b/services/orchestrator/app/services/dashboard_service.py index 2b9a5935..099a1813 100644 --- a/services/orchestrator/app/services/dashboard_service.py +++ b/services/orchestrator/app/services/dashboard_service.py @@ -402,7 +402,13 @@ class DashboardService: # Get reasoning type and convert to i18n key 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({ "id": po["id"], @@ -418,7 +424,7 @@ class DashboardService: }, "reasoning_i18n": { "key": reasoning_type_i18n_key, - "params": reasoning_data.get('parameters', {}) + "params": params }, "consequence_i18n": { "key": "action_queue.consequences.delayed_delivery", @@ -465,18 +471,41 @@ class DashboardService: return actions - def _get_reasoning_type_i18n_key(self, reasoning_type: str) -> str: - """Map reasoning type identifiers to i18n keys""" - reasoning_type_map = { - "low_stock_detection": "reasoning.types.low_stock_detection", - "stockout_prevention": "reasoning.types.stockout_prevention", - "forecast_demand": "reasoning.types.forecast_demand", - "customer_orders": "reasoning.types.customer_orders", - "seasonal_demand": "reasoning.types.seasonal_demand", - "inventory_replenishment": "reasoning.types.inventory_replenishment", - "production_schedule": "reasoning.types.production_schedule", - } - return reasoning_type_map.get(reasoning_type, "reasoning.types.other") + def _get_reasoning_type_i18n_key(self, reasoning_type: str, context: str = "purchaseOrder") -> str: + """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 = { + "forecast_demand": "reasoning.productionBatch.forecast_demand", + "customer_order": "reasoning.productionBatch.customer_order", + "stock_replenishment": "reasoning.productionBatch.stock_replenishment", + "seasonal_preparation": "reasoning.productionBatch.seasonal_preparation", + "promotion_event": "reasoning.productionBatch.promotion_event", + "urgent_order": "reasoning.productionBatch.urgent_order", + "regular_schedule": "reasoning.productionBatch.regular_schedule", + } + 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: """Calculate urgency of PO approval based on delivery date""" @@ -579,7 +608,7 @@ class DashboardService: # Get reasoning type and convert to i18n key 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({ "id": batch["id"],