feat: Add i18n support for AI insights with structured reasoning
Complete i18n implementation for internal service reasoning: - Update AIInsight interface to include reasoning_data field - Integrate useReasoningTranslation hook in AI Insights page - Add translation keys for safety stock, price forecaster, and optimization Translation coverage (EN/ES/EU): - Safety Stock: statistical z-score, advanced variability, fixed percentage, errors - Price Forecaster: price change predictions, volatility alerts, buying recommendations - Optimization: EOQ calculations, MOQ/max constraints, tier pricing Benefits: - AI insights now display in user's preferred language - Consistent with PO/Batch reasoning translation pattern - Structured parameters enable rich, contextualized translations - Falls back gracefully to description field if translation missing Implementation: - frontend/src/api/services/aiInsights.ts: Add reasoning_data to interface - frontend/src/pages/app/analytics/ai-insights/AIInsightsPage.tsx: Translate insights - frontend/src/locales/*/reasoning.json: Add safetyStock, priceForecaster, optimization keys This completes the full i18n implementation for the bakery AI system.
This commit is contained in:
@@ -25,6 +25,11 @@ export interface AIInsight {
|
|||||||
category: 'demand' | 'procurement' | 'inventory' | 'production' | 'sales' | 'system' | 'business';
|
category: 'demand' | 'procurement' | 'inventory' | 'production' | 'sales' | 'system' | 'business';
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
reasoning_data?: {
|
||||||
|
type: string;
|
||||||
|
parameters: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
impact_type: 'cost_savings' | 'waste_reduction' | 'yield_improvement' | 'revenue' | 'system_health' | 'process_improvement';
|
impact_type: 'cost_savings' | 'waste_reduction' | 'yield_improvement' | 'revenue' | 'system_health' | 'process_improvement';
|
||||||
impact_value?: number;
|
impact_value?: number;
|
||||||
impact_unit?: string;
|
impact_unit?: string;
|
||||||
|
|||||||
@@ -44,6 +44,29 @@
|
|||||||
"LEAD_TIME_INVALID": "Lead time or demand deviation is zero or negative",
|
"LEAD_TIME_INVALID": "Lead time or demand deviation is zero or negative",
|
||||||
"NO_DEMAND_DATA": "No historical demand data available (minimum 2 data points required)"
|
"NO_DEMAND_DATA": "No historical demand data available (minimum 2 data points required)"
|
||||||
},
|
},
|
||||||
|
"safetyStock": {
|
||||||
|
"statistical_z_score": "Safety stock calculated using statistical method (service level {{service_level}}%, z-score {{z_score}}). Based on demand std dev {{demand_std_dev}} and {{lead_time_days}}-day lead time. Result: {{safety_stock}} units.",
|
||||||
|
"advanced_variability": "Safety stock calculated with advanced variability analysis. Accounts for both demand variability (σ={{demand_std_dev}}) and lead time uncertainty (σ={{lead_time_std_dev}} days). Result: {{safety_stock}} units.",
|
||||||
|
"fixed_percentage": "Safety stock set at {{percentage}}% of {{lead_time_days}}-day demand ({{lead_time_demand}} units). Result: {{safety_stock}} units.",
|
||||||
|
"error_lead_time_invalid": "Cannot calculate safety stock: lead time ({{lead_time_days}} days) or demand std dev ({{demand_std_dev}}) is invalid.",
|
||||||
|
"error_insufficient_data": "Insufficient demand history for safety stock calculation ({{data_points}} data points, need {{min_required}})."
|
||||||
|
},
|
||||||
|
"priceForecaster": {
|
||||||
|
"decrease_expected": "Price expected to decrease {{change_pct}}% over next {{forecast_days}} days. Current: €{{current_price}}, forecast: €{{forecast_mean}}. Recommendation: Wait for better price.",
|
||||||
|
"increase_expected": "Price expected to increase {{change_pct}}% over next {{forecast_days}} days. Current: €{{current_price}}, forecast: €{{forecast_mean}}. Recommendation: Buy now to lock in current price.",
|
||||||
|
"high_volatility": "High price volatility detected (CV={{coefficient}}). Average daily price change: {{avg_daily_change_pct}}%. Recommendation: Wait for price dip.",
|
||||||
|
"below_average": "Current price €{{current_price}} is {{below_avg_pct}}% below historical average (€{{mean_price}}). Favorable buying opportunity.",
|
||||||
|
"stable": "Price is stable. Current: €{{current_price}}, forecast: €{{forecast_mean}} ({{expected_change_pct}}% change expected). Normal procurement timing recommended.",
|
||||||
|
"insufficient_data": "Insufficient price history for reliable forecast ({{history_days}} days available, need {{min_required_days}} days)."
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"eoq_base": "Economic Order Quantity calculated: {{eoq}} units (required: {{required_quantity}}, annual demand: {{annual_demand}}). Optimized for {{optimal_quantity}} units.",
|
||||||
|
"moq_applied": "Minimum order quantity constraint applied: {{moq}} units.",
|
||||||
|
"max_applied": "Maximum order quantity constraint applied: {{max_qty}} units.",
|
||||||
|
"no_tiers": "No price tiers available for this product. Base quantity: {{base_quantity}} units at €{{unit_price}} per unit.",
|
||||||
|
"current_tier": "Current pricing tier: €{{current_tier_price}} per unit for {{base_quantity}} units (total: €{{base_cost}}).",
|
||||||
|
"tier_upgraded": "Upgraded to better price tier! Ordering {{tier_min_qty}} units ({{additional_qty}} extra) at €{{tier_price}} per unit saves €{{savings}} compared to {{base_quantity}} units at €{{base_price}}."
|
||||||
|
},
|
||||||
"jtbd": {
|
"jtbd": {
|
||||||
"health_status": {
|
"health_status": {
|
||||||
"green": "Everything is running smoothly",
|
"green": "Everything is running smoothly",
|
||||||
|
|||||||
@@ -44,6 +44,29 @@
|
|||||||
"LEAD_TIME_INVALID": "El tiempo de entrega o la desviación de la demanda es cero o negativo",
|
"LEAD_TIME_INVALID": "El tiempo de entrega o la desviación de la demanda es cero o negativo",
|
||||||
"NO_DEMAND_DATA": "No hay datos históricos de demanda disponibles (se requieren mínimo 2 puntos de datos)"
|
"NO_DEMAND_DATA": "No hay datos históricos de demanda disponibles (se requieren mínimo 2 puntos de datos)"
|
||||||
},
|
},
|
||||||
|
"safetyStock": {
|
||||||
|
"statistical_z_score": "Stock de seguridad calculado con método estadístico (nivel de servicio {{service_level}}%, z-score {{z_score}}). Basado en desviación estándar de demanda {{demand_std_dev}} y tiempo de entrega de {{lead_time_days}} días. Resultado: {{safety_stock}} unidades.",
|
||||||
|
"advanced_variability": "Stock de seguridad calculado con análisis avanzado de variabilidad. Considera tanto la variabilidad de demanda (σ={{demand_std_dev}}) como la incertidumbre del tiempo de entrega (σ={{lead_time_std_dev}} días). Resultado: {{safety_stock}} unidades.",
|
||||||
|
"fixed_percentage": "Stock de seguridad establecido en {{percentage}}% de la demanda de {{lead_time_days}} días ({{lead_time_demand}} unidades). Resultado: {{safety_stock}} unidades.",
|
||||||
|
"error_lead_time_invalid": "No se puede calcular el stock de seguridad: el tiempo de entrega ({{lead_time_days}} días) o la desviación estándar de demanda ({{demand_std_dev}}) no son válidos.",
|
||||||
|
"error_insufficient_data": "Historial de demanda insuficiente para calcular stock de seguridad ({{data_points}} puntos de datos, se necesitan {{min_required}})."
|
||||||
|
},
|
||||||
|
"priceForecaster": {
|
||||||
|
"decrease_expected": "Se espera que el precio disminuya {{change_pct}}% en los próximos {{forecast_days}} días. Actual: €{{current_price}}, pronóstico: €{{forecast_mean}}. Recomendación: Esperar mejor precio.",
|
||||||
|
"increase_expected": "Se espera que el precio aumente {{change_pct}}% en los próximos {{forecast_days}} días. Actual: €{{current_price}}, pronóstico: €{{forecast_mean}}. Recomendación: Comprar ahora para asegurar el precio actual.",
|
||||||
|
"high_volatility": "Alta volatilidad de precio detectada (CV={{coefficient}}). Cambio diario promedio de precio: {{avg_daily_change_pct}}%. Recomendación: Esperar caída de precio.",
|
||||||
|
"below_average": "El precio actual €{{current_price}} está {{below_avg_pct}}% por debajo del promedio histórico (€{{mean_price}}). Oportunidad favorable de compra.",
|
||||||
|
"stable": "El precio es estable. Actual: €{{current_price}}, pronóstico: €{{forecast_mean}} (se espera cambio de {{expected_change_pct}}%). Se recomienda tiempo de compra normal.",
|
||||||
|
"insufficient_data": "Historial de precios insuficiente para pronóstico confiable ({{history_days}} días disponibles, se necesitan {{min_required_days}} días)."
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"eoq_base": "Cantidad Económica de Pedido calculada: {{eoq}} unidades (requerido: {{required_quantity}}, demanda anual: {{annual_demand}}). Optimizado para {{optimal_quantity}} unidades.",
|
||||||
|
"moq_applied": "Restricción de cantidad mínima de pedido aplicada: {{moq}} unidades.",
|
||||||
|
"max_applied": "Restricción de cantidad máxima de pedido aplicada: {{max_qty}} unidades.",
|
||||||
|
"no_tiers": "No hay niveles de precio disponibles para este producto. Cantidad base: {{base_quantity}} unidades a €{{unit_price}} por unidad.",
|
||||||
|
"current_tier": "Nivel de precio actual: €{{current_tier_price}} por unidad para {{base_quantity}} unidades (total: €{{base_cost}}).",
|
||||||
|
"tier_upgraded": "¡Actualizado a mejor nivel de precio! Pedir {{tier_min_qty}} unidades ({{additional_qty}} extra) a €{{tier_price}} por unidad ahorra €{{savings}} comparado con {{base_quantity}} unidades a €{{base_price}}."
|
||||||
|
},
|
||||||
"jtbd": {
|
"jtbd": {
|
||||||
"health_status": {
|
"health_status": {
|
||||||
"green": "Todo funciona correctamente",
|
"green": "Todo funciona correctamente",
|
||||||
|
|||||||
@@ -46,6 +46,29 @@ ko da.",
|
|||||||
"LEAD_TIME_INVALID": "Entrega denbora edo eskaeraren desbideratzea zero edo negatiboa da",
|
"LEAD_TIME_INVALID": "Entrega denbora edo eskaeraren desbideratzea zero edo negatiboa da",
|
||||||
"NO_DEMAND_DATA": "Ez dago eskaeraren datu historikorik eskuragarri (gutxienez 2 datu puntu behar dira)"
|
"NO_DEMAND_DATA": "Ez dago eskaeraren datu historikorik eskuragarri (gutxienez 2 datu puntu behar dira)"
|
||||||
},
|
},
|
||||||
|
"safetyStock": {
|
||||||
|
"statistical_z_score": "Segurtasun-stock kalkulatua metodo estatistikoarekin (zerbitzu-maila {{service_level}}%, z-score {{z_score}}). Eskaeraren desbideratze estandarrean {{demand_std_dev}} eta {{lead_time_days}} eguneko entrega-denboran oinarrituta. Emaitza: {{safety_stock}} unitate.",
|
||||||
|
"advanced_variability": "Segurtasun-stock kalkulatua aldakortasun-analisi aurreratuarekin. Eskaeraren aldakortasuna (σ={{demand_std_dev}}) eta entrega-denboraren ziurgabetasuna (σ={{lead_time_std_dev}} egun) kontuan hartzen ditu. Emaitza: {{safety_stock}} unitate.",
|
||||||
|
"fixed_percentage": "Segurtasun-stock ezarri da {{lead_time_days}} eguneko eskaeraren {{percentage}}%n ({{lead_time_demand}} unitate). Emaitza: {{safety_stock}} unitate.",
|
||||||
|
"error_lead_time_invalid": "Ezin da segurtasun-stock kalkulatu: entrega-denbora ({{lead_time_days}} egun) edo eskaeraren desbideratze estandarra ({{demand_std_dev}}) ez dira baliodunak.",
|
||||||
|
"error_insufficient_data": "Eskaeraren historiala ez da nahikoa segurtasun-stock kalkulatzeko ({{data_points}} datu puntu, {{min_required}} behar dira)."
|
||||||
|
},
|
||||||
|
"priceForecaster": {
|
||||||
|
"decrease_expected": "Espero da prezioa {{change_pct}}% jaitsiko dela hurrengo {{forecast_days}} egunetan. Oraingoa: €{{current_price}}, aurreikuspena: €{{forecast_mean}}. Gomendio: Itxaron prezio hobeago baterako.",
|
||||||
|
"increase_expected": "Espero da prezioa {{change_pct}}% igoko dela hurrengo {{forecast_days}} egunetan. Oraingoa: €{{current_price}}, aurreikuspena: €{{forecast_mean}}. Gomendio: Erosi orain oraingo prezioa ziurtatzeko.",
|
||||||
|
"high_volatility": "Prezio-aldakortasun altua detektatu da (CV={{coefficient}}). Prezioaren eguneko batez besteko aldaketa: {{avg_daily_change_pct}}%. Gomendio: Itxaron prezioaren jaitsierara.",
|
||||||
|
"below_average": "Oraingo prezioa €{{current_price}} batez besteko historikoaren (€{{mean_price}}) {{below_avg_pct}}% azpitik dago. Erosketa-aukera egokia.",
|
||||||
|
"stable": "Prezioa egonkorra da. Oraingoa: €{{current_price}}, aurreikuspena: €{{forecast_mean}} ({{expected_change_pct}}% aldaketa espero da). Ohiko erosketa-denbora gomendatzen da.",
|
||||||
|
"insufficient_data": "Prezioen historiala ez da nahikoa aurreikuspen fidagarrirako ({{history_days}} egun eskuragarri, {{min_required_days}} egun behar dira)."
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"eoq_base": "Eskaera Kantitate Ekonomikoa kalkulatu da: {{eoq}} unitate (beharrezkoa: {{required_quantity}}, urteko eskaera: {{annual_demand}}). {{optimal_quantity}} unitatetan optimizatuta.",
|
||||||
|
"moq_applied": "Gutxieneko eskaera-kantitatearen murrizketa aplikatu da: {{moq}} unitate.",
|
||||||
|
"max_applied": "Gehienezko eskaera-kantitatearen murrizketa aplikatu da: {{max_qty}} unitate.",
|
||||||
|
"no_tiers": "Ez dago prezio-mailarik eskuragarri produktu honetarako. Oinarrizko kantitatea: {{base_quantity}} unitate €{{unit_price}} unitateko.",
|
||||||
|
"current_tier": "Oraingo prezio-maila: €{{current_tier_price}} unitateko {{base_quantity}} unitateko (guztira: €{{base_cost}}).",
|
||||||
|
"tier_upgraded": "Prezio-maila hobea lortu da! {{tier_min_qty}} unitate eskatzeak ({{additional_qty}} gehiago) €{{tier_price}} unitateko €{{savings}} aurrezten ditu {{base_quantity}} unitateri €{{base_price}}n konparatuta."
|
||||||
|
},
|
||||||
"jtbd": {
|
"jtbd": {
|
||||||
"health_status": {
|
"health_status": {
|
||||||
"green": "Dena ondo dabil",
|
"green": "Dena ondo dabil",
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { useCurrentTenant } from '../../../../stores/tenant.store';
|
|||||||
import { useAuthUser } from '../../../../stores/auth.store';
|
import { useAuthUser } from '../../../../stores/auth.store';
|
||||||
import { useAIInsights, useAIInsightStats, useApplyInsight, useDismissInsight } from '../../../../api/hooks/aiInsights';
|
import { useAIInsights, useAIInsightStats, useApplyInsight, useDismissInsight } from '../../../../api/hooks/aiInsights';
|
||||||
import { AIInsight } from '../../../../api/services/aiInsights';
|
import { AIInsight } from '../../../../api/services/aiInsights';
|
||||||
|
import { useReasoningTranslation } from '../../../../hooks/useReasoningTranslation';
|
||||||
|
|
||||||
const AIInsightsPage: React.FC = () => {
|
const AIInsightsPage: React.FC = () => {
|
||||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||||
const currentTenant = useCurrentTenant();
|
const currentTenant = useCurrentTenant();
|
||||||
const user = useAuthUser();
|
const user = useAuthUser();
|
||||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||||
|
const { t } = useReasoningTranslation();
|
||||||
|
|
||||||
// Fetch real insights from API
|
// Fetch real insights from API
|
||||||
const { data: insightsData, isLoading, refetch } = useAIInsights(
|
const { data: insightsData, isLoading, refetch } = useAIInsights(
|
||||||
@@ -118,6 +120,32 @@ const AIInsightsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translated description for an insight
|
||||||
|
* Uses reasoning_data if available, otherwise falls back to description field
|
||||||
|
*/
|
||||||
|
const getInsightDescription = (insight: AIInsight): string => {
|
||||||
|
if (insight.reasoning_data?.type) {
|
||||||
|
// Determine the category prefix based on source
|
||||||
|
let category = 'aiInsights';
|
||||||
|
if (insight.source_model === 'safety_stock_calculator') {
|
||||||
|
category = 'safetyStock';
|
||||||
|
} else if (insight.source_model === 'price_forecaster') {
|
||||||
|
category = 'priceForecaster';
|
||||||
|
} else if (insight.source_model === 'optimization') {
|
||||||
|
category = 'optimization';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return t(`${category}.${insight.reasoning_data.type}`, insight.reasoning_data.parameters);
|
||||||
|
} catch (e) {
|
||||||
|
// Fall back to description if translation key not found
|
||||||
|
return insight.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return insight.description;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnalyticsPageLayout
|
<AnalyticsPageLayout
|
||||||
title="Inteligencia Artificial"
|
title="Inteligencia Artificial"
|
||||||
@@ -224,7 +252,7 @@ const AIInsightsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{insight.title}</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{insight.title}</h3>
|
||||||
<p className="text-[var(--text-secondary)] mb-3">{insight.description}</p>
|
<p className="text-[var(--text-secondary)] mb-3">{getInsightDescription(insight)}</p>
|
||||||
<p className="text-sm font-medium text-[var(--color-success)] mb-4">{insight.impact}</p>
|
<p className="text-sm font-medium text-[var(--color-success)] mb-4">{insight.impact}</p>
|
||||||
|
|
||||||
{/* Metrics */}
|
{/* Metrics */}
|
||||||
|
|||||||
Reference in New Issue
Block a user