Merge develop: i18n completion for recipes and meal plan
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+263
-215
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
{"ts":1777391782}
|
||||
+43
-43
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user