feat: expired banner for opened products, AI model fallback, TTS cooking improvements
- Banner: detect expired opened-products via effective shelf-life (opened_at +
estimateOpenedExpiryDays), not just raw expiry_date — fixes Fagioli/Panna case
- Banner: expired items show safety tip inline; danger-level items (fridge dairy,
meat, fish) get red banner + 'L'ho buttato' as primary button, 'Usa comunque'
demoted to grey; safety-ok/warning items keep original button order
- Banner: anomaly dismiss button now shows current inventory qty ('La quantità è
giusta (2 pz)') so the action is unambiguous
- AI: add callGeminiWithFallback() helper — tries gemini-2.5-flash first (separate
quota), falls back to gemini-2.0-flash; applied to all endpoints (expiry, chat,
identify, recipe non-streaming, shopping name classifier)
- AI: show friendly 'Quota AI esaurita' message instead of raw Gemini error string
- Cooking TTS: fix auto-speak broken since 'auto-speak removed' comment — each step
is now read automatically on navigate and on first step when entering cooking mode
- Cooking TTS: remove incorrect s.tts_enabled gate — _cookingTTS toggle is the only
gate; browser Web Speech API used by default without requiring Settings config
- Cooking TTS: timer fires '10 secondi rimanenti' warning at T-10s
- Cooking TTS: announce recipe completion ('Buon appetito!') on last step confirm
- i18n: add timer_warning_tts, recipe_done_tts, error.ai_quota keys (IT/EN/DE)
- CSS: add banner-expired-danger, banner-safety-* styles for unsafe expired items
This commit is contained in:
@@ -16,12 +16,12 @@
|
|||||||
|
|
||||||
### 📦 Inventory Management
|
### 📦 Inventory Management
|
||||||
- **Barcode scanning** — Scan products with your phone camera using QuaggaJS
|
- **Barcode scanning** — Scan products with your phone camera using QuaggaJS
|
||||||
- **AI identification** — Take a photo and let Google Gemini identify the product, with suggestions from your existing inventory
|
- **AI identification** — Take a photo and let Google Gemini identify the product, with suggestions from your existing inventory; gracefully shows a friendly message when AI quota is exhausted instead of a raw API error
|
||||||
- **Smart locations** — Track items across Pantry, Fridge, Freezer, and custom locations
|
- **Smart locations** — Track items across Pantry, Fridge, Freezer, and custom locations
|
||||||
- **Expiry tracking** — Automatic shelf-life estimation based on product type and storage
|
- **Expiry tracking** — Automatic shelf-life estimation based on product type and storage
|
||||||
- **Opened product tracking** — Reduced shelf-life calculation when packages are opened
|
- **Opened product tracking** — Reduced shelf-life calculation when packages are opened; opened-product expiry is now also checked when building banner alerts (not just the dashboard section)
|
||||||
- **Vacuum-sealed support** — Extended expiry dates for vacuum-sealed items
|
- **Vacuum-sealed support** — Extended expiry dates for vacuum-sealed items
|
||||||
- **Anomaly detection** — Banner alerts for suspicious quantities and consumption predictions with inline correction
|
- **Anomaly detection** — Banner alerts for suspicious quantities and consumption predictions with inline correction; dismiss button now shows the current inventory quantity so the action is unambiguous ("La quantità è giusta (2 pz)")
|
||||||
|
|
||||||
### 🤖 AI-Powered (Google Gemini)
|
### 🤖 AI-Powered (Google Gemini)
|
||||||
- **Expiry date reading** — Photograph a label and extract the expiry date automatically
|
- **Expiry date reading** — Photograph a label and extract the expiry date automatically
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
- **Recipe generation** — Get personalized recipes based on what's in your pantry; streams live via Server-Sent Events so results appear as they are generated
|
- **Recipe generation** — Get personalized recipes based on what's in your pantry; streams live via Server-Sent Events so results appear as they are generated
|
||||||
- **Smart chat assistant** — Ask questions about your inventory, get cooking tips
|
- **Smart chat assistant** — Ask questions about your inventory, get cooking tips
|
||||||
- **Shopping suggestions** — AI-powered purchase recommendations
|
- **Shopping suggestions** — AI-powered purchase recommendations
|
||||||
|
- **Model fallback** — All AI endpoints try `gemini-2.5-flash` first (separate quota) and fall back to `gemini-2.0-flash` automatically, matching the resilience already used for recipe generation
|
||||||
|
|
||||||
### 🛒 Shopping List
|
### 🛒 Shopping List
|
||||||
- **Bring! integration** — Sync with the [Bring!](https://www.getbring.com/) shopping list app
|
- **Bring! integration** — Sync with the [Bring!](https://www.getbring.com/) shopping list app
|
||||||
@@ -41,14 +42,18 @@
|
|||||||
|
|
||||||
### 🍳 Cooking Mode
|
### 🍳 Cooking Mode
|
||||||
- **Step-by-step guidance** — Follow recipes with a hands-free cooking interface
|
- **Step-by-step guidance** — Follow recipes with a hands-free cooking interface
|
||||||
- **Text-to-Speech** — Voice readout of recipe steps; supports browser Web Speech API, native Android TTS (kiosk), or a custom REST endpoint (Home Assistant, etc.); retries voice loading for up to 10 seconds with a fallback refresh button
|
- **Text-to-Speech** — Voice readout of recipe steps; supports browser Web Speech API, native Android TTS (kiosk), or a custom REST endpoint (Home Assistant, etc.); retries voice loading for up to 10 seconds with a fallback refresh button; TTS activates automatically without requiring the global TTS setting to be enabled
|
||||||
|
- **Auto-read on navigate** — Each step is read aloud automatically when you tap Next or Previous; the first step is read when entering cooking mode
|
||||||
|
- **Timer voice alerts** — 10-second countdown warning spoken aloud before each timer expires; expiry announced vocally when time is up
|
||||||
|
- **Recipe completion** — "Buon appetito!" spoken when the last step is confirmed
|
||||||
- **Built-in timer** — Automatic timer suggestions based on recipe instructions
|
- **Built-in timer** — Automatic timer suggestions based on recipe instructions
|
||||||
- **Ingredient tracking** — Mark ingredients as used during cooking; leftover quantities prompt a "move to another location" flow
|
- **Ingredient tracking** — Mark ingredients as used during cooking; leftover quantities prompt a "move to another location" flow
|
||||||
|
|
||||||
### 📊 Dashboard
|
### 📊 Dashboard
|
||||||
- **Waste tracking** — Monitor consumed vs. wasted products over 30 days
|
- **Waste tracking** — Monitor consumed vs. wasted products over 30 days
|
||||||
- **Expiry alerts** — Visual warnings for expired and soon-to-expire items
|
- **Expiry alerts** — Visual warnings for expired and soon-to-expire items
|
||||||
- **Safety ratings** — Smart assessment of expired product safety (by category)
|
- **Safety ratings** — Smart assessment of expired product safety (by category and location); expired unsafe items shown with a red danger banner and "L'ho buttato" as the primary action
|
||||||
|
- **Expired product banner** — Products that have passed their effective shelf-life (including opened-product reduced expiry) appear in the top notification banner with safety tip, danger styling for high-risk items, and a prominent discard action
|
||||||
- **Quick recipe bar** — One-tap recipe suggestion using expiring products
|
- **Quick recipe bar** — One-tap recipe suggestion using expiring products
|
||||||
- **Anomaly banner** — Scrollable banner with suspicious quantities and consumption prediction mismatches, with one-tap correction or inline edit
|
- **Anomaly banner** — Scrollable banner with suspicious quantities and consumption prediction mismatches, with one-tap correction or inline edit
|
||||||
- **Expired/expiring alerts** — Priority-sorted banner notifications for expired and soon-to-expire products with use, throw, edit, and dismiss actions
|
- **Expired/expiring alerts** — Priority-sorted banner notifications for expired and soon-to-expire products with use, throw, edit, and dismiss actions
|
||||||
|
|||||||
+22
-15
@@ -1972,6 +1972,23 @@ function callGemini(string $url, array $payload, int $timeout = 60): array {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like callGemini() but tries gemini-2.5-flash first, falls back to gemini-2.0-flash
|
||||||
|
* on quota/rate-limit errors (429/503). Builds the URL from model name + API key.
|
||||||
|
*/
|
||||||
|
function callGeminiWithFallback(string $apiKey, array $payload, int $timeout = 30): array {
|
||||||
|
$models = ['gemini-2.5-flash', 'gemini-2.0-flash'];
|
||||||
|
$last = ['http_code' => 0, 'body' => '', 'data' => null];
|
||||||
|
foreach ($models as $model) {
|
||||||
|
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
|
||||||
|
$last = callGemini($url, $payload, $timeout);
|
||||||
|
if ($last['http_code'] === 200) return $last;
|
||||||
|
if ($last['http_code'] !== 429 && $last['http_code'] !== 503) return $last; // non-retryable
|
||||||
|
// 429/503 on this model → try next model
|
||||||
|
}
|
||||||
|
return $last;
|
||||||
|
}
|
||||||
|
|
||||||
function geminiReadExpiry(): void {
|
function geminiReadExpiry(): void {
|
||||||
$apiKey = env('GEMINI_API_KEY');
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
@@ -1988,8 +2005,6 @@ function geminiReadExpiry(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call Gemini API
|
// Call Gemini API
|
||||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}";
|
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'contents' => [
|
'contents' => [
|
||||||
[
|
[
|
||||||
@@ -2012,7 +2027,7 @@ function geminiReadExpiry(): void {
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = callGemini($url, $payload, 30);
|
$result = callGeminiWithFallback($apiKey, $payload, 30);
|
||||||
$httpCode = $result['http_code'];
|
$httpCode = $result['http_code'];
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
if ($httpCode !== 200) {
|
||||||
@@ -2161,8 +2176,6 @@ PROMPT;
|
|||||||
'parts' => [['text' => $message]]
|
'parts' => [['text' => $message]]
|
||||||
];
|
];
|
||||||
|
|
||||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}";
|
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'contents' => $contents,
|
'contents' => $contents,
|
||||||
'generationConfig' => [
|
'generationConfig' => [
|
||||||
@@ -2171,7 +2184,7 @@ PROMPT;
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = callGemini($url, $payload, 60);
|
$result = callGeminiWithFallback($apiKey, $payload, 60);
|
||||||
$httpCode = $result['http_code'];
|
$httpCode = $result['http_code'];
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
if ($httpCode !== 200) {
|
||||||
@@ -2562,8 +2575,6 @@ 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":"…"}
|
{"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":"…"}
|
||||||
PROMPT;
|
PROMPT;
|
||||||
|
|
||||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}";
|
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'contents' => [
|
'contents' => [
|
||||||
[
|
[
|
||||||
@@ -2578,7 +2589,7 @@ PROMPT;
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = callGemini($url, $payload, 60);
|
$result = callGeminiWithFallback($apiKey, $payload, 60);
|
||||||
$httpCode = $result['http_code'];
|
$httpCode = $result['http_code'];
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
if ($httpCode !== 200) {
|
||||||
@@ -3337,8 +3348,6 @@ Rispondi SOLO con un JSON valido (senza markdown, senza backtick):
|
|||||||
}
|
}
|
||||||
PROMPT;
|
PROMPT;
|
||||||
|
|
||||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}";
|
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'contents' => [
|
'contents' => [
|
||||||
[
|
[
|
||||||
@@ -3359,7 +3368,7 @@ PROMPT;
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = callGemini($url, $payload, 30);
|
$result = callGeminiWithFallback($apiKey, $payload, 30);
|
||||||
$httpCode = $result['http_code'];
|
$httpCode = $result['http_code'];
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
if ($httpCode !== 200) {
|
||||||
@@ -3638,8 +3647,6 @@ function _geminiClassifyProduct(string $name, string $brand, string $category):
|
|||||||
$cacheKey = md5(mb_strtolower($name . '|' . $brand));
|
$cacheKey = md5(mb_strtolower($name . '|' . $brand));
|
||||||
if (isset($cache[$cacheKey])) return $cache[$cacheKey];
|
if (isset($cache[$cacheKey])) return $cache[$cacheKey];
|
||||||
|
|
||||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}";
|
|
||||||
|
|
||||||
// Build catalog list so Gemini picks an existing Bring! entry when possible
|
// Build catalog list so Gemini picks an existing Bring! entry when possible
|
||||||
$catalog = bringCatalog();
|
$catalog = bringCatalog();
|
||||||
$catalogList = implode(', ', array_slice(array_values($catalog['de2it']), 0, 200));
|
$catalogList = implode(', ', array_slice(array_values($catalog['de2it']), 0, 200));
|
||||||
@@ -3668,7 +3675,7 @@ PROMPT;
|
|||||||
'generationConfig' => ['temperature' => 0.1, 'maxOutputTokens' => 16],
|
'generationConfig' => ['temperature' => 0.1, 'maxOutputTokens' => 16],
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = callGemini($url, $payload, 15);
|
$result = callGeminiWithFallback($apiKey, $payload, 15);
|
||||||
if ($result['http_code'] !== 200 || !isset($result['data']['candidates'][0])) return null;
|
if ($result['http_code'] !== 200 || !isset($result['data']['candidates'][0])) return null;
|
||||||
|
|
||||||
$text = trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? '');
|
$text = trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? '');
|
||||||
|
|||||||
@@ -5679,3 +5679,36 @@ body {
|
|||||||
background: #fee2e2;
|
background: #fee2e2;
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
.alert-banner.banner-expired-danger {
|
||||||
|
background: linear-gradient(135deg, #fca5a5 0%, #f87171 100%);
|
||||||
|
border-color: #b91c1c;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
.banner-expired-danger .alert-banner-title {
|
||||||
|
color: #7f1d1d;
|
||||||
|
}
|
||||||
|
.banner-safety-tip {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.82em;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.banner-safety-danger {
|
||||||
|
color: #b91c1c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.banner-safety-warning {
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
.banner-safety-ok {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
.btn-banner-throw-primary {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.btn-banner-use-danger {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|||||||
+320
-258
File diff suppressed because it is too large
Load Diff
+180
-19
@@ -26,7 +26,9 @@
|
|||||||
"save_config": "💾 Konfiguration speichern",
|
"save_config": "💾 Konfiguration speichern",
|
||||||
"save_product": "💾 Produkt speichern",
|
"save_product": "💾 Produkt speichern",
|
||||||
"restart": "↺ Neustart",
|
"restart": "↺ Neustart",
|
||||||
"reset_default": "↺ Standard wiederherstellen"
|
"reset_default": "↺ Standard wiederherstellen",
|
||||||
|
"save_info": "💾 Info speichern",
|
||||||
|
"retry": "🔄 Erneut versuchen"
|
||||||
},
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"dispensa": "Vorratskammer",
|
"dispensa": "Vorratskammer",
|
||||||
@@ -97,10 +99,10 @@
|
|||||||
"banner_expired_today": "Heute abgelaufen",
|
"banner_expired_today": "Heute abgelaufen",
|
||||||
"banner_expired_days": "Seit {days} Tagen abgelaufen",
|
"banner_expired_days": "Seit {days} Tagen abgelaufen",
|
||||||
"banner_expired_action_use": "Trotzdem verwenden",
|
"banner_expired_action_use": "Trotzdem verwenden",
|
||||||
"banner_expired_action_throw": "Wegwerfen",
|
"banner_expired_action_throw": "Habe ich weggeworfen",
|
||||||
"banner_expired_action_edit": "Datum korrigieren",
|
"banner_expired_action_edit": "Datum korrigieren",
|
||||||
"banner_anomaly_action_edit": "Bestand korrigieren",
|
"banner_anomaly_action_edit": "Bestand korrigieren",
|
||||||
"banner_anomaly_action_dismiss": "Passt so",
|
"banner_anomaly_action_dismiss": "Menge ist korrekt",
|
||||||
"banner_expiring_title": "Bald ablaufend",
|
"banner_expiring_title": "Bald ablaufend",
|
||||||
"banner_expiring_today": "Läuft heute ab!",
|
"banner_expiring_today": "Läuft heute ab!",
|
||||||
"banner_expiring_tomorrow": "Läuft morgen ab",
|
"banner_expiring_tomorrow": "Läuft morgen ab",
|
||||||
@@ -127,7 +129,11 @@
|
|||||||
"banner_anomaly_phantom_title": "mehr Bestand als erwartet",
|
"banner_anomaly_phantom_title": "mehr Bestand als erwartet",
|
||||||
"banner_anomaly_phantom_detail": "Bestand zeigt {inv_qty} {unit}, aber laut Buchungen solltest du nur {expected_qty} {unit} haben. Hast du Bestand ohne Buchung hinzugefügt?",
|
"banner_anomaly_phantom_detail": "Bestand zeigt {inv_qty} {unit}, aber laut Buchungen solltest du nur {expected_qty} {unit} haben. Hast du Bestand ohne Buchung hinzugefügt?",
|
||||||
"banner_anomaly_ghost_title": "weniger Bestand als erwartet",
|
"banner_anomaly_ghost_title": "weniger Bestand als erwartet",
|
||||||
"banner_anomaly_ghost_detail": "Laut Buchungen solltest du {expected_qty} {unit} von {name} haben, aber der Bestand zeigt nur {inv_qty} {unit}. Hast du etwas ohne Buchung entnommen?"
|
"banner_anomaly_ghost_detail": "Laut Buchungen solltest du {expected_qty} {unit} von {name} haben, aber der Bestand zeigt nur {inv_qty} {unit}. Hast du etwas ohne Buchung entnommen?",
|
||||||
|
"consumed": "Verbraucht: {n} ({pct}%)",
|
||||||
|
"wasted": "Weggeworfen: {n} ({pct}%)",
|
||||||
|
"more_opened": "und {n} weitere geöffnet...",
|
||||||
|
"banner_expired_detail": "{when} · du hast noch <strong>{qty}</strong>."
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"title": "Vorrat",
|
"title": "Vorrat",
|
||||||
@@ -136,7 +142,19 @@
|
|||||||
"recent_title": "🕐 Zuletzt verwendet",
|
"recent_title": "🕐 Zuletzt verwendet",
|
||||||
"popular_title": "⭐ Meistverwendet",
|
"popular_title": "⭐ Meistverwendet",
|
||||||
"empty": "Keine Produkte hier.\nScanne ein Produkt, um es hinzuzufügen!",
|
"empty": "Keine Produkte hier.\nScanne ein Produkt, um es hinzuzufügen!",
|
||||||
"no_items_found": "Keine Bestandseinträge gefunden"
|
"no_items_found": "Keine Bestandseinträge gefunden",
|
||||||
|
"qty_remainder_suffix": "übrig",
|
||||||
|
"vacuum_badge": "🫙 Vakuumiert",
|
||||||
|
"opened_badge": "📭 Geöffnet",
|
||||||
|
"label_expiry": "📅 Ablaufdatum",
|
||||||
|
"label_storage": "🫙 Aufbewahrung",
|
||||||
|
"label_status": "📭 Status",
|
||||||
|
"opened_since": "Geöffnet seit {date}",
|
||||||
|
"label_position": "📍 Standort",
|
||||||
|
"label_quantity": "📦 Menge",
|
||||||
|
"label_added": "📅 Hinzugefügt",
|
||||||
|
"empty_text": "Keine Produkte hier.<br>Scanne ein Produkt, um es hinzuzufügen!",
|
||||||
|
"empty_db": "Keine Produkte in der Datenbank.<br>Scanne ein Produkt, um loszulegen!"
|
||||||
},
|
},
|
||||||
"scan": {
|
"scan": {
|
||||||
"title": "Produkt scannen",
|
"title": "Produkt scannen",
|
||||||
@@ -181,7 +199,14 @@
|
|||||||
"remaining_label": "📦 Verbleibende Menge",
|
"remaining_label": "📦 Verbleibende Menge",
|
||||||
"remaining_hint": "Ungefähr wie viel ist noch übrig?",
|
"remaining_hint": "Ungefähr wie viel ist noch übrig?",
|
||||||
"remaining_full": "🟢 Voll",
|
"remaining_full": "🟢 Voll",
|
||||||
"remaining_half": "🟠 Halb"
|
"remaining_half": "🟠 Halb",
|
||||||
|
"estimated_expiry": "Geschätzte Haltbarkeit:",
|
||||||
|
"suffix_freezer": "(Tiefkühler)",
|
||||||
|
"suffix_vacuum": "(vakuumversiegelt)",
|
||||||
|
"hint_modify": "📝 Du kannst das Datum ändern oder mit der Kamera scannen",
|
||||||
|
"scan_expiry_title": "📷 Ablaufdatum scannen",
|
||||||
|
"product_added": "✅ {name} hinzugefügt!{qty}",
|
||||||
|
"suffix_freezer_vacuum": "(Tiefkühler + vakuumversiegelt)"
|
||||||
},
|
},
|
||||||
"use": {
|
"use": {
|
||||||
"title": "Verwenden / Verbrauchen",
|
"title": "Verwenden / Verbrauchen",
|
||||||
@@ -197,7 +222,13 @@
|
|||||||
"throw_all": "🗑️ ALLES entsorgen ({qty})",
|
"throw_all": "🗑️ ALLES entsorgen ({qty})",
|
||||||
"throw_qty_label": "Wie viel wegwerfen?",
|
"throw_qty_label": "Wie viel wegwerfen?",
|
||||||
"throw_qty_hint": "oder Menge angeben:",
|
"throw_qty_hint": "oder Menge angeben:",
|
||||||
"throw_partial_btn": "🗑️ Diese Menge entsorgen"
|
"throw_partial_btn": "🗑️ Diese Menge entsorgen",
|
||||||
|
"when_expired": "seit {n} Tagen abgelaufen",
|
||||||
|
"when_today": "läuft <strong>heute</strong> ab",
|
||||||
|
"when_tomorrow": "läuft <strong>morgen</strong> ab",
|
||||||
|
"when_days": "läuft in <strong>{n} Tagen</strong> ab",
|
||||||
|
"toast_used": "📤 {qty} von {name} verwendet",
|
||||||
|
"toast_bring": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt"
|
||||||
},
|
},
|
||||||
"product": {
|
"product": {
|
||||||
"title_new": "Neues Produkt",
|
"title_new": "Neues Produkt",
|
||||||
@@ -229,7 +260,9 @@
|
|||||||
"edit_catalog": "⚙️ Produktinfo bearbeiten (Name, Marke, Kategorie…)",
|
"edit_catalog": "⚙️ Produktinfo bearbeiten (Name, Marke, Kategorie…)",
|
||||||
"not_recognized": "⚠️ Produkt nicht erkannt",
|
"not_recognized": "⚠️ Produkt nicht erkannt",
|
||||||
"edit_info": "✏️ Informationen bearbeiten",
|
"edit_info": "✏️ Informationen bearbeiten",
|
||||||
"modify_details": "BEARBEITEN\nAblauf, Ort…"
|
"modify_details": "BEARBEITEN\nAblauf, Ort…",
|
||||||
|
"already_in_pantry": "📋 Bereits im Vorratsschrank",
|
||||||
|
"no_barcode": "Kein Barcode"
|
||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"title": "📦 Alle Produkte",
|
"title": "📦 Alle Produkte",
|
||||||
@@ -273,7 +306,27 @@
|
|||||||
"migration_done": "✅ {migrated} aktualisiert, {skipped} bereits ok",
|
"migration_done": "✅ {migrated} aktualisiert, {skipped} bereits ok",
|
||||||
"added_to_bring": "🛒 {n} Produkte zu Bring! hinzugefügt",
|
"added_to_bring": "🛒 {n} Produkte zu Bring! hinzugefügt",
|
||||||
"added_to_bring_skip": "{n} bereits vorhanden",
|
"added_to_bring_skip": "{n} bereits vorhanden",
|
||||||
"all_on_bring": "Alle Produkte waren bereits auf Bring!"
|
"all_on_bring": "Alle Produkte waren bereits auf Bring!",
|
||||||
|
"freq_high": "📈 Häufig",
|
||||||
|
"freq_regular": "📊 Regelmäßig",
|
||||||
|
"freq_occasional": "📉 Gelegentlich",
|
||||||
|
"out_of_stock": "Ausverkauft",
|
||||||
|
"scan_toast": "📷 Scannen: {name}",
|
||||||
|
"empty_category": "Keine Produkte in dieser Kategorie",
|
||||||
|
"session_empty": "🛒 Noch keine Produkte",
|
||||||
|
"urgency_critical": "Dringend",
|
||||||
|
"urgency_high": "Bald",
|
||||||
|
"urgency_medium": "Planen",
|
||||||
|
"urgency_low": "Vorschau",
|
||||||
|
"urgency_medium_short": "Mittel",
|
||||||
|
"urgency_low_short": "Ok",
|
||||||
|
"tag_urgent": "🔴 Dringend",
|
||||||
|
"tag_priority": "⭐ Priorität",
|
||||||
|
"tag_check": "✅ Prüfen",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"title": "🤖 KI-Identifikation",
|
"title": "🤖 KI-Identifikation",
|
||||||
@@ -282,10 +335,27 @@
|
|||||||
"hint": "Mache ein Foto des Produkts und die KI versucht es zu identifizieren",
|
"hint": "Mache ein Foto des Produkts und die KI versucht es zu identifizieren",
|
||||||
"identifying": "🤖 Identifiziere Produkt...",
|
"identifying": "🤖 Identifiziere Produkt...",
|
||||||
"no_api_key": "⚠️ Gemini API-Schlüssel nicht konfiguriert.\n<small>Füge GEMINI_API_KEY in der .env Datei auf dem Server hinzu.</small>",
|
"no_api_key": "⚠️ Gemini API-Schlüssel nicht konfiguriert.\n<small>Füge GEMINI_API_KEY in der .env Datei auf dem Server hinzu.</small>",
|
||||||
"fields_filled": "✅ Felder von KI ausgefüllt"
|
"fields_filled": "✅ Felder von KI ausgefüllt",
|
||||||
|
"use_data": "✅ KI-Daten verwenden",
|
||||||
|
"use_data_no_barcode": "✅ KI-Daten verwenden (ohne Barcode)"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"title": "� Verlauf"
|
"title": "� Verlauf",
|
||||||
|
"type_added": "Hinzugefügt",
|
||||||
|
"type_waste": "Entsorgt",
|
||||||
|
"type_used": "Verwendet",
|
||||||
|
"type_bring": "Zu Bring! hinzugefügt",
|
||||||
|
"undone_badge": "Rückgängig",
|
||||||
|
"undo_title": "Diese Operation rückgängig machen",
|
||||||
|
"load_error": "Fehler beim Laden des Verlaufs",
|
||||||
|
"empty": "Keine Operationen aufgezeichnet.",
|
||||||
|
"undo_action_remove": "Entfernen von",
|
||||||
|
"undo_action_restore": "Wiederherstellen von",
|
||||||
|
"undo_confirm": "Vorgang rückgängig machen?\n→ {action} {name}",
|
||||||
|
"undo_success": "↩ Vorgang rückgängig gemacht für {name}",
|
||||||
|
"already_undone": "Vorgang bereits rückgängig gemacht",
|
||||||
|
"too_old": "Vorgänge älter als 24 Stunden können nicht rückgängig gemacht werden",
|
||||||
|
"undo_error": "Fehler beim Rückgängigmachen"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"title": "Gemini Chef",
|
"title": "Gemini Chef",
|
||||||
@@ -296,7 +366,12 @@
|
|||||||
"suggestion_light": "🥗 Etwas Leichtes",
|
"suggestion_light": "🥗 Etwas Leichtes",
|
||||||
"suggestion_expiry": "⏰ Ablaufende nutzen",
|
"suggestion_expiry": "⏰ Ablaufende nutzen",
|
||||||
"clear": "Neues Gespräch",
|
"clear": "Neues Gespräch",
|
||||||
"placeholder": "Frag etwas..."
|
"placeholder": "Frag etwas...",
|
||||||
|
"cleared": "Chat geleert",
|
||||||
|
"suggestion_snack_text": "Was kann ich als schnellen Snack machen?",
|
||||||
|
"suggestion_juice_text": "Mach mir einen Saft oder Smoothie mit dem was ich habe",
|
||||||
|
"suggestion_light_text": "Ich habe Hunger, möchte aber etwas Leichtes",
|
||||||
|
"suggestion_expiry_text": "Was läuft bald ab und wie kann ich es verwenden?"
|
||||||
},
|
},
|
||||||
"cooking": {
|
"cooking": {
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
@@ -305,7 +380,13 @@
|
|||||||
"replay": "🔊 Nochmal",
|
"replay": "🔊 Nochmal",
|
||||||
"timer": "⏱️ {time} · Timer",
|
"timer": "⏱️ {time} · Timer",
|
||||||
"prev": "◀ Zurück",
|
"prev": "◀ Zurück",
|
||||||
"next": "Weiter ▶"
|
"next": "Weiter ▶",
|
||||||
|
"ingredient_used": "✔️ Abgezogen",
|
||||||
|
"ingredient_use_btn": "📦 Verwenden",
|
||||||
|
"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!"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "⚙️ Einstellungen",
|
"title": "⚙️ Einstellungen",
|
||||||
@@ -450,12 +531,45 @@
|
|||||||
"days": "{days} Tage",
|
"days": "{days} Tage",
|
||||||
"expired_days": "Seit {days}T",
|
"expired_days": "Seit {days}T",
|
||||||
"expired_yesterday": "Seit gestern",
|
"expired_yesterday": "Seit gestern",
|
||||||
"expired_today": "Heute"
|
"expired_today": "Heute",
|
||||||
|
"badge_today": "⚠️ Läuft heute ab!",
|
||||||
|
"badge_tomorrow": "⏰ Morgen",
|
||||||
|
"badge_tomorrow_long": "⏰ Läuft morgen ab",
|
||||||
|
"badge_days": "⏰ {n} Tage",
|
||||||
|
"badge_expired_ago": "⚠️ Seit {n}T abgel.",
|
||||||
|
"badge_expired": "⛔ Abgelaufen!",
|
||||||
|
"badge_stable": "✅ Stabil",
|
||||||
|
"badge_expiring_short": "⏰ Läuft in {n}T ab",
|
||||||
|
"badge_ok_still": "✅ Noch {n}T",
|
||||||
|
"badge_expires_red": "🔴 In {n}T",
|
||||||
|
"badge_expires_yellow": "🟡 In {n}T",
|
||||||
|
"badge_expired_bare": "⚠️ Abgelaufen",
|
||||||
|
"badge_expires_warn": "⚠️ In {n}T",
|
||||||
|
"badge_days_left": "⏳ ~{n}T übrig",
|
||||||
|
"days_approx": "~{n} Tage",
|
||||||
|
"weeks_approx": "~{n} Wochen",
|
||||||
|
"months_approx": "~{n} Monate",
|
||||||
|
"years_approx": "~{n} Jahre",
|
||||||
|
"expired_today_long": "Heute abgelaufen",
|
||||||
|
"expired_ago_long": "Seit {n} Tagen abgelaufen",
|
||||||
|
"expired_suffix": "— Abgelaufen!",
|
||||||
|
"days_compact": "{n}T"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"check": "Prüfen",
|
"check": "Prüfen",
|
||||||
"discard": "Entsorgen"
|
"discard": "Entsorgen",
|
||||||
|
"tip_freezer_ok": "Im Gefrierschrank: noch sicher (~{n}T Puffer)",
|
||||||
|
"tip_freezer_check": "Seit langem im Gefrierschrank, könnte an Qualität verloren haben. Bald verbrauchen",
|
||||||
|
"tip_freezer_danger": "Zu lange im Gefrierschrank, Gefrierbrand- und Qualitätsverlust-Risiko",
|
||||||
|
"tip_highRisk_check": "Kürzlich abgelaufen, Geruch und Aussehen vor dem Verzehr prüfen",
|
||||||
|
"tip_highRisk_danger": "Verderbliches Produkt abgelaufen: aus Sicherheitsgründen entsorgen",
|
||||||
|
"tip_medRisk_check1": "Aussehen und Geruch vor dem Verzehr prüfen",
|
||||||
|
"tip_medRisk_check2": "Schon eine Weile abgelaufen, vor dem Verzehr gut prüfen",
|
||||||
|
"tip_medRisk_danger": "Zu lange seit dem Ablaufdatum, lieber entsorgen",
|
||||||
|
"tip_lowRisk_ok": "Haltbares Produkt, noch sicher zu verzehren",
|
||||||
|
"tip_lowRisk_check": "Seit über einem Monat abgelaufen, Verpackungsintegrität prüfen",
|
||||||
|
"tip_lowRisk_danger": "Zu lange abgelaufen, besser kein Risiko eingehen"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"product_saved": "Produkt gespeichert!",
|
"product_saved": "Produkt gespeichert!",
|
||||||
@@ -489,19 +603,23 @@
|
|||||||
"bring_add": "Fehler beim Hinzufügen zu Bring!",
|
"bring_add": "Fehler beim Hinzufügen zu Bring!",
|
||||||
"bring_connection": "Bring! Verbindungsfehler",
|
"bring_connection": "Bring! Verbindungsfehler",
|
||||||
"identification": "Identifikationsfehler",
|
"identification": "Identifikationsfehler",
|
||||||
|
"ai_quota": "KI-Kontingent erschöpft. Bitte in ein paar Minuten erneut versuchen.",
|
||||||
"barcode_empty": "Barcode eingeben",
|
"barcode_empty": "Barcode eingeben",
|
||||||
"barcode_format": "Barcode darf nur Zahlen enthalten (4-14 Ziffern)",
|
"barcode_format": "Barcode darf nur Zahlen enthalten (4-14 Ziffern)",
|
||||||
"min_chars": "Mindestens 2 Zeichen eingeben",
|
"min_chars": "Mindestens 2 Zeichen eingeben",
|
||||||
"not_in_inventory": "Produkt nicht im Bestand",
|
"not_in_inventory": "Produkt nicht im Bestand",
|
||||||
"appliance_exists": "Gerät bereits vorhanden",
|
"appliance_exists": "Gerät bereits vorhanden",
|
||||||
"already_exists": "Bereits vorhanden"
|
"already_exists": "Bereits vorhanden",
|
||||||
|
"network_retry": "Verbindungsfehler. Erneut versuchen."
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?",
|
"remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?",
|
||||||
"kiosk_exit": "Kioskmodus verlassen?"
|
"kiosk_exit": "Kioskmodus verlassen?"
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"title": "{name} bearbeiten"
|
"title": "{name} bearbeiten",
|
||||||
|
"unknown_hint": "Produktname und Informationen eingeben",
|
||||||
|
"label_name": "🏷️ Produktname"
|
||||||
},
|
},
|
||||||
"screensaver": {
|
"screensaver": {
|
||||||
"recipe_btn": "Rezepte",
|
"recipe_btn": "Rezepte",
|
||||||
@@ -542,11 +660,54 @@
|
|||||||
"weight_detected": "Gewicht erkannt — 10s Stabilität abwarten…",
|
"weight_detected": "Gewicht erkannt — 10s Stabilität abwarten…",
|
||||||
"weight_too_low": "Gewicht zu niedrig — warten…",
|
"weight_too_low": "Gewicht zu niedrig — warten…",
|
||||||
"stable": "✓ Stabil",
|
"stable": "✓ Stabil",
|
||||||
"auto_confirm": "✅ {val} {unit} — Auto-Bestätigung in 5s (tippen zum Abbrechen)"
|
"auto_confirm": "✅ {val} {unit} — Auto-Bestätigung in 5s (tippen zum Abbrechen)",
|
||||||
|
"cancelled_replace": "Abgebrochen — lege die Zutat wieder auf die Waage, um fortzufahren"
|
||||||
},
|
},
|
||||||
"prediction": {
|
"prediction": {
|
||||||
"expected_qty": "Erwartet: {expected} {unit}",
|
"expected_qty": "Erwartet: {expected} {unit}",
|
||||||
"actual_qty": "Aktuell: {actual} {unit}",
|
"actual_qty": "Aktuell: {actual} {unit}",
|
||||||
"check_suggestion": "Überprüfe oder wiege die Restmenge"
|
"check_suggestion": "Überprüfe oder wiege die Restmenge"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"today": "📅 Heute",
|
||||||
|
"yesterday": "📅 Gestern"
|
||||||
|
},
|
||||||
|
"scanner": {
|
||||||
|
"title_barcode": "🔖 Barcode scannen",
|
||||||
|
"barcode_hint": "Produktbarcode einrahmen",
|
||||||
|
"barcode_manual_placeholder": "Oder manuell eingeben...",
|
||||||
|
"barcode_use_btn": "✅ Diesen Code verwenden",
|
||||||
|
"ai_identifying": "🤖 Produkt wird erkannt...",
|
||||||
|
"ai_analyzing": "🤖 KI-Analyse läuft...",
|
||||||
|
"product_label_hint": "Produktetikett einrahmen",
|
||||||
|
"expiry_label_hint": "Ablaufdatum auf dem Produkt einrahmen",
|
||||||
|
"capture_btn": "📸 Aufnehmen",
|
||||||
|
"capture_photo_btn": "📸 Foto aufnehmen",
|
||||||
|
"retake_btn": "🔄 Erneut aufnehmen",
|
||||||
|
"camera_error_hint": "Stelle sicher, dass du HTTPS verwendest und Kameraberechtigungen erteilt hast.<br>Du kannst den Barcode manuell eingeben oder die KI-Identifikation verwenden.",
|
||||||
|
"no_barcode": "Kein Barcode"
|
||||||
|
},
|
||||||
|
"lowstock": {
|
||||||
|
"title": "⚠️ Wird knapp!",
|
||||||
|
"message": "{name} wird knapp — nur noch {qty} übrig.",
|
||||||
|
"question": "Möchtest du es zur Einkaufsliste hinzufügen?",
|
||||||
|
"yes": "🛒 Ja, zu Bring! hinzufügen",
|
||||||
|
"no": "Nein, passt für jetzt"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"title": "📦 Den Rest bewegen?",
|
||||||
|
"question": "Möchtest du {thing} von {name} an einen anderen Ort bewegen?",
|
||||||
|
"question_short": "Möchtest du {thing} an einen anderen Ort bewegen?",
|
||||||
|
"thing_opened": "die offene Packung",
|
||||||
|
"thing_rest": "den Rest",
|
||||||
|
"stay_btn": "Nein, bleibt in {location}",
|
||||||
|
"moved_toast": "📦 Offene Packung bewegt nach {location}",
|
||||||
|
"vacuum_restore": "🫙 Vakuum wiederherstellen"
|
||||||
|
},
|
||||||
|
"nova": {
|
||||||
|
"1": "Unverarbeitet",
|
||||||
|
"2": "Kulinarische Zutat",
|
||||||
|
"3": "Verarbeitet",
|
||||||
|
"4": "Hochverarbeitet"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+180
-19
@@ -26,7 +26,9 @@
|
|||||||
"save_config": "💾 Save Configuration",
|
"save_config": "💾 Save Configuration",
|
||||||
"save_product": "💾 Save Product",
|
"save_product": "💾 Save Product",
|
||||||
"restart": "↺ Restart",
|
"restart": "↺ Restart",
|
||||||
"reset_default": "↺ Reset to default"
|
"reset_default": "↺ Reset to default",
|
||||||
|
"save_info": "💾 Save information",
|
||||||
|
"retry": "🔄 Retry"
|
||||||
},
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"dispensa": "Pantry",
|
"dispensa": "Pantry",
|
||||||
@@ -97,10 +99,10 @@
|
|||||||
"banner_expired_today": "Expired today",
|
"banner_expired_today": "Expired today",
|
||||||
"banner_expired_days": "Expired {days} days ago",
|
"banner_expired_days": "Expired {days} days ago",
|
||||||
"banner_expired_action_use": "Use anyway",
|
"banner_expired_action_use": "Use anyway",
|
||||||
"banner_expired_action_throw": "Throw away",
|
"banner_expired_action_throw": "I threw it away",
|
||||||
"banner_expired_action_edit": "Fix date",
|
"banner_expired_action_edit": "Fix date",
|
||||||
"banner_anomaly_action_edit": "Fix inventory",
|
"banner_anomaly_action_edit": "Fix inventory",
|
||||||
"banner_anomaly_action_dismiss": "Looks fine",
|
"banner_anomaly_action_dismiss": "Quantity is correct",
|
||||||
"banner_expiring_title": "Expiring soon",
|
"banner_expiring_title": "Expiring soon",
|
||||||
"banner_expiring_today": "Expires today!",
|
"banner_expiring_today": "Expires today!",
|
||||||
"banner_expiring_tomorrow": "Expires tomorrow",
|
"banner_expiring_tomorrow": "Expires tomorrow",
|
||||||
@@ -127,7 +129,11 @@
|
|||||||
"banner_anomaly_phantom_title": "you have more stock than expected",
|
"banner_anomaly_phantom_title": "you have more stock than expected",
|
||||||
"banner_anomaly_phantom_detail": "Inventory shows {inv_qty} {unit}, but based on records you should only have {expected_qty} {unit}. Did you add stock without recording it?",
|
"banner_anomaly_phantom_detail": "Inventory shows {inv_qty} {unit}, but based on records you should only have {expected_qty} {unit}. Did you add stock without recording it?",
|
||||||
"banner_anomaly_ghost_title": "you have less stock than expected",
|
"banner_anomaly_ghost_title": "you have less stock than expected",
|
||||||
"banner_anomaly_ghost_detail": "Based on recorded operations you should have {expected_qty} {unit} of {name}, but inventory shows only {inv_qty} {unit}. Did you take stock without recording it?"
|
"banner_anomaly_ghost_detail": "Based on recorded operations you should have {expected_qty} {unit} of {name}, but inventory shows only {inv_qty} {unit}. Did you take stock without recording it?",
|
||||||
|
"consumed": "Consumed: {n} ({pct}%)",
|
||||||
|
"wasted": "Wasted: {n} ({pct}%)",
|
||||||
|
"more_opened": "and {n} more opened...",
|
||||||
|
"banner_expired_detail": "{when} · you still have <strong>{qty}</strong>."
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"title": "Pantry",
|
"title": "Pantry",
|
||||||
@@ -136,7 +142,19 @@
|
|||||||
"recent_title": "🕐 Recently used",
|
"recent_title": "🕐 Recently used",
|
||||||
"popular_title": "⭐ Most used",
|
"popular_title": "⭐ Most used",
|
||||||
"empty": "No products here.\nScan a product to add it!",
|
"empty": "No products here.\nScan a product to add it!",
|
||||||
"no_items_found": "No inventory items found"
|
"no_items_found": "No inventory items found",
|
||||||
|
"qty_remainder_suffix": "left",
|
||||||
|
"vacuum_badge": "🫙 Vacuum sealed",
|
||||||
|
"opened_badge": "📭 Opened",
|
||||||
|
"label_expiry": "📅 Expiry",
|
||||||
|
"label_storage": "🫙 Storage",
|
||||||
|
"label_status": "📭 Status",
|
||||||
|
"opened_since": "Opened since {date}",
|
||||||
|
"label_position": "📍 Location",
|
||||||
|
"label_quantity": "📦 Quantity",
|
||||||
|
"label_added": "📅 Added",
|
||||||
|
"empty_text": "No products here.<br>Scan a product to add it!",
|
||||||
|
"empty_db": "No products in the database.<br>Scan a product to get started!"
|
||||||
},
|
},
|
||||||
"scan": {
|
"scan": {
|
||||||
"title": "Scan Product",
|
"title": "Scan Product",
|
||||||
@@ -181,7 +199,14 @@
|
|||||||
"remaining_label": "📦 Remaining quantity",
|
"remaining_label": "📦 Remaining quantity",
|
||||||
"remaining_hint": "Approximately how much is left?",
|
"remaining_hint": "Approximately how much is left?",
|
||||||
"remaining_full": "🟢 Full",
|
"remaining_full": "🟢 Full",
|
||||||
"remaining_half": "🟠 Half"
|
"remaining_half": "🟠 Half",
|
||||||
|
"estimated_expiry": "Estimated expiry:",
|
||||||
|
"suffix_freezer": "(freezer)",
|
||||||
|
"suffix_vacuum": "(vacuum sealed)",
|
||||||
|
"hint_modify": "📝 You can change the date or scan it with the camera",
|
||||||
|
"scan_expiry_title": "📷 Scan Expiry Date",
|
||||||
|
"product_added": "✅ {name} added!{qty}",
|
||||||
|
"suffix_freezer_vacuum": "(freezer + vacuum sealed)"
|
||||||
},
|
},
|
||||||
"use": {
|
"use": {
|
||||||
"title": "Use / Consume",
|
"title": "Use / Consume",
|
||||||
@@ -197,7 +222,13 @@
|
|||||||
"throw_all": "🗑️ Discard ALL ({qty})",
|
"throw_all": "🗑️ Discard ALL ({qty})",
|
||||||
"throw_qty_label": "How much to discard?",
|
"throw_qty_label": "How much to discard?",
|
||||||
"throw_qty_hint": "or enter a quantity:",
|
"throw_qty_hint": "or enter a quantity:",
|
||||||
"throw_partial_btn": "🗑️ Discard this quantity"
|
"throw_partial_btn": "🗑️ Discard this quantity",
|
||||||
|
"when_expired": "expired {n} days ago",
|
||||||
|
"when_today": "expires <strong>today</strong>",
|
||||||
|
"when_tomorrow": "expires <strong>tomorrow</strong>",
|
||||||
|
"when_days": "expires in <strong>{n} days</strong>",
|
||||||
|
"toast_used": "📤 Used {qty} of {name}",
|
||||||
|
"toast_bring": "🛒 Product finished → added to Bring!"
|
||||||
},
|
},
|
||||||
"product": {
|
"product": {
|
||||||
"title_new": "New Product",
|
"title_new": "New Product",
|
||||||
@@ -229,7 +260,9 @@
|
|||||||
"edit_catalog": "⚙️ Edit product info (name, brand, category…)",
|
"edit_catalog": "⚙️ Edit product info (name, brand, category…)",
|
||||||
"not_recognized": "⚠️ Product not recognized",
|
"not_recognized": "⚠️ Product not recognized",
|
||||||
"edit_info": "✏️ Edit information",
|
"edit_info": "✏️ Edit information",
|
||||||
"modify_details": "EDIT\nexpiry, location…"
|
"modify_details": "EDIT\nexpiry, location…",
|
||||||
|
"already_in_pantry": "📋 Already in pantry",
|
||||||
|
"no_barcode": "No barcode"
|
||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"title": "📦 All Products",
|
"title": "📦 All Products",
|
||||||
@@ -273,7 +306,27 @@
|
|||||||
"migration_done": "✅ {migrated} updated, {skipped} already ok",
|
"migration_done": "✅ {migrated} updated, {skipped} already ok",
|
||||||
"added_to_bring": "🛒 {n} products added to Bring!",
|
"added_to_bring": "🛒 {n} products added to Bring!",
|
||||||
"added_to_bring_skip": "{n} already present",
|
"added_to_bring_skip": "{n} already present",
|
||||||
"all_on_bring": "All products were already on Bring!"
|
"all_on_bring": "All products were already on Bring!",
|
||||||
|
"freq_high": "📈 Frequent",
|
||||||
|
"freq_regular": "📊 Regular",
|
||||||
|
"freq_occasional": "📉 Occasional",
|
||||||
|
"out_of_stock": "Out of stock",
|
||||||
|
"scan_toast": "📷 Scan: {name}",
|
||||||
|
"empty_category": "No products in this category",
|
||||||
|
"session_empty": "🛒 No products yet",
|
||||||
|
"urgency_critical": "Urgent",
|
||||||
|
"urgency_high": "Soon",
|
||||||
|
"urgency_medium": "Plan",
|
||||||
|
"urgency_low": "Forecast",
|
||||||
|
"urgency_medium_short": "Medium",
|
||||||
|
"urgency_low_short": "Ok",
|
||||||
|
"tag_urgent": "🔴 Urgent",
|
||||||
|
"tag_priority": "⭐ Priority",
|
||||||
|
"tag_check": "✅ Check",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"title": "🤖 AI Identification",
|
"title": "🤖 AI Identification",
|
||||||
@@ -282,10 +335,27 @@
|
|||||||
"hint": "Take a photo of the product and AI will try to identify it",
|
"hint": "Take a photo of the product and AI will try to identify it",
|
||||||
"identifying": "🤖 Identifying product...",
|
"identifying": "🤖 Identifying product...",
|
||||||
"no_api_key": "⚠️ Gemini API key not configured.\n<small>Add GEMINI_API_KEY to the .env file on the server.</small>",
|
"no_api_key": "⚠️ Gemini API key not configured.\n<small>Add GEMINI_API_KEY to the .env file on the server.</small>",
|
||||||
"fields_filled": "✅ Fields filled by AI"
|
"fields_filled": "✅ Fields filled by AI",
|
||||||
|
"use_data": "✅ Use AI data",
|
||||||
|
"use_data_no_barcode": "✅ Use AI data (no barcode)"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"title": "📒 Operations Log"
|
"title": "📒 Operations Log",
|
||||||
|
"type_added": "Added",
|
||||||
|
"type_waste": "Discarded",
|
||||||
|
"type_used": "Used",
|
||||||
|
"type_bring": "Added to Bring!",
|
||||||
|
"undone_badge": "Undone",
|
||||||
|
"undo_title": "Undo this operation",
|
||||||
|
"load_error": "Error loading log",
|
||||||
|
"empty": "No operations recorded.",
|
||||||
|
"undo_action_remove": "removal of",
|
||||||
|
"undo_action_restore": "restock of",
|
||||||
|
"undo_confirm": "Undo this operation?\n→ {action} {name}",
|
||||||
|
"undo_success": "↩ Operation undone for {name}",
|
||||||
|
"already_undone": "Operation already undone",
|
||||||
|
"too_old": "Cannot undo operations older than 24 hours",
|
||||||
|
"undo_error": "Error during undo"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"title": "Gemini Chef",
|
"title": "Gemini Chef",
|
||||||
@@ -296,7 +366,12 @@
|
|||||||
"suggestion_light": "🥗 Something light",
|
"suggestion_light": "🥗 Something light",
|
||||||
"suggestion_expiry": "⏰ Use expiring items",
|
"suggestion_expiry": "⏰ Use expiring items",
|
||||||
"clear": "New conversation",
|
"clear": "New conversation",
|
||||||
"placeholder": "Ask something..."
|
"placeholder": "Ask something...",
|
||||||
|
"cleared": "Chat cleared",
|
||||||
|
"suggestion_snack_text": "What can I make for a quick snack?",
|
||||||
|
"suggestion_juice_text": "Make me a juice or smoothie with what I have",
|
||||||
|
"suggestion_light_text": "I'm hungry but want something light",
|
||||||
|
"suggestion_expiry_text": "What's about to expire and how can I use it?"
|
||||||
},
|
},
|
||||||
"cooking": {
|
"cooking": {
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
@@ -305,7 +380,13 @@
|
|||||||
"replay": "🔊 Replay",
|
"replay": "🔊 Replay",
|
||||||
"timer": "⏱️ {time} · Timer",
|
"timer": "⏱️ {time} · Timer",
|
||||||
"prev": "◀ Previous",
|
"prev": "◀ Previous",
|
||||||
"next": "Next ▶"
|
"next": "Next ▶",
|
||||||
|
"ingredient_used": "✔️ Deducted",
|
||||||
|
"ingredient_use_btn": "📦 Use",
|
||||||
|
"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!"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "⚙️ Settings",
|
"title": "⚙️ Settings",
|
||||||
@@ -450,12 +531,45 @@
|
|||||||
"days": "{days} days",
|
"days": "{days} days",
|
||||||
"expired_days": "{days}d ago",
|
"expired_days": "{days}d ago",
|
||||||
"expired_yesterday": "Yesterday",
|
"expired_yesterday": "Yesterday",
|
||||||
"expired_today": "Today"
|
"expired_today": "Today",
|
||||||
|
"badge_today": "⚠️ Expires today!",
|
||||||
|
"badge_tomorrow": "⏰ Tomorrow",
|
||||||
|
"badge_tomorrow_long": "⏰ Expires tomorrow",
|
||||||
|
"badge_days": "⏰ {n} days",
|
||||||
|
"badge_expired_ago": "⚠️ Expired {n}d ago",
|
||||||
|
"badge_expired": "⛔ Expired!",
|
||||||
|
"badge_stable": "✅ Stable",
|
||||||
|
"badge_expiring_short": "⏰ Exp. in {n}d",
|
||||||
|
"badge_ok_still": "✅ Still {n}d",
|
||||||
|
"badge_expires_red": "🔴 Exp. in {n}d",
|
||||||
|
"badge_expires_yellow": "🟡 Exp. in {n}d",
|
||||||
|
"badge_expired_bare": "⚠️ Expired",
|
||||||
|
"badge_expires_warn": "⚠️ Exp. in {n}d",
|
||||||
|
"badge_days_left": "⏳ ~{n}d left",
|
||||||
|
"days_approx": "~{n} days",
|
||||||
|
"weeks_approx": "~{n} weeks",
|
||||||
|
"months_approx": "~{n} months",
|
||||||
|
"years_approx": "~{n} years",
|
||||||
|
"expired_today_long": "Expired today",
|
||||||
|
"expired_ago_long": "Expired {n} days ago",
|
||||||
|
"expired_suffix": "— Expired!",
|
||||||
|
"days_compact": "{n}d"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"check": "Check",
|
"check": "Check",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"tip_freezer_ok": "In freezer: still safe (~{n}d margin)",
|
||||||
|
"tip_freezer_check": "In freezer for a long time, may have lost quality. Consume soon",
|
||||||
|
"tip_freezer_danger": "In freezer too long, risk of freezer burn and degradation",
|
||||||
|
"tip_highRisk_check": "Expired recently, check smell and appearance before consuming",
|
||||||
|
"tip_highRisk_danger": "Perishable product expired: discard for safety",
|
||||||
|
"tip_medRisk_check1": "Check appearance and smell before consuming",
|
||||||
|
"tip_medRisk_check2": "Expired a while ago, check carefully before use",
|
||||||
|
"tip_medRisk_danger": "Too long since expiry, better to discard",
|
||||||
|
"tip_lowRisk_ok": "Long-lasting product, still safe to consume",
|
||||||
|
"tip_lowRisk_check": "Expired over a month ago, check package integrity",
|
||||||
|
"tip_lowRisk_danger": "Expired too long ago, better not to risk it"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"product_saved": "Product saved!",
|
"product_saved": "Product saved!",
|
||||||
@@ -489,19 +603,23 @@
|
|||||||
"bring_add": "Error adding to Bring!",
|
"bring_add": "Error adding to Bring!",
|
||||||
"bring_connection": "Bring! connection error",
|
"bring_connection": "Bring! connection error",
|
||||||
"identification": "Identification error",
|
"identification": "Identification error",
|
||||||
|
"ai_quota": "AI quota exhausted. Please try again in a few minutes.",
|
||||||
"barcode_empty": "Enter a barcode",
|
"barcode_empty": "Enter a barcode",
|
||||||
"barcode_format": "Barcode must contain only numbers (4-14 digits)",
|
"barcode_format": "Barcode must contain only numbers (4-14 digits)",
|
||||||
"min_chars": "Type at least 2 characters",
|
"min_chars": "Type at least 2 characters",
|
||||||
"not_in_inventory": "Product not in inventory",
|
"not_in_inventory": "Product not in inventory",
|
||||||
"appliance_exists": "Appliance already exists",
|
"appliance_exists": "Appliance already exists",
|
||||||
"already_exists": "Already exists"
|
"already_exists": "Already exists",
|
||||||
|
"network_retry": "Connection error. Try again."
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"remove_item": "Do you really want to remove this product from inventory?",
|
"remove_item": "Do you really want to remove this product from inventory?",
|
||||||
"kiosk_exit": "Exit kiosk mode?"
|
"kiosk_exit": "Exit kiosk mode?"
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"title": "Edit {name}"
|
"title": "Edit {name}",
|
||||||
|
"unknown_hint": "Enter the product name and information",
|
||||||
|
"label_name": "🏷️ Product name"
|
||||||
},
|
},
|
||||||
"screensaver": {
|
"screensaver": {
|
||||||
"recipe_btn": "Recipes",
|
"recipe_btn": "Recipes",
|
||||||
@@ -542,11 +660,54 @@
|
|||||||
"weight_detected": "Weight detected — wait 10s for stability…",
|
"weight_detected": "Weight detected — wait 10s for stability…",
|
||||||
"weight_too_low": "Weight too low — waiting…",
|
"weight_too_low": "Weight too low — waiting…",
|
||||||
"stable": "✓ Stable",
|
"stable": "✓ Stable",
|
||||||
"auto_confirm": "✅ {val} {unit} — auto-confirm in 5s (tap to cancel)"
|
"auto_confirm": "✅ {val} {unit} — auto-confirm in 5s (tap to cancel)",
|
||||||
|
"cancelled_replace": "Cancelled — replace the ingredient on the scale to resume"
|
||||||
},
|
},
|
||||||
"prediction": {
|
"prediction": {
|
||||||
"expected_qty": "Expected: {expected} {unit}",
|
"expected_qty": "Expected: {expected} {unit}",
|
||||||
"actual_qty": "Current: {actual} {unit}",
|
"actual_qty": "Current: {actual} {unit}",
|
||||||
"check_suggestion": "Check or weigh the remaining quantity"
|
"check_suggestion": "Check or weigh the remaining quantity"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"today": "📅 Today",
|
||||||
|
"yesterday": "📅 Yesterday"
|
||||||
|
},
|
||||||
|
"scanner": {
|
||||||
|
"title_barcode": "🔖 Scan Barcode",
|
||||||
|
"barcode_hint": "Frame the product barcode",
|
||||||
|
"barcode_manual_placeholder": "Or enter manually...",
|
||||||
|
"barcode_use_btn": "✅ Use this code",
|
||||||
|
"ai_identifying": "🤖 Identifying product...",
|
||||||
|
"ai_analyzing": "🤖 AI analysis in progress...",
|
||||||
|
"product_label_hint": "Frame the product label",
|
||||||
|
"expiry_label_hint": "Frame the expiry date printed on the product",
|
||||||
|
"capture_btn": "📸 Capture",
|
||||||
|
"capture_photo_btn": "📸 Take Photo",
|
||||||
|
"retake_btn": "🔄 Retake",
|
||||||
|
"camera_error_hint": "Ensure you use HTTPS and have granted camera permissions.<br>You can enter the barcode manually or use AI identification.",
|
||||||
|
"no_barcode": "No barcode"
|
||||||
|
},
|
||||||
|
"lowstock": {
|
||||||
|
"title": "⚠️ Running low!",
|
||||||
|
"message": "{name} is running low — only {qty} remaining.",
|
||||||
|
"question": "Do you want to add it to the shopping list?",
|
||||||
|
"yes": "🛒 Yes, add to Bring!",
|
||||||
|
"no": "No, I'm fine for now"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"title": "📦 Move the rest?",
|
||||||
|
"question": "Do you want to move the {thing} of {name} to another location?",
|
||||||
|
"question_short": "Do you want to move the {thing} to another location?",
|
||||||
|
"thing_opened": "opened package",
|
||||||
|
"thing_rest": "rest",
|
||||||
|
"stay_btn": "No, stay in {location}",
|
||||||
|
"moved_toast": "📦 Opened package moved to {location}",
|
||||||
|
"vacuum_restore": "🫙 Restore vacuum sealed"
|
||||||
|
},
|
||||||
|
"nova": {
|
||||||
|
"1": "Unprocessed",
|
||||||
|
"2": "Culinary ingredient",
|
||||||
|
"3": "Processed",
|
||||||
|
"4": "Ultra-processed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+180
-19
@@ -26,7 +26,9 @@
|
|||||||
"save_config": "💾 Salva Configurazione",
|
"save_config": "💾 Salva Configurazione",
|
||||||
"save_product": "💾 Salva Prodotto",
|
"save_product": "💾 Salva Prodotto",
|
||||||
"restart": "↺ Ricomincia",
|
"restart": "↺ Ricomincia",
|
||||||
"reset_default": "↺ Ripristina default"
|
"reset_default": "↺ Ripristina default",
|
||||||
|
"save_info": "💾 Salva informazioni",
|
||||||
|
"retry": "🔄 Riprova"
|
||||||
},
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"dispensa": "Dispensa",
|
"dispensa": "Dispensa",
|
||||||
@@ -97,10 +99,10 @@
|
|||||||
"banner_expired_today": "Scaduto oggi",
|
"banner_expired_today": "Scaduto oggi",
|
||||||
"banner_expired_days": "Scaduto da {days} giorni",
|
"banner_expired_days": "Scaduto da {days} giorni",
|
||||||
"banner_expired_action_use": "Usa comunque",
|
"banner_expired_action_use": "Usa comunque",
|
||||||
"banner_expired_action_throw": "Butta via",
|
"banner_expired_action_throw": "L'ho buttato",
|
||||||
"banner_expired_action_edit": "Correggi data",
|
"banner_expired_action_edit": "Correggi data",
|
||||||
"banner_anomaly_action_edit": "Correggi inventario",
|
"banner_anomaly_action_edit": "Correggi inventario",
|
||||||
"banner_anomaly_action_dismiss": "Va bene così",
|
"banner_anomaly_action_dismiss": "La quantità è giusta",
|
||||||
"banner_expiring_title": "In scadenza",
|
"banner_expiring_title": "In scadenza",
|
||||||
"banner_expiring_today": "Scade oggi!",
|
"banner_expiring_today": "Scade oggi!",
|
||||||
"banner_expiring_tomorrow": "Scade domani",
|
"banner_expiring_tomorrow": "Scade domani",
|
||||||
@@ -127,7 +129,11 @@
|
|||||||
"banner_anomaly_phantom_title": "hai più scorte del previsto",
|
"banner_anomaly_phantom_title": "hai più scorte del previsto",
|
||||||
"banner_anomaly_phantom_detail": "L'inventario segna {inv_qty} {unit}, ma in base alle registrazioni ne dovresti avere solo {expected_qty} {unit}. Hai aggiunto scorte senza registrarle?",
|
"banner_anomaly_phantom_detail": "L'inventario segna {inv_qty} {unit}, ma in base alle registrazioni ne dovresti avere solo {expected_qty} {unit}. Hai aggiunto scorte senza registrarle?",
|
||||||
"banner_anomaly_ghost_title": "hai meno scorte del previsto",
|
"banner_anomaly_ghost_title": "hai meno scorte del previsto",
|
||||||
"banner_anomaly_ghost_detail": "In base alle operazioni registrate dovresti avere {expected_qty} {unit} di {name}, ma l'inventario mostra solo {inv_qty} {unit}. Hai prelevato senza registrarlo?"
|
"banner_anomaly_ghost_detail": "In base alle operazioni registrate dovresti avere {expected_qty} {unit} di {name}, ma l'inventario mostra solo {inv_qty} {unit}. Hai prelevato senza registrarlo?",
|
||||||
|
"consumed": "Consumati: {n} ({pct}%)",
|
||||||
|
"wasted": "Buttati: {n} ({pct}%)",
|
||||||
|
"more_opened": "e altri {n} prodotti aperti...",
|
||||||
|
"banner_expired_detail": "{when} · hai ancora <strong>{qty}</strong>."
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"title": "Dispensa",
|
"title": "Dispensa",
|
||||||
@@ -136,7 +142,19 @@
|
|||||||
"recent_title": "🕐 Ultimi usati",
|
"recent_title": "🕐 Ultimi usati",
|
||||||
"popular_title": "⭐ Più usati",
|
"popular_title": "⭐ Più usati",
|
||||||
"empty": "Nessun prodotto qui.\nScansiona un prodotto per aggiungerlo!",
|
"empty": "Nessun prodotto qui.\nScansiona un prodotto per aggiungerlo!",
|
||||||
"no_items_found": "Nessuna voce di inventario trovata"
|
"no_items_found": "Nessuna voce di inventario trovata",
|
||||||
|
"qty_remainder_suffix": "rimasti",
|
||||||
|
"vacuum_badge": "🫙 Sotto vuoto",
|
||||||
|
"opened_badge": "📭 Aperto",
|
||||||
|
"label_expiry": "📅 Scadenza",
|
||||||
|
"label_storage": "🫙 Conservazione",
|
||||||
|
"label_status": "📭 Stato",
|
||||||
|
"opened_since": "Aperto dal {date}",
|
||||||
|
"label_position": "📍 Posizione",
|
||||||
|
"label_quantity": "📦 Quantità",
|
||||||
|
"label_added": "📅 Aggiunto",
|
||||||
|
"empty_text": "Nessun prodotto qui.<br>Scansiona un prodotto per aggiungerlo!",
|
||||||
|
"empty_db": "Nessun prodotto nel database.<br>Scansiona un prodotto per iniziare!"
|
||||||
},
|
},
|
||||||
"scan": {
|
"scan": {
|
||||||
"title": "Scansiona Prodotto",
|
"title": "Scansiona Prodotto",
|
||||||
@@ -181,7 +199,14 @@
|
|||||||
"remaining_label": "📦 Quantità rimasta",
|
"remaining_label": "📦 Quantità rimasta",
|
||||||
"remaining_hint": "Quanto è rimasto approssimativamente?",
|
"remaining_hint": "Quanto è rimasto approssimativamente?",
|
||||||
"remaining_full": "🟢 Pieno",
|
"remaining_full": "🟢 Pieno",
|
||||||
"remaining_half": "🟠 Metà"
|
"remaining_half": "🟠 Metà",
|
||||||
|
"estimated_expiry": "Scadenza stimata:",
|
||||||
|
"suffix_freezer": "(freezer)",
|
||||||
|
"suffix_vacuum": "(sotto vuoto)",
|
||||||
|
"hint_modify": "📝 Puoi modificare la data o scansionarla con la fotocamera",
|
||||||
|
"scan_expiry_title": "📷 Scansiona Data Scadenza",
|
||||||
|
"product_added": "✅ {name} aggiunto!{qty}",
|
||||||
|
"suffix_freezer_vacuum": "(freezer + sotto vuoto)"
|
||||||
},
|
},
|
||||||
"use": {
|
"use": {
|
||||||
"title": "Usa / Consuma",
|
"title": "Usa / Consuma",
|
||||||
@@ -197,7 +222,13 @@
|
|||||||
"throw_all": "🗑️ Butta TUTTO ({qty})",
|
"throw_all": "🗑️ Butta TUTTO ({qty})",
|
||||||
"throw_qty_label": "Quanto butti?",
|
"throw_qty_label": "Quanto butti?",
|
||||||
"throw_qty_hint": "oppure specifica la quantità:",
|
"throw_qty_hint": "oppure specifica la quantità:",
|
||||||
"throw_partial_btn": "🗑️ Butta questa quantità"
|
"throw_partial_btn": "🗑️ Butta questa quantità",
|
||||||
|
"when_expired": "scaduta da {n} giorni",
|
||||||
|
"when_today": "scade <strong>oggi</strong>",
|
||||||
|
"when_tomorrow": "scade <strong>domani</strong>",
|
||||||
|
"when_days": "scade tra <strong>{n} giorni</strong>",
|
||||||
|
"toast_used": "📤 Usato {qty} di {name}",
|
||||||
|
"toast_bring": "🛒 Prodotto finito → aggiunto a Bring!"
|
||||||
},
|
},
|
||||||
"product": {
|
"product": {
|
||||||
"title_new": "Nuovo Prodotto",
|
"title_new": "Nuovo Prodotto",
|
||||||
@@ -229,7 +260,9 @@
|
|||||||
"edit_catalog": "⚙️ Modifica scheda prodotto (nome, marca, categoria…)",
|
"edit_catalog": "⚙️ Modifica scheda prodotto (nome, marca, categoria…)",
|
||||||
"not_recognized": "⚠️ Prodotto non riconosciuto",
|
"not_recognized": "⚠️ Prodotto non riconosciuto",
|
||||||
"edit_info": "✏️ Modifica informazioni",
|
"edit_info": "✏️ Modifica informazioni",
|
||||||
"modify_details": "MODIFICA\nscadenza, luogo…"
|
"modify_details": "MODIFICA\nscadenza, luogo…",
|
||||||
|
"already_in_pantry": "📋 Già in dispensa",
|
||||||
|
"no_barcode": "Senza barcode"
|
||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"title": "📦 Tutti i Prodotti",
|
"title": "📦 Tutti i Prodotti",
|
||||||
@@ -273,7 +306,27 @@
|
|||||||
"migration_done": "✅ {migrated} aggiornati, {skipped} già ok",
|
"migration_done": "✅ {migrated} aggiornati, {skipped} già ok",
|
||||||
"added_to_bring": "🛒 {n} prodotti aggiunti a Bring!",
|
"added_to_bring": "🛒 {n} prodotti aggiunti a Bring!",
|
||||||
"added_to_bring_skip": "{n} già presenti",
|
"added_to_bring_skip": "{n} già presenti",
|
||||||
"all_on_bring": "Tutti i prodotti erano già su Bring!"
|
"all_on_bring": "Tutti i prodotti erano già su Bring!",
|
||||||
|
"freq_high": "📈 Uso frequente",
|
||||||
|
"freq_regular": "📊 Uso regolare",
|
||||||
|
"freq_occasional": "📉 Uso occasionale",
|
||||||
|
"out_of_stock": "Esaurito",
|
||||||
|
"scan_toast": "📷 Scansiona: {name}",
|
||||||
|
"empty_category": "Nessun prodotto in questa categoria",
|
||||||
|
"session_empty": "🛒 Nessun prodotto ancora",
|
||||||
|
"urgency_critical": "Urgente",
|
||||||
|
"urgency_high": "Presto",
|
||||||
|
"urgency_medium": "Pianifica",
|
||||||
|
"urgency_low": "Previsione",
|
||||||
|
"urgency_medium_short": "Medio",
|
||||||
|
"urgency_low_short": "Ok",
|
||||||
|
"tag_urgent": "🔴 Urgente",
|
||||||
|
"tag_priority": "⭐ Priorità",
|
||||||
|
"tag_check": "✅ Verificare",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"title": "🤖 Identificazione AI",
|
"title": "🤖 Identificazione AI",
|
||||||
@@ -282,10 +335,27 @@
|
|||||||
"hint": "Scatta una foto del prodotto e l'AI cercherà di identificarlo",
|
"hint": "Scatta una foto del prodotto e l'AI cercherà di identificarlo",
|
||||||
"identifying": "🤖 Identifico il prodotto...",
|
"identifying": "🤖 Identifico il prodotto...",
|
||||||
"no_api_key": "⚠️ Chiave API Gemini non configurata.\n<small>Aggiungi GEMINI_API_KEY nel file .env sul server.</small>",
|
"no_api_key": "⚠️ Chiave API Gemini non configurata.\n<small>Aggiungi GEMINI_API_KEY nel file .env sul server.</small>",
|
||||||
"fields_filled": "✅ Campi compilati dall'AI"
|
"fields_filled": "✅ Campi compilati dall'AI",
|
||||||
|
"use_data": "✅ Usa dati AI",
|
||||||
|
"use_data_no_barcode": "✅ Usa dati AI (senza barcode)"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"title": "� Storico"
|
"title": "� Storico",
|
||||||
|
"type_added": "Aggiunto",
|
||||||
|
"type_waste": "Buttato",
|
||||||
|
"type_used": "Usato",
|
||||||
|
"type_bring": "Aggiunto a Bring!",
|
||||||
|
"undone_badge": "Annullato",
|
||||||
|
"undo_title": "Annulla questa operazione",
|
||||||
|
"load_error": "Errore nel caricamento log",
|
||||||
|
"empty": "Nessuna operazione registrata.",
|
||||||
|
"undo_action_remove": "rimozione di",
|
||||||
|
"undo_action_restore": "ripristino di",
|
||||||
|
"undo_confirm": "Annullare questa operazione?\n→ {action} {name}",
|
||||||
|
"undo_success": "↩ Operazione annullata per {name}",
|
||||||
|
"already_undone": "Operazione già annullata",
|
||||||
|
"too_old": "Non è possibile annullare operazioni più vecchie di 24 ore",
|
||||||
|
"undo_error": "Errore durante l'annullamento"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"title": "Gemini Chef",
|
"title": "Gemini Chef",
|
||||||
@@ -296,7 +366,12 @@
|
|||||||
"suggestion_light": "🥗 Qualcosa di leggero",
|
"suggestion_light": "🥗 Qualcosa di leggero",
|
||||||
"suggestion_expiry": "⏰ Usa le scadenze",
|
"suggestion_expiry": "⏰ Usa le scadenze",
|
||||||
"clear": "Nuova conversazione",
|
"clear": "Nuova conversazione",
|
||||||
"placeholder": "Chiedi qualcosa..."
|
"placeholder": "Chiedi qualcosa...",
|
||||||
|
"cleared": "Chat cancellata",
|
||||||
|
"suggestion_snack_text": "Cosa posso preparare per uno spuntino veloce?",
|
||||||
|
"suggestion_juice_text": "Fammi un succo o frullato con quello che ho",
|
||||||
|
"suggestion_light_text": "Ho fame ma voglio qualcosa di leggero",
|
||||||
|
"suggestion_expiry_text": "Cosa sta per scadere e come posso usarlo?"
|
||||||
},
|
},
|
||||||
"cooking": {
|
"cooking": {
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
@@ -305,7 +380,13 @@
|
|||||||
"replay": "🔊 Rileggi",
|
"replay": "🔊 Rileggi",
|
||||||
"timer": "⏱️ {time} · Timer",
|
"timer": "⏱️ {time} · Timer",
|
||||||
"prev": "◀ Precedente",
|
"prev": "◀ Precedente",
|
||||||
"next": "Successivo ▶"
|
"next": "Successivo ▶",
|
||||||
|
"ingredient_used": "✔️ Scalato",
|
||||||
|
"ingredient_use_btn": "📦 Usa",
|
||||||
|
"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!"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "⚙️ Configurazione",
|
"title": "⚙️ Configurazione",
|
||||||
@@ -450,12 +531,45 @@
|
|||||||
"days": "{days} giorni",
|
"days": "{days} giorni",
|
||||||
"expired_days": "Da {days}g",
|
"expired_days": "Da {days}g",
|
||||||
"expired_yesterday": "Da ieri",
|
"expired_yesterday": "Da ieri",
|
||||||
"expired_today": "Oggi"
|
"expired_today": "Oggi",
|
||||||
|
"badge_today": "⚠️ Scade oggi!",
|
||||||
|
"badge_tomorrow": "⏰ Domani",
|
||||||
|
"badge_tomorrow_long": "⏰ Scade domani",
|
||||||
|
"badge_days": "⏰ {n} giorni",
|
||||||
|
"badge_expired_ago": "⚠️ Scaduto da {n}g",
|
||||||
|
"badge_expired": "⛔ Scaduto!",
|
||||||
|
"badge_stable": "✅ Stabile",
|
||||||
|
"badge_expiring_short": "⏰ Scade fra {n}gg",
|
||||||
|
"badge_ok_still": "✅ Ancora {n}gg",
|
||||||
|
"badge_expires_red": "🔴 Scade tra {n}g",
|
||||||
|
"badge_expires_yellow": "🟡 Scade tra {n}g",
|
||||||
|
"badge_expired_bare": "⚠️ Scaduto",
|
||||||
|
"badge_expires_warn": "⚠️ Scade tra {n}gg",
|
||||||
|
"badge_days_left": "⏳ ~{n}gg rimasti",
|
||||||
|
"days_approx": "~{n} giorni",
|
||||||
|
"weeks_approx": "~{n} settimane",
|
||||||
|
"months_approx": "~{n} mesi",
|
||||||
|
"years_approx": "~{n} anni",
|
||||||
|
"expired_today_long": "Scaduto oggi",
|
||||||
|
"expired_ago_long": "Scaduto da {n} giorni",
|
||||||
|
"expired_suffix": "— Scaduto!",
|
||||||
|
"days_compact": "{n}gg"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"check": "Controlla",
|
"check": "Controlla",
|
||||||
"discard": "Buttare"
|
"discard": "Buttare",
|
||||||
|
"tip_freezer_ok": "In freezer: ancora sicuro (~{n}g di margine)",
|
||||||
|
"tip_freezer_check": "In freezer da molto, potrebbe aver perso qualità. Consumare presto",
|
||||||
|
"tip_freezer_danger": "In freezer da troppo tempo, rischio di bruciatura da gelo e degrado",
|
||||||
|
"tip_highRisk_check": "Scaduto da poco, controlla odore e aspetto prima di consumare",
|
||||||
|
"tip_highRisk_danger": "Prodotto deperibile scaduto: da buttare per sicurezza",
|
||||||
|
"tip_medRisk_check1": "Controlla aspetto e odore prima di consumare",
|
||||||
|
"tip_medRisk_check2": "Scaduto da un po', verificare bene prima dell'uso",
|
||||||
|
"tip_medRisk_danger": "Troppo tempo dalla scadenza, meglio buttare",
|
||||||
|
"tip_lowRisk_ok": "Prodotto a lunga conservazione, ancora sicuro da consumare",
|
||||||
|
"tip_lowRisk_check": "Scaduto da oltre un mese, controllare integrità confezione",
|
||||||
|
"tip_lowRisk_danger": "Scaduto da troppo tempo, meglio non rischiare"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"product_saved": "Prodotto salvato!",
|
"product_saved": "Prodotto salvato!",
|
||||||
@@ -489,19 +603,23 @@
|
|||||||
"bring_add": "Errore nell'aggiunta a Bring!",
|
"bring_add": "Errore nell'aggiunta a Bring!",
|
||||||
"bring_connection": "Errore connessione Bring!",
|
"bring_connection": "Errore connessione Bring!",
|
||||||
"identification": "Errore nell'identificazione",
|
"identification": "Errore nell'identificazione",
|
||||||
|
"ai_quota": "Quota AI esaurita. Riprova tra qualche minuto.",
|
||||||
"barcode_empty": "Inserisci un codice a barre",
|
"barcode_empty": "Inserisci un codice a barre",
|
||||||
"barcode_format": "Il codice a barre deve contenere solo numeri (4-14 cifre)",
|
"barcode_format": "Il codice a barre deve contenere solo numeri (4-14 cifre)",
|
||||||
"min_chars": "Scrivi almeno 2 caratteri",
|
"min_chars": "Scrivi almeno 2 caratteri",
|
||||||
"not_in_inventory": "Prodotto non nell'inventario",
|
"not_in_inventory": "Prodotto non nell'inventario",
|
||||||
"appliance_exists": "Elettrodomestico già presente",
|
"appliance_exists": "Elettrodomestico già presente",
|
||||||
"already_exists": "Già presente"
|
"already_exists": "Già presente",
|
||||||
|
"network_retry": "Errore di connessione. Riprova."
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"remove_item": "Vuoi davvero rimuovere questo prodotto dall'inventario?",
|
"remove_item": "Vuoi davvero rimuovere questo prodotto dall'inventario?",
|
||||||
"kiosk_exit": "Uscire dalla modalità kiosk?"
|
"kiosk_exit": "Uscire dalla modalità kiosk?"
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"title": "Modifica {name}"
|
"title": "Modifica {name}",
|
||||||
|
"unknown_hint": "Inserisci il nome e le informazioni del prodotto",
|
||||||
|
"label_name": "🏷️ Nome prodotto"
|
||||||
},
|
},
|
||||||
"screensaver": {
|
"screensaver": {
|
||||||
"recipe_btn": "Ricette",
|
"recipe_btn": "Ricette",
|
||||||
@@ -542,11 +660,54 @@
|
|||||||
"weight_detected": "Peso rilevato — attendi 10s di stabilità…",
|
"weight_detected": "Peso rilevato — attendi 10s di stabilità…",
|
||||||
"weight_too_low": "Peso troppo basso — attendi…",
|
"weight_too_low": "Peso troppo basso — attendi…",
|
||||||
"stable": "✓ Stabile",
|
"stable": "✓ Stabile",
|
||||||
"auto_confirm": "✅ {val} {unit} — conferma automatica tra 5s (tocca per annullare)"
|
"auto_confirm": "✅ {val} {unit} — conferma automatica tra 5s (tocca per annullare)",
|
||||||
|
"cancelled_replace": "Annullato — rimetti l'ingrediente sulla bilancia per riprendere"
|
||||||
},
|
},
|
||||||
"prediction": {
|
"prediction": {
|
||||||
"expected_qty": "Previsto: {expected} {unit}",
|
"expected_qty": "Previsto: {expected} {unit}",
|
||||||
"actual_qty": "Attuale: {actual} {unit}",
|
"actual_qty": "Attuale: {actual} {unit}",
|
||||||
"check_suggestion": "Verifica o pesa la quantità residua"
|
"check_suggestion": "Verifica o pesa la quantità residua"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"today": "📅 Oggi",
|
||||||
|
"yesterday": "📅 Ieri"
|
||||||
|
},
|
||||||
|
"scanner": {
|
||||||
|
"title_barcode": "🔖 Scansiona Barcode",
|
||||||
|
"barcode_hint": "Inquadra il codice a barre del prodotto",
|
||||||
|
"barcode_manual_placeholder": "O inserisci manualmente...",
|
||||||
|
"barcode_use_btn": "✅ Usa questo codice",
|
||||||
|
"ai_identifying": "🤖 Identifico il prodotto...",
|
||||||
|
"ai_analyzing": "🤖 Analisi AI in corso...",
|
||||||
|
"product_label_hint": "Inquadra l'etichetta del prodotto",
|
||||||
|
"expiry_label_hint": "Inquadra la data di scadenza stampata sul prodotto",
|
||||||
|
"capture_btn": "📸 Scatta",
|
||||||
|
"capture_photo_btn": "📸 Scatta Foto",
|
||||||
|
"retake_btn": "🔄 Riscatta",
|
||||||
|
"camera_error_hint": "Assicurati di usare HTTPS e di aver concesso i permessi della fotocamera.<br>Puoi inserire il barcode manualmente o usare l'identificazione AI.",
|
||||||
|
"no_barcode": "Senza barcode"
|
||||||
|
},
|
||||||
|
"lowstock": {
|
||||||
|
"title": "⚠️ Sta per finire!",
|
||||||
|
"message": "{name} sta per finire — rimangono solo {qty}.",
|
||||||
|
"question": "Vuoi aggiungerlo alla lista della spesa?",
|
||||||
|
"yes": "🛒 Sì, aggiungi a Bring!",
|
||||||
|
"no": "No, per ora va bene"
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"title": "📦 Spostare il resto?",
|
||||||
|
"question": "Vuoi spostare {thing} di {name} in un'altra posizione?",
|
||||||
|
"question_short": "Vuoi spostare {thing} in un'altra posizione?",
|
||||||
|
"thing_opened": "la confezione aperta",
|
||||||
|
"thing_rest": "il resto",
|
||||||
|
"stay_btn": "No, resta in {location}",
|
||||||
|
"moved_toast": "📦 Confezione aperta spostata in {location}",
|
||||||
|
"vacuum_restore": "🫙 Torna sotto vuoto"
|
||||||
|
},
|
||||||
|
"nova": {
|
||||||
|
"1": "Non trasformato",
|
||||||
|
"2": "Ingrediente culinario",
|
||||||
|
"3": "Trasformato",
|
||||||
|
"4": "Ultra-trasformato"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user