Complete i18n pass for recipes and meal plan labels

This commit is contained in:
dadaloop82
2026-04-28 17:28:54 +00:00
parent 8722f15aa0
commit 8558db1925
7 changed files with 645 additions and 249 deletions
+128 -24
View File
@@ -2203,6 +2203,95 @@ PROMPT;
echo json_encode(['success' => true, 'reply' => $reply]);
}
function recipeNormalizeLang($lang): string {
$lang = is_string($lang) ? strtolower(trim($lang)) : 'it';
return in_array($lang, ['it', 'en', 'de'], true) ? $lang : 'it';
}
function recipeLangName(string $lang): string {
return [
'it' => 'Italian',
'en' => 'English',
'de' => 'German',
][$lang] ?? 'Italian';
}
function recipeText(string $lang, string $key, array $vars = []): string {
$dict = [
'it' => [
'status_analyze_pantry' => '📦 Analizzo la dispensa...',
'status_products_found' => '{n} prodotti trovati',
'status_passed_ai' => ' ({n} passati all\'AI)',
'status_all_passed_ai' => ' — tutti passati all\'AI',
'status_urgent' => '⚠️ {n} urgenti: {items}',
'status_evaluate_ingredients' => '🧠 Valuto gli ingredienti disponibili...',
'status_preparing_recipe' => '👨‍🍳 Preparo la ricetta...',
'status_recipe_with' => '🥘 Ricetta con {a} e {b}',
'status_variant' => ' — variante #{n}',
'status_dish_based_on' => '🎯 Piatto a base di {type}',
'status_creating_full_recipe' => '✍️ Creo la ricetta completa...',
'status_quota_wait' => '⏳ Quota TPM esaurita ({model}), attendo {s}s... (tentativo {a}/{m})',
'status_retry_generation' => '✍️ Riprovo la generazione...',
'status_switch_model' => '🔄 Cambio modello → {model}...',
'error_pantry_empty' => 'La dispensa è vuota!',
'error_gemini_api' => 'Errore API Gemini',
'error_cannot_generate' => 'Impossibile generare la ricetta',
'error_empty_reply' => 'Risposta vuota da Gemini',
'prompt_lang_rule' => 'IMPORTANTE: scrivi tutti i campi testuali della ricetta in Italiano.',
'prompt_step_example' => 'Passo 1…',
],
'en' => [
'status_analyze_pantry' => '📦 Analyzing pantry...',
'status_products_found' => '{n} products found',
'status_passed_ai' => ' ({n} sent to AI)',
'status_all_passed_ai' => ' — all sent to AI',
'status_urgent' => '⚠️ {n} urgent: {items}',
'status_evaluate_ingredients' => '🧠 Evaluating available ingredients...',
'status_preparing_recipe' => '👨‍🍳 Preparing recipe...',
'status_recipe_with' => '🥘 Recipe with {a} and {b}',
'status_variant' => ' — variation #{n}',
'status_dish_based_on' => '🎯 Dish based on {type}',
'status_creating_full_recipe' => '✍️ Creating full recipe...',
'status_quota_wait' => '⏳ TPM quota reached ({model}), waiting {s}s... (attempt {a}/{m})',
'status_retry_generation' => '✍️ Retrying generation...',
'status_switch_model' => '🔄 Switching model → {model}...',
'error_pantry_empty' => 'Pantry is empty!',
'error_gemini_api' => 'Gemini API error',
'error_cannot_generate' => 'Unable to generate recipe',
'error_empty_reply' => 'Empty response from Gemini',
'prompt_lang_rule' => 'IMPORTANT: write all textual recipe fields in English only. Do not use Italian or German.',
'prompt_step_example' => 'Step 1…',
],
'de' => [
'status_analyze_pantry' => '📦 Vorrat wird analysiert...',
'status_products_found' => '{n} Produkte gefunden',
'status_passed_ai' => ' ({n} an die KI gesendet)',
'status_all_passed_ai' => ' — alle an die KI gesendet',
'status_urgent' => '⚠️ {n} dringend: {items}',
'status_evaluate_ingredients' => '🧠 Verfuegbare Zutaten werden bewertet...',
'status_preparing_recipe' => '👨‍🍳 Rezept wird vorbereitet...',
'status_recipe_with' => '🥘 Rezept mit {a} und {b}',
'status_variant' => ' — Variante #{n}',
'status_dish_based_on' => '🎯 Gericht auf Basis von {type}',
'status_creating_full_recipe' => '✍️ Vollstaendiges Rezept wird erstellt...',
'status_quota_wait' => '⏳ TPM-Limit erreicht ({model}), warte {s}s... (Versuch {a}/{m})',
'status_retry_generation' => '✍️ Generierung wird erneut versucht...',
'status_switch_model' => '🔄 Modellwechsel → {model}...',
'error_pantry_empty' => 'Die Vorratskammer ist leer!',
'error_gemini_api' => 'Gemini-API-Fehler',
'error_cannot_generate' => 'Rezept konnte nicht erstellt werden',
'error_empty_reply' => 'Leere Antwort von Gemini',
'prompt_lang_rule' => 'WICHTIG: schreibe alle textuellen Rezeptfelder nur auf Deutsch. Verwende kein Italienisch oder Englisch.',
'prompt_step_example' => 'Schritt 1…',
],
];
$text = $dict[$lang][$key] ?? $dict['it'][$key] ?? $key;
foreach ($vars as $name => $value) {
$text = str_replace('{' . $name . '}', (string)$value, $text);
}
return $text;
}
// ===== RECIPE GENERATION WITH GEMINI =====
function generateRecipe(PDO $db): void {
$apiKey = env('GEMINI_API_KEY');
@@ -2212,6 +2301,8 @@ function generateRecipe(PDO $db): void {
}
$input = json_decode(file_get_contents('php://input'), true);
$lang = recipeNormalizeLang($input['lang'] ?? 'it');
$recipeLangName = recipeLangName($lang);
$mealType = $input['meal'] ?? 'pranzo';
$persons = max(1, intval($input['persons'] ?? 1));
$subType = $input['sub_type'] ?? '';
@@ -2235,7 +2326,7 @@ function generateRecipe(PDO $db): void {
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($items)) {
echo json_encode(['success' => false, 'error' => 'La dispensa è vuota!']);
echo json_encode(['success' => false, 'error' => recipeText($lang, 'error_pantry_empty')]);
return;
}
@@ -2556,8 +2647,11 @@ function generateRecipe(PDO $db): void {
}
}
$promptLanguageRule = recipeText($lang, 'prompt_lang_rule');
$promptStepExample = recipeText($lang, 'prompt_step_example');
$prompt = <<<PROMPT
Sei uno chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando gli ingredienti disponibili sotto.
You are an expert home chef. Generate ONE recipe for $mealLabel for $persons person(s) using the available ingredients below.
{$extraRulesText}{$appliancesText}{$dietaryText}{$subTypeText}{$mealPlanText}{$varietyText}{$regenText}{$mustUseText}
REGOLE:
@@ -2567,12 +2661,14 @@ REGOLE:
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0.
5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`). Keep `meal` unchanged.
DISPENSA:
$ingredientsText
Rispondi SOLO JSON valido (no markdown):
{"title":"…","meal":"$mealType","persons":$persons,"prep_time":"…","cook_time":"…","tags":["…"],"expiry_note":"…","ingredients":[{"name":"…","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["Passo 1…"],"nutrition_note":"…"}
{$promptLanguageRule}
{"title":"…","meal":"$mealType","persons":$persons,"prep_time":"…","cook_time":"…","tags":["…"],"expiry_note":"…","ingredients":[{"name":"…","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":"…"}
PROMPT;
$payload = [
@@ -2594,7 +2690,7 @@ PROMPT;
if ($httpCode !== 200) {
$errDetail = $result['data']['error']['message'] ?? substr($result['body'], 0, 300);
echo json_encode(['success' => false, 'error' => 'Errore API Gemini', 'http_code' => $httpCode, 'detail' => $errDetail]);
echo json_encode(['success' => false, 'error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => $errDetail]);
return;
}
@@ -2801,7 +2897,7 @@ PROMPT;
echo json_encode(['success' => true, 'recipe' => $recipe]);
} else {
echo json_encode(['success' => false, 'error' => 'Impossibile generare la ricetta', 'raw' => $text]);
echo json_encode(['success' => false, 'error' => recipeText($lang, 'error_cannot_generate'), 'raw' => $text]);
}
}
@@ -2825,6 +2921,8 @@ function generateRecipeStream(PDO $db): void {
if (empty($apiKey)) { $send('error', ['error' => 'no_api_key']); return; }
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$lang = recipeNormalizeLang($input['lang'] ?? 'it');
$recipeLangName = recipeLangName($lang);
$mealType = $input['meal'] ?? 'pranzo';
$persons = max(1, intval($input['persons'] ?? 1));
$subType = $input['sub_type'] ?? '';
@@ -2837,7 +2935,7 @@ function generateRecipeStream(PDO $db): void {
$rejectedIngredients = $input['rejected_ingredients'] ?? [];
// ── AGENTE PASSO 1: Analisi dispensa ─────────────────────────────────────
$send('status', ['step' => 1, 'message' => '📦 Analizzo la dispensa...']);
$send('status', ['step' => 1, 'message' => recipeText($lang, 'status_analyze_pantry')]);
$stmt = $db->query("
SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, i.opened_at,
@@ -2849,7 +2947,7 @@ function generateRecipeStream(PDO $db): void {
");
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($items)) { $send('error', ['error' => 'La dispensa è vuota!']); return; }
if (empty($items)) { $send('error', ['error' => recipeText($lang, 'error_pantry_empty')]); return; }
$getItemPriority = function($item): int {
$daysLeft = floatval($item['days_left']);
@@ -2917,13 +3015,13 @@ function generateRecipeStream(PDO $db): void {
$urgentNames = array_slice(array_map(
fn($l) => trim(preg_replace('/\s[\[\x{26A0}\x{1F534}\x{1F7E0}].*/u', '', explode(':', ltrim($l, '- '))[0])),
$urgentRaw), 0, 3);
$send('status', ['step' => 1, 'message' => "⚠️ {$urgentCount} urgenti: " . implode(', ', $urgentNames)]);
$send('status', ['step' => 1, 'message' => recipeText($lang, 'status_urgent', ['n' => $urgentCount, 'items' => implode(', ', $urgentNames)])]);
} else {
$countMsg = count($items) . ' prodotti trovati';
$countMsg = recipeText($lang, 'status_products_found', ['n' => count($items)]);
if ($hasMealPlan && $totalIngredientsSent < count($items)) {
$countMsg .= " ({$totalIngredientsSent} passati all'AI)";
$countMsg .= recipeText($lang, 'status_passed_ai', ['n' => $totalIngredientsSent]);
} elseif ($hasMealPlan) {
$countMsg .= ' — tutti passati all\'AI';
$countMsg .= recipeText($lang, 'status_all_passed_ai');
}
$send('status', ['step' => 1, 'message' => '✅ ' . $countMsg]);
}
@@ -3041,7 +3139,7 @@ function generateRecipeStream(PDO $db): void {
// ── AGENTE PASSO 2: Selezione concetto (locale, nessuna chiamata AI) ────────
// Determina il concetto della ricetta in base agli ingredienti disponibili
// e ai parametri selezionati — senza consumare quote Gemini.
$send('status', ['step' => 2, 'message' => "🧠 Valuto gli ingredienti disponibili..."]);
$send('status', ['step' => 2, 'message' => recipeText($lang, 'status_evaluate_ingredients')]);
// Raccoglie i nomi degli ingredienti di maggiore priorità
$conceptIngredients = [];
@@ -3057,11 +3155,11 @@ function generateRecipeStream(PDO $db): void {
}
// Costruisce un messaggio di stato informativo basato su ciò che verrà cucinato
$conceptMsg = '👨‍🍳 Preparo la ricetta...';
$conceptMsg = recipeText($lang, 'status_preparing_recipe');
if (!empty($mealPlanType) && isset($mealPlanTypeLabels[$mealPlanType]) && $mealPlanTypeLabels[$mealPlanType] !== '') {
// Tipo di pasto dal piano settimanale — mostra la categoria
$shortLabel = explode(' (', $mealPlanTypeLabels[$mealPlanType])[0];
$conceptMsg = "🎯 Piatto a base di {$shortLabel}";
$conceptMsg = recipeText($lang, 'status_dish_based_on', ['type' => $shortLabel]);
// Aggiungi l'ingrediente principale se disponibile
if (!empty($matchingItems)) {
$firstMatch = ltrim(reset($matchingItems), '→ ');
@@ -3071,8 +3169,10 @@ function generateRecipeStream(PDO $db): void {
} elseif (!empty($conceptIngredients)) {
// Mostra i primi 2 ingredienti più urgenti
$shown = array_slice($conceptIngredients, 0, 2);
$conceptMsg = "🥘 Ricetta con " . implode(' e ', array_map('mb_strtolower', $shown));
if ($variation > 0) $conceptMsg .= " — variante #{$variation}";
$a = mb_strtolower($shown[0] ?? '');
$b = mb_strtolower($shown[1] ?? '');
$conceptMsg = recipeText($lang, 'status_recipe_with', ['a' => $a, 'b' => $b]);
if ($variation > 0) $conceptMsg .= recipeText($lang, 'status_variant', ['n' => $variation]);
} elseif (!empty($subType) && !empty($subTypeLabels[$mealType][$subType])) {
$conceptMsg = "🎨 " . explode(' (', $subTypeLabels[$mealType][$subType])[0];
}
@@ -3080,10 +3180,12 @@ function generateRecipeStream(PDO $db): void {
// ── AGENTE PASSO 3: Generazione ricetta (A+C: retry SSE-aware + fallback modello) ──
$conceptHint = '';
$send('status', ['step' => 3, 'message' => '✍️ Creo la ricetta completa...']);
$send('status', ['step' => 3, 'message' => recipeText($lang, 'status_creating_full_recipe')]);
$promptLanguageRule = recipeText($lang, 'prompt_lang_rule');
$promptStepExample = recipeText($lang, 'prompt_step_example');
$prompt = <<<PROMPT
Sei uno chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando gli ingredienti disponibili sotto.{$extraRulesText}{$appliancesText}{$dietaryText}{$subTypeText}{$mealPlanText}{$varietyText}{$regenText}{$mustUseText}
You are an expert home chef. Generate ONE recipe for $mealLabel for $persons person(s) using the available ingredients below.{$extraRulesText}{$appliancesText}{$dietaryText}{$subTypeText}{$mealPlanText}{$varietyText}{$regenText}{$mustUseText}
REGOLE:
{$mealPlanRule}1. PRIORITÀ: usa prima gli ingredienti scaduti/in scadenza (⚠️🔴🟠), poi quelli [APERTO], poi il resto.
@@ -3092,12 +3194,14 @@ REGOLE:
4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0.
5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`). Keep `meal` unchanged.
DISPENSA:
$ingredientsText
Rispondi SOLO JSON valido (no markdown):
{"title":"…","meal":"$mealType","persons":$persons,"prep_time":"…","cook_time":"…","tags":["…"],"expiry_note":"…","ingredients":[{"name":"…","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["Passo 1…"],"nutrition_note":"…"}
{$promptLanguageRule}
{"title":"…","meal":"$mealType","persons":$persons,"prep_time":"…","cook_time":"…","tags":["…"],"expiry_note":"…","ingredients":[{"name":"…","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":"…"}
PROMPT;
$genConfig = [
@@ -3171,15 +3275,15 @@ PROMPT;
// A: feedback live con countdown
$modelName = str_replace('gemini-', 'Gemini ', $model);
$send('status', ['step' => 3, 'message' => "⏳ Quota TPM esaurita ({$modelName}), attendo {$waitSec}s... (tentativo {$attempt}/{$maxRetries})"]);
$send('status', ['step' => 3, 'message' => recipeText($lang, 'status_quota_wait', ['model' => $modelName, 's' => $waitSec, 'a' => $attempt, 'm' => $maxRetries])]);
sleep($waitSec);
$send('status', ['step' => 3, 'message' => '✍️ Riprovo la generazione...']);
$send('status', ['step' => 3, 'message' => recipeText($lang, 'status_retry_generation')]);
}
// C: se primario esaurito dopo tutti i retry, cambia modello immediatamente
if ($httpCode === 429 && $modelIdx === 0) {
$fallbackName = str_replace('gemini-', 'Gemini ', $models[1]);
$send('status', ['step' => 3, 'message' => "🔄 Cambio modello → {$fallbackName}..."]);
$send('status', ['step' => 3, 'message' => recipeText($lang, 'status_switch_model', ['model' => $fallbackName])]);
continue;
}
break;
@@ -3187,7 +3291,7 @@ PROMPT;
if ($httpCode !== 200) {
$errDetail = $result['data']['error']['message'] ?? substr($result['body'], 0, 300);
$send('error', ['error' => 'Errore API Gemini', 'http_code' => $httpCode, 'detail' => $errDetail]);
$send('error', ['error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => $errDetail]);
return;
}
@@ -3198,7 +3302,7 @@ PROMPT;
$recipe = json_decode($text, true);
if (!$recipe || empty($recipe['title'])) {
$send('error', ['error' => 'Impossibile generare la ricetta', 'raw' => $text]);
$send('error', ['error' => recipeText($lang, 'error_cannot_generate'), 'raw' => $text]);
return;
}