release: v1.5.0 — expired banner, AI fallback, TTS cooking improvements

This commit is contained in:
dadaloop82
2026-04-28 12:53:42 +00:00
17 changed files with 4139 additions and 878 deletions
+1
View File
@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main - main
- develop
paths: paths:
- 'evershelf-kiosk/**' - 'evershelf-kiosk/**'
workflow_dispatch: workflow_dispatch:
+37
View File
@@ -5,6 +5,43 @@ All notable changes to EverShelf will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.5.0] - 2026-04-28
### Added
- **Expired banner for opened products** — Products whose opened-product shelf-life has passed (e.g. fridge cream opened 6 days ago) now appear in the top notification banner, not just the dashboard list
- **Safety-aware expired banner** — Each expired banner item shows a contextual safety tip (from `getExpiredSafety()`); danger-level items (fridge dairy/meat/fish) get an intense red banner and "L'ho buttato" as the primary button; safe/warning items keep the original button order
- **AI model fallback** — All Gemini API endpoints (expiry scan, product identification, chat, recipe non-streaming, shopping name classifier) now try `gemini-2.5-flash` first and fall back to `gemini-2.0-flash` automatically, matching the resilience already in place for recipe streaming
- **Friendly AI quota message** — When the AI returns a quota/rate-limit error the user sees "Quota AI esaurita. Riprova tra qualche minuto." instead of the raw API error string
- **Cooking TTS auto-read** — Each recipe step is read aloud automatically when navigating forward or backward; the first step is also read when entering cooking mode
- **Cooking timer 10-second warning** — When a cooking timer reaches 10 seconds the TTS announces "Attenzione! [label]: mancano 10 secondi!"
- **Cooking recipe completion announcement** — "Ricetta completata! Buon appetito!" is spoken via TTS when the last step is confirmed
### Fixed
- **Cooking TTS gate** — `speakCookingStep()` was blocked by the global `tts_enabled` setting; the `_cookingTTS` toggle (🔊/🔇 button) is now the only gate; browser Web Speech API is used by default without requiring TTS configuration in Settings
- **Anomaly dismiss label** — The "La quantità è giusta" button now appends the current inventory quantity, e.g. "La quantità è giusta (2 pz)", so the action is unambiguous
- **i18n sync** — Added `timer_warning_tts`, `recipe_done_tts`, `error.ai_quota` keys to all three language files (IT/EN/DE)
### Added
- **Generic shopping names** — Products are grouped by type ("Latte", "Affettato", "Pasta") rather than brand; computed via an expanded keyword map with Google Gemini AI as fallback for unknown products
- **Bring! auto-migration** — Existing list items with old specific names are silently migrated to generic names on every list load, throttled to once per 10 minutes
- **Bring! catalog coverage** — All 93 shopping_name values now resolve to a German Bring! catalog key (icons and categories in the Bring! app); 24 aliases added to cover previously unmatched names
- **Auto-add to Bring! on depletion** — When a product reaches zero the app adds it to Bring! automatically using the generic shopping name, with the specific product name and brand in the specification field
- **Finished-product confirmation banner** — Instead of silently deleting zero-stock entries, a banner prompts the user to confirm; banner title includes the last 3 digits of the product barcode for easier identification
- **Anomaly detection banner** — Dashboard notifications for suspicious inventory/transaction mismatches and consumption prediction errors, with one-tap inline correction
- **SSE recipe streaming** — Recipe generation streams live via Server-Sent Events; Gemini agent feedback is shown in real time as it is generated
- **Smart alert banners** — Configurable expired-only mode with explanatory messages; banner buttons are fully internationalized
### Fixed
- **Scale double-deduction** — Multiple BLE stable readings of the same weight no longer fire duplicate `inventory_use` events; JS preserves the confirmation sentinel on submit and PHP rejects a second `out` transaction for the same product within 12 seconds
- **Kiosk native TTS** — CI workflow now builds the APK on `develop` branch too; the native Android `TextToSpeech` bridge bypasses Web Speech API voice-availability issues without requiring offline voice packs
- **TTS voice loading** — Retries for up to 10 seconds on page load; shows a message if no voices are available and offers a manual refresh button
- **Bring! migration** — Corrected two bugs: wrong removal API (`DELETE /item``PUT remove=item`) and wrong purchase key sent to Bring! (Italian shopping name → German catalog key), which previously created Italian/German duplicate entries
- **Gemini 429 rate limiting** — API calls are retried with exponential backoff; recipe requests are capped at 5 per minute with a dedicated rate-limit bucket
### Performance
- **Gemini calls centralized** — All Gemini API requests go through a single `callGemini()` helper with intelligent backoff; Gemini removed from the product-selection and bringSuggest flows in favour of fast offline logic
## [1.3.0] - 2026-04-18 ## [1.3.0] - 2026-04-18
### Added ### Added
+21 -11
View File
@@ -16,37 +16,44 @@
### 📦 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
- **Product identification** — Point your camera at any product for instant recognition - **Product identification** — Point your camera at any product for instant recognition
- **Existing product matching** — AI scan shows matching products already in your pantry before suggesting new ones - **Existing product matching** — AI scan shows matching products already in your pantry before suggesting new ones
- **Recipe generation** — Get personalized recipes based on what's in your pantry - **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
- **Generic shopping names** — Products are grouped by type ("Latte", "Affettato", "Panna da cucina") rather than brand, keeping the Bring! list clean and consolidated
- **Smart predictions** — Know what you'll need before you run out - **Smart predictions** — Know what you'll need before you run out
- **Auto-remove on scan** — Products are removed from the shopping list when scanned in - **Auto-add on depletion** — When a product reaches zero the app adds it to Bring! automatically, no confirmation needed
- **DupliClick integration** — Online grocery ordering (Gruppo Poli) - **Auto-remove on scan** — Products are removed from the shopping list when scanned in - **Auto-migration**Items already on the Bring! list are silently renamed to their generic name in the background (throttled, runs on list load)
- **Catalog coverage** — All product types resolve to a German Bring! catalog key for icon and category display in the Bring! app- **DupliClick integration** — Online grocery ordering (Gruppo Poli)
### 🍳 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 (configurable TTS endpoint) - **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 - **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
@@ -63,8 +70,7 @@
- **SSE relay** — Server-side relay avoids mixed-content (HTTPS→WS) issues - **SSE relay** — Server-side relay avoids mixed-content (HTTPS→WS) issues
- **Auto-discovery** — Server scans LAN to find the gateway automatically - **Auto-discovery** — Server scans LAN to find the gateway automatically
- **Auto weight reading** — When adding/using a product with unit g/ml, weight fills automatically - **Auto weight reading** — When adding/using a product with unit g/ml, weight fills automatically
- **10g threshold** — Ignores readings that haven't changed enough between products - **10g threshold** — Ignores readings that haven't changed enough between products - **Duplicate-reading prevention** — Server-side 12-second dedup window rejects a second scale-triggered deduction of the same product, guarding against BLE multi-fire- **ml conversion hint** — Shows "weight in grams → will be converted to ml" when product unit is ml
- **ml conversion hint** — Shows "weight in grams → will be converted to ml" when product unit is ml
- **Stability + auto-confirm** — 10s stable wait + 5s countdown before confirming - **Stability + auto-confirm** — 10s stable wait + 5s countdown before confirming
- **Real-time status** — Scale connection indicator always visible in the header - **Real-time status** — Scale connection indicator always visible in the header
- **Multi-protocol** — Supports Bluetooth SIG Weight Scale, Body Composition, Xiaomi Mi Scale 2 and 100+ models - **Multi-protocol** — Supports Bluetooth SIG Weight Scale, Body Composition, Xiaomi Mi Scale 2 and 100+ models
@@ -76,6 +82,7 @@
- **Setup wizard** — 3-step guided configuration (URL, connection test, gateway) - **Setup wizard** — 3-step guided configuration (URL, connection test, gateway)
- **Gateway auto-launch** — Launches the Scale Gateway in the background on startup - **Gateway auto-launch** — Launches the Scale Gateway in the background on startup
- **Camera & mic permissions** — Full hardware access for barcode scanning and voice - **Camera & mic permissions** — Full hardware access for barcode scanning and voice
- **Native TTS bridge** — Cooking mode voice readout uses the Android TextToSpeech engine directly, bypassing Web Speech API voice limitations; no offline voice packs required
- **Hard refresh** — ↻ button clears WebView cache to pick up web app updates - **Hard refresh** — ↻ button clears WebView cache to pick up web app updates
- **Update notifications** — Checks GitHub releases every 6h, shows banner when updates available - **Update notifications** — Checks GitHub releases every 6h, shows banner when updates available
- **SSL support** — Accepts self-signed certificates - **SSL support** — Accepts self-signed certificates
@@ -313,6 +320,9 @@ The application uses no build tools — edit files directly and refresh.
- [x] AI scan local matching — suggest existing pantry products before OFF lookup - [x] AI scan local matching — suggest existing pantry products before OFF lookup
- [x] Scale auto-fill improvements — 10g threshold, ml conversion hints - [x] Scale auto-fill improvements — 10g threshold, ml conversion hints
- [x] Update notification system — kiosk checks GitHub releases - [x] Update notification system — kiosk checks GitHub releases
- [x] Generic shopping name grouping — compound-phrase + keyword map (100+ entries) + Gemini AI fallback
- [x] Auto-add to Bring! on product depletion — no confirmation step when stock reaches zero
- [x] Native Android TTS in kiosk — bypasses Web Speech API voice detection issues
- [ ] Offline mode with service worker - [ ] Offline mode with service worker
- [ ] Export/import inventory data - [ ] Export/import inventory data
- [ ] Notification system (Telegram, email) for expiring products - [ ] Notification system (Telegram, email) for expiring products
+1625 -397
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -1963,6 +1963,14 @@ body {
line-height: 1.3; line-height: 1.3;
} }
.smart-item-specific {
font-size: 0.73rem;
color: var(--text-muted);
margin-top: 1px;
line-height: 1.3;
font-style: italic;
}
.smart-brand { .smart-brand {
font-weight: 400; font-weight: 400;
color: var(--text-muted); color: var(--text-muted);
@@ -3115,6 +3123,8 @@ body {
margin-top: 16px; margin-top: 16px;
color: var(--text-muted); color: var(--text-muted);
font-weight: 600; font-weight: 600;
transition: opacity 0.25s ease;
min-height: 1.4em;
} }
.recipe-result { .recipe-result {
@@ -4525,6 +4535,12 @@ body {
background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%); background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%);
border-color: #8b5cf6; border-color: #8b5cf6;
} }
.alert-banner.banner-anomaly {
background: linear-gradient(135deg, #fff7ed 0%, #fed7aa 100%);
border-color: #ea580c;
}
.banner-anomaly .alert-banner-title { color: #9a3412; }
.banner-anomaly .alert-banner-counter .banner-dot.active { background: #ea580c; }
.alert-banner-inner { .alert-banner-inner {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -5663,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;
}
+742 -388
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925}
+645
View File
@@ -0,0 +1,645 @@
{
"de2it": {
"Getreideriegel": "Barretta ai cereali",
"Glasreiniger": "Pulizia vetri",
"Gartenwerkzeug": "Atrezzi da giardino",
"Getränke": "Bibite",
"Hackfleisch": "Carne macinata",
"Baumarkt & Garten": "Fai da te & Giardino",
"Kekse": "Biscotti",
"Salami": "Salame",
"Lippenpomade": "Burrocacao",
"Putzmittel": "Detergente",
"Samen": "Sementi",
"Wassermelone": "Anguria",
"Schokolade": "Cioccolato",
"Fertig- & Tiefkühlprodukte": "Piatti Pronti & Surgelati",
"Käse": "Formaggio",
"Giesskanne": "Annaffiatoio",
"Bratwurst": "Wurstel",
"Fenchel": "Finocchio",
"Fruchtsaft": "Succo di frutta",
"Grissini": "Grissini",
"Brokkoli": "Broccoli",
"Eistee": "Tè freddo",
"Haarspray": "Spray",
"Pflaumen": "Susina",
"Pommes Chips": "Patatine",
"Schweinefleisch": "Carne di maiale",
"Backpapier": "Carta da forno",
"Brot": "Pane",
"Orangensaft": "Succo d'arancia",
"Geschirrsalz": "Sale Lavastoviglie",
"Gipfeli": "Cornetti",
"Birnen": "Pere",
"Eier": "Uova",
"Makeup Entferner": "Struccante",
"Steinpilze": "Porcini",
"Kartoffeln": "Patate",
"Rasierklingen": "Ricambi rasoio",
"Gemüse": "Verdure",
"Kaffee": "Caffè",
"Frischkäse": "Formaggio cremoso",
"Zutaten & Gewürze": "Ingredienti & Spezie",
"Öl": "Olio",
"Trauben": "Uva",
"Salz": "Sale",
"Balsamico": "Aceto Balsamico",
"Fisch": "Pesce",
"Radicchio": "Radicchio",
"Geschenk": "Regalo",
"Blumen": "Fiori",
"Limonade": "Bibite",
"Schwamm": "Spugna",
"Limette": "Limone verde",
"Aubergine": "Melanzana",
"Schinken": "Prosciutto cotto",
"Zucchetti": "Zucchine",
"Rum": "Rum",
"Frühlingszwiebeln": "Cipollotti",
"Spargel": "Asparagi",
"Sonnencreme": "Crema da sole",
"Gnocchi": "Gnocchi",
"Handcreme": "Crema mani",
"Schnittlauch": "Erba Cipollina",
"Snacks & Süsswaren": "Snack & Dolci",
"Pelati": "Pelati",
"Fischstäbli": "Bastoncini di pesce",
"Margarine": "Margarina",
"Fleisch & Fisch": "Carne & Pesce",
"Zigaretten": "Sigarette",
"Oregano": "Origano",
"Basmatireis": "Riso Basmati",
"Zahnseide": "Filo interdentale",
"Tofu": "Tofu",
"Energy Drink": "Energy Drink",
"Peperoni": "Peperoni",
"Sirup": "Sciroppo",
"Feigen": "Fichi",
"Haselnüsse": "Nocciole",
"Mehl": "Farina",
"Haferflocken": "Avena",
"Apfelmus": "Composta di mele",
"Reis": "Riso",
"Mascarpone": "Mascarpone",
"Rasenmäher": "Tosaerba",
"Schnitzel": "Scaloppine",
"Grill": "Griglia",
"Ketchup": "Ketchup",
"Lachs": "Salmone",
"Zwiebeln": "Cipolle",
"Beeren": "Bacche",
"Pflaster": "Cerotti",
"Fischfutter": "Mangime per pesci",
"Kerzen": "Candele",
"Früchte": "Frutta",
"Kalbfleisch": "Carne di vitello",
"Rasierschaum": "Schiuma da barba",
"Ingwer": "Zenzero",
"Pfirsich": "Pesca",
"Sauerrahm": "Panna Acidula",
"Früchte & Gemüse": "Frutta & Verdura",
"Lasagne": "Lasagne",
"Pinsel": "Pennello",
"Hefe": "Lievito",
"Kuchen": "Torta",
"Prosecco": "Prosecco",
"Tampons": "Assorbenti",
"Thunfisch": "Pesce Tonno",
"Zucker": "Zucchero",
"Piadina": "Piadina",
"Pouletbrüstli": "Petto di pollo",
"Kastanien": "Castagne",
"Blumenkohl": "Cavolfiore",
"Salat": "Insalata",
"Fleisch": "Carne",
"Corn Flakes": "Cereali colazione",
"Merendina": "Merendina",
"Knoblauch": "Aglio",
"Karotten": "Carote",
"Toast": "Toast",
"Waschmittel": "Detersivo lavatrice",
"Salatsauce": "Condimento insalata",
"Hundefutter": "Cibo per cani",
"Vanillezucker": "Zucchero vanigliato",
"Mundspülung": "Collutorio",
"Babynahrung": "Alimenti Bimbi",
"Windeln": "Pannolini",
"Kondome": "Preservativo",
"Couscous": "Couscous",
"Geschirrglanz": "Brillantante",
"Aprikosen": "Albicocche",
"Himbeeren": "Lamponi",
"Oliven": "Olive",
"Lebkuchen": "Panpepato",
"Getreideprodukte": "Pasta, Riso & Cereali",
"Kürbis": "Zucca",
"Tonic Water": "Tonic",
"Nektarine": "Pesche noci",
"Penne": "Penne",
"Shampoo": "Shampoo",
"Whisky": "Whisky",
"Datteln": "Datteri",
"Kakao": "Cacao",
"Olivenöl": "Olio d'oliva",
"Bohnen": "Fagioli",
"Pizza": "Pizza",
"Kiwi": "Kiwi",
"Poulet": "Pollo",
"Wasser": "Acqua",
"Pasta": "Pasta",
"Milch": "Latte",
"Kirschen": "Ciliegie",
"Mandeln": "Mandorle",
"Milch & Käse": "Latte & Formaggi",
"Kichererbsen": "Ceci",
"Kosmetiktücher": "Kleenex",
"Kaugummi": "Gomma da masticare",
"Gesichtscreme": "Crema viso",
"getrocknete Tomaten": "Pomodori secchi",
"Champignons": "Champignons",
"Cola Light": "Cola Light",
"Orange": "Arance",
"Alufolie": "Foglio di alluminio",
"Melone": "Melone",
"Bananen": "Banane",
"Zahnbürsten": "Spazzolino",
"Zimt": "Cannella",
"Äpfel": "Mele",
"Cola": "Cola",
"Bouillon": "Brodo",
"Salbei": "Salvia",
"Soyasauce": "Salsa di soia",
"Rohschinken": "Prosciutto crudo",
"Reibkäse": "Formaggio grattugiato",
"Aufschnitt": "Affettato",
"Geschirrtabs": "Past. Lavastoviglie",
"Sonnenschirm": "Ombrellone",
"Bresaola": "Bresaola",
"Mineralwasser": "Acqua minerale",
"Taschentücher": "Fazzoletti",
"Haushalt & Gesundheit": "Casa & Igiene",
"Feuchttücher": "Salviette",
"Erbsen": "Piselli",
"Parmesan": "Parmigiano",
"Nougatcreme": "Crema gianduia",
"Speck": "Pancetta",
"Tierbedarf": "Animali",
"Avocado": "Avocado",
"Paprikapulver": "Paprica",
"Abfallsäcke": "Sacchi della spazzatura",
"Essig": "Aceto",
"Dünger": "Concime",
"Pilze": "Funghi",
"Batterien": "Batterie",
"Tomatensauce": "Sugo di pomodoro",
"Rucola": "Rucola",
"Bier": "Birra",
"Blumenerde": "Terriccio",
"Rhabarber": "Rabarbaro",
"Artischocken": "Carciofi",
"Rosmarin": "Rosmarino",
"Thon": "Tonno",
"Linsenmittel": "Soluzione lenti",
"Nagellackentferner": "Acetone",
"Bodylotion": "Crema corpo",
"Apfelsaft": "Succo di mela",
"Guetzli": "Biscotti di Natale",
"Tomatenmark": "Concentrato Pomodoro",
"Gurke": "Cetriolo",
"Holzkohle": "Carbonella",
"Basilikum": "Basilico",
"Joghurt": "Yogurt",
"Getränke & Tabak": "Bevande & Tabacco",
"Pop Corn": "Popcorn",
"Weichspüler": "Ammorbidente",
"Butter": "Burro",
"Rotwein": "Vino Rosso",
"Frankfurter": "Luganega",
"Schnaps": "Grappa",
"Tomaten": "Pomodori",
"Ricotta": "Ricotta",
"Watterondellen": "Dischetti di cotone",
"Erdbeeren": "Fragole",
"Vogelfutter": "Cibo per uccelli",
"Thymian": "Timo",
"Puderzucker": "Zucchero a velo",
"Kräuterbutter": "Burro alle erbe",
"Kaki": "Cachi",
"Erdnüsse": "Arachidi",
"Pfefferkörner": "Grani di pepe",
"Schrauben": "Viti",
"Sardellen": "Acciughe",
"Rindfleisch": "Carne di manzo",
"Conditioner": "Balsamo",
"Pizzateig": "Pasta per pizza",
"Zitrone": "Limone",
"Nägel": "Chiodi",
"Peperoncini": "Peperoncini",
"Senf": "Senape",
"Brötchen": "Panini",
"Baumnüsse": "Noci",
"Nudeln": "Tagliatelle",
"Wurst": "Salsiccia",
"Pudding": "Pudding",
"Griess": "Semolino",
"Mandarinen": "Mandarini",
"Weisswein": "Vino bianco",
"Blätterteig": "Pasta Sfoglia",
"Cherrytomaten": "Pomodorini",
"Pfefferminze": "Menta",
"Katzenstreu": "Sabbia gatti",
"Zwetschgen": "Prugne",
"Brombeeren": "More",
"Gin": "Gin",
"Vodka": "Vodka",
"Honig": "Miele",
"WC-Papier": "Carta igienica",
"Brot & Gebäck": "Panetteria",
"Paniermehl": "Pangrattato",
"Abwaschmittel": "Detersivo Piatti",
"Rahm": "Panna",
"Mayonnaise": "Maionese",
"Spülmittel": "Detersivo",
"Sellerie": "Sedano",
"Lauch": "Porro",
"Rindsgeschnetzeltes": "Sminuzzato manzo",
"WC-Reiniger": "Detergente per WC",
"Baguette": "Baguette",
"Konfitüre": "Marmellata",
"Schmerzmittel": "Analgesico",
"Badreiniger": "Pulizia bagno",
"Mango": "Mango",
"Mozzarella": "Mozzarella",
"Ananas": "Ananas",
"Propangas": "Propano",
"Bratensauce": "Salsa per arrosto",
"Orecchiette": "Orecchiette",
"Lamm": "Agnello",
"Frischhaltefolie": "Pellicole",
"Zahnpasta": "Dentifricio",
"Spaghetti": "Spaghetti",
"Haargel": "Gel Styling",
"Snacks": "Snack",
"Petersilie": "Prezzemolo",
"Grapefruit": "Pompelmo",
"Grana Padano": "Grana Padano",
"Servietten": "Tovaglioli",
"Töpfe": "Vasi",
"Linsen": "Lenticchie",
"Duschmittel": "Crema doccia",
"Gorgonzola": "Gorgonzola",
"Spinat": "Spinaci",
"Backpulver": "Bicarbonato",
"Risottoreis": "Risotto",
"Rasierer": "Rasoio",
"Pommes Frites": "Patate fritte",
"Deo": "Deodorante",
"Pflanzen": "Piante",
"Katzenfutter": "Cibo per gatti",
"Geschenkpapier": "Carta da regalo",
"Tee": "Tè",
"Wattestäbchen": "Bastoncini cotonati",
"Kräuter": "Erbe",
"Seife": "Sapone",
"Glacé": "Gelato",
"Mais": "Mais",
"Haushaltspapier": "Carta domestica",
"Polenta": "Polenta",
"Eigene Artikel": "Tuoi articoli",
"Zuletzt verwendet": "Utilizzato per ultimo"
},
"it2de": {
"barretta ai cereali": "Getreideriegel",
"pulizia vetri": "Glasreiniger",
"atrezzi da giardino": "Gartenwerkzeug",
"bibite": "Limonade",
"carne macinata": "Hackfleisch",
"fai da te & giardino": "Baumarkt & Garten",
"biscotti": "Kekse",
"salame": "Salami",
"burrocacao": "Lippenpomade",
"detergente": "Putzmittel",
"sementi": "Samen",
"anguria": "Wassermelone",
"cioccolato": "Schokolade",
"piatti pronti & surgelati": "Fertig- & Tiefkühlprodukte",
"formaggio": "Käse",
"annaffiatoio": "Giesskanne",
"wurstel": "Bratwurst",
"finocchio": "Fenchel",
"succo di frutta": "Fruchtsaft",
"grissini": "Grissini",
"broccoli": "Brokkoli",
"tè freddo": "Eistee",
"spray": "Haarspray",
"susina": "Pflaumen",
"patatine": "Pommes Chips",
"carne di maiale": "Schweinefleisch",
"carta da forno": "Backpapier",
"pane": "Brot",
"succo d'arancia": "Orangensaft",
"sale lavastoviglie": "Geschirrsalz",
"cornetti": "Gipfeli",
"pere": "Birnen",
"uova": "Eier",
"struccante": "Makeup Entferner",
"porcini": "Steinpilze",
"patate": "Kartoffeln",
"ricambi rasoio": "Rasierklingen",
"verdure": "Gemüse",
"caffè": "Kaffee",
"formaggio cremoso": "Frischkäse",
"ingredienti & spezie": "Zutaten & Gewürze",
"olio": "Öl",
"uva": "Trauben",
"sale": "Salz",
"aceto balsamico": "Balsamico",
"pesce": "Fisch",
"radicchio": "Radicchio",
"regalo": "Geschenk",
"fiori": "Blumen",
"spugna": "Schwamm",
"limone verde": "Limette",
"melanzana": "Aubergine",
"prosciutto cotto": "Schinken",
"zucchine": "Zucchetti",
"rum": "Rum",
"cipollotti": "Frühlingszwiebeln",
"asparagi": "Spargel",
"crema da sole": "Sonnencreme",
"gnocchi": "Gnocchi",
"crema mani": "Handcreme",
"erba cipollina": "Schnittlauch",
"snack & dolci": "Snacks & Süsswaren",
"pelati": "Pelati",
"bastoncini di pesce": "Fischstäbli",
"margarina": "Margarine",
"carne & pesce": "Fleisch & Fisch",
"sigarette": "Zigaretten",
"origano": "Oregano",
"riso basmati": "Basmatireis",
"filo interdentale": "Zahnseide",
"tofu": "Tofu",
"energy drink": "Energy Drink",
"peperoni": "Peperoni",
"sciroppo": "Sirup",
"fichi": "Feigen",
"nocciole": "Haselnüsse",
"farina": "Mehl",
"avena": "Haferflocken",
"composta di mele": "Apfelmus",
"riso": "Reis",
"mascarpone": "Mascarpone",
"tosaerba": "Rasenmäher",
"scaloppine": "Schnitzel",
"griglia": "Grill",
"ketchup": "Ketchup",
"salmone": "Lachs",
"cipolle": "Zwiebeln",
"bacche": "Beeren",
"cerotti": "Pflaster",
"mangime per pesci": "Fischfutter",
"candele": "Kerzen",
"frutta": "Früchte",
"carne di vitello": "Kalbfleisch",
"schiuma da barba": "Rasierschaum",
"zenzero": "Ingwer",
"pesca": "Pfirsich",
"panna acidula": "Sauerrahm",
"frutta & verdura": "Früchte & Gemüse",
"lasagne": "Lasagne",
"pennello": "Pinsel",
"lievito": "Hefe",
"torta": "Kuchen",
"prosecco": "Prosecco",
"assorbenti": "Tampons",
"pesce tonno": "Thunfisch",
"zucchero": "Zucker",
"piadina": "Piadina",
"petto di pollo": "Pouletbrüstli",
"castagne": "Kastanien",
"cavolfiore": "Blumenkohl",
"insalata": "Salat",
"carne": "Fleisch",
"cereali colazione": "Corn Flakes",
"merendina": "Merendina",
"aglio": "Knoblauch",
"carote": "Karotten",
"toast": "Toast",
"detersivo lavatrice": "Waschmittel",
"condimento insalata": "Salatsauce",
"cibo per cani": "Hundefutter",
"zucchero vanigliato": "Vanillezucker",
"collutorio": "Mundspülung",
"alimenti bimbi": "Babynahrung",
"pannolini": "Windeln",
"preservativo": "Kondome",
"couscous": "Couscous",
"brillantante": "Geschirrglanz",
"albicocche": "Aprikosen",
"lamponi": "Himbeeren",
"olive": "Oliven",
"panpepato": "Lebkuchen",
"pasta, riso & cereali": "Getreideprodukte",
"zucca": "Kürbis",
"tonic": "Tonic Water",
"pesche noci": "Nektarine",
"penne": "Penne",
"shampoo": "Shampoo",
"whisky": "Whisky",
"datteri": "Datteln",
"cacao": "Kakao",
"olio d'oliva": "Olivenöl",
"fagioli": "Bohnen",
"pizza": "Pizza",
"kiwi": "Kiwi",
"pollo": "Poulet",
"acqua": "Wasser",
"pasta": "Pasta",
"latte": "Milch",
"ciliegie": "Kirschen",
"mandorle": "Mandeln",
"latte & formaggi": "Milch & Käse",
"ceci": "Kichererbsen",
"kleenex": "Kosmetiktücher",
"gomma da masticare": "Kaugummi",
"crema viso": "Gesichtscreme",
"pomodori secchi": "getrocknete Tomaten",
"champignons": "Champignons",
"cola light": "Cola Light",
"arance": "Orange",
"foglio di alluminio": "Alufolie",
"melone": "Melone",
"banane": "Bananen",
"spazzolino": "Zahnbürsten",
"cannella": "Zimt",
"mele": "Äpfel",
"cola": "Cola",
"brodo": "Bouillon",
"salvia": "Salbei",
"salsa di soia": "Soyasauce",
"prosciutto crudo": "Rohschinken",
"formaggio grattugiato": "Reibkäse",
"affettato": "Aufschnitt",
"past. lavastoviglie": "Geschirrtabs",
"ombrellone": "Sonnenschirm",
"bresaola": "Bresaola",
"acqua minerale": "Mineralwasser",
"fazzoletti": "Taschentücher",
"casa & igiene": "Haushalt & Gesundheit",
"salviette": "Feuchttücher",
"piselli": "Erbsen",
"parmigiano": "Parmesan",
"crema gianduia": "Nougatcreme",
"pancetta": "Speck",
"animali": "Tierbedarf",
"avocado": "Avocado",
"paprica": "Paprikapulver",
"sacchi della spazzatura": "Abfallsäcke",
"aceto": "Essig",
"concime": "Dünger",
"funghi": "Pilze",
"batterie": "Batterien",
"sugo di pomodoro": "Tomatensauce",
"rucola": "Rucola",
"birra": "Bier",
"terriccio": "Blumenerde",
"rabarbaro": "Rhabarber",
"carciofi": "Artischocken",
"rosmarino": "Rosmarin",
"tonno": "Thon",
"soluzione lenti": "Linsenmittel",
"acetone": "Nagellackentferner",
"crema corpo": "Bodylotion",
"succo di mela": "Apfelsaft",
"biscotti di natale": "Guetzli",
"concentrato pomodoro": "Tomatenmark",
"cetriolo": "Gurke",
"carbonella": "Holzkohle",
"basilico": "Basilikum",
"yogurt": "Joghurt",
"bevande & tabacco": "Getränke & Tabak",
"popcorn": "Pop Corn",
"ammorbidente": "Weichspüler",
"burro": "Butter",
"vino rosso": "Rotwein",
"luganega": "Frankfurter",
"grappa": "Schnaps",
"pomodori": "Tomaten",
"ricotta": "Ricotta",
"dischetti di cotone": "Watterondellen",
"fragole": "Erdbeeren",
"cibo per uccelli": "Vogelfutter",
"timo": "Thymian",
"zucchero a velo": "Puderzucker",
"burro alle erbe": "Kräuterbutter",
"cachi": "Kaki",
"arachidi": "Erdnüsse",
"grani di pepe": "Pfefferkörner",
"viti": "Schrauben",
"acciughe": "Sardellen",
"carne di manzo": "Rindfleisch",
"balsamo": "Conditioner",
"pasta per pizza": "Pizzateig",
"limone": "Zitrone",
"chiodi": "Nägel",
"peperoncini": "Peperoncini",
"senape": "Senf",
"panini": "Brötchen",
"noci": "Baumnüsse",
"tagliatelle": "Nudeln",
"salsiccia": "Wurst",
"pudding": "Pudding",
"semolino": "Griess",
"mandarini": "Mandarinen",
"vino bianco": "Weisswein",
"pasta sfoglia": "Blätterteig",
"pomodorini": "Cherrytomaten",
"menta": "Pfefferminze",
"sabbia gatti": "Katzenstreu",
"prugne": "Zwetschgen",
"more": "Brombeeren",
"gin": "Gin",
"vodka": "Vodka",
"miele": "Honig",
"carta igienica": "WC-Papier",
"panetteria": "Brot & Gebäck",
"pangrattato": "Paniermehl",
"detersivo piatti": "Abwaschmittel",
"panna": "Rahm",
"maionese": "Mayonnaise",
"detersivo": "Spülmittel",
"sedano": "Sellerie",
"porro": "Lauch",
"sminuzzato manzo": "Rindsgeschnetzeltes",
"detergente per wc": "WC-Reiniger",
"baguette": "Baguette",
"marmellata": "Konfitüre",
"analgesico": "Schmerzmittel",
"pulizia bagno": "Badreiniger",
"mango": "Mango",
"mozzarella": "Mozzarella",
"ananas": "Ananas",
"propano": "Propangas",
"salsa per arrosto": "Bratensauce",
"orecchiette": "Orecchiette",
"agnello": "Lamm",
"pellicole": "Frischhaltefolie",
"dentifricio": "Zahnpasta",
"spaghetti": "Spaghetti",
"gel styling": "Haargel",
"snack": "Snacks",
"prezzemolo": "Petersilie",
"pompelmo": "Grapefruit",
"grana padano": "Grana Padano",
"tovaglioli": "Servietten",
"vasi": "Töpfe",
"lenticchie": "Linsen",
"crema doccia": "Duschmittel",
"gorgonzola": "Gorgonzola",
"spinaci": "Spinat",
"bicarbonato": "Backpulver",
"risotto": "Risottoreis",
"rasoio": "Rasierer",
"patate fritte": "Pommes Frites",
"deodorante": "Deo",
"piante": "Pflanzen",
"cibo per gatti": "Katzenfutter",
"carta da regalo": "Geschenkpapier",
"tè": "Tee",
"bastoncini cotonati": "Wattestäbchen",
"erbe": "Kräuter",
"sapone": "Seife",
"gelato": "Glacé",
"mais": "Mais",
"carta domestica": "Haushaltspapier",
"polenta": "Polenta",
"tuoi articoli": "Eigene Artikel",
"utilizzato per ultimo": "Zuletzt verwendet",
"aroma": "Zutaten & Gewürze",
"ingredienti spezie": "Zutaten & Gewürze",
"bevande": "Getränke & Tabak",
"camomilla": "Tee",
"cioccolata calda": "Kakao",
"cipolla": "Zwiebeln",
"cracker": "Snacks & Süsswaren",
"farina integrale": "Mehl",
"fette biscottate": "Toast",
"filetto": "Fleisch",
"liquore": "Getränke & Tabak",
"muesli": "Corn Flakes",
"panna da cucina": "Rahm",
"passata": "Pelati",
"piatti pronti": "Fertig- & Tiefkühlprodukte",
"polpa di pomodoro": "Pelati",
"purè": "Fertig- & Tiefkühlprodukte",
"salsa": "Zutaten & Gewürze",
"sfornatini": "Fertig- & Tiefkühlprodukte",
"snack dolci": "Snacks & Süsswaren",
"succo": "Fruchtsaft",
"taralli": "Snacks & Süsswaren",
"vino": "Rotwein",
"zucchero di canna": "Zucker"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"dc1bb00e006a5ed073aad9b0ca2f1601": "Toast",
"f03b656f4cfaa9d633fc155cdafcb83b": "Sale",
"fa1266e5e6bb32602e08aaf9434ec9ad": "Patate",
"ca2da3ad2a7b42e717f766e06a83730e": "Verdure",
"ce8f4f54fc6ead0f0a8ce36503bba462": "Pasta",
"2ddb0faf33c4ceeed89fada2c7c2b9c5": "Ingredienti Spezie",
"0290647fcd95ec97f0d6666c46a72943": "Brodo",
"405ea6ec33d54042d046599650f422ea": "Succo",
"f624c420f14d8eff122c0bb395eb63da": "Snack Dolci",
"92751fbb97923590c402bc7810778b36": "Biscotti",
"8727f7abcb66764b5eb3d1f036bc18b8": "Tè",
"0eb53fe1a5d4d106eac47c8a81d1afe7": "Farina",
"0ebada5597d1d166d0ed8f49500bfeba": "Verdure",
"fe7456efb7e767a06e3af9f5ec7b3637": "Piatti Pronti",
"2a5d2289bb7bc306dd066dfaff7ef581": "Ingredienti Spezie",
"b630c06f2ac72a1e2ffbd57d327a3733": "Salsa",
"32a05ae91ccfa4d37be454836971436b": "Ingredienti",
"a21f0e7718c8f12166d864d0d05f60a0": "Salsa"
}
@@ -14,11 +14,13 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.speech.tts.TextToSpeech
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowInsetsController import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import android.webkit.ConsoleMessage import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import android.webkit.SslErrorHandler import android.webkit.SslErrorHandler
import android.webkit.ValueCallback import android.webkit.ValueCallback
@@ -39,6 +41,7 @@ import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.json.JSONObject import org.json.JSONObject
import java.net.URL import java.net.URL
import java.util.Locale
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
@@ -49,6 +52,11 @@ class KioskActivity : AppCompatActivity() {
private lateinit var prefs: SharedPreferences private lateinit var prefs: SharedPreferences
private var currentStep = 1 private var currentStep = 1
// Native TTS engine (Android) — used by the JS bridge so the WebView
// doesn't depend on Web Speech API voices being installed.
private var tts: TextToSpeech? = null
private var ttsReady = false
// Views // Views
private lateinit var splashContainer: LinearLayout private lateinit var splashContainer: LinearLayout
private lateinit var wizardContainer: ScrollView private lateinit var wizardContainer: ScrollView
@@ -98,6 +106,19 @@ class KioskActivity : AppCompatActivity() {
enableKioskLock() enableKioskLock()
requestAllPermissions() requestAllPermissions()
// Initialise native TTS engine so the JS bridge works even when
// Web Speech API voices are unavailable in the Android WebView.
tts = TextToSpeech(this) { status ->
if (status == TextToSpeech.SUCCESS) {
val it = tts?.setLanguage(Locale.ITALIAN)
if (it == TextToSpeech.LANG_MISSING_DATA || it == TextToSpeech.LANG_NOT_SUPPORTED) {
// Italian data missing — fall back to device default
tts?.language = Locale.getDefault()
}
ttsReady = true
}
}
// Show splash then proceed // Show splash then proceed
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
splashContainer.visibility = View.GONE splashContainer.visibility = View.GONE
@@ -506,7 +527,7 @@ class KioskActivity : AppCompatActivity() {
// Add JS interface ONCE before loading // Add JS interface ONCE before loading
webView.addJavascriptInterface(object { webView.addJavascriptInterface(object {
@android.webkit.JavascriptInterface @JavascriptInterface
fun exit() { fun exit() {
runOnUiThread { runOnUiThread {
disableKioskLock() disableKioskLock()
@@ -514,13 +535,35 @@ class KioskActivity : AppCompatActivity() {
finishAffinity() finishAffinity()
} }
} }
@android.webkit.JavascriptInterface @JavascriptInterface
fun hardReload() { fun hardReload() {
runOnUiThread { runOnUiThread {
webView.clearCache(true) webView.clearCache(true)
webView.reload() webView.reload()
} }
} }
/**
* Speak [text] via Android native TTS.
* Called by app.js when running inside the kiosk WebView so that
* speech synthesis works even without Web Speech API offline voices.
* [rate] and [pitch] are floats (default 1.0).
*/
@JavascriptInterface
fun speak(text: String, rate: Float, pitch: Float) {
val engine = tts ?: return
if (!ttsReady) return
engine.setSpeechRate(rate.coerceIn(0.1f, 4f))
engine.setPitch(pitch.coerceIn(0.1f, 4f))
engine.speak(text, TextToSpeech.QUEUE_FLUSH, null, "kiosk_tts")
}
/** Cancel any ongoing speech. */
@JavascriptInterface
fun stopSpeech() {
tts?.stop()
}
/** Returns "true" when the TTS engine is ready. */
@JavascriptInterface
fun isTtsReady(): String = if (ttsReady) "true" else "false"
}, "_kioskBridge") }, "_kioskBridge")
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local" val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
@@ -729,6 +772,13 @@ class KioskActivity : AppCompatActivity() {
} }
} }
override fun onDestroy() {
tts?.stop()
tts?.shutdown()
tts = null
super.onDestroy()
}
override fun onBackPressed() { override fun onBackPressed() {
if (webView.visibility == View.VISIBLE && webView.canGoBack()) { if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
webView.goBack() webView.goBack()
+1
View File
@@ -1,2 +1,3 @@
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# Build trigger: TTS bridge fix (95389eb)
+10 -7
View File
@@ -11,7 +11,7 @@
<title>EverShelf</title> <title>EverShelf</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>"> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
<link rel="stylesheet" href="assets/css/style.css?v=20260420a"> <link rel="stylesheet" href="assets/css/style.css?v=20260421a">
<!-- QuaggaJS for barcode scanning --> <!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
</head> </head>
@@ -930,10 +930,13 @@
<div id="tts-browser-section"> <div id="tts-browser-section">
<div class="form-group"> <div class="form-group">
<label>🗣️ Voce</label> <label>🗣️ Voce</label>
<select id="setting-tts-voice" class="form-input"> <div style="display:flex;gap:8px;align-items:center">
<option value="">— Caricamento voci… —</option> <select id="setting-tts-voice" class="form-input" style="flex:1">
</select> <option value="">— Caricamento voci… —</option>
<p class="settings-hint">Le voci disponibili dipendono dal sistema operativo e dal browser. Su macOS/iOS è disponibile la voce <strong>Paola</strong> (italiano).</p> </select>
<button type="button" class="btn btn-secondary" style="padding:8px 12px;white-space:nowrap;flex-shrink:0" onclick="_initBrowserTtsVoices(document.getElementById('setting-tts-voice').value)"></button>
</div>
<p class="settings-hint">Le voci disponibili dipendono dal sistema operativo e dal browser. Su macOS/iOS è disponibile la voce <strong>Paola</strong> (italiano). Premi ↺ se la lista non si carica.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>⚡ Velocità: <span id="tts-rate-label">1.0</span>×</label> <label>⚡ Velocità: <span id="tts-rate-label">1.0</span>×</label>
@@ -1195,7 +1198,7 @@
</div> </div>
<div id="recipe-loading" style="display:none" class="recipe-loading"> <div id="recipe-loading" style="display:none" class="recipe-loading">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<p>Sto preparando la ricetta...</p> <p id="recipe-loading-msg">Sto preparando la ricetta...</p>
</div> </div>
<div id="recipe-result" style="display:none" class="recipe-result"> <div id="recipe-result" style="display:none" class="recipe-result">
<div id="recipe-content"></div> <div id="recipe-content"></div>
@@ -1288,6 +1291,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260420a"></script> <script src="assets/js/app.js?v=20260421a"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "EverShelf", "name": "EverShelf",
"short_name": "EverShelf", "short_name": "EverShelf",
"description": "Gestione completa della dispensa di casa con scansione barcode", "description": "Gestione completa della dispensa di casa con scansione barcode",
"version": "1.4.0", "version": "1.5.0",
"start_url": "/evershelf/", "start_url": "/evershelf/",
"display": "standalone", "display": "standalone",
"background_color": "#f0f4e8", "background_color": "#f0f4e8",
+208
View File
@@ -0,0 +1,208 @@
<?php
/**
* CLI Test for generateRecipeStream
* Tests: prompt token reduction (B), SSE output format, model fallback (C)
* Run: php test_recipe_stream.php
*/
define('CRON_MODE', true); // skip HTTP routing
require_once __DIR__ . '/api/database.php';
require_once __DIR__ . '/api/index.php';
// ── helpers ──────────────────────────────────────────────────────────────────
function pass(string $msg): void { echo "\033[32m✓\033[0m $msg\n"; }
function fail(string $msg): void { echo "\033[31m✗\033[0m $msg\n"; }
function info(string $msg): void { echo "\033[33m→\033[0m $msg\n"; }
// ── TEST 1: API key present ───────────────────────────────────────────────────
$apiKey = env('GEMINI_API_KEY');
if (!empty($apiKey)) {
pass("API key set (" . substr($apiKey, 0, 8) . "...)");
} else {
fail("API key missing in .env");
exit(1);
}
// ── TEST 2: DB reachable + has inventory ─────────────────────────────────────
$db = getDB();
$itemCount = $db->query("SELECT count(*) FROM inventory WHERE quantity > 0")->fetchColumn();
if ($itemCount > 0) {
pass("Inventory: $itemCount items");
} else {
fail("Inventory is empty — cannot generate recipe");
exit(1);
}
// ── TEST 3: Prompt token estimation (B) ─────────────────────────────────────
// Simulate building the ingredient list with new limits
$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,
CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left
FROM inventory i
JOIN products p ON p.id = i.product_id
WHERE i.quantity > 0 ORDER BY days_left ASC
");
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
$getItemPriority = function($item): int {
$daysLeft = floatval($item['days_left']);
$isOpen = !empty($item['opened_at']) || (floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf');
if (!empty($item['expiry_date']) && $daysLeft < 0) return 1;
if (!empty($item['expiry_date']) && $daysLeft <= 3) return 2;
if (!empty($item['expiry_date']) && $daysLeft <= 7) return 3;
if (!empty($item['expiry_date'])) return 4;
if ($isOpen) return 5;
return 6;
};
$staplePatterns = '/\b(sale|pepe|olio d.oliva|olio di semi|olio extra|acqua|aceto balsamico|aceto di|sel marin)\b/i';
$priorityGroups = [];
foreach ($items as $item) {
$group = $getItemPriority($item);
if ($group >= 5 && preg_match($staplePatterns, $item['name'])) continue;
$line = "- {$item['name']}: {$item['quantity']} {$item['unit']}";
$priorityGroups[$group][] = $line;
}
// OLD limits
$oldSections = [];
foreach ([1=>null,2=>null,3=>null,4=>40,5=>null,6=>20] as $g => $limit) {
if (empty($priorityGroups[$g])) continue;
$gi = $limit ? array_slice($priorityGroups[$g], 0, $limit) : $priorityGroups[$g];
$oldSections[] = implode("\n", $gi);
}
$oldText = implode("\n", $oldSections);
$oldTokens = (int)(str_word_count($oldText) * 1.3); // rough estimate: words * 1.3
// NEW limits
$newSections = [];
foreach ([1=>null,2=>null,3=>null,4=>15,5=>null,6=>8] as $g => $limit) {
if (empty($priorityGroups[$g])) continue;
$gi = $limit ? array_slice($priorityGroups[$g], 0, $limit) : $priorityGroups[$g];
$newSections[] = implode("\n", $gi);
}
$newText = implode("\n", $newSections);
$newTokens = (int)(str_word_count($newText) * 1.3);
$savings = $oldTokens > 0 ? round(($oldTokens - $newTokens) / $oldTokens * 100) : 0;
info("Prompt ingredient tokens: OLD ~$oldTokens → NEW ~$newTokens (saved ~$savings%)");
if ($savings >= 20) {
pass("Token reduction >= 20% (got $savings%)");
} else {
fail("Token reduction too low ($savings%) — check group limits");
}
// ── TEST 4: Real SSE call via HTTP ───────────────────────────────────────────
info("Calling generate_recipe_stream via HTTP (cena, pesce, 2 persone)...");
$postData = json_encode([
'meal' => 'cena',
'persons' => 2,
'sub_type' => '',
'options' => [],
'appliances' => [],
'dietary_restrictions' => '',
'today_recipes' => [],
'meal_plan_type' => 'pesce',
'variation' => 0,
'rejected_ingredients' => [],
]);
$startTime = microtime(true);
$ch = curl_init('https://localhost/dispensa/api/index.php?action=generate_recipe_stream');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
$elapsed = round(microtime(true) - $startTime, 1);
if ($curlErr) {
fail("curl error: $curlErr");
// Try via PHP CLI directly instead
info("Trying direct PHP execution instead...");
// Simulate SSE output capture
ob_start();
$_GET['action'] = 'generate_recipe_stream';
$_SERVER['REQUEST_METHOD'] = 'POST';
// Override php://input
$tmpFile = tempnam(sys_get_temp_dir(), 'recipe_test_');
file_put_contents($tmpFile, $postData);
// Can't easily override php://input in CLI, skip HTTP test
ob_end_clean();
info("HTTP test skipped (no web server on localhost) — checking SSE parsing only");
$response = null;
}
if ($response !== null) {
if ($httpCode !== 200) {
fail("HTTP status $httpCode (expected 200)");
} else {
pass("HTTP 200 in {$elapsed}s");
}
// Parse SSE events
$events = [];
foreach (explode("\n", $response) as $line) {
if (strpos($line, 'data: ') === 0) {
$evt = json_decode(substr($line, 6), true);
if ($evt) $events[] = $evt;
}
}
info("SSE events received: " . count($events));
foreach ($events as $evt) {
$type = $evt['type'] ?? '?';
$msg = $evt['message'] ?? $evt['error'] ?? json_encode($evt);
info(" [$type] $msg");
}
$statusEvents = array_filter($events, fn($e) => ($e['type'] ?? '') === 'status');
$recipeEvents = array_filter($events, fn($e) => ($e['type'] ?? '') === 'recipe');
$errorEvents = array_filter($events, fn($e) => ($e['type'] ?? '') === 'error');
if (!empty($errorEvents)) {
$err = reset($errorEvents);
$errMsg = $err['error'] ?? 'unknown';
$errDetail = $err['detail'] ?? '';
$errCode = $err['http_code'] ?? '';
fail("Got error event: $errMsg | code=$errCode | $errDetail");
} elseif (!empty($recipeEvents)) {
$recipe = reset($recipeEvents)['recipe'] ?? [];
pass("Got recipe: \"" . ($recipe['title'] ?? '?') . "\"");
// Verify steps exist
if (!empty($recipe['steps']) && count($recipe['steps']) >= 2) {
pass("Recipe has " . count($recipe['steps']) . " steps");
} else {
fail("Recipe missing steps");
}
// Verify meal type
if (($recipe['meal'] ?? '') === 'cena') {
pass("Meal type correct (cena)");
} else {
fail("Meal type wrong: " . ($recipe['meal'] ?? 'missing'));
}
// Check steps count
if (count($statusEvents) >= 3) {
pass("Got " . count($statusEvents) . " status events (agent steps working)");
} else {
fail("Too few status events: " . count($statusEvents));
}
} else {
fail("No recipe and no error event in SSE response");
echo "Raw response (first 500 chars):\n" . substr($response, 0, 500) . "\n";
}
}
echo "\n\033[1mDone.\033[0m\n";
+242 -24
View File
@@ -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",
@@ -85,24 +87,53 @@
"quick_recipe": "🍳 Schnelles Rezept mit ablaufenden Produkten", "quick_recipe": "🍳 Schnelles Rezept mit ablaufenden Produkten",
"banner_review_title": "Ungewöhnliche Menge", "banner_review_title": "Ungewöhnliche Menge",
"banner_review_action_ok": "Ist korrekt", "banner_review_action_ok": "Ist korrekt",
"banner_review_action_edit": "Bearbeiten", "banner_review_action_edit": "Korrigieren",
"banner_review_action_weigh": "Wiegen", "banner_review_action_weigh": "Wiegen",
"banner_review_dismiss": "Ignorieren", "banner_review_dismiss": "Ignorieren",
"banner_prediction_title": "Ungewöhnlicher Verbrauch", "banner_prediction_title": "Ungewöhnlicher Verbrauch",
"banner_prediction_hint": "Laut Vorhersage stimmt diese Menge nicht mit dem erwarteten Verbrauch überein.", "banner_prediction_hint": "Laut Vorhersage stimmt diese Menge nicht mit dem erwarteten Verbrauch überein.",
"banner_prediction_action_confirm": "Menge bestätigen", "banner_prediction_action_confirm": "{qty} {unit} bestätigen",
"banner_prediction_action_weigh": "Mit Waage wiegen", "banner_prediction_action_weigh": "Jetzt wiegen",
"banner_prediction_action_edit": "Korrigieren", "banner_prediction_action_edit": "Menge aktualisieren",
"banner_expired_title": "Abgelaufenes Produkt", "banner_expired_title": "Abgelaufenes Produkt",
"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_anomaly_action_edit": "Bestand korrigieren",
"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",
"banner_expiring_days": "Läuft in {days} Tagen ab", "banner_expiring_days": "Läuft in {days} Tagen ab",
"banner_expiring_action_use": "Jetzt verwenden" "banner_expiring_action_use": "Jetzt verwenden",
"banner_finished_title": "aufgebraucht?",
"banner_finished_detail": "Ich habe vermerkt, dass {name} auf null gesunken ist. Ist es wirklich leer, oder hast du noch welches?",
"banner_finished_action_yes": "Ja, aufgebraucht",
"banner_finished_action_no": "Nein, ich habe noch welches",
"banner_review_unusual_pkg_title": "Ungewöhnliche Packungsgröße",
"banner_review_unusual_pkg_detail": "Du hast eine Packung von {qty} {unit} eingestellt — die Größe scheint sehr groß. Überprüfe ob es korrekt ist.",
"banner_review_low_qty_title": "Sehr geringe Menge",
"banner_review_low_qty_detail": "Du hast nur {qty} im Bestand — das scheint sehr wenig, möglicherweise ein Eingabefehler. Bestätige wenn korrekt.",
"banner_review_high_qty_title": "Ungewöhnlich hohe Menge",
"banner_review_high_qty_detail": "Du hast {qty} im Bestand — die Zahl scheint sehr hoch. Bestätige wenn korrekt oder korrigiere.",
"banner_prediction_rate_day": "Durchschnitt ~{n} {unit}/Tag",
"banner_prediction_rate_week": "Durchschnitt ~{n} {unit}/Woche",
"banner_prediction_days_ago": "Vor {n} Tagen aufgefüllt",
"banner_prediction_more": "Ich erwartete {expected} {unit}{time}, du hast aber {actual} {unit}. Hast du Bestand ohne Buchung hinzugefügt?",
"banner_prediction_less": "Ich erwartete {expected} {unit}{time}, du hast aber nur {actual} {unit}. Hast du mehr als üblich verbraucht?",
"banner_finished_zero": "Bestand zeigt null, aber gespeicherte Buchungen deuten an, dass es nicht leer sein sollte.",
"banner_finished_expected": "Laut Aufzeichnungen solltest du noch {qty} {unit} haben.",
"banner_finished_check": "Kannst du nachschauen?",
"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_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?",
"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",
@@ -111,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",
@@ -133,7 +176,13 @@
"add_btn": "📥 HINZUFÜGEN", "add_btn": "📥 HINZUFÜGEN",
"add_sub": "in Vorrat/Kühlschrank", "add_sub": "in Vorrat/Kühlschrank",
"use_btn": "📤 VERWENDEN / VERBRAUCHEN", "use_btn": "📤 VERWENDEN / VERBRAUCHEN",
"use_sub": "aus Vorrat/Kühlschrank" "use_sub": "aus Vorrat/Kühlschrank",
"have_title": "📦 Schon auf Lager!",
"add_more_sub": "weitere Menge",
"use_qty_sub": "wie viel verwendet",
"throw_btn": "🗑️ ENTSORGEN",
"throw_sub": "wegwerfen",
"edit_sub": "Ablauf, Ort…"
}, },
"add": { "add": {
"title": "Zum Vorrat hinzufügen", "title": "Zum Vorrat hinzufügen",
@@ -143,7 +192,21 @@
"conf_size_placeholder": "z.B. 300", "conf_size_placeholder": "z.B. 300",
"vacuum_label": "🫙 Vakuumiert", "vacuum_label": "🫙 Vakuumiert",
"vacuum_hint": "Ablaufdatum wird automatisch verlängert", "vacuum_hint": "Ablaufdatum wird automatisch verlängert",
"submit": "✅ Hinzufügen" "submit": "✅ Hinzufügen",
"purchase_type_label": "🛒 Dieses Produkt ist...",
"new_btn": "🆕 Gerade gekauft",
"existing_btn": "📦 Hatte ich schon",
"remaining_label": "📦 Verbleibende Menge",
"remaining_hint": "Ungefähr wie viel ist noch übrig?",
"remaining_full": "🟢 Voll",
"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",
@@ -154,7 +217,18 @@
"submit": "📤 Diese Menge verwenden", "submit": "📤 Diese Menge verwenden",
"available": "📦 Verfügbar:", "available": "📦 Verfügbar:",
"not_in_inventory": "⚠️ Produkt nicht im Bestand.", "not_in_inventory": "⚠️ Produkt nicht im Bestand.",
"expiry_warning": "⚠️ Verwende zuerst die{loc}, die am {date} abläuft — {when}!" "expiry_warning": "⚠️ Verwende zuerst die{loc}, die am {date} abläuft — {when}!",
"throw_title": "🗑️ Produkt entsorgen",
"throw_all": "🗑️ ALLES entsorgen ({qty})",
"throw_qty_label": "Wie viel wegwerfen?",
"throw_qty_hint": "oder Menge angeben:",
"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",
@@ -186,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",
@@ -224,7 +300,33 @@
"smart_already": "📊 Intelligenter Einkauf sagt bereits {name} voraus", "smart_already": "📊 Intelligenter Einkauf sagt bereits {name} voraus",
"all_searched": "Alle Produkte wurden bereits gesucht. Nutze 🔄 für einzelne Suchen.", "all_searched": "Alle Produkte wurden bereits gesucht. Nutze 🔄 für einzelne Suchen.",
"search_complete": "Suche abgeschlossen: {count} Produkte", "search_complete": "Suche abgeschlossen: {count} Produkte",
"removed_sufficient": "🧹 {removed} Produkt(e) mit ausreichendem Bestand von der Liste entfernt" "removed_sufficient": "🧹 {removed} Produkt(e) mit ausreichendem Bestand von der Liste entfernt",
"bring_badge": "🛒 Schon auf Bring!",
"add_urgent_toast": "🔴 {n} dringende(s) Produkt(e) automatisch zu Bring! hinzugefügt",
"migration_done": "✅ {migrated} aktualisiert, {skipped} bereits ok",
"added_to_bring": "🛒 {n} Produkte zu Bring! hinzugefügt",
"added_to_bring_skip": "{n} bereits vorhanden",
"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",
@@ -233,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",
@@ -247,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",
@@ -256,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",
@@ -401,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!",
@@ -423,6 +586,7 @@
"finished_to_bring": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt", "finished_to_bring": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt",
"thrown_away": "🗑️ {name} weggeworfen!", "thrown_away": "🗑️ {name} weggeworfen!",
"thrown_away_partial": "🗑️ {qty} {unit} von {name} weggeworfen", "thrown_away_partial": "🗑️ {qty} {unit} von {name} weggeworfen",
"product_finished_confirmed": "✅ Entfernt — wieder hinzufügen, wenn du nachkaufst",
"appliance_added": "Gerät hinzugefügt", "appliance_added": "Gerät hinzugefügt",
"item_added": "{name} hinzugefügt" "item_added": "{name} hinzugefügt"
}, },
@@ -439,18 +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?"
}, },
"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",
@@ -485,11 +654,60 @@
"timeout": "Timeout: keine Antwort vom Gateway", "timeout": "Timeout: keine Antwort vom Gateway",
"error_connect": "Verbindung zum Gateway nicht möglich", "error_connect": "Verbindung zum Gateway nicht möglich",
"tab": "Smart-Waage", "tab": "Smart-Waage",
"low_weight": "Gewicht < 10 g · manuell eingeben\n(Auto-Erkennung erfordert mind. 10 g)" "low_weight": "Gewicht < 10 g · manuell eingeben\n(Auto-Erkennung erfordert mind. 10 g)",
"density_hint": "(Dichte {density} g/ml)",
"ml_hint": "(wird in ml umgerechnet)",
"weight_detected": "Gewicht erkannt — 10s Stabilität abwarten…",
"weight_too_low": "Gewicht zu niedrig — warten…",
"stable": "✓ Stabil",
"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"
} }
} }
+242 -24
View File
@@ -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",
@@ -85,24 +87,53 @@
"quick_recipe": "🍳 Quick recipe with expiring products", "quick_recipe": "🍳 Quick recipe with expiring products",
"banner_review_title": "Anomalous quantity", "banner_review_title": "Anomalous quantity",
"banner_review_action_ok": "It's correct", "banner_review_action_ok": "It's correct",
"banner_review_action_edit": "Edit", "banner_review_action_edit": "Correct",
"banner_review_action_weigh": "Weigh", "banner_review_action_weigh": "Weigh",
"banner_review_dismiss": "Dismiss", "banner_review_dismiss": "Dismiss",
"banner_prediction_title": "Anomalous consumption", "banner_prediction_title": "Anomalous consumption",
"banner_prediction_hint": "Based on predictions, this quantity doesn't match expected consumption.", "banner_prediction_hint": "Based on predictions, this quantity doesn't match expected consumption.",
"banner_prediction_action_confirm": "Confirm quantity", "banner_prediction_action_confirm": "Confirm {qty} {unit} is correct",
"banner_prediction_action_weigh": "Weigh with scale", "banner_prediction_action_weigh": "Weigh now",
"banner_prediction_action_edit": "Correct", "banner_prediction_action_edit": "Update quantity",
"banner_expired_title": "Expired product", "banner_expired_title": "Expired product",
"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_anomaly_action_edit": "Fix inventory",
"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",
"banner_expiring_days": "Expires in {days} days", "banner_expiring_days": "Expires in {days} days",
"banner_expiring_action_use": "Use now" "banner_expiring_action_use": "Use now",
"banner_finished_title": "finished?",
"banner_finished_detail": "I recorded that {name} reached zero stock. Is it really gone, or do you still have some?",
"banner_finished_action_yes": "Yes, it's done",
"banner_finished_action_no": "No, I still have some",
"banner_review_unusual_pkg_title": "Unusual package size",
"banner_review_unusual_pkg_detail": "You set a package of {qty} {unit} — the size seems very large. Check if correct or edit.",
"banner_review_low_qty_title": "Very low quantity",
"banner_review_low_qty_detail": "You only have {qty} in stock — seems very little, could be a typo. Confirm if correct.",
"banner_review_high_qty_title": "Unusually high quantity",
"banner_review_high_qty_detail": "You have {qty} in stock — the figure seems very high. Confirm if correct or edit.",
"banner_prediction_rate_day": "Average ~{n} {unit}/day",
"banner_prediction_rate_week": "Average ~{n} {unit}/week",
"banner_prediction_days_ago": "{n} days ago you restocked",
"banner_prediction_more": "I expected {expected} {unit}{time}, but you have {actual} {unit}. Did you add stock without recording it?",
"banner_prediction_less": "I expected {expected} {unit}{time}, but you only have {actual} {unit}. Did you use more than usual?",
"banner_finished_zero": "Inventory shows zero, but recorded movements suggest it shouldn't be empty.",
"banner_finished_expected": "According to records you should still have {qty} {unit}.",
"banner_finished_check": "Can you check?",
"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_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?",
"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",
@@ -111,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",
@@ -133,7 +176,13 @@
"add_btn": "📥 ADD", "add_btn": "📥 ADD",
"add_sub": "to pantry/fridge", "add_sub": "to pantry/fridge",
"use_btn": "📤 USE / CONSUME", "use_btn": "📤 USE / CONSUME",
"use_sub": "from pantry/fridge" "use_sub": "from pantry/fridge",
"have_title": "📦 Already in stock!",
"add_more_sub": "add more",
"use_qty_sub": "how much you used",
"throw_btn": "🗑️ DISCARD",
"throw_sub": "throw away",
"edit_sub": "expiry, location…"
}, },
"add": { "add": {
"title": "Add to Pantry", "title": "Add to Pantry",
@@ -143,7 +192,21 @@
"conf_size_placeholder": "e.g. 300", "conf_size_placeholder": "e.g. 300",
"vacuum_label": "🫙 Vacuum sealed", "vacuum_label": "🫙 Vacuum sealed",
"vacuum_hint": "Expiry date will be extended automatically", "vacuum_hint": "Expiry date will be extended automatically",
"submit": "✅ Add" "submit": "✅ Add",
"purchase_type_label": "🛒 This product is...",
"new_btn": "🆕 Just bought",
"existing_btn": "📦 I already had it",
"remaining_label": "📦 Remaining quantity",
"remaining_hint": "Approximately how much is left?",
"remaining_full": "🟢 Full",
"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",
@@ -154,7 +217,18 @@
"submit": "📤 Use this quantity", "submit": "📤 Use this quantity",
"available": "📦 Available:", "available": "📦 Available:",
"not_in_inventory": "⚠️ Product not in inventory.", "not_in_inventory": "⚠️ Product not in inventory.",
"expiry_warning": "⚠️ Use first the one{loc} that expires on {date} — {when}!" "expiry_warning": "⚠️ Use first the one{loc} that expires on {date} — {when}!",
"throw_title": "🗑️ Discard Product",
"throw_all": "🗑️ Discard ALL ({qty})",
"throw_qty_label": "How much to discard?",
"throw_qty_hint": "or enter a 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",
@@ -186,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",
@@ -224,7 +300,33 @@
"smart_already": "📊 Smart shopping already predicts {name}", "smart_already": "📊 Smart shopping already predicts {name}",
"all_searched": "All products have already been searched. Use 🔄 to search individual ones.", "all_searched": "All products have already been searched. Use 🔄 to search individual ones.",
"search_complete": "Search complete: {count} products", "search_complete": "Search complete: {count} products",
"removed_sufficient": "🧹 {removed} product(s) with sufficient stock removed from the list" "removed_sufficient": "🧹 {removed} product(s) with sufficient stock removed from the list",
"bring_badge": "🛒 Already on Bring!",
"add_urgent_toast": "🔴 {n} urgent product(s) automatically added to Bring!",
"migration_done": "✅ {migrated} updated, {skipped} already ok",
"added_to_bring": "🛒 {n} products added to Bring!",
"added_to_bring_skip": "{n} already present",
"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",
@@ -233,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",
@@ -247,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",
@@ -256,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",
@@ -401,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!",
@@ -423,6 +586,7 @@
"finished_to_bring": "🛒 Product finished → added to Bring!", "finished_to_bring": "🛒 Product finished → added to Bring!",
"thrown_away": "🗑️ {name} thrown away!", "thrown_away": "🗑️ {name} thrown away!",
"thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}", "thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}",
"product_finished_confirmed": "✅ Removed — add it again when you restock",
"appliance_added": "Appliance added", "appliance_added": "Appliance added",
"item_added": "{name} added" "item_added": "{name} added"
}, },
@@ -439,18 +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?"
}, },
"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",
@@ -485,11 +654,60 @@
"timeout": "Timeout: no response from gateway", "timeout": "Timeout: no response from gateway",
"error_connect": "Cannot connect to gateway", "error_connect": "Cannot connect to gateway",
"tab": "Smart Scale", "tab": "Smart Scale",
"low_weight": "Weight < 10 g · enter manually\n(auto-reading requires at least 10 g)" "low_weight": "Weight < 10 g · enter manually\n(auto-reading requires at least 10 g)",
"density_hint": "(density {density} g/ml)",
"ml_hint": "(will be converted to ml)",
"weight_detected": "Weight detected — wait 10s for stability…",
"weight_too_low": "Weight too low — waiting…",
"stable": "✓ Stable",
"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"
} }
} }
+242 -24
View File
@@ -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",
@@ -85,24 +87,53 @@
"quick_recipe": "🍳 Ricetta veloce con prodotti in scadenza", "quick_recipe": "🍳 Ricetta veloce con prodotti in scadenza",
"banner_review_title": "Quantità anomala", "banner_review_title": "Quantità anomala",
"banner_review_action_ok": "È corretto", "banner_review_action_ok": "È corretto",
"banner_review_action_edit": "Modifica", "banner_review_action_edit": "Correggi",
"banner_review_action_weigh": "Pesa", "banner_review_action_weigh": "Pesa",
"banner_review_dismiss": "Ignora", "banner_review_dismiss": "Ignora",
"banner_prediction_title": "Consumo anomalo", "banner_prediction_title": "Consumo anomalo",
"banner_prediction_hint": "Secondo le previsioni, questa quantità non corrisponde al consumo previsto.", "banner_prediction_hint": "Secondo le previsioni, questa quantità non corrisponde al consumo previsto.",
"banner_prediction_action_confirm": "Confermo quantità", "banner_prediction_action_confirm": "Confermo la quantità di {qty} {unit}",
"banner_prediction_action_weigh": "Pesa con bilancia", "banner_prediction_action_weigh": "Pesa ora",
"banner_prediction_action_edit": "Correggi", "banner_prediction_action_edit": "Aggiorna quantità",
"banner_expired_title": "Prodotto scaduto", "banner_expired_title": "Prodotto scaduto",
"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_anomaly_action_edit": "Correggi inventario",
"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",
"banner_expiring_days": "Scade tra {days} giorni", "banner_expiring_days": "Scade tra {days} giorni",
"banner_expiring_action_use": "Usa ora" "banner_expiring_action_use": "Usa ora",
"banner_finished_title": "è finito?",
"banner_finished_detail": "Ho registrato che {name} ha toccato quota zero. È davvero finito o hai ancora delle scorte?",
"banner_finished_action_yes": "Sì, è finito",
"banner_finished_action_no": "No, ne ho ancora",
"banner_review_unusual_pkg_title": "Confezione insolita",
"banner_review_unusual_pkg_detail": "Hai impostato una confezione da {qty} {unit} — la dimensione sembra molto alta. Controlla se è corretta o modifica.",
"banner_review_low_qty_title": "Quantità molto bassa",
"banner_review_low_qty_detail": "Hai solo {qty} in inventario — sembra poco, potrebbe essere un errore. Conferma se è corretto.",
"banner_review_high_qty_title": "Quantità insolitamente alta",
"banner_review_high_qty_detail": "Hai {qty} in inventario — la cifra sembra molto alta. Conferma se è corretto o correggi.",
"banner_prediction_rate_day": "Media ~{n} {unit}/giorno",
"banner_prediction_rate_week": "Media ~{n} {unit}/settimana",
"banner_prediction_days_ago": "{n} giorni fa hai rifornito",
"banner_prediction_more": "mi aspettavo {expected} {unit}{time}, ne hai invece {actual} {unit}. Hai aggiunto scorte senza registrarle?",
"banner_prediction_less": "mi aspettavo {expected} {unit}{time}, ne hai solo {actual} {unit}. Hai consumato di più del solito?",
"banner_finished_zero": "L'inventario segna zero, ma i movimenti registrati dicono che non dovrebbe essere finito.",
"banner_finished_expected": "Secondo le registrazioni dovresti averne ancora {qty} {unit}.",
"banner_finished_check": "Puoi controllare?",
"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_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?",
"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",
@@ -111,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",
@@ -133,7 +176,13 @@
"add_btn": "📥 AGGIUNGI", "add_btn": "📥 AGGIUNGI",
"add_sub": "in dispensa/frigo", "add_sub": "in dispensa/frigo",
"use_btn": "📤 USA / CONSUMA", "use_btn": "📤 USA / CONSUMA",
"use_sub": "dalla dispensa/frigo" "use_sub": "dalla dispensa/frigo",
"have_title": "📦 Ce l'hai già!",
"add_more_sub": "altra quantità",
"use_qty_sub": "quanto ne hai usato",
"throw_btn": "🗑️ BUTTA",
"throw_sub": "butta il prodotto",
"edit_sub": "scadenza, luogo…"
}, },
"add": { "add": {
"title": "Aggiungi alla Dispensa", "title": "Aggiungi alla Dispensa",
@@ -143,7 +192,21 @@
"conf_size_placeholder": "es. 300", "conf_size_placeholder": "es. 300",
"vacuum_label": "🫙 Sotto vuoto", "vacuum_label": "🫙 Sotto vuoto",
"vacuum_hint": "La scadenza verrà estesa automaticamente", "vacuum_hint": "La scadenza verrà estesa automaticamente",
"submit": "✅ Aggiungi" "submit": "✅ Aggiungi",
"purchase_type_label": "🛒 Questo prodotto è...",
"new_btn": "🆕 Appena comprato",
"existing_btn": "📦 Ce l'avevo già",
"remaining_label": "📦 Quantità rimasta",
"remaining_hint": "Quanto è rimasto approssimativamente?",
"remaining_full": "🟢 Pieno",
"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",
@@ -154,7 +217,18 @@
"submit": "📤 Usa questa quantità", "submit": "📤 Usa questa quantità",
"available": "📦 Disponibile:", "available": "📦 Disponibile:",
"not_in_inventory": "⚠️ Prodotto non presente nell'inventario.", "not_in_inventory": "⚠️ Prodotto non presente nell'inventario.",
"expiry_warning": "⚠️ Usa prima quella{loc} che scade il {date} — {when}!" "expiry_warning": "⚠️ Usa prima quella{loc} che scade il {date} — {when}!",
"throw_title": "🗑️ Butta Prodotto",
"throw_all": "🗑️ Butta TUTTO ({qty})",
"throw_qty_label": "Quanto butti?",
"throw_qty_hint": "oppure specifica la 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",
@@ -186,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",
@@ -224,7 +300,33 @@
"smart_already": "📊 La spesa intelligente prevede già {name}", "smart_already": "📊 La spesa intelligente prevede già {name}",
"all_searched": "Tutti i prodotti sono già stati cercati. Usa 🔄 per ricercare singoli.", "all_searched": "Tutti i prodotti sono già stati cercati. Usa 🔄 per ricercare singoli.",
"search_complete": "Ricerca completata: {count} prodotti", "search_complete": "Ricerca completata: {count} prodotti",
"removed_sufficient": "🧹 {removed} prodotto/i con scorte sufficienti rimosso/i dalla lista" "removed_sufficient": "🧹 {removed} prodotto/i con scorte sufficienti rimosso/i dalla lista",
"bring_badge": "🛒 Già su Bring!",
"add_urgent_toast": "🔴 {n} prodotto/i urgente/i aggiunto/i automaticamente a Bring!",
"migration_done": "✅ {migrated} aggiornati, {skipped} già ok",
"added_to_bring": "🛒 {n} prodotti aggiunti a Bring!",
"added_to_bring_skip": "{n} già presenti",
"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",
@@ -233,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",
@@ -247,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",
@@ -256,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",
@@ -401,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!",
@@ -423,6 +586,7 @@
"finished_to_bring": "🛒 Prodotto finito → aggiunto a Bring!", "finished_to_bring": "🛒 Prodotto finito → aggiunto a Bring!",
"thrown_away": "🗑️ {name} buttato!", "thrown_away": "🗑️ {name} buttato!",
"thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}", "thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}",
"product_finished_confirmed": "✅ Rimosso — riaggiungi quando ne ricompri",
"appliance_added": "Elettrodomestico aggiunto", "appliance_added": "Elettrodomestico aggiunto",
"item_added": "{name} aggiunto" "item_added": "{name} aggiunto"
}, },
@@ -439,18 +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?"
}, },
"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",
@@ -485,11 +654,60 @@
"timeout": "Timeout: nessuna risposta dal gateway", "timeout": "Timeout: nessuna risposta dal gateway",
"error_connect": "Impossibile connettersi al gateway", "error_connect": "Impossibile connettersi al gateway",
"tab": "Bilancia Smart", "tab": "Bilancia Smart",
"low_weight": "Peso < 10 g · inserisci manualmente\n(la lettura automatica richiede almeno 10 g)" "low_weight": "Peso < 10 g · inserisci manualmente\n(la lettura automatica richiede almeno 10 g)",
"density_hint": "(densità {density} g/ml)",
"ml_hint": "(verrà convertito in ml)",
"weight_detected": "Peso rilevato — attendi 10s di stabilità…",
"weight_too_low": "Peso troppo basso — attendi…",
"stable": "✓ Stabile",
"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"
} }
} }