release: v1.5.0 — expired banner, AI fallback, TTS cooking improvements
This commit is contained in:
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
- 'evershelf-kiosk/**'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -16,37 +16,44 @@
|
||||
|
||||
### 📦 Inventory Management
|
||||
- **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
|
||||
- **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
|
||||
- **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)
|
||||
- **Expiry date reading** — Photograph a label and extract the expiry date automatically
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **Auto-remove on scan** — Products are removed from the shopping list when scanned in
|
||||
- **DupliClick integration** — Online grocery ordering (Gruppo Poli)
|
||||
- **Auto-add on depletion** — When a product reaches zero the app adds it to Bring! automatically, no confirmation needed
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **Waste tracking** — Monitor consumed vs. wasted products over 30 days
|
||||
- **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
|
||||
- **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
|
||||
@@ -63,8 +70,7 @@
|
||||
- **SSE relay** — Server-side relay avoids mixed-content (HTTPS→WS) issues
|
||||
- **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
|
||||
- **10g threshold** — Ignores readings that haven't changed enough between products
|
||||
- **ml conversion hint** — Shows "weight in grams → will be converted to ml" when product unit is ml
|
||||
- **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
|
||||
- **Stability + auto-confirm** — 10s stable wait + 5s countdown before confirming
|
||||
- **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
|
||||
@@ -76,6 +82,7 @@
|
||||
- **Setup wizard** — 3-step guided configuration (URL, connection test, gateway)
|
||||
- **Gateway auto-launch** — Launches the Scale Gateway in the background on startup
|
||||
- **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
|
||||
- **Update notifications** — Checks GitHub releases every 6h, shows banner when updates available
|
||||
- **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] Scale auto-fill improvements — 10g threshold, ml conversion hints
|
||||
- [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
|
||||
- [ ] Export/import inventory data
|
||||
- [ ] Notification system (Telegram, email) for expiring products
|
||||
|
||||
+1594
-366
File diff suppressed because it is too large
Load Diff
@@ -1963,6 +1963,14 @@ body {
|
||||
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 {
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
@@ -3115,6 +3123,8 @@ body {
|
||||
margin-top: 16px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
transition: opacity 0.25s ease;
|
||||
min-height: 1.4em;
|
||||
}
|
||||
|
||||
.recipe-result {
|
||||
@@ -4525,6 +4535,12 @@ body {
|
||||
background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%);
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -5663,3 +5679,36 @@ body {
|
||||
background: #fee2e2;
|
||||
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;
|
||||
}
|
||||
|
||||
+736
-382
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.Handler
|
||||
import android.os.Looper
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.ValueCallback
|
||||
@@ -39,6 +41,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.json.JSONObject
|
||||
import java.net.URL
|
||||
import java.util.Locale
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
@@ -49,6 +52,11 @@ class KioskActivity : AppCompatActivity() {
|
||||
private lateinit var prefs: SharedPreferences
|
||||
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
|
||||
private lateinit var splashContainer: LinearLayout
|
||||
private lateinit var wizardContainer: ScrollView
|
||||
@@ -98,6 +106,19 @@ class KioskActivity : AppCompatActivity() {
|
||||
enableKioskLock()
|
||||
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
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
splashContainer.visibility = View.GONE
|
||||
@@ -506,7 +527,7 @@ class KioskActivity : AppCompatActivity() {
|
||||
|
||||
// Add JS interface ONCE before loading
|
||||
webView.addJavascriptInterface(object {
|
||||
@android.webkit.JavascriptInterface
|
||||
@JavascriptInterface
|
||||
fun exit() {
|
||||
runOnUiThread {
|
||||
disableKioskLock()
|
||||
@@ -514,13 +535,35 @@ class KioskActivity : AppCompatActivity() {
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
@android.webkit.JavascriptInterface
|
||||
@JavascriptInterface
|
||||
fun hardReload() {
|
||||
runOnUiThread {
|
||||
webView.clearCache(true)
|
||||
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")
|
||||
|
||||
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() {
|
||||
if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# Build trigger: TTS bridge fix (95389eb)
|
||||
|
||||
+8
-5
@@ -11,7 +11,7 @@
|
||||
<title>EverShelf</title>
|
||||
<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="stylesheet" href="assets/css/style.css?v=20260420a">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260421a">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
</head>
|
||||
@@ -930,10 +930,13 @@
|
||||
<div id="tts-browser-section">
|
||||
<div class="form-group">
|
||||
<label>🗣️ Voce</label>
|
||||
<select id="setting-tts-voice" class="form-input">
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<select id="setting-tts-voice" class="form-input" style="flex:1">
|
||||
<option value="">— Caricamento voci… —</option>
|
||||
</select>
|
||||
<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>
|
||||
<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 class="form-group">
|
||||
<label>⚡ Velocità: <span id="tts-rate-label">1.0</span>×</label>
|
||||
@@ -1195,7 +1198,7 @@
|
||||
</div>
|
||||
<div id="recipe-loading" style="display:none" class="recipe-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Sto preparando la ricetta...</p>
|
||||
<p id="recipe-loading-msg">Sto preparando la ricetta...</p>
|
||||
</div>
|
||||
<div id="recipe-result" style="display:none" class="recipe-result">
|
||||
<div id="recipe-content"></div>
|
||||
@@ -1288,6 +1291,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260420a"></script>
|
||||
<script src="assets/js/app.js?v=20260421a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "EverShelf",
|
||||
"short_name": "EverShelf",
|
||||
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"start_url": "/evershelf/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0f4e8",
|
||||
|
||||
@@ -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";
|
||||
+241
-23
@@ -26,7 +26,9 @@
|
||||
"save_config": "💾 Konfiguration speichern",
|
||||
"save_product": "💾 Produkt speichern",
|
||||
"restart": "↺ Neustart",
|
||||
"reset_default": "↺ Standard wiederherstellen"
|
||||
"reset_default": "↺ Standard wiederherstellen",
|
||||
"save_info": "💾 Info speichern",
|
||||
"retry": "🔄 Erneut versuchen"
|
||||
},
|
||||
"locations": {
|
||||
"dispensa": "Vorratskammer",
|
||||
@@ -85,24 +87,53 @@
|
||||
"quick_recipe": "🍳 Schnelles Rezept mit ablaufenden Produkten",
|
||||
"banner_review_title": "Ungewöhnliche Menge",
|
||||
"banner_review_action_ok": "Ist korrekt",
|
||||
"banner_review_action_edit": "Bearbeiten",
|
||||
"banner_review_action_edit": "Korrigieren",
|
||||
"banner_review_action_weigh": "Wiegen",
|
||||
"banner_review_dismiss": "Ignorieren",
|
||||
"banner_prediction_title": "Ungewöhnlicher Verbrauch",
|
||||
"banner_prediction_hint": "Laut Vorhersage stimmt diese Menge nicht mit dem erwarteten Verbrauch überein.",
|
||||
"banner_prediction_action_confirm": "Menge bestätigen",
|
||||
"banner_prediction_action_weigh": "Mit Waage wiegen",
|
||||
"banner_prediction_action_edit": "Korrigieren",
|
||||
"banner_prediction_action_confirm": "{qty} {unit} bestätigen",
|
||||
"banner_prediction_action_weigh": "Jetzt wiegen",
|
||||
"banner_prediction_action_edit": "Menge aktualisieren",
|
||||
"banner_expired_title": "Abgelaufenes Produkt",
|
||||
"banner_expired_today": "Heute abgelaufen",
|
||||
"banner_expired_days": "Seit {days} Tagen abgelaufen",
|
||||
"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_today": "Läuft heute ab!",
|
||||
"banner_expiring_tomorrow": "Läuft morgen 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": {
|
||||
"title": "Vorrat",
|
||||
@@ -111,7 +142,19 @@
|
||||
"recent_title": "🕐 Zuletzt verwendet",
|
||||
"popular_title": "⭐ Meistverwendet",
|
||||
"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": {
|
||||
"title": "Produkt scannen",
|
||||
@@ -133,7 +176,13 @@
|
||||
"add_btn": "📥 HINZUFÜGEN",
|
||||
"add_sub": "in Vorrat/Kühlschrank",
|
||||
"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": {
|
||||
"title": "Zum Vorrat hinzufügen",
|
||||
@@ -143,7 +192,21 @@
|
||||
"conf_size_placeholder": "z.B. 300",
|
||||
"vacuum_label": "🫙 Vakuumiert",
|
||||
"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": {
|
||||
"title": "Verwenden / Verbrauchen",
|
||||
@@ -154,7 +217,18 @@
|
||||
"submit": "📤 Diese Menge verwenden",
|
||||
"available": "📦 Verfügbar:",
|
||||
"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": {
|
||||
"title_new": "Neues Produkt",
|
||||
@@ -186,7 +260,9 @@
|
||||
"edit_catalog": "⚙️ Produktinfo bearbeiten (Name, Marke, Kategorie…)",
|
||||
"not_recognized": "⚠️ Produkt nicht erkannt",
|
||||
"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": {
|
||||
"title": "📦 Alle Produkte",
|
||||
@@ -224,7 +300,33 @@
|
||||
"smart_already": "📊 Intelligenter Einkauf sagt bereits {name} voraus",
|
||||
"all_searched": "Alle Produkte wurden bereits gesucht. Nutze 🔄 für einzelne Suchen.",
|
||||
"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": {
|
||||
"title": "🤖 KI-Identifikation",
|
||||
@@ -233,10 +335,27 @@
|
||||
"hint": "Mache ein Foto des Produkts und die KI versucht es zu identifizieren",
|
||||
"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>",
|
||||
"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": {
|
||||
"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": {
|
||||
"title": "Gemini Chef",
|
||||
@@ -247,7 +366,12 @@
|
||||
"suggestion_light": "🥗 Etwas Leichtes",
|
||||
"suggestion_expiry": "⏰ Ablaufende nutzen",
|
||||
"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": {
|
||||
"close": "Schließen",
|
||||
@@ -256,7 +380,13 @@
|
||||
"replay": "🔊 Nochmal",
|
||||
"timer": "⏱️ {time} · Timer",
|
||||
"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": {
|
||||
"title": "⚙️ Einstellungen",
|
||||
@@ -401,12 +531,45 @@
|
||||
"days": "{days} Tage",
|
||||
"expired_days": "Seit {days}T",
|
||||
"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": {
|
||||
"ok": "OK",
|
||||
"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": {
|
||||
"product_saved": "Produkt gespeichert!",
|
||||
@@ -423,6 +586,7 @@
|
||||
"finished_to_bring": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt",
|
||||
"thrown_away": "🗑️ {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",
|
||||
"item_added": "{name} hinzugefügt"
|
||||
},
|
||||
@@ -439,18 +603,23 @@
|
||||
"bring_add": "Fehler beim Hinzufügen zu Bring!",
|
||||
"bring_connection": "Bring! Verbindungsfehler",
|
||||
"identification": "Identifikationsfehler",
|
||||
"ai_quota": "KI-Kontingent erschöpft. Bitte in ein paar Minuten erneut versuchen.",
|
||||
"barcode_empty": "Barcode eingeben",
|
||||
"barcode_format": "Barcode darf nur Zahlen enthalten (4-14 Ziffern)",
|
||||
"min_chars": "Mindestens 2 Zeichen eingeben",
|
||||
"not_in_inventory": "Produkt nicht im Bestand",
|
||||
"appliance_exists": "Gerät bereits vorhanden",
|
||||
"already_exists": "Bereits vorhanden"
|
||||
"already_exists": "Bereits vorhanden",
|
||||
"network_retry": "Verbindungsfehler. Erneut versuchen."
|
||||
},
|
||||
"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": {
|
||||
"title": "{name} bearbeiten"
|
||||
"title": "{name} bearbeiten",
|
||||
"unknown_hint": "Produktname und Informationen eingeben",
|
||||
"label_name": "🏷️ Produktname"
|
||||
},
|
||||
"screensaver": {
|
||||
"recipe_btn": "Rezepte",
|
||||
@@ -485,11 +654,60 @@
|
||||
"timeout": "Timeout: keine Antwort vom Gateway",
|
||||
"error_connect": "Verbindung zum Gateway nicht möglich",
|
||||
"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": {
|
||||
"expected_qty": "Erwartet: {expected} {unit}",
|
||||
"actual_qty": "Aktuell: {actual} {unit}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
+241
-23
@@ -26,7 +26,9 @@
|
||||
"save_config": "💾 Save Configuration",
|
||||
"save_product": "💾 Save Product",
|
||||
"restart": "↺ Restart",
|
||||
"reset_default": "↺ Reset to default"
|
||||
"reset_default": "↺ Reset to default",
|
||||
"save_info": "💾 Save information",
|
||||
"retry": "🔄 Retry"
|
||||
},
|
||||
"locations": {
|
||||
"dispensa": "Pantry",
|
||||
@@ -85,24 +87,53 @@
|
||||
"quick_recipe": "🍳 Quick recipe with expiring products",
|
||||
"banner_review_title": "Anomalous quantity",
|
||||
"banner_review_action_ok": "It's correct",
|
||||
"banner_review_action_edit": "Edit",
|
||||
"banner_review_action_edit": "Correct",
|
||||
"banner_review_action_weigh": "Weigh",
|
||||
"banner_review_dismiss": "Dismiss",
|
||||
"banner_prediction_title": "Anomalous consumption",
|
||||
"banner_prediction_hint": "Based on predictions, this quantity doesn't match expected consumption.",
|
||||
"banner_prediction_action_confirm": "Confirm quantity",
|
||||
"banner_prediction_action_weigh": "Weigh with scale",
|
||||
"banner_prediction_action_edit": "Correct",
|
||||
"banner_prediction_action_confirm": "Confirm {qty} {unit} is correct",
|
||||
"banner_prediction_action_weigh": "Weigh now",
|
||||
"banner_prediction_action_edit": "Update quantity",
|
||||
"banner_expired_title": "Expired product",
|
||||
"banner_expired_today": "Expired today",
|
||||
"banner_expired_days": "Expired {days} days ago",
|
||||
"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_today": "Expires today!",
|
||||
"banner_expiring_tomorrow": "Expires tomorrow",
|
||||
"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": {
|
||||
"title": "Pantry",
|
||||
@@ -111,7 +142,19 @@
|
||||
"recent_title": "🕐 Recently used",
|
||||
"popular_title": "⭐ Most used",
|
||||
"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": {
|
||||
"title": "Scan Product",
|
||||
@@ -133,7 +176,13 @@
|
||||
"add_btn": "📥 ADD",
|
||||
"add_sub": "to pantry/fridge",
|
||||
"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": {
|
||||
"title": "Add to Pantry",
|
||||
@@ -143,7 +192,21 @@
|
||||
"conf_size_placeholder": "e.g. 300",
|
||||
"vacuum_label": "🫙 Vacuum sealed",
|
||||
"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": {
|
||||
"title": "Use / Consume",
|
||||
@@ -154,7 +217,18 @@
|
||||
"submit": "📤 Use this quantity",
|
||||
"available": "📦 Available:",
|
||||
"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": {
|
||||
"title_new": "New Product",
|
||||
@@ -186,7 +260,9 @@
|
||||
"edit_catalog": "⚙️ Edit product info (name, brand, category…)",
|
||||
"not_recognized": "⚠️ Product not recognized",
|
||||
"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": {
|
||||
"title": "📦 All Products",
|
||||
@@ -224,7 +300,33 @@
|
||||
"smart_already": "📊 Smart shopping already predicts {name}",
|
||||
"all_searched": "All products have already been searched. Use 🔄 to search individual ones.",
|
||||
"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": {
|
||||
"title": "🤖 AI Identification",
|
||||
@@ -233,10 +335,27 @@
|
||||
"hint": "Take a photo of the product and AI will try to identify it",
|
||||
"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>",
|
||||
"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": {
|
||||
"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": {
|
||||
"title": "Gemini Chef",
|
||||
@@ -247,7 +366,12 @@
|
||||
"suggestion_light": "🥗 Something light",
|
||||
"suggestion_expiry": "⏰ Use expiring items",
|
||||
"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": {
|
||||
"close": "Close",
|
||||
@@ -256,7 +380,13 @@
|
||||
"replay": "🔊 Replay",
|
||||
"timer": "⏱️ {time} · Timer",
|
||||
"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": {
|
||||
"title": "⚙️ Settings",
|
||||
@@ -401,12 +531,45 @@
|
||||
"days": "{days} days",
|
||||
"expired_days": "{days}d ago",
|
||||
"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": {
|
||||
"ok": "OK",
|
||||
"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": {
|
||||
"product_saved": "Product saved!",
|
||||
@@ -423,6 +586,7 @@
|
||||
"finished_to_bring": "🛒 Product finished → added to Bring!",
|
||||
"thrown_away": "🗑️ {name} thrown away!",
|
||||
"thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}",
|
||||
"product_finished_confirmed": "✅ Removed — add it again when you restock",
|
||||
"appliance_added": "Appliance added",
|
||||
"item_added": "{name} added"
|
||||
},
|
||||
@@ -439,18 +603,23 @@
|
||||
"bring_add": "Error adding to Bring!",
|
||||
"bring_connection": "Bring! connection error",
|
||||
"identification": "Identification error",
|
||||
"ai_quota": "AI quota exhausted. Please try again in a few minutes.",
|
||||
"barcode_empty": "Enter a barcode",
|
||||
"barcode_format": "Barcode must contain only numbers (4-14 digits)",
|
||||
"min_chars": "Type at least 2 characters",
|
||||
"not_in_inventory": "Product not in inventory",
|
||||
"appliance_exists": "Appliance already exists",
|
||||
"already_exists": "Already exists"
|
||||
"already_exists": "Already exists",
|
||||
"network_retry": "Connection error. Try again."
|
||||
},
|
||||
"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": {
|
||||
"title": "Edit {name}"
|
||||
"title": "Edit {name}",
|
||||
"unknown_hint": "Enter the product name and information",
|
||||
"label_name": "🏷️ Product name"
|
||||
},
|
||||
"screensaver": {
|
||||
"recipe_btn": "Recipes",
|
||||
@@ -485,11 +654,60 @@
|
||||
"timeout": "Timeout: no response from gateway",
|
||||
"error_connect": "Cannot connect to gateway",
|
||||
"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": {
|
||||
"expected_qty": "Expected: {expected} {unit}",
|
||||
"actual_qty": "Current: {actual} {unit}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
+241
-23
@@ -26,7 +26,9 @@
|
||||
"save_config": "💾 Salva Configurazione",
|
||||
"save_product": "💾 Salva Prodotto",
|
||||
"restart": "↺ Ricomincia",
|
||||
"reset_default": "↺ Ripristina default"
|
||||
"reset_default": "↺ Ripristina default",
|
||||
"save_info": "💾 Salva informazioni",
|
||||
"retry": "🔄 Riprova"
|
||||
},
|
||||
"locations": {
|
||||
"dispensa": "Dispensa",
|
||||
@@ -85,24 +87,53 @@
|
||||
"quick_recipe": "🍳 Ricetta veloce con prodotti in scadenza",
|
||||
"banner_review_title": "Quantità anomala",
|
||||
"banner_review_action_ok": "È corretto",
|
||||
"banner_review_action_edit": "Modifica",
|
||||
"banner_review_action_edit": "Correggi",
|
||||
"banner_review_action_weigh": "Pesa",
|
||||
"banner_review_dismiss": "Ignora",
|
||||
"banner_prediction_title": "Consumo anomalo",
|
||||
"banner_prediction_hint": "Secondo le previsioni, questa quantità non corrisponde al consumo previsto.",
|
||||
"banner_prediction_action_confirm": "Confermo quantità",
|
||||
"banner_prediction_action_weigh": "Pesa con bilancia",
|
||||
"banner_prediction_action_edit": "Correggi",
|
||||
"banner_prediction_action_confirm": "Confermo la quantità di {qty} {unit}",
|
||||
"banner_prediction_action_weigh": "Pesa ora",
|
||||
"banner_prediction_action_edit": "Aggiorna quantità",
|
||||
"banner_expired_title": "Prodotto scaduto",
|
||||
"banner_expired_today": "Scaduto oggi",
|
||||
"banner_expired_days": "Scaduto da {days} giorni",
|
||||
"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_today": "Scade oggi!",
|
||||
"banner_expiring_tomorrow": "Scade domani",
|
||||
"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": {
|
||||
"title": "Dispensa",
|
||||
@@ -111,7 +142,19 @@
|
||||
"recent_title": "🕐 Ultimi usati",
|
||||
"popular_title": "⭐ Più usati",
|
||||
"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": {
|
||||
"title": "Scansiona Prodotto",
|
||||
@@ -133,7 +176,13 @@
|
||||
"add_btn": "📥 AGGIUNGI",
|
||||
"add_sub": "in dispensa/frigo",
|
||||
"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": {
|
||||
"title": "Aggiungi alla Dispensa",
|
||||
@@ -143,7 +192,21 @@
|
||||
"conf_size_placeholder": "es. 300",
|
||||
"vacuum_label": "🫙 Sotto vuoto",
|
||||
"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": {
|
||||
"title": "Usa / Consuma",
|
||||
@@ -154,7 +217,18 @@
|
||||
"submit": "📤 Usa questa quantità",
|
||||
"available": "📦 Disponibile:",
|
||||
"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": {
|
||||
"title_new": "Nuovo Prodotto",
|
||||
@@ -186,7 +260,9 @@
|
||||
"edit_catalog": "⚙️ Modifica scheda prodotto (nome, marca, categoria…)",
|
||||
"not_recognized": "⚠️ Prodotto non riconosciuto",
|
||||
"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": {
|
||||
"title": "📦 Tutti i Prodotti",
|
||||
@@ -224,7 +300,33 @@
|
||||
"smart_already": "📊 La spesa intelligente prevede già {name}",
|
||||
"all_searched": "Tutti i prodotti sono già stati cercati. Usa 🔄 per ricercare singoli.",
|
||||
"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": {
|
||||
"title": "🤖 Identificazione AI",
|
||||
@@ -233,10 +335,27 @@
|
||||
"hint": "Scatta una foto del prodotto e l'AI cercherà di identificarlo",
|
||||
"identifying": "🤖 Identifico il prodotto...",
|
||||
"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": {
|
||||
"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": {
|
||||
"title": "Gemini Chef",
|
||||
@@ -247,7 +366,12 @@
|
||||
"suggestion_light": "🥗 Qualcosa di leggero",
|
||||
"suggestion_expiry": "⏰ Usa le scadenze",
|
||||
"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": {
|
||||
"close": "Chiudi",
|
||||
@@ -256,7 +380,13 @@
|
||||
"replay": "🔊 Rileggi",
|
||||
"timer": "⏱️ {time} · Timer",
|
||||
"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": {
|
||||
"title": "⚙️ Configurazione",
|
||||
@@ -401,12 +531,45 @@
|
||||
"days": "{days} giorni",
|
||||
"expired_days": "Da {days}g",
|
||||
"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": {
|
||||
"ok": "OK",
|
||||
"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": {
|
||||
"product_saved": "Prodotto salvato!",
|
||||
@@ -423,6 +586,7 @@
|
||||
"finished_to_bring": "🛒 Prodotto finito → aggiunto a Bring!",
|
||||
"thrown_away": "🗑️ {name} buttato!",
|
||||
"thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}",
|
||||
"product_finished_confirmed": "✅ Rimosso — riaggiungi quando ne ricompri",
|
||||
"appliance_added": "Elettrodomestico aggiunto",
|
||||
"item_added": "{name} aggiunto"
|
||||
},
|
||||
@@ -439,18 +603,23 @@
|
||||
"bring_add": "Errore nell'aggiunta a Bring!",
|
||||
"bring_connection": "Errore connessione Bring!",
|
||||
"identification": "Errore nell'identificazione",
|
||||
"ai_quota": "Quota AI esaurita. Riprova tra qualche minuto.",
|
||||
"barcode_empty": "Inserisci un codice a barre",
|
||||
"barcode_format": "Il codice a barre deve contenere solo numeri (4-14 cifre)",
|
||||
"min_chars": "Scrivi almeno 2 caratteri",
|
||||
"not_in_inventory": "Prodotto non nell'inventario",
|
||||
"appliance_exists": "Elettrodomestico già presente",
|
||||
"already_exists": "Già presente"
|
||||
"already_exists": "Già presente",
|
||||
"network_retry": "Errore di connessione. Riprova."
|
||||
},
|
||||
"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": {
|
||||
"title": "Modifica {name}"
|
||||
"title": "Modifica {name}",
|
||||
"unknown_hint": "Inserisci il nome e le informazioni del prodotto",
|
||||
"label_name": "🏷️ Nome prodotto"
|
||||
},
|
||||
"screensaver": {
|
||||
"recipe_btn": "Ricette",
|
||||
@@ -485,11 +654,60 @@
|
||||
"timeout": "Timeout: nessuna risposta dal gateway",
|
||||
"error_connect": "Impossibile connettersi al gateway",
|
||||
"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": {
|
||||
"expected_qty": "Previsto: {expected} {unit}",
|
||||
"actual_qty": "Attuale: {actual} {unit}",
|
||||
"check_suggestion": "Verifica o pesa la quantità residua"
|
||||
},
|
||||
"date": {
|
||||
"today": "📅 Oggi",
|
||||
"yesterday": "📅 Ieri"
|
||||
},
|
||||
"scanner": {
|
||||
"title_barcode": "🔖 Scansiona Barcode",
|
||||
"barcode_hint": "Inquadra il codice a barre del prodotto",
|
||||
"barcode_manual_placeholder": "O inserisci manualmente...",
|
||||
"barcode_use_btn": "✅ Usa questo codice",
|
||||
"ai_identifying": "🤖 Identifico il prodotto...",
|
||||
"ai_analyzing": "🤖 Analisi AI in corso...",
|
||||
"product_label_hint": "Inquadra l'etichetta del prodotto",
|
||||
"expiry_label_hint": "Inquadra la data di scadenza stampata sul prodotto",
|
||||
"capture_btn": "📸 Scatta",
|
||||
"capture_photo_btn": "📸 Scatta Foto",
|
||||
"retake_btn": "🔄 Riscatta",
|
||||
"camera_error_hint": "Assicurati di usare HTTPS e di aver concesso i permessi della fotocamera.<br>Puoi inserire il barcode manualmente o usare l'identificazione AI.",
|
||||
"no_barcode": "Senza barcode"
|
||||
},
|
||||
"lowstock": {
|
||||
"title": "⚠️ Sta per finire!",
|
||||
"message": "{name} sta per finire — rimangono solo {qty}.",
|
||||
"question": "Vuoi aggiungerlo alla lista della spesa?",
|
||||
"yes": "🛒 Sì, aggiungi a Bring!",
|
||||
"no": "No, per ora va bene"
|
||||
},
|
||||
"move": {
|
||||
"title": "📦 Spostare il resto?",
|
||||
"question": "Vuoi spostare {thing} di {name} in un'altra posizione?",
|
||||
"question_short": "Vuoi spostare {thing} in un'altra posizione?",
|
||||
"thing_opened": "la confezione aperta",
|
||||
"thing_rest": "il resto",
|
||||
"stay_btn": "No, resta in {location}",
|
||||
"moved_toast": "📦 Confezione aperta spostata in {location}",
|
||||
"vacuum_restore": "🫙 Torna sotto vuoto"
|
||||
},
|
||||
"nova": {
|
||||
"1": "Non trasformato",
|
||||
"2": "Ingrediente culinario",
|
||||
"3": "Trasformato",
|
||||
"4": "Ultra-trasformato"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user