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:
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
reasoning_type_map = {
|
|
||||||
"low_stock_detection": "reasoning.types.low_stock_detection",
|
Args:
|
||||||
"stockout_prevention": "reasoning.types.stockout_prevention",
|
reasoning_type: The type of reasoning (e.g., "low_stock_detection")
|
||||||
"forecast_demand": "reasoning.types.forecast_demand",
|
context: The context (either "purchaseOrder" or "productionBatch")
|
||||||
"customer_orders": "reasoning.types.customer_orders",
|
|
||||||
"seasonal_demand": "reasoning.types.seasonal_demand",
|
Returns:
|
||||||
"inventory_replenishment": "reasoning.types.inventory_replenishment",
|
Full i18n key with namespace and context prefix
|
||||||
"production_schedule": "reasoning.types.production_schedule",
|
"""
|
||||||
}
|
if context == "productionBatch":
|
||||||
return reasoning_type_map.get(reasoning_type, "reasoning.types.other")
|
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:
|
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"],
|
||||||
|
|||||||
Reference in New Issue
Block a user