Merge develop: i18n completion for recipes and meal plan

This commit is contained in:
dadaloop82
2026-04-28 17:29:25 +00:00
9 changed files with 906 additions and 315 deletions
+6
View File
@@ -12,6 +12,12 @@
---
## 🌍 Recent i18n Updates
- Recipe and meal-plan labels now resolve at runtime from translations, preventing raw placeholders like `meal_types.*` and `meal_plan_types.*` from appearing in the UI.
- Recipe generation now receives the selected app language (`it`/`en`/`de`) and enforces localized output in both streaming and non-streaming API flows.
- Added missing shared error keys (`error.network`, `error.no_api_key`) across all language files to keep fallback/error toasts fully translated.
## ✨ Features
### 📦 Inventory Management
+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;
}
+264 -216
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925}
{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925,"a_191_1":1777300414,"a_183_291":1777310462,"a_213_150":1777378506,"a_183_290":1777380000}
+1
View File
@@ -0,0 +1 @@
{"ts":1777391782}
+43 -43
View File
@@ -167,25 +167,25 @@
<div class="barcode-manual-entry">
<div class="barcode-input-row">
<input type="text" id="manual-barcode-input" class="form-input" placeholder="Inserisci codice a barre..." inputmode="numeric" pattern="[0-9]*" onkeydown="if(event.key==='Enter')submitManualBarcode()" data-i18n-placeholder="scan.barcode_placeholder">
<button class="btn btn-primary" onclick="submitManualBarcode()">🔍 Cerca</button>
<button class="btn btn-primary" onclick="submitManualBarcode()" data-i18n="btn.search">🔍 Cerca</button>
</div>
</div>
<div class="quick-name-entry">
<div class="quick-name-divider"><span>oppure scrivi il nome</span></div>
<div class="quick-name-divider"><span data-i18n="scan.quick_name_divider">oppure scrivi il nome</span></div>
<div class="barcode-input-row">
<input type="text" id="quick-product-name" class="form-input" placeholder="Es: Mele, Zucchine, Pane..." list="common-products" autocomplete="off" onkeydown="if(event.key==='Enter')submitQuickName()">
<button class="btn btn-accent" onclick="submitQuickName()">✅ Vai</button>
<input type="text" id="quick-product-name" class="form-input" placeholder="Es: Mele, Zucchine, Pane..." list="common-products" autocomplete="off" onkeydown="if(event.key==='Enter')submitQuickName()" data-i18n-placeholder="scan.quick_name_placeholder">
<button class="btn btn-accent" onclick="submitQuickName()" data-i18n="btn.go">✅ Vai</button>
</div>
</div>
<div class="scan-actions">
<button class="btn btn-large btn-secondary" onclick="startManualEntry()">
<button class="btn btn-large btn-secondary" onclick="startManualEntry()" data-i18n="scan.manual_entry">
✏️ Inserimento Manuale
</button>
<button class="btn btn-large btn-accent" onclick="captureForAI()">
<button class="btn btn-large btn-accent" onclick="captureForAI()" data-i18n="scan.ai_identify">
🤖 Identifica con AI
</button>
</div>
<p class="scan-hint">Scansiona il barcode, scrivi il nome del prodotto, oppure usa l'AI per identificarlo</p>
<p class="scan-hint" data-i18n="scan.hint">Scansiona il barcode, scrivi il nome del prodotto, oppure usa l'AI per identificarlo</p>
<div id="scan-debug-log" style="display:none;margin-top:12px;padding:10px;background:#1a1a2e;color:#0f0;font-family:monospace;font-size:0.7rem;max-height:200px;overflow-y:auto;border-radius:8px;white-space:pre-wrap"></div>
<button class="btn btn-small btn-secondary" style="margin-top:8px;opacity:0.5" onclick="toggleScanDebug()">🐛 Debug Log</button>
</div>
@@ -194,8 +194,8 @@
<!-- ===== PRODUCT ACTION (IN/OUT after scan) ===== -->
<section class="page" id="page-action">
<div class="page-header">
<button class="back-btn" id="action-back-btn" onclick="showPage('scan')">← Indietro</button>
<h2>Cosa vuoi fare?</h2>
<button class="back-btn" id="action-back-btn" onclick="showPage('scan')" data-i18n="btn.back">← Indietro</button>
<h2 data-i18n="action.title">Cosa vuoi fare?</h2>
</div>
<!-- Banner: shopping list scan context -->
<div id="shopping-scan-target-banner" class="shopping-scan-target-banner" style="display:none"></div>
@@ -560,10 +560,10 @@
<!-- Tab navigation -->
<div class="shopping-tabs" id="shopping-tabs" style="display:none">
<button class="shopping-tab active" id="tab-acquisto" onclick="switchShoppingTab('acquisto')">
🛍️ Da comprare <span class="shopping-tab-count" id="tab-count-acquisto">0</span>
<span data-i18n="shopping.tab_to_buy">🛍️ Da comprare</span> <span class="shopping-tab-count" id="tab-count-acquisto">0</span>
</button>
<button class="shopping-tab" id="tab-previsione" onclick="switchShoppingTab('previsione')">
🧠 In previsione <span class="shopping-tab-count" id="tab-count-previsione">0</span>
<span data-i18n="shopping.tab_forecast">🧠 In previsione</span> <span class="shopping-tab-count" id="tab-count-previsione">0</span>
</button>
</div>
@@ -579,31 +579,31 @@
</div>
<div class="shopping-current" id="shopping-current" style="display:none">
<div class="shopping-section-header">
<h3>🛍️ Da comprare</h3>
<h3 data-i18n="shopping.section_to_buy">🛍️ Da comprare</h3>
<span class="shopping-count" id="shopping-count">0</span>
</div>
<div class="shopping-items" id="shopping-items"></div>
</div>
<div class="shopping-suggestions" id="shopping-suggestions" style="display:none">
<div class="shopping-section-header">
<h3>💡 Suggerimenti AI</h3>
<h3 data-i18n="shopping.suggestions_title">💡 Suggerimenti AI</h3>
</div>
<div class="seasonal-tip" id="seasonal-tip" style="display:none"></div>
<div class="suggestion-items" id="suggestion-items"></div>
<div class="suggestion-actions" id="suggestion-actions" style="display:none">
<button class="btn btn-success" onclick="addSelectedSuggestions()">
<button class="btn btn-success" onclick="addSelectedSuggestions()" data-i18n="shopping.bring_add_selected">
✅ Aggiungi selezionati a Bring!
</button>
</div>
</div>
<div class="shopping-actions">
<button class="btn btn-large btn-accent" onclick="searchAllPrices()" id="btn-search-prices">
<button class="btn btn-large btn-accent" onclick="searchAllPrices()" id="btn-search-prices" data-i18n="shopping.search_prices">
🔍 Cerca tutti i prezzi
</button>
<button class="btn btn-large btn-accent" onclick="generateSuggestions()" id="btn-suggest">
<button class="btn btn-large btn-accent" onclick="generateSuggestions()" id="btn-suggest" data-i18n="shopping.suggest_btn">
🤖 Suggerisci cosa comprare
</button>
<button class="btn btn-secondary" onclick="forceSyncBring()" style="margin-top:4px">
<button class="btn btn-secondary" onclick="forceSyncBring()" style="margin-top:4px" data-i18n="shopping.force_sync">
🔄 Forza sincronizzazione Bring!
</button>
</div>
@@ -616,32 +616,32 @@
<div class="smart-shopping-empty" id="smart-shopping-empty" style="display:none">
<div class="empty-state" style="padding:30px">
<div class="empty-state-icon">🧠</div>
<p>Nessuna previsione disponibile.<br>Aggiungi prodotti alla dispensa per ricevere previsioni intelligenti.</p>
<p data-i18n-html="shopping.smart_empty">Nessuna previsione disponibile.<br>Aggiungi prodotti alla dispensa per ricevere previsioni intelligenti.</p>
</div>
</div>
<div id="smart-shopping-content">
<div class="shopping-section-header" style="margin-bottom:4px">
<h3>🧠 Previsioni intelligenti</h3>
<h3 data-i18n="shopping.smart_title">🧠 Previsioni intelligenti</h3>
<span class="shopping-count" id="smart-count">0</span>
</div>
<div class="smart-last-update-row">
<span id="smart-last-update" class="smart-last-update"></span>
</div>
<div class="smart-filter-row" id="smart-filter-row">
<button class="smart-filter active" data-filter="all" onclick="filterSmart('all')">Tutti</button>
<button class="smart-filter" data-filter="critical" onclick="filterSmart('critical')">🔴 Urgenti</button>
<button class="smart-filter" data-filter="high" onclick="filterSmart('high')">🟠 Presto</button>
<button class="smart-filter" data-filter="medium" onclick="filterSmart('medium')">🟡 Pianifica</button>
<button class="smart-filter" data-filter="low" onclick="filterSmart('low')">🟢 Previsione</button>
<button class="smart-filter active" data-filter="all" onclick="filterSmart('all')" data-i18n="shopping.smart_filter_all">Tutti</button>
<button class="smart-filter" data-filter="critical" onclick="filterSmart('critical')" data-i18n="shopping.smart_filter_critical">🔴 Urgenti</button>
<button class="smart-filter" data-filter="high" onclick="filterSmart('high')" data-i18n="shopping.smart_filter_high">🟠 Presto</button>
<button class="smart-filter" data-filter="medium" onclick="filterSmart('medium')" data-i18n="shopping.smart_filter_medium">🟡 Pianifica</button>
<button class="smart-filter" data-filter="low" onclick="filterSmart('low')" data-i18n="shopping.smart_filter_low">🟢 Previsione</button>
</div>
<div class="smart-items" id="smart-items"></div>
<div class="smart-actions" id="smart-actions" style="display:none">
<button class="btn btn-success full-width" onclick="addSmartToBring()">
<button class="btn btn-success full-width" onclick="addSmartToBring()" data-i18n="shopping.smart_add">
🛒 Aggiungi selezionati a Bring!
</button>
</div>
<div style="text-align:center;margin-top:8px">
<button class="btn btn-secondary btn-sm" onclick="forceSyncBring()" id="btn-force-sync">
<button class="btn btn-secondary btn-sm" onclick="forceSyncBring()" id="btn-force-sync" data-i18n="shopping.force_sync">
🔄 Forza sincronizzazione Bring!
</button>
</div>
@@ -1161,16 +1161,16 @@
<div class="modal-content recipe-dialog" onclick="event.stopPropagation()">
<div id="recipe-mealplan-banner" class="recipe-mealplan-banner" style="display:none"></div>
<div id="recipe-ask" class="recipe-ask">
<h3 id="recipe-meal-title">🍳 Ricetta</h3>
<p class="recipe-desc">Genero una ricetta sana con gli ingredienti in dispensa, dando priorità a quelli in scadenza.</p>
<h3 id="recipe-meal-title" data-i18n="recipes.dialog_title">🍳 Ricetta</h3>
<p class="recipe-desc" data-i18n="recipes.dialog_desc">Genero una ricetta sana con gli ingredienti in dispensa, dando priorità a quelli in scadenza.</p>
<div class="form-group" style="text-align:left">
<label>🕐 Per quale pasto?</label>
<label data-i18n="recipes.meal_label">🕐 Per quale pasto?</label>
<div class="recipe-meal-grid" id="recipe-meal-grid" onchange="updateRecipeMealTitle()"></div>
<div class="recipe-meal-grid recipe-subtype-grid" id="recipe-subtype-group" style="display:none"></div>
</div>
<div id="recipe-mealplan-hint" class="recipe-mealplan-hint" style="display:none"></div>
<div class="form-group">
<label>👥 Quante persone?</label>
<label data-i18n="recipes.persons_label">👥 Quante persone?</label>
<div class="qty-control">
<button type="button" class="qty-btn" onclick="adjustRecipePersons(-1)"></button>
<input type="number" id="recipe-persons" value="1" min="1" max="20" class="qty-input">
@@ -1178,37 +1178,37 @@
</div>
</div>
<div class="form-group" style="text-align:left">
<label>🎯 Tipo di pasto</label>
<label data-i18n="recipes.meal_type_label">🎯 Tipo di pasto</label>
<div class="recipe-options-grid">
<label class="recipe-option-chip recipe-opt-mealplan-chip" id="recipe-opt-mealplan-wrap" style="display:none"><input type="checkbox" id="recipe-opt-mealplan" checked onchange="onMealPlanChipChange(this)"> <span id="recipe-opt-mealplan-label"></span></label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-veloce"> ⚡ Pasto Veloce</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-pocafame"> 🥗 Poca Fame</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-scadenze"> ⏰ Priorità Scadenze</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-healthy"> 💚 Extra Salutare</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-opened"> 📦 Priorità Cose Aperte</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-zerowaste"> ♻️ Zero Sprechi</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-veloce"> <span data-i18n="recipes.opt_fast">⚡ Pasto Veloce</span></label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-pocafame"> <span data-i18n="recipes.opt_light">🥗 Poca Fame</span></label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-scadenze"> <span data-i18n="recipes.opt_expiry">⏰ Priorità Scadenze</span></label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-healthy"> <span data-i18n="recipes.opt_healthy">💚 Extra Salutare</span></label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-opened"> <span data-i18n="recipes.opt_opened">📦 Priorità Cose Aperte</span></label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-zerowaste"> <span data-i18n="recipes.opt_zero_waste">♻️ Zero Sprechi</span></label>
</div>
</div>
<button class="btn btn-large btn-success full-width" onclick="generateRecipe()">
<button class="btn btn-large btn-success full-width" onclick="generateRecipe()" data-i18n="recipes.generate_btn">
✨ Genera Ricetta
</button>
<button class="btn btn-large btn-secondary full-width mt-2" onclick="closeRecipeDialog()">
<button class="btn btn-large btn-secondary full-width mt-2" onclick="closeRecipeDialog()" data-i18n="btn.cancel">
Annulla
</button>
</div>
<div id="recipe-loading" style="display:none" class="recipe-loading">
<div class="loading-spinner"></div>
<p id="recipe-loading-msg">Sto preparando la ricetta...</p>
<p id="recipe-loading-msg" data-i18n="recipes.loading_msg">Sto preparando la ricetta...</p>
</div>
<div id="recipe-result" style="display:none" class="recipe-result">
<div id="recipe-content"></div>
<button class="btn btn-large btn-cooking full-width mt-2" onclick="startCookingMode()">
<button class="btn btn-large btn-cooking full-width mt-2" onclick="startCookingMode()" data-i18n="recipes.start_cooking">
👨‍🍳 Modalità Cucina
</button>
<button class="btn btn-large btn-secondary full-width mt-2" onclick="regenerateRecipe()">
<button class="btn btn-large btn-secondary full-width mt-2" onclick="regenerateRecipe()" data-i18n="recipes.regenerate">
🔄 Generane un'altra
</button>
<button class="btn btn-large btn-primary full-width mt-2" onclick="closeRecipeDialog()">
<button class="btn btn-large btn-primary full-width mt-2" onclick="closeRecipeDialog()" data-i18n="recipes.close_btn">
✅ Chiudi
</button>
</div>
+152 -8
View File
@@ -169,7 +169,9 @@
"hint": "Barcode scannen, Produktname eingeben oder KI zur Identifikation nutzen",
"debug_toggle": "🐛 Debug Log",
"barcode_acquired": "🔖 Barcode gescannt: {code}",
"scan_barcode": "🔖 Barcode scannen"
"scan_barcode": "🔖 Barcode scannen",
"create_named": "{name} erstellen",
"new_without_barcode": "Neues Produkt ohne Barcode"
},
"action": {
"title": "Was möchtest du tun?",
@@ -212,7 +214,11 @@
"title": "Verwenden / Verbrauchen",
"location_label": "📍 Woher?",
"quantity_label": "Wie viel hast du benutzt?",
"change": "ändern",
"partial_hint": "Oder genaue Menge angeben:",
"partial_piece_hint": "Hast du nur einen Teil verwendet?",
"piece": "Stück",
"one_whole": "1 ganzes",
"use_all": "🗑️ ALLES verwendet / Aufgebraucht",
"submit": "📤 Diese Menge verwenden",
"available": "📦 Verfügbar:",
@@ -272,7 +278,42 @@
},
"recipes": {
"title": "🍳 Rezepte",
"generate": "✨ Neues Rezept generieren"
"generate": "✨ Neues Rezept generieren",
"archive_empty": "Keine Rezepte gespeichert. Erstelle dein erstes Rezept!",
"dialog_title": "🍳 Rezept",
"dialog_desc": "Ich erstelle ein gesundes Rezept mit Zutaten aus dem Vorrat und priorisiere Produkte mit nahendem Ablaufdatum.",
"meal_label": "🕐 Für welche Mahlzeit?",
"persons_label": "👥 Für wie viele Personen?",
"meal_type_label": "🎯 Art der Mahlzeit",
"opt_fast": "⚡ Schnelle Mahlzeit",
"opt_light": "🥗 Kleiner Hunger",
"opt_expiry": "⏰ Ablaufdaten priorisieren",
"opt_healthy": "💚 Extra gesund",
"opt_opened": "📦 Geöffnete Produkte priorisieren",
"opt_zero_waste": "♻️ Zero Waste",
"generate_btn": "✨ Rezept generieren",
"loading_msg": "Rezept wird vorbereitet...",
"start_cooking": "👨‍🍳 Kochmodus",
"regenerate": "🔄 Noch eins generieren",
"close_btn": "✅ Schließen",
"ingredients_title": "🧾 Zutaten",
"steps_title": "👨‍🍳 Zubereitung",
"no_steps": "Keine Zubereitungsschritte verfügbar",
"generate_error": "Fehler bei der Generierung",
"persons_short": "Pers.",
"use_ingredient_title": "Zutat verwenden",
"recipe_qty_label": "Rezept",
"from_where_label": "Von wo?",
"amount_label": "Wie viel",
"use_amount_btn": "Diese Menge verwenden",
"use_all_btn": "ALLES verwenden / Aufgebraucht",
"packs_label": "Packungen",
"quantity_in_total": "Menge in {unit} (gesamt: {total})",
"packs_of_have": "Packungen à {size} (du hast {count} Pack.)",
"scale_wait_stable": "10s stabiles Gewicht für Auto-Ausfüllen abwarten…",
"ingredient_scaled_toast": "📦 Zutat vom Vorrat abgezogen!",
"finished_added_bring_toast": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt!",
"load_error": "Fehler beim Laden"
},
"shopping": {
"title": "🛒 Einkaufsliste",
@@ -326,7 +367,37 @@
"smart_already_predicted": "📊 Einkauf wird bereits vorhergesagt: <strong>{name}</strong>{urgency}.",
"item_removed": "✅ {name} von der Liste entfernt!",
"urgency_spec_critical": "⚡ Dringend",
"urgency_spec_high": "🟠 Bald"
"urgency_spec_high": "🟠 Bald",
"bring_add_n": "{n} zu Bring! hinzufügen",
"bring_add_selected": "Ausgewählte zu Bring! hinzufügen",
"bring_adding": "Wird hinzugefügt...",
"bring_added_one": "1 Produkt zu Bring! hinzugefügt",
"bring_added_many": "{n} Produkte zu Bring! hinzugefügt",
"bring_skipped": "({n} bereits in Liste)",
"force_sync": "Bring!-Synchronisierung erzwingen",
"scan_target_label": "Du suchst",
"scan_target_found": "Gefunden! Aus Liste entfernen",
"bring_add_one": "1 Produkt zu Bring! hinzufügen",
"bring_add_many": "{n} Produkte zu Bring! hinzufügen",
"syncing": "Synchronisiere…",
"sync_done": "Synchronisierung abgeschlossen",
"price_searching": "Suche...",
"search_action": "Suchen",
"open_action": "Öffnen",
"not_found": "Nicht gefunden",
"search_price": "Preis suchen",
"tap_to_scan": "Zum Scannen tippen",
"tag_title": "Tag",
"remove_title": "Entfernen",
"found_count": "{found}/{total} Produkte gefunden",
"savings_offers": "· 🏷️ Du sparst €{amount} mit Angeboten",
"searching_progress": "Suche {current}/{total}...",
"remove_error": "Fehler beim Entfernen",
"suggest_loading": "Analyse läuft...",
"suggest_error": "Fehler bei der Vorschlagserstellung",
"priority_high": "Hoch",
"priority_medium": "Mittel",
"priority_low": "Niedrig"
},
"ai": {
"title": "🤖 KI-Identifikation",
@@ -386,7 +457,9 @@
"ingredient_deduct_title": "Von Vorrat abziehen",
"timer_expired_tts": "Timer {label} abgelaufen!",
"timer_warning_tts": "Achtung! {label}: noch 10 Sekunden!",
"recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!"
"recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!",
"expires_chip": "läuft ab {date}",
"finish": "✅ Fertig"
},
"settings": {
"title": "⚙️ Einstellungen",
@@ -460,7 +533,18 @@
"ai_prompt_label": "🤖 KI-Produktauswahl Prompt",
"ai_prompt_placeholder": "Anweisungen für die KI bei der Auswahl zwischen mehreren Produkten...",
"ai_prompt_hint": "Die KI verwendet diesen Prompt zur Auswahl des passendsten Produkts. Leer lassen für Standardverhalten.",
"configure_first": "Konfiguriere zuerst den Online-Einkauf in den Einstellungen"
"configure_first": "Konfiguriere zuerst den Online-Einkauf in den Einstellungen",
"missing_credentials": "E-Mail und Passwort eingeben",
"login_in_progress": "Anmeldung läuft...",
"login_error_prefix": "Fehler:",
"login_network_error_prefix": "Netzwerkfehler:",
"login_success_default": "Anmeldung erfolgreich!",
"result_name_label": "Name",
"result_card_label": "Karte",
"result_pickup_label": "Abholpunkt",
"result_points_label": "Treuepunkte",
"connected_relogin": "✅ Verbunden — Erneut anmelden",
"connected_as": "Verbunden als {name}"
},
"camera": {
"title": "📷 Kamera",
@@ -592,6 +676,8 @@
},
"error": {
"generic": "Fehler",
"network": "Netzwerkfehler",
"no_api_key": "API-Schluessel in den Einstellungen konfigurieren",
"loading": "Fehler beim Laden des Produkts",
"not_found": "Produkt nicht gefunden",
"not_found_manual": "Produkt nicht gefunden. Manuell eingeben.",
@@ -610,7 +696,8 @@
"not_in_inventory": "Produkt nicht im Bestand",
"appliance_exists": "Gerät bereits vorhanden",
"already_exists": "Bereits vorhanden",
"network_retry": "Verbindungsfehler. Erneut versuchen."
"network_retry": "Verbindungsfehler. Erneut versuchen.",
"select_items": "Wähle mindestens ein Produkt aus"
},
"confirm": {
"remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?",
@@ -632,11 +719,24 @@
"thu": "Donnerstag",
"fri": "Freitag",
"sat": "Samstag",
"sun": "Sonntag"
"sun": "Sonntag",
"mon_short": "Mo",
"tue_short": "Di",
"wed_short": "Mi",
"thu_short": "Do",
"fri_short": "Fr",
"sat_short": "Sa",
"sun_short": "So"
},
"meal_types": {
"lunch": "Mittagessen",
"dinner": "Abendessen"
"dinner": "Abendessen",
"colazione": "Frühstück",
"merenda": "Nachmittagssnack",
"dolce": "Dessert",
"succo": "Fruchtsaft",
"pranzo": "Mittagessen",
"cena": "Abendessen"
},
"scale": {
"status_connected": "Waage verbunden",
@@ -709,5 +809,49 @@
"2": "Kulinarische Zutat",
"3": "Verarbeitet",
"4": "Hochverarbeitet"
},
"meal_plan_types": {
"pasta": "Pasta",
"riso": "Reis",
"carne": "Fleisch",
"pesce": "Fisch",
"legumi": "Hülsenfrüchte",
"uova": "Eier",
"formaggio": "Käse",
"pizza": "Pizza",
"affettati": "Aufschnitt",
"verdure": "Gemüse",
"zuppa": "Suppe",
"insalata": "Salat",
"pane": "Brot/Sandwich",
"dolce": "Dessert",
"libero": "Frei"
},
"meal_sub": {
"dolce_torta": "Kuchen",
"dolce_crema": "Creme / Pudding",
"dolce_crumble": "Crumble / Tarte",
"dolce_biscotti": "Kekse / Gebäck",
"dolce_frutta": "Fruchtdessert",
"succo_dolce": "Süß / Fruchtig",
"succo_energizzante": "Energetisierend",
"succo_detox": "Detox / Grün",
"succo_rinfrescante": "Erfrischend",
"succo_vitaminico": "Vitamin / Zitrus"
},
"meal_plan": {
"reset_success": "Wochenplan zurückgesetzt",
"suggested_by": "vom Wochenplan vorgeschlagen"
},
"kiosk_session": {
"first_item": "Erstes Produkt: {name}!",
"items_two_four": "{n} Artikel — Trägheit überwinden 🚀",
"items_five_nine": "{n} Artikel — super Tempo! 💪",
"items_ten_twenty": "{n} Artikel — fast Rekord 🏆",
"items_twenty_plus": "{n} Artikel — epischer Einkauf! 🛒🔥",
"duplicates_one": "1 Duplikat (gleiches Produkt zweimal)",
"duplicates_many": "{n} Duplikate (mehrfach genommen)",
"top_category": "Top-Kategorie: {cat} ({count}×)",
"items_fallback": "{n} Artikel hinzugefügt"
}
}
+152 -8
View File
@@ -169,7 +169,9 @@
"hint": "Scan the barcode, type the product name, or use AI to identify it",
"debug_toggle": "🐛 Debug Log",
"barcode_acquired": "🔖 Barcode scanned: {code}",
"scan_barcode": "🔖 Scan Barcode"
"scan_barcode": "🔖 Scan Barcode",
"create_named": "Create {name}",
"new_without_barcode": "New product without barcode"
},
"action": {
"title": "What do you want to do?",
@@ -212,7 +214,11 @@
"title": "Use / Consume",
"location_label": "📍 From where?",
"quantity_label": "How much did you use?",
"change": "change",
"partial_hint": "Or specify the quantity used:",
"partial_piece_hint": "Did you use only a part?",
"piece": "piece",
"one_whole": "1 whole",
"use_all": "🗑️ Used ALL / Finished",
"submit": "📤 Use this quantity",
"available": "📦 Available:",
@@ -272,7 +278,42 @@
},
"recipes": {
"title": "🍳 Recipes",
"generate": "✨ Generate new recipe"
"generate": "✨ Generate new recipe",
"archive_empty": "No recipes saved. Generate your first recipe!",
"dialog_title": "🍳 Recipe",
"dialog_desc": "I will generate a healthy recipe using pantry ingredients, prioritizing expiring items.",
"meal_label": "🕐 Which meal?",
"persons_label": "👥 How many people?",
"meal_type_label": "🎯 Meal type",
"opt_fast": "⚡ Quick meal",
"opt_light": "🥗 Light appetite",
"opt_expiry": "⏰ Prioritize expiring items",
"opt_healthy": "💚 Extra healthy",
"opt_opened": "📦 Prioritize opened items",
"opt_zero_waste": "♻️ Zero waste",
"generate_btn": "✨ Generate Recipe",
"loading_msg": "Preparing your recipe...",
"start_cooking": "👨‍🍳 Cooking Mode",
"regenerate": "🔄 Generate another one",
"close_btn": "✅ Close",
"ingredients_title": "🧾 Ingredients",
"steps_title": "👨‍🍳 Steps",
"no_steps": "No steps available",
"generate_error": "Generation error",
"persons_short": "serv.",
"use_ingredient_title": "Use ingredient",
"recipe_qty_label": "Recipe",
"from_where_label": "From where?",
"amount_label": "How much",
"use_amount_btn": "Use this amount",
"use_all_btn": "Use ALL / Finished",
"packs_label": "Packs",
"quantity_in_total": "Quantity in {unit} (total: {total})",
"packs_of_have": "Packs of {size} (you have {count} packs)",
"scale_wait_stable": "Wait 10s of stable weight for auto-fill…",
"ingredient_scaled_toast": "📦 Ingredient deducted from pantry!",
"finished_added_bring_toast": "🛒 Finished product → added to Bring!",
"load_error": "Loading error"
},
"shopping": {
"title": "🛒 Shopping List",
@@ -326,7 +367,37 @@
"smart_already_predicted": "📊 Smart shopping already predicts <strong>{name}</strong>{urgency}.",
"item_removed": "✅ {name} removed from list!",
"urgency_spec_critical": "⚡ Urgent",
"urgency_spec_high": "🟠 Soon"
"urgency_spec_high": "🟠 Soon",
"bring_add_n": "Add {n} to Bring!",
"bring_add_selected": "Add selected to Bring!",
"bring_adding": "Adding...",
"bring_added_one": "1 product added to Bring!",
"bring_added_many": "{n} products added to Bring!",
"bring_skipped": "({n} already in list)",
"force_sync": "Force Bring! sync",
"scan_target_label": "You are looking for",
"scan_target_found": "Found! Remove from list",
"bring_add_one": "Add 1 product to Bring!",
"bring_add_many": "Add {n} products to Bring!",
"syncing": "Syncing…",
"sync_done": "Sync completed",
"price_searching": "Searching...",
"search_action": "Search",
"open_action": "Open",
"not_found": "Not found",
"search_price": "Search price",
"tap_to_scan": "Tap to scan",
"tag_title": "Tag",
"remove_title": "Remove",
"found_count": "{found}/{total} products found",
"savings_offers": "· 🏷️ You save €{amount} with offers",
"searching_progress": "Searching {current}/{total}...",
"remove_error": "Removal error",
"suggest_loading": "Analyzing...",
"suggest_error": "Suggestion generation error",
"priority_high": "High",
"priority_medium": "Medium",
"priority_low": "Low"
},
"ai": {
"title": "🤖 AI Identification",
@@ -386,7 +457,9 @@
"ingredient_deduct_title": "Deduct from pantry",
"timer_expired_tts": "Timer {label} expired!",
"timer_warning_tts": "Heads up! {label}: 10 seconds left!",
"recipe_done_tts": "Recipe complete! Enjoy your meal!"
"recipe_done_tts": "Recipe complete! Enjoy your meal!",
"expires_chip": "exp. {date}",
"finish": "✅ Finish"
},
"settings": {
"title": "⚙️ Settings",
@@ -460,7 +533,18 @@
"ai_prompt_label": "🤖 AI product selection prompt",
"ai_prompt_placeholder": "Instructions for AI when choosing between multiple products...",
"ai_prompt_hint": "AI uses this prompt to choose the most appropriate product from results. Leave empty for default behavior.",
"configure_first": "Configure Online Shopping in settings first"
"configure_first": "Configure Online Shopping in settings first",
"missing_credentials": "Enter email and password",
"login_in_progress": "Signing in...",
"login_error_prefix": "Error:",
"login_network_error_prefix": "Network error:",
"login_success_default": "Login successful!",
"result_name_label": "Name",
"result_card_label": "Card",
"result_pickup_label": "Pickup point",
"result_points_label": "Loyalty points",
"connected_relogin": "✅ Connected — Sign in again",
"connected_as": "Connected as {name}"
},
"camera": {
"title": "📷 Camera",
@@ -592,6 +676,8 @@
},
"error": {
"generic": "Error",
"network": "Network error",
"no_api_key": "Configure the API key in settings",
"loading": "Error loading product",
"not_found": "Product not found",
"not_found_manual": "Product not found. Enter it manually.",
@@ -610,7 +696,8 @@
"not_in_inventory": "Product not in inventory",
"appliance_exists": "Appliance already exists",
"already_exists": "Already exists",
"network_retry": "Connection error. Try again."
"network_retry": "Connection error. Try again.",
"select_items": "Select at least one product"
},
"confirm": {
"remove_item": "Do you really want to remove this product from inventory?",
@@ -632,11 +719,24 @@
"thu": "Thursday",
"fri": "Friday",
"sat": "Saturday",
"sun": "Sunday"
"sun": "Sunday",
"mon_short": "Mon",
"tue_short": "Tue",
"wed_short": "Wed",
"thu_short": "Thu",
"fri_short": "Fri",
"sat_short": "Sat",
"sun_short": "Sun"
},
"meal_types": {
"lunch": "Lunch",
"dinner": "Dinner"
"dinner": "Dinner",
"colazione": "Breakfast",
"merenda": "Snack",
"dolce": "Dessert",
"succo": "Fruit Juice",
"pranzo": "Lunch",
"cena": "Dinner"
},
"scale": {
"status_connected": "Scale connected",
@@ -709,5 +809,49 @@
"2": "Culinary ingredient",
"3": "Processed",
"4": "Ultra-processed"
},
"meal_plan_types": {
"pasta": "Pasta",
"riso": "Rice",
"carne": "Meat",
"pesce": "Fish",
"legumi": "Legumes",
"uova": "Eggs",
"formaggio": "Cheese",
"pizza": "Pizza",
"affettati": "Cold Cuts",
"verdure": "Veggies",
"zuppa": "Soup",
"insalata": "Salad",
"pane": "Bread/Sandwich",
"dolce": "Dessert",
"libero": "Free"
},
"meal_sub": {
"dolce_torta": "Cake",
"dolce_crema": "Cream / Pudding",
"dolce_crumble": "Crumble / Tart",
"dolce_biscotti": "Cookies / Pastries",
"dolce_frutta": "Fruit Dessert",
"succo_dolce": "Sweet / Fruity",
"succo_energizzante": "Energizing",
"succo_detox": "Detox / Green",
"succo_rinfrescante": "Refreshing",
"succo_vitaminico": "Vitamin / Citrus"
},
"meal_plan": {
"reset_success": "Weekly plan reset",
"suggested_by": "suggested by weekly plan"
},
"kiosk_session": {
"first_item": "First item: {name}!",
"items_two_four": "{n} items — warming up 🚀",
"items_five_nine": "{n} items — great pace! 💪",
"items_ten_twenty": "{n} items — almost a record 🏆",
"items_twenty_plus": "{n} items — epic shopping! 🛒🔥",
"duplicates_one": "1 duplicate (same thing twice)",
"duplicates_many": "{n} duplicates (picked multiple times)",
"top_category": "Top category: {cat} ({count}×)",
"items_fallback": "{n} item{plural} added"
}
}
+152 -8
View File
@@ -169,7 +169,9 @@
"hint": "Scansiona il barcode, scrivi il nome del prodotto, oppure usa l'AI per identificarlo",
"debug_toggle": "🐛 Debug Log",
"barcode_acquired": "🔖 Barcode acquisito: {code}",
"scan_barcode": "🔖 Scansiona Barcode"
"scan_barcode": "🔖 Scansiona Barcode",
"create_named": "Crea {name}",
"new_without_barcode": "Nuovo prodotto senza barcode"
},
"action": {
"title": "Cosa vuoi fare?",
@@ -212,7 +214,11 @@
"title": "Usa / Consuma",
"location_label": "📍 Da dove?",
"quantity_label": "Quanto hai usato?",
"change": "cambia",
"partial_hint": "Oppure specifica la quantità usata:",
"partial_piece_hint": "Hai usato solo una parte?",
"piece": "pezzo",
"one_whole": "1 intero",
"use_all": "🗑️ Usato TUTTO / Finito",
"submit": "📤 Usa questa quantità",
"available": "📦 Disponibile:",
@@ -272,7 +278,42 @@
},
"recipes": {
"title": "🍳 Ricette",
"generate": "✨ Genera nuova ricetta"
"generate": "✨ Genera nuova ricetta",
"archive_empty": "Nessuna ricetta salvata. Genera la tua prima ricetta!",
"dialog_title": "🍳 Ricetta",
"dialog_desc": "Genero una ricetta sana con gli ingredienti in dispensa, dando priorità a quelli in scadenza.",
"meal_label": "🕐 Per quale pasto?",
"persons_label": "👥 Quante persone?",
"meal_type_label": "🎯 Tipo di pasto",
"opt_fast": "⚡ Pasto Veloce",
"opt_light": "🥗 Poca Fame",
"opt_expiry": "⏰ Priorità Scadenze",
"opt_healthy": "💚 Extra Salutare",
"opt_opened": "📦 Priorità Cose Aperte",
"opt_zero_waste": "♻️ Zero Sprechi",
"generate_btn": "✨ Genera Ricetta",
"loading_msg": "Sto preparando la ricetta...",
"start_cooking": "👨‍🍳 Modalità Cucina",
"regenerate": "🔄 Generane un'altra",
"close_btn": "✅ Chiudi",
"ingredients_title": "🧾 Ingredienti",
"steps_title": "👨‍🍳 Procedimento",
"no_steps": "Nessun procedimento disponibile",
"generate_error": "Errore nella generazione",
"persons_short": "pers.",
"use_ingredient_title": "Usa ingrediente",
"recipe_qty_label": "Ricetta",
"from_where_label": "Da dove?",
"amount_label": "Quanto",
"use_amount_btn": "Usa questa quantità",
"use_all_btn": "Usa TUTTO / Finito",
"packs_label": "Confezioni",
"quantity_in_total": "Quantità in {unit} (totale: {total})",
"packs_of_have": "Confezioni da {size} (hai {count} conf)",
"scale_wait_stable": "Attendi 10s di stabilità per la compilazione automatica…",
"ingredient_scaled_toast": "📦 Ingrediente scalato dalla dispensa!",
"finished_added_bring_toast": "🛒 Prodotto finito → aggiunto a Bring!",
"load_error": "Errore nel caricamento"
},
"shopping": {
"title": "🛒 Lista della Spesa",
@@ -326,7 +367,37 @@
"smart_already_predicted": "📊 La spesa intelligente prevede già <strong>{name}</strong>{urgency}.",
"item_removed": "✅ {name} rimosso dalla lista!",
"urgency_spec_critical": "⚡ Urgente",
"urgency_spec_high": "🟠 Presto"
"urgency_spec_high": "🟠 Presto",
"bring_add_n": "Aggiungi {n} a Bring!",
"bring_add_selected": "Aggiungi selezionati a Bring!",
"bring_adding": "Aggiunta in corso...",
"bring_added_one": "1 prodotto aggiunto a Bring!",
"bring_added_many": "{n} prodotti aggiunti a Bring!",
"bring_skipped": "({n} già in lista)",
"force_sync": "Forza sincronizzazione Bring!",
"scan_target_label": "Stai cercando",
"scan_target_found": "Trovato! Rimuovi dalla lista",
"bring_add_one": "Aggiungi 1 prodotto a Bring!",
"bring_add_many": "Aggiungi {n} prodotti a Bring!",
"syncing": "Sincronizzazione…",
"sync_done": "Sincronizzazione completata",
"price_searching": "Cerco...",
"search_action": "Ricerca",
"open_action": "Apri",
"not_found": "Non trovato",
"search_price": "Cerca prezzo",
"tap_to_scan": "Tocca per scansionare",
"tag_title": "Tag",
"remove_title": "Rimuovi",
"found_count": "{found}/{total} prodotti trovati",
"savings_offers": "· 🏷️ Risparmi €{amount} con le offerte",
"searching_progress": "Cerco {current}/{total}...",
"remove_error": "Errore nella rimozione",
"suggest_loading": "Analisi in corso...",
"suggest_error": "Errore nella generazione",
"priority_high": "Alta",
"priority_medium": "Media",
"priority_low": "Bassa"
},
"ai": {
"title": "🤖 Identificazione AI",
@@ -386,7 +457,9 @@
"ingredient_deduct_title": "Scala dalla dispensa",
"timer_expired_tts": "Timer {label} scaduto!",
"timer_warning_tts": "Attenzione! {label}: mancano 10 secondi!",
"recipe_done_tts": "Ricetta completata! Buon appetito!"
"recipe_done_tts": "Ricetta completata! Buon appetito!",
"expires_chip": "scade {date}",
"finish": "✅ Fine"
},
"settings": {
"title": "⚙️ Configurazione",
@@ -460,7 +533,18 @@
"ai_prompt_label": "🤖 Prompt AI selezione prodotto",
"ai_prompt_placeholder": "Istruzioni per l'AI quando deve scegliere tra più prodotti...",
"ai_prompt_hint": "L'AI usa questo prompt per scegliere il prodotto più appropriato tra i risultati. Lascia vuoto per il comportamento predefinito.",
"configure_first": "Configura prima la Spesa Online nelle impostazioni"
"configure_first": "Configura prima la Spesa Online nelle impostazioni",
"missing_credentials": "Inserisci email e password",
"login_in_progress": "Accesso in corso...",
"login_error_prefix": "Errore:",
"login_network_error_prefix": "Errore di rete:",
"login_success_default": "Login effettuato!",
"result_name_label": "Nome",
"result_card_label": "Tessera",
"result_pickup_label": "Punto Ritiro",
"result_points_label": "Punti Fedeltà",
"connected_relogin": "✅ Connesso — Riaccedi",
"connected_as": "Connesso come {name}"
},
"camera": {
"title": "📷 Fotocamera",
@@ -592,6 +676,8 @@
},
"error": {
"generic": "Errore",
"network": "Errore di rete",
"no_api_key": "Configura la chiave API nelle impostazioni",
"loading": "Errore nel caricamento del prodotto",
"not_found": "Prodotto non trovato",
"not_found_manual": "Prodotto non trovato. Inseriscilo manualmente.",
@@ -610,7 +696,8 @@
"not_in_inventory": "Prodotto non nell'inventario",
"appliance_exists": "Elettrodomestico già presente",
"already_exists": "Già presente",
"network_retry": "Errore di connessione. Riprova."
"network_retry": "Errore di connessione. Riprova.",
"select_items": "Seleziona almeno un prodotto"
},
"confirm": {
"remove_item": "Vuoi davvero rimuovere questo prodotto dall'inventario?",
@@ -632,11 +719,24 @@
"thu": "Giovedì",
"fri": "Venerdì",
"sat": "Sabato",
"sun": "Domenica"
"sun": "Domenica",
"mon_short": "Lun",
"tue_short": "Mar",
"wed_short": "Mer",
"thu_short": "Gio",
"fri_short": "Ven",
"sat_short": "Sab",
"sun_short": "Dom"
},
"meal_types": {
"lunch": "Pranzo",
"dinner": "Cena"
"dinner": "Cena",
"colazione": "Colazione",
"merenda": "Merenda",
"dolce": "Dolce",
"succo": "Succo di Frutta",
"pranzo": "Pranzo",
"cena": "Cena"
},
"scale": {
"status_connected": "Bilancia connessa",
@@ -709,5 +809,49 @@
"2": "Ingrediente culinario",
"3": "Trasformato",
"4": "Ultra-trasformato"
},
"meal_plan_types": {
"pasta": "Pasta",
"riso": "Riso",
"carne": "Carne",
"pesce": "Pesce",
"legumi": "Legumi",
"uova": "Uova",
"formaggio": "Formaggio",
"pizza": "Pizza",
"affettati": "Affettati",
"verdure": "Verdure",
"zuppa": "Zuppa",
"insalata": "Insalata",
"pane": "Pane/Sandwich",
"dolce": "Dolce",
"libero": "Libero"
},
"meal_sub": {
"dolce_torta": "Torta",
"dolce_crema": "Crema / Budino",
"dolce_crumble": "Crumble / Crostata",
"dolce_biscotti": "Biscotti / Pasticcini",
"dolce_frutta": "Dolce alla Frutta",
"succo_dolce": "Dolce / Fruttato",
"succo_energizzante": "Energizzante",
"succo_detox": "Detox / Verde",
"succo_rinfrescante": "Rinfrescante",
"succo_vitaminico": "Vitaminico / Agrumi"
},
"meal_plan": {
"reset_success": "Piano settimanale ripristinato",
"suggested_by": "suggerito dal piano settimanale"
},
"kiosk_session": {
"first_item": "Primo prodotto: {name}!",
"items_two_four": "{n} prodotti — stai scaldando i motori 🚀",
"items_five_nine": "{n} prodotti — ottimo ritmo! 💪",
"items_ten_twenty": "{n} prodotti — quasi un recordman 🏆",
"items_twenty_plus": "{n} prodotti — spesa epica! 🛒🔥",
"duplicates_one": "1 bis (stessa cosa due volte)",
"duplicates_many": "{n} bis (roba presa più volte)",
"top_category": "Categoria top: {cat} ({count}×)",
"items_fallback": "{n} prodott{n} aggiunti"
}
}