From 8722f15aa09e875de03cb1387439c8aee178e800 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Tue, 28 Apr 2026 16:03:07 +0000 Subject: [PATCH 1/2] i18n: Translate all hardcoded Italian labels to English & German - Convert LOCATIONS labels to use t('locations.*') - Convert SHOPPING_SECTIONS labels to use t('shopping_sections.*') - Convert CATEGORY_LABELS to use t('categories.*') - Convert MEAL_PLAN_TYPES to use t('meal_plan_types.*') - Convert WEEK_DAYS_SHORT to use t('days.*_short') - Convert MEAL_TYPES to use t('meal_types.*') - Convert MEAL_SUB_TYPES to use t('meal_sub.*') - Convert meal-plan column headers to use translated meal_types - Replace inline locLabels/LOC_LABELS with translated LOCATIONS object - Fix shopping action buttons: bring_add_n, bring_add_selected, bring_adding, bring_added_* - Fix recipe archive empty state - Fix meal plan reset success toast - Fix meal plan suggestion hint and screensaver display - Fix settings save status messages (saved, saved_local, saved_local_error) - Fix product edit form title - Fix kiosk session phrases for screensaver counter - Add cooking.expires_chip translation for expiry date format - Add meal_plan section (reset_success, suggested_by) - Add error.select_items for Bring shopping validation - All strings now properly internationalized for EN/DE languages --- assets/js/app.js | 188 ++++++++++++++++++------------------ data/anomaly_dismissed.json | 2 +- data/bring_migrate_ts.json | 1 + translations/de.json | 78 +++++++++++++-- translations/en.json | 78 +++++++++++++-- translations/it.json | 78 +++++++++++++-- 6 files changed, 310 insertions(+), 115 deletions(-) create mode 100644 data/bring_migrate_ts.json diff --git a/assets/js/app.js b/assets/js/app.js index fcb6f4c..0dd2764 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -959,10 +959,10 @@ function changeLanguage(lang) { } const LOCATIONS = { - 'dispensa': { icon: '🗄️', label: 'Dispensa' }, - 'frigo': { icon: '🧊', label: 'Frigo' }, - 'freezer': { icon: '❄️', label: 'Freezer' }, - 'altro': { icon: '📦', label: 'Altro' }, + 'dispensa': { icon: '🗄️', label: t('locations.dispensa') }, + 'frigo': { icon: '🧊', label: t('locations.frigo') }, + 'freezer': { icon: '❄️', label: t('locations.freezer') }, + 'altro': { icon: '📦', label: t('locations.altro') }, }; const CATEGORY_ICONS = { 'latticini': '🥛', 'carne': '🥩', 'pesce': '🐟', 'frutta': '🍎', @@ -982,16 +982,16 @@ const CATEGORY_LOCATION = { // Shopping section (reparto) map — groups categories into grocery departments const SHOPPING_SECTIONS = [ - { key: 'frutta_verdura', icon: '🥬', label: 'Frutta & Verdura', cats: new Set(['frutta','verdura']) }, - { key: 'carne_pesce', icon: '🥩', label: 'Carne & Pesce', cats: new Set(['carne','pesce']) }, - { key: 'latticini', icon: '🥛', label: 'Latticini & Fresco', cats: new Set(['latticini']) }, - { key: 'pane_dolci', icon: '🍞', label: 'Pane & Dolci', cats: new Set(['pane','snack','cereali']) }, - { key: 'pasta', icon: '🍝', label: 'Pasta & Cereali', cats: new Set(['pasta']) }, - { key: 'conserve', icon: '🥫', label: 'Conserve & Salse', cats: new Set(['conserve','condimenti']) }, - { key: 'surgelati', icon: '❄️', label: 'Surgelati', cats: new Set(['surgelati']) }, - { key: 'bevande', icon: '🥤', label: 'Bevande', cats: new Set(['bevande']) }, - { key: 'pulizia_igiene', icon: '🧴', label: 'Pulizia & Igiene', cats: new Set(['igiene','pulizia']) }, - { key: 'altro', icon: '📦', label: 'Altro', cats: new Set(['altro']) }, + { key: 'frutta_verdura', icon: '🥬', label: t('shopping_sections.frutta_verdura'), cats: new Set(['frutta','verdura']) }, + { key: 'carne_pesce', icon: '🥩', label: t('shopping_sections.carne_pesce'), cats: new Set(['carne','pesce']) }, + { key: 'latticini', icon: '🥛', label: t('shopping_sections.latticini'), cats: new Set(['latticini']) }, + { key: 'pane_dolci', icon: '🍞', label: t('shopping_sections.pane_dolci'), cats: new Set(['pane','snack','cereali']) }, + { key: 'pasta', icon: '🍝', label: t('shopping_sections.pasta'), cats: new Set(['pasta']) }, + { key: 'conserve', icon: '🥫', label: t('shopping_sections.conserve'), cats: new Set(['conserve','condimenti']) }, + { key: 'surgelati', icon: '❄️', label: t('shopping_sections.surgelati'), cats: new Set(['surgelati']) }, + { key: 'bevande', icon: '🥤', label: t('shopping_sections.bevande'), cats: new Set(['bevande']) }, + { key: 'pulizia_igiene', icon: '🧴', label: t('shopping_sections.pulizia_igiene'), cats: new Set(['igiene','pulizia']) }, + { key: 'altro', icon: '📦', label: t('shopping_sections.altro'), cats: new Set(['altro']) }, ]; function getItemSection(name) { @@ -1150,14 +1150,14 @@ function getExpiredSafety(item, daysExpired) { return { level: 'danger', icon: '🗑️', label: t('status.discard'), tip: t('status.tip_lowRisk_danger') }; } -// Nice Italian labels for local categories +// Localized labels for local categories const CATEGORY_LABELS = { - 'latticini': '🥛 Latticini', 'carne': '🥩 Carne', 'pesce': '🐟 Pesce', - 'frutta': '🍎 Frutta', 'verdura': '🥬 Verdura', 'pasta': '🍝 Pasta & Riso', - 'pane': '🍞 Pane & Forno', 'surgelati': '🧊 Surgelati', 'bevande': '🥤 Bevande', - 'condimenti': '🧂 Condimenti', 'snack': '🍪 Snack & Dolci', 'conserve': '🥫 Conserve', - 'cereali': '🌾 Cereali & Legumi', 'igiene': '🧴 Igiene', 'pulizia': '🧹 Pulizia', - 'altro': '📦 Altro' + 'latticini': `🥛 ${t('categories.latticini')}`, 'carne': `🥩 ${t('categories.carne')}`, 'pesce': `🐟 ${t('categories.pesce')}`, + 'frutta': `🍎 ${t('categories.frutta')}`, 'verdura': `🥬 ${t('categories.verdura')}`, 'pasta': `🍝 ${t('categories.pasta')}`, + 'pane': `🍞 ${t('categories.pane')}`, 'surgelati': `🧊 ${t('categories.surgelati')}`, 'bevande': `🥤 ${t('categories.bevande')}`, + 'condimenti': `🧂 ${t('categories.condimenti')}`, 'snack': `🍪 ${t('categories.snack')}`, 'conserve': `🥫 ${t('categories.conserve')}`, + 'cereali': `🌾 ${t('categories.cereali')}`, 'igiene': `🧴 ${t('categories.igiene')}`, 'pulizia': `🧹 ${t('categories.pulizia')}`, + 'altro': `📦 ${t('categories.altro')}` }; // Detect best unit/quantity from Open Food Facts quantity_info string @@ -1946,17 +1946,17 @@ async function saveSettings() { const statusEl = document.getElementById('settings-status'); if (result.success) { statusEl.className = 'settings-status success'; - statusEl.textContent = '✅ Configurazione salvata!'; + statusEl.textContent = `✅ ${t('settings.saved')}`; } else { statusEl.className = 'settings-status error'; - statusEl.textContent = '⚠️ Salvato localmente, errore server: ' + (result.error || ''); + statusEl.textContent = `⚠️ ${t('settings.saved_local_error').replace('{error}', result.error || '')}`; } statusEl.style.display = 'block'; setTimeout(() => statusEl.style.display = 'none', 4000); } catch(e) { const statusEl = document.getElementById('settings-status'); statusEl.className = 'settings-status success'; - statusEl.textContent = '✅ Configurazione salvata localmente'; + statusEl.textContent = `✅ ${t('settings.saved_local')}`; statusEl.style.display = 'block'; setTimeout(() => statusEl.style.display = 'none', 4000); } @@ -4653,7 +4653,7 @@ function editProductFromAction() { document.getElementById('pf-notes').value = currentProduct.notes || ''; document.getElementById('pf-unit').value = currentProduct.unit || 'pz'; document.getElementById('pf-defqty').value = currentProduct.default_quantity || 1; - document.getElementById('product-form-title').textContent = 'Modifica Prodotto'; + document.getElementById('product-form-title').textContent = t('product.title_edit'); const pfAiRow = document.getElementById('pf-ai-fill-row'); if (pfAiRow) pfAiRow.style.display = 'none'; // Keep barcode hint hidden in edit mode @@ -7484,7 +7484,7 @@ async function migrateBringNames(btn) { async function addSmartToBring() { const checks = document.querySelectorAll('.smart-check:checked'); if (checks.length === 0) { - showToast('Seleziona almeno un prodotto', 'info'); + showToast(t('error.select_items'), 'info'); return; } @@ -8156,21 +8156,23 @@ function updateSuggestionActionBtn() { const selected = suggestionItems.filter(s => s.selected); const btn = document.querySelector('#suggestion-actions .btn-success'); if (btn) { - btn.textContent = `✅ Aggiungi ${selected.length} prodott${selected.length === 1 ? 'o' : 'i'} a Bring!`; - btn.disabled = selected.length === 0; + const nItems = selected.length; + const prodStr = nItems === 1 ? 'prodotto' : 'prodotti'; + btn.textContent = `✅ ${t('shopping.bring_add_n').replace('{n}', nItems + ' ' + prodStr)}!`; + btn.disabled = nItems === 0; } } async function addSelectedSuggestions() { const selected = suggestionItems.filter(s => s.selected); if (selected.length === 0) { - showToast('Seleziona almeno un prodotto', 'error'); + showToast(t('error.select_items'), 'error'); return; } const btn = document.querySelector('#suggestion-actions .btn-success'); btn.disabled = true; - btn.innerHTML = '
Aggiunta in corso...'; + btn.innerHTML = `
${t('shopping.bring_adding')}`; try { const items = selected.map(s => { @@ -8180,8 +8182,8 @@ async function addSelectedSuggestions() { const data = await api('bring_add', {}, 'POST', { items, listUUID: shoppingListUUID }); if (data.success) { - let msg = `${data.added} prodott${data.added === 1 ? 'o aggiunto' : 'i aggiunti'} a Bring!`; - if (data.skipped > 0) msg += ` (${data.skipped} già in lista)`; + let msg = data.added === 1 ? t('shopping.bring_added_one') : t('shopping.bring_added_many').replace('{n}', data.added); + if (data.skipped > 0) msg += ` ${t('shopping.bring_skipped').replace('{n}', data.skipped)}`; showToast(msg, 'success'); // Refresh list await loadShoppingList(); @@ -8198,7 +8200,7 @@ async function addSelectedSuggestions() { } btn.disabled = false; - btn.innerHTML = '✅ Aggiungi selezionati a Bring!'; + btn.innerHTML = `✅ ${t('shopping.bring_add_selected')}`; } // ===== UTILITY FUNCTIONS ===== @@ -8515,28 +8517,28 @@ async function undoTransactionEntry(id, type, name) { * id must be URL-safe; icon + label shown in UI. */ const MEAL_PLAN_TYPES = [ - { id: 'pasta', icon: '🍝', label: 'Pasta' }, - { id: 'riso', icon: '🍚', label: 'Riso' }, - { id: 'carne', icon: '🥩', label: 'Carne' }, - { id: 'pesce', icon: '🐟', label: 'Pesce' }, - { id: 'legumi', icon: '🫘', label: 'Legumi' }, - { id: 'uova', icon: '🥚', label: 'Uova' }, - { id: 'formaggio', icon: '🧀', label: 'Formaggio' }, - { id: 'pizza', icon: '🍕', label: 'Pizza' }, - { id: 'affettati', icon: '🥓', label: 'Affettati' }, - { id: 'verdure', icon: '🥦', label: 'Verdure' }, - { id: 'zuppa', icon: '🍲', label: 'Zuppa' }, - { id: 'insalata', icon: '🥗', label: 'Insalata' }, - { id: 'pane', icon: '🥪', label: 'Pane/Sandwich' }, - { id: 'dolce', icon: '🍰', label: 'Dolce' }, - { id: 'libero', icon: '🎲', label: 'Libero' }, + { id: 'pasta', icon: '🍝', label: t('meal_plan_types.pasta') }, + { id: 'riso', icon: '🍚', label: t('meal_plan_types.riso') }, + { id: 'carne', icon: '🥩', label: t('meal_plan_types.carne') }, + { id: 'pesce', icon: '🐟', label: t('meal_plan_types.pesce') }, + { id: 'legumi', icon: '🫘', label: t('meal_plan_types.legumi') }, + { id: 'uova', icon: '🥚', label: t('meal_plan_types.uova') }, + { id: 'formaggio', icon: '🧀', label: t('meal_plan_types.formaggio') }, + { id: 'pizza', icon: '🍕', label: t('meal_plan_types.pizza') }, + { id: 'affettati', icon: '🥓', label: t('meal_plan_types.affettati') }, + { id: 'verdure', icon: '🥦', label: t('meal_plan_types.verdure') }, + { id: 'zuppa', icon: '🍲', label: t('meal_plan_types.zuppa') }, + { id: 'insalata', icon: '🥗', label: t('meal_plan_types.insalata') }, + { id: 'pane', icon: '🥪', label: t('meal_plan_types.pane') }, + { id: 'dolce', icon: '🍰', label: t('meal_plan_types.dolce') }, + { id: 'libero', icon: '🎲', label: t('meal_plan_types.libero') }, ]; const MEAL_PLAN_TYPE_MAP = {}; -MEAL_PLAN_TYPES.forEach(t => { MEAL_PLAN_TYPE_MAP[t.id] = t; }); +MEAL_PLAN_TYPES.forEach(mpt => { MEAL_PLAN_TYPE_MAP[mpt.id] = mpt; }); const WEEK_DAYS = [t('days.mon'),t('days.tue'),t('days.wed'),t('days.thu'),t('days.fri'),t('days.sat'),t('days.sun')]; -const WEEK_DAYS_SHORT = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom']; +const WEEK_DAYS_SHORT = [t('days.mon_short'),t('days.tue_short'),t('days.wed_short'),t('days.thu_short'),t('days.fri_short'),t('days.sat_short'),t('days.sun_short')]; /** Default weekly plan as requested. */ const DEFAULT_MEAL_PLAN = { @@ -8590,8 +8592,8 @@ function renderMealPlanEditor() { const today = new Date().getDay(); const header = `
- 🌤️ Pranzo - 🌙 Cena + 🌤️ ${t('meal_types.pranzo')} + 🌙 ${t('meal_types.cena')}
`; const rows = dayOrder.map((dow, i) => { @@ -8660,33 +8662,33 @@ function resetMealPlan() { s.meal_plan = JSON.parse(JSON.stringify(DEFAULT_MEAL_PLAN)); saveSettingsToStorage(s); renderMealPlanEditor(); - showToast('Piano settimanale ripristinato', 'success'); + showToast(t('meal_plan.reset_success'), 'success'); } // ===== RECIPE GENERATION ===== const MEAL_TYPES = [ - { id: 'colazione', icon: '☀️', label: 'Colazione', from: 6, to: 11 }, - { id: 'pranzo', icon: '🍽️', label: 'Pranzo', from: 11, to: 14 }, - { id: 'merenda', icon: '🍪', label: 'Merenda', from: 14, to: 17 }, - { id: 'cena', icon: '🌙', label: 'Cena', from: 17, to: 6 }, - { id: 'dolce', icon: '🍰', label: 'Dolce', from: -1, to: -1 }, - { id: 'succo', icon: '🧃', label: 'Succo di Frutta', from: -1, to: -1 }, + { id: 'colazione', icon: '☀️', label: t('meal_types.colazione'), from: 6, to: 11 }, + { id: 'pranzo', icon: '🍽️', label: t('meal_types.pranzo'), from: 11, to: 14 }, + { id: 'merenda', icon: '🍪', label: t('meal_types.merenda'), from: 14, to: 17 }, + { id: 'cena', icon: '🌙', label: t('meal_types.cena'), from: 17, to: 6 }, + { id: 'dolce', icon: '🍰', label: t('meal_types.dolce'), from: -1, to: -1 }, + { id: 'succo', icon: '🧃', label: t('meal_types.succo'), from: -1, to: -1 }, ]; const MEAL_SUB_TYPES = { dolce: [ - { id: 'torta', icon: '🎂', label: 'Torta' }, - { id: 'crema', icon: '🍮', label: 'Crema / Budino' }, - { id: 'crumble', icon: '🥧', label: 'Crumble / Crostata' }, - { id: 'biscotti', icon: '🍪', label: 'Biscotti / Pasticcini' }, - { id: 'frutta', icon: '🍓', label: 'Dolce alla Frutta' }, + { id: 'torta', icon: '🎂', label: t('meal_sub.dolce_torta') }, + { id: 'crema', icon: '🍮', label: t('meal_sub.dolce_crema') }, + { id: 'crumble', icon: '🥧', label: t('meal_sub.dolce_crumble') }, + { id: 'biscotti', icon: '🍪', label: t('meal_sub.dolce_biscotti') }, + { id: 'frutta', icon: '🍓', label: t('meal_sub.dolce_frutta') }, ], succo: [ - { id: 'dolce', icon: '🍑', label: 'Dolce / Fruttato' }, - { id: 'energizzante', icon: '⚡', label: 'Energizzante' }, - { id: 'detox', icon: '🥬', label: 'Detox / Verde' }, - { id: 'rinfrescante', icon: '🧊', label: 'Rinfrescante' }, - { id: 'vitaminico', icon: '🍊', label: 'Vitaminico / Agrumi' }, + { id: 'dolce', icon: '🍑', label: t('meal_sub.succo_dolce') }, + { id: 'energizzante', icon: '⚡', label: t('meal_sub.succo_energizzante') }, + { id: 'detox', icon: '🥬', label: t('meal_sub.succo_detox') }, + { id: 'rinfrescante', icon: '🧊', label: t('meal_sub.succo_rinfrescante') }, + { id: 'vitaminico', icon: '🍊', label: t('meal_sub.succo_vitaminico') }, ] }; @@ -8749,7 +8751,7 @@ async function loadRecipeArchive() { _recipeArchiveEntries = archive; if (archive.length === 0) { - container.innerHTML = '
🍳

Nessuna ricetta salvata.
Genera la tua prima ricetta!

'; + container.innerHTML = `
🍳

${t('recipes.archive_empty')}

`; return; } @@ -9240,8 +9242,8 @@ function renderRecipe(r) { html += `${ing.name}${ing.brand ? ' (' + ing.brand + ')' : ''}: ${ing.qty} ✅`; // Detail line: location + expiry let details = []; - const locLabels = { 'frigo': '🧊 Frigo', 'freezer': '🧊 Freezer', 'dispensa': '🗄️ Dispensa' }; - details.push(locLabels[ing.location] || ('📍 ' + ing.location)); + const ingredientLocLabels = Object.fromEntries(Object.entries(LOCATIONS).map(([k,v]) => [k, `${v.icon} ${v.label}`])); + details.push(ingredientLocLabels[ing.location] || ('📍 ' + ing.location)); if (ing.expiry_date) { const exp = new Date(ing.expiry_date); const now = new Date(); now.setHours(0,0,0,0); @@ -9362,19 +9364,19 @@ function renderCookingStep() { const ingsEl = document.getElementById('cooking-step-ings'); if (ings.length > 0) { - const LOC_LABELS = { dispensa: '🏠 Dispensa', frigo: '❄️ Frigo', freezer: '🧊 Freezer' }; + const cookingLocLabels = Object.fromEntries(Object.entries(LOCATIONS).map(([k,v]) => [k, `${v.icon} ${v.label}`])); ingsEl.innerHTML = ings.map(ing => { const loc = (ing.location || 'dispensa').replace(/'/g, "\\'"); const qtyNum = ing.qty_number || 0; // Build info chips: brand, location, expiry const chips = []; if (ing.brand) chips.push(`${escapeHtml(ing.brand)}`); - const locLabel = LOC_LABELS[ing.location] || (ing.location ? `📍 ${ing.location}` : '🏠 Dispensa'); + const locLabel = cookingLocLabels[ing.location] || (ing.location ? `📍 ${ing.location}` : `${LOCATIONS.dispensa.icon} ${LOCATIONS.dispensa.label}`); chips.push(`${locLabel}`); if (ing.expiry_date) { const daysLeft = Math.round((new Date(ing.expiry_date) - new Date()) / 86400000); const expClass = daysLeft <= 3 ? 'exp-soon' : daysLeft <= 7 ? 'exp-close' : ''; - chips.push(`📅 scade ${formatDate(ing.expiry_date)}`); + chips.push(`📅 ${t('cooking.expires_chip').replace('{date}', formatDate(ing.expiry_date))}`); } return `
@@ -9942,26 +9944,26 @@ function _renderMealPlanHint(mealSlot) { if (chipWrap) chipWrap.style.display = 'none'; return; } - const t = MEAL_PLAN_TYPE_MAP[typeId]; - if (!t) { + const mpt = MEAL_PLAN_TYPE_MAP[typeId]; + if (!mpt) { if (el) el.style.display = 'none'; if (banner) banner.style.display = 'none'; if (chipWrap) chipWrap.style.display = 'none'; return; } if (el) { - el.innerHTML = `${t.icon} ${t.label} suggerito dal piano settimanale`; + el.innerHTML = `${mpt.icon} ${mpt.label} ${t('meal_plan.suggested_by')}`; el.style.display = 'flex'; } if (banner) { - const slotLabel = mealSlot === 'pranzo' ? '🌤️ Pranzo' : '🌙 Cena'; - banner.innerHTML = `${slotLabel}·${t.icon} ${t.label}`; + const slotLabel = mealSlot === 'pranzo' ? '🌤️ ' + t('meal_types.pranzo') : '🌙 ' + t('meal_types.cena'); + banner.innerHTML = `${slotLabel}·${mpt.icon} ${mpt.label}`; banner.style.display = 'flex'; } // Show the meal-plan chip (active by default, user can uncheck to ignore the plan) if (chipWrap) { chipWrap.style.display = ''; - if (chipLabel) chipLabel.textContent = `${t.icon} ${t.label}`; + if (chipLabel) chipLabel.textContent = `${mpt.icon} ${mpt.label}`; if (chipCb) chipCb.checked = true; } } @@ -10346,10 +10348,10 @@ function updateScreensaverMealPlan() { const slot = hour < 15 ? 'pranzo' : 'cena'; const typeId = getTodayMealPlanType(slot); if (!typeId || typeId === 'libero') { el.style.display = 'none'; return; } - const t = MEAL_PLAN_TYPE_MAP[typeId]; - if (!t) { el.style.display = 'none'; return; } - const slotLabel = slot === 'pranzo' ? '🌤️ Pranzo' : '🌙 Cena'; - el.innerHTML = `${slotLabel} · ${t.icon} ${t.label}`; + const mpt = MEAL_PLAN_TYPE_MAP[typeId]; + if (!mpt) { el.style.display = 'none'; return; } + const slotLabel = slot === 'pranzo' ? '🌤️ ' + t('meal_types.pranzo') : '🌙 ' + t('meal_types.cena'); + el.innerHTML = `${slotLabel} · ${mpt.icon} ${mpt.label}`; el.style.display = 'block'; } @@ -10814,15 +10816,15 @@ function _spesaBannerStat() { const unique = [...new Set(names)]; const dupes = names.length - unique.length; const phrases = [ - n === 1 ? `Primo prodotto: ${_spesaSession[0].name}!` : null, - n >= 2 && n < 5 ? `${n} prodotti — stai scaldando i motori 🚀` : null, - n >= 5 && n < 10 ? `${n} prodotti — ottimo ritmo! 💪` : null, - n >= 10 && n < 20 ? `${n} prodotti — quasi un recordman 🏆` : null, - n >= 20 ? `${n} prodotti — spesa epica! 🛒🔥` : null, - dupes > 0 ? `${dupes} bis ${dupes===1?'(stessa cosa due volte)':'(roba presa più volte)'}` : null, - topCat && topCat[1] > 1 ? `Categoria top: ${topCat[0]} (${topCat[1]}×)` : null, + n === 1 ? t('kiosk_session.first_item').replace('{name}', _spesaSession[0].name) : null, + n >= 2 && n < 5 ? t('kiosk_session.items_two_four').replace('{n}', n) : null, + n >= 5 && n < 10 ? t('kiosk_session.items_five_nine').replace('{n}', n) : null, + n >= 10 && n < 20 ? t('kiosk_session.items_ten_twenty').replace('{n}', n) : null, + n >= 20 ? t('kiosk_session.items_twenty_plus').replace('{n}', n) : null, + dupes > 0 ? (dupes === 1 ? t('kiosk_session.duplicates_one') : t('kiosk_session.duplicates_many').replace('{n}', dupes)) : null, + topCat && topCat[1] > 1 ? t('kiosk_session.top_category').replace('{cat}', topCat[0]).replace('{count}', topCat[1]) : null, ].filter(Boolean); - return phrases[n % phrases.length] || `${n} prodott${n===1?'o':'i'} aggiunti`; + return phrases[n % phrases.length] || t('kiosk_session.items_fallback').replace('{n}', n).replace('{plural}', n===1?'o':'i'); } function _initScreensaverShortcutBtn(btnId, targetPage, longPressFn) { diff --git a/data/anomaly_dismissed.json b/data/anomaly_dismissed.json index 2d2ff98..2ecf9fa 100644 --- a/data/anomaly_dismissed.json +++ b/data/anomaly_dismissed.json @@ -1 +1 @@ -{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925} \ No newline at end of file +{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925,"a_191_1":1777300414,"a_183_291":1777310462,"a_213_150":1777378506,"a_183_290":1777380000} \ No newline at end of file diff --git a/data/bring_migrate_ts.json b/data/bring_migrate_ts.json new file mode 100644 index 0000000..e6fcf3f --- /dev/null +++ b/data/bring_migrate_ts.json @@ -0,0 +1 @@ +{"ts":1777391782} \ No newline at end of file diff --git a/translations/de.json b/translations/de.json index 20eba96..cbd01a5 100644 --- a/translations/de.json +++ b/translations/de.json @@ -272,7 +272,8 @@ }, "recipes": { "title": "🍳 Rezepte", - "generate": "✨ Neues Rezept generieren" + "generate": "✨ Neues Rezept generieren", + "archive_empty": "Keine Rezepte gespeichert. Erstelle dein erstes Rezept!" }, "shopping": { "title": "🛒 Einkaufsliste", @@ -326,7 +327,13 @@ "smart_already_predicted": "📊 Einkauf wird bereits vorhergesagt: {name}{urgency}.", "item_removed": "✅ {name} von der Liste entfernt!", "urgency_spec_critical": "⚡ Dringend", - "urgency_spec_high": "🟠 Bald" + "urgency_spec_high": "🟠 Bald", + "bring_add_n": "{n} zu Bring! hinzufügen", + "bring_add_selected": "Ausgewählte zu Bring! hinzufügen", + "bring_adding": "Wird hinzugefügt...", + "bring_added_one": "1 Produkt zu Bring! hinzugefügt", + "bring_added_many": "{n} Produkte zu Bring! hinzugefügt", + "bring_skipped": "({n} bereits in Liste)" }, "ai": { "title": "🤖 KI-Identifikation", @@ -386,7 +393,8 @@ "ingredient_deduct_title": "Von Vorrat abziehen", "timer_expired_tts": "Timer {label} abgelaufen!", "timer_warning_tts": "Achtung! {label}: noch 10 Sekunden!", - "recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!" + "recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!", + "expires_chip": "läuft ab {date}" }, "settings": { "title": "⚙️ Einstellungen", @@ -610,7 +618,8 @@ "not_in_inventory": "Produkt nicht im Bestand", "appliance_exists": "Gerät bereits vorhanden", "already_exists": "Bereits vorhanden", - "network_retry": "Verbindungsfehler. Erneut versuchen." + "network_retry": "Verbindungsfehler. Erneut versuchen.", + "select_items": "Wähle mindestens ein Produkt aus" }, "confirm": { "remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?", @@ -632,11 +641,22 @@ "thu": "Donnerstag", "fri": "Freitag", "sat": "Samstag", - "sun": "Sonntag" + "sun": "Sonntag", + "mon_short": "Mo", + "tue_short": "Di", + "wed_short": "Mi", + "thu_short": "Do", + "fri_short": "Fr", + "sat_short": "Sa", + "sun_short": "So" }, "meal_types": { "lunch": "Mittagessen", - "dinner": "Abendessen" + "dinner": "Abendessen", + "colazione": "Frühstück", + "merenda": "Nachmittagssnack", + "dolce": "Dessert", + "succo": "Fruchtsaft" }, "scale": { "status_connected": "Waage verbunden", @@ -709,5 +729,49 @@ "2": "Kulinarische Zutat", "3": "Verarbeitet", "4": "Hochverarbeitet" + }, + "meal_plan_types": { + "pasta": "Pasta", + "riso": "Reis", + "carne": "Fleisch", + "pesce": "Fisch", + "legumi": "Hülsenfrüchte", + "uova": "Eier", + "formaggio": "Käse", + "pizza": "Pizza", + "affettati": "Aufschnitt", + "verdure": "Gemüse", + "zuppa": "Suppe", + "insalata": "Salat", + "pane": "Brot/Sandwich", + "dolce": "Dessert", + "libero": "Frei" + }, + "meal_sub": { + "dolce_torta": "Kuchen", + "dolce_crema": "Creme / Pudding", + "dolce_crumble": "Crumble / Tarte", + "dolce_biscotti": "Kekse / Gebäck", + "dolce_frutta": "Fruchtdessert", + "succo_dolce": "Süß / Fruchtig", + "succo_energizzante": "Energetisierend", + "succo_detox": "Detox / Grün", + "succo_rinfrescante": "Erfrischend", + "succo_vitaminico": "Vitamin / Zitrus" + }, + "meal_plan": { + "reset_success": "Wochenplan zurückgesetzt", + "suggested_by": "vom Wochenplan vorgeschlagen" + }, + "kiosk_session": { + "first_item": "Erstes Produkt: {name}!", + "items_two_four": "{n} Artikel — Trägheit überwinden 🚀", + "items_five_nine": "{n} Artikel — super Tempo! 💪", + "items_ten_twenty": "{n} Artikel — fast Rekord 🏆", + "items_twenty_plus": "{n} Artikel — epischer Einkauf! 🛒🔥", + "duplicates_one": "1 Duplikat (gleiches Produkt zweimal)", + "duplicates_many": "{n} Duplikate (mehrfach genommen)", + "top_category": "Top-Kategorie: {cat} ({count}×)", + "items_fallback": "{n} Artikel hinzugefügt" } -} +} \ No newline at end of file diff --git a/translations/en.json b/translations/en.json index 2ac0d72..6bef3ef 100644 --- a/translations/en.json +++ b/translations/en.json @@ -272,7 +272,8 @@ }, "recipes": { "title": "🍳 Recipes", - "generate": "✨ Generate new recipe" + "generate": "✨ Generate new recipe", + "archive_empty": "No recipes saved. Generate your first recipe!" }, "shopping": { "title": "🛒 Shopping List", @@ -326,7 +327,13 @@ "smart_already_predicted": "📊 Smart shopping already predicts {name}{urgency}.", "item_removed": "✅ {name} removed from list!", "urgency_spec_critical": "⚡ Urgent", - "urgency_spec_high": "🟠 Soon" + "urgency_spec_high": "🟠 Soon", + "bring_add_n": "Add {n} to Bring!", + "bring_add_selected": "Add selected to Bring!", + "bring_adding": "Adding...", + "bring_added_one": "1 product added to Bring!", + "bring_added_many": "{n} products added to Bring!", + "bring_skipped": "({n} already in list)" }, "ai": { "title": "🤖 AI Identification", @@ -386,7 +393,8 @@ "ingredient_deduct_title": "Deduct from pantry", "timer_expired_tts": "Timer {label} expired!", "timer_warning_tts": "Heads up! {label}: 10 seconds left!", - "recipe_done_tts": "Recipe complete! Enjoy your meal!" + "recipe_done_tts": "Recipe complete! Enjoy your meal!", + "expires_chip": "exp. {date}" }, "settings": { "title": "⚙️ Settings", @@ -610,7 +618,8 @@ "not_in_inventory": "Product not in inventory", "appliance_exists": "Appliance already exists", "already_exists": "Already exists", - "network_retry": "Connection error. Try again." + "network_retry": "Connection error. Try again.", + "select_items": "Select at least one product" }, "confirm": { "remove_item": "Do you really want to remove this product from inventory?", @@ -632,11 +641,22 @@ "thu": "Thursday", "fri": "Friday", "sat": "Saturday", - "sun": "Sunday" + "sun": "Sunday", + "mon_short": "Mon", + "tue_short": "Tue", + "wed_short": "Wed", + "thu_short": "Thu", + "fri_short": "Fri", + "sat_short": "Sat", + "sun_short": "Sun" }, "meal_types": { "lunch": "Lunch", - "dinner": "Dinner" + "dinner": "Dinner", + "colazione": "Breakfast", + "merenda": "Snack", + "dolce": "Dessert", + "succo": "Fruit Juice" }, "scale": { "status_connected": "Scale connected", @@ -709,5 +729,49 @@ "2": "Culinary ingredient", "3": "Processed", "4": "Ultra-processed" + }, + "meal_plan_types": { + "pasta": "Pasta", + "riso": "Rice", + "carne": "Meat", + "pesce": "Fish", + "legumi": "Legumes", + "uova": "Eggs", + "formaggio": "Cheese", + "pizza": "Pizza", + "affettati": "Cold Cuts", + "verdure": "Veggies", + "zuppa": "Soup", + "insalata": "Salad", + "pane": "Bread/Sandwich", + "dolce": "Dessert", + "libero": "Free" + }, + "meal_sub": { + "dolce_torta": "Cake", + "dolce_crema": "Cream / Pudding", + "dolce_crumble": "Crumble / Tart", + "dolce_biscotti": "Cookies / Pastries", + "dolce_frutta": "Fruit Dessert", + "succo_dolce": "Sweet / Fruity", + "succo_energizzante": "Energizing", + "succo_detox": "Detox / Green", + "succo_rinfrescante": "Refreshing", + "succo_vitaminico": "Vitamin / Citrus" + }, + "meal_plan": { + "reset_success": "Weekly plan reset", + "suggested_by": "suggested by weekly plan" + }, + "kiosk_session": { + "first_item": "First item: {name}!", + "items_two_four": "{n} items — warming up 🚀", + "items_five_nine": "{n} items — great pace! 💪", + "items_ten_twenty": "{n} items — almost a record 🏆", + "items_twenty_plus": "{n} items — epic shopping! 🛒🔥", + "duplicates_one": "1 duplicate (same thing twice)", + "duplicates_many": "{n} duplicates (picked multiple times)", + "top_category": "Top category: {cat} ({count}×)", + "items_fallback": "{n} item{plural} added" } -} +} \ No newline at end of file diff --git a/translations/it.json b/translations/it.json index 26d9d8a..33e0000 100644 --- a/translations/it.json +++ b/translations/it.json @@ -272,7 +272,8 @@ }, "recipes": { "title": "🍳 Ricette", - "generate": "✨ Genera nuova ricetta" + "generate": "✨ Genera nuova ricetta", + "archive_empty": "Nessuna ricetta salvata. Genera la tua prima ricetta!" }, "shopping": { "title": "🛒 Lista della Spesa", @@ -326,7 +327,13 @@ "smart_already_predicted": "📊 La spesa intelligente prevede già {name}{urgency}.", "item_removed": "✅ {name} rimosso dalla lista!", "urgency_spec_critical": "⚡ Urgente", - "urgency_spec_high": "🟠 Presto" + "urgency_spec_high": "🟠 Presto", + "bring_add_n": "Aggiungi {n} a Bring!", + "bring_add_selected": "Aggiungi selezionati a Bring!", + "bring_adding": "Aggiunta in corso...", + "bring_added_one": "1 prodotto aggiunto a Bring!", + "bring_added_many": "{n} prodotti aggiunti a Bring!", + "bring_skipped": "({n} già in lista)" }, "ai": { "title": "🤖 Identificazione AI", @@ -386,7 +393,8 @@ "ingredient_deduct_title": "Scala dalla dispensa", "timer_expired_tts": "Timer {label} scaduto!", "timer_warning_tts": "Attenzione! {label}: mancano 10 secondi!", - "recipe_done_tts": "Ricetta completata! Buon appetito!" + "recipe_done_tts": "Ricetta completata! Buon appetito!", + "expires_chip": "scade {date}" }, "settings": { "title": "⚙️ Configurazione", @@ -610,7 +618,8 @@ "not_in_inventory": "Prodotto non nell'inventario", "appliance_exists": "Elettrodomestico già presente", "already_exists": "Già presente", - "network_retry": "Errore di connessione. Riprova." + "network_retry": "Errore di connessione. Riprova.", + "select_items": "Seleziona almeno un prodotto" }, "confirm": { "remove_item": "Vuoi davvero rimuovere questo prodotto dall'inventario?", @@ -632,11 +641,22 @@ "thu": "Giovedì", "fri": "Venerdì", "sat": "Sabato", - "sun": "Domenica" + "sun": "Domenica", + "mon_short": "Lun", + "tue_short": "Mar", + "wed_short": "Mer", + "thu_short": "Gio", + "fri_short": "Ven", + "sat_short": "Sab", + "sun_short": "Dom" }, "meal_types": { "lunch": "Pranzo", - "dinner": "Cena" + "dinner": "Cena", + "colazione": "Colazione", + "merenda": "Merenda", + "dolce": "Dolce", + "succo": "Succo di Frutta" }, "scale": { "status_connected": "Bilancia connessa", @@ -709,5 +729,49 @@ "2": "Ingrediente culinario", "3": "Trasformato", "4": "Ultra-trasformato" + }, + "meal_plan_types": { + "pasta": "Pasta", + "riso": "Riso", + "carne": "Carne", + "pesce": "Pesce", + "legumi": "Legumi", + "uova": "Uova", + "formaggio": "Formaggio", + "pizza": "Pizza", + "affettati": "Affettati", + "verdure": "Verdure", + "zuppa": "Zuppa", + "insalata": "Insalata", + "pane": "Pane/Sandwich", + "dolce": "Dolce", + "libero": "Libero" + }, + "meal_sub": { + "dolce_torta": "Torta", + "dolce_crema": "Crema / Budino", + "dolce_crumble": "Crumble / Crostata", + "dolce_biscotti": "Biscotti / Pasticcini", + "dolce_frutta": "Dolce alla Frutta", + "succo_dolce": "Dolce / Fruttato", + "succo_energizzante": "Energizzante", + "succo_detox": "Detox / Verde", + "succo_rinfrescante": "Rinfrescante", + "succo_vitaminico": "Vitaminico / Agrumi" + }, + "meal_plan": { + "reset_success": "Piano settimanale ripristinato", + "suggested_by": "suggerito dal piano settimanale" + }, + "kiosk_session": { + "first_item": "Primo prodotto: {name}!", + "items_two_four": "{n} prodotti — stai scaldando i motori 🚀", + "items_five_nine": "{n} prodotti — ottimo ritmo! 💪", + "items_ten_twenty": "{n} prodotti — quasi un recordman 🏆", + "items_twenty_plus": "{n} prodotti — spesa epica! 🛒🔥", + "duplicates_one": "1 bis (stessa cosa due volte)", + "duplicates_many": "{n} bis (roba presa più volte)", + "top_category": "Categoria top: {cat} ({count}×)", + "items_fallback": "{n} prodott{n} aggiunti" } -} +} \ No newline at end of file From 8558db1925824ce814dad0ac4583ebc33ab85a04 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Tue, 28 Apr 2026 17:28:54 +0000 Subject: [PATCH 2/2] Complete i18n pass for recipes and meal plan labels --- README.md | 6 + api/index.php | 152 +++++++++++++++--- assets/js/app.js | 374 ++++++++++++++++++++++++------------------- index.html | 86 +++++----- translations/de.json | 92 ++++++++++- translations/en.json | 92 ++++++++++- translations/it.json | 92 ++++++++++- 7 files changed, 645 insertions(+), 249 deletions(-) diff --git a/README.md b/README.md index 2cbbd41..5eee1f2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ --- +## 🌍 Recent i18n Updates + +- Recipe and meal-plan labels now resolve at runtime from translations, preventing raw placeholders like `meal_types.*` and `meal_plan_types.*` from appearing in the UI. +- Recipe generation now receives the selected app language (`it`/`en`/`de`) and enforces localized output in both streaming and non-streaming API flows. +- Added missing shared error keys (`error.network`, `error.no_api_key`) across all language files to keep fallback/error toasts fully translated. + ## ✨ Features ### 📦 Inventory Management diff --git a/api/index.php b/api/index.php index 9b30d2a..f415eae 100644 --- a/api/index.php +++ b/api/index.php @@ -2203,6 +2203,95 @@ PROMPT; echo json_encode(['success' => true, 'reply' => $reply]); } + function recipeNormalizeLang($lang): string { + $lang = is_string($lang) ? strtolower(trim($lang)) : 'it'; + return in_array($lang, ['it', 'en', 'de'], true) ? $lang : 'it'; + } + + function recipeLangName(string $lang): string { + return [ + 'it' => 'Italian', + 'en' => 'English', + 'de' => 'German', + ][$lang] ?? 'Italian'; + } + + function recipeText(string $lang, string $key, array $vars = []): string { + $dict = [ + 'it' => [ + 'status_analyze_pantry' => '📦 Analizzo la dispensa...', + 'status_products_found' => '{n} prodotti trovati', + 'status_passed_ai' => ' ({n} passati all\'AI)', + 'status_all_passed_ai' => ' — tutti passati all\'AI', + 'status_urgent' => '⚠️ {n} urgenti: {items}', + 'status_evaluate_ingredients' => '🧠 Valuto gli ingredienti disponibili...', + 'status_preparing_recipe' => '👨‍🍳 Preparo la ricetta...', + 'status_recipe_with' => '🥘 Ricetta con {a} e {b}', + 'status_variant' => ' — variante #{n}', + 'status_dish_based_on' => '🎯 Piatto a base di {type}', + 'status_creating_full_recipe' => '✍️ Creo la ricetta completa...', + 'status_quota_wait' => '⏳ Quota TPM esaurita ({model}), attendo {s}s... (tentativo {a}/{m})', + 'status_retry_generation' => '✍️ Riprovo la generazione...', + 'status_switch_model' => '🔄 Cambio modello → {model}...', + 'error_pantry_empty' => 'La dispensa è vuota!', + 'error_gemini_api' => 'Errore API Gemini', + 'error_cannot_generate' => 'Impossibile generare la ricetta', + 'error_empty_reply' => 'Risposta vuota da Gemini', + 'prompt_lang_rule' => 'IMPORTANTE: scrivi tutti i campi testuali della ricetta in Italiano.', + 'prompt_step_example' => 'Passo 1…', + ], + 'en' => [ + 'status_analyze_pantry' => '📦 Analyzing pantry...', + 'status_products_found' => '{n} products found', + 'status_passed_ai' => ' ({n} sent to AI)', + 'status_all_passed_ai' => ' — all sent to AI', + 'status_urgent' => '⚠️ {n} urgent: {items}', + 'status_evaluate_ingredients' => '🧠 Evaluating available ingredients...', + 'status_preparing_recipe' => '👨‍🍳 Preparing recipe...', + 'status_recipe_with' => '🥘 Recipe with {a} and {b}', + 'status_variant' => ' — variation #{n}', + 'status_dish_based_on' => '🎯 Dish based on {type}', + 'status_creating_full_recipe' => '✍️ Creating full recipe...', + 'status_quota_wait' => '⏳ TPM quota reached ({model}), waiting {s}s... (attempt {a}/{m})', + 'status_retry_generation' => '✍️ Retrying generation...', + 'status_switch_model' => '🔄 Switching model → {model}...', + 'error_pantry_empty' => 'Pantry is empty!', + 'error_gemini_api' => 'Gemini API error', + 'error_cannot_generate' => 'Unable to generate recipe', + 'error_empty_reply' => 'Empty response from Gemini', + 'prompt_lang_rule' => 'IMPORTANT: write all textual recipe fields in English only. Do not use Italian or German.', + 'prompt_step_example' => 'Step 1…', + ], + 'de' => [ + 'status_analyze_pantry' => '📦 Vorrat wird analysiert...', + 'status_products_found' => '{n} Produkte gefunden', + 'status_passed_ai' => ' ({n} an die KI gesendet)', + 'status_all_passed_ai' => ' — alle an die KI gesendet', + 'status_urgent' => '⚠️ {n} dringend: {items}', + 'status_evaluate_ingredients' => '🧠 Verfuegbare Zutaten werden bewertet...', + 'status_preparing_recipe' => '👨‍🍳 Rezept wird vorbereitet...', + 'status_recipe_with' => '🥘 Rezept mit {a} und {b}', + 'status_variant' => ' — Variante #{n}', + 'status_dish_based_on' => '🎯 Gericht auf Basis von {type}', + 'status_creating_full_recipe' => '✍️ Vollstaendiges Rezept wird erstellt...', + 'status_quota_wait' => '⏳ TPM-Limit erreicht ({model}), warte {s}s... (Versuch {a}/{m})', + 'status_retry_generation' => '✍️ Generierung wird erneut versucht...', + 'status_switch_model' => '🔄 Modellwechsel → {model}...', + 'error_pantry_empty' => 'Die Vorratskammer ist leer!', + 'error_gemini_api' => 'Gemini-API-Fehler', + 'error_cannot_generate' => 'Rezept konnte nicht erstellt werden', + 'error_empty_reply' => 'Leere Antwort von Gemini', + 'prompt_lang_rule' => 'WICHTIG: schreibe alle textuellen Rezeptfelder nur auf Deutsch. Verwende kein Italienisch oder Englisch.', + 'prompt_step_example' => 'Schritt 1…', + ], + ]; + $text = $dict[$lang][$key] ?? $dict['it'][$key] ?? $key; + foreach ($vars as $name => $value) { + $text = str_replace('{' . $name . '}', (string)$value, $text); + } + return $text; + } + // ===== RECIPE GENERATION WITH GEMINI ===== function generateRecipe(PDO $db): void { $apiKey = env('GEMINI_API_KEY'); @@ -2212,6 +2301,8 @@ function generateRecipe(PDO $db): void { } $input = json_decode(file_get_contents('php://input'), true); + $lang = recipeNormalizeLang($input['lang'] ?? 'it'); + $recipeLangName = recipeLangName($lang); $mealType = $input['meal'] ?? 'pranzo'; $persons = max(1, intval($input['persons'] ?? 1)); $subType = $input['sub_type'] ?? ''; @@ -2235,7 +2326,7 @@ function generateRecipe(PDO $db): void { $items = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($items)) { - echo json_encode(['success' => false, 'error' => 'La dispensa è vuota!']); + echo json_encode(['success' => false, 'error' => recipeText($lang, 'error_pantry_empty')]); return; } @@ -2556,8 +2647,11 @@ function generateRecipe(PDO $db): void { } } + $promptLanguageRule = recipeText($lang, 'prompt_lang_rule'); + $promptStepExample = recipeText($lang, 'prompt_step_example'); + $prompt = << false, 'error' => 'Errore API Gemini', 'http_code' => $httpCode, 'detail' => $errDetail]); + echo json_encode(['success' => false, 'error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => $errDetail]); return; } @@ -2801,7 +2897,7 @@ PROMPT; echo json_encode(['success' => true, 'recipe' => $recipe]); } else { - echo json_encode(['success' => false, 'error' => 'Impossibile generare la ricetta', 'raw' => $text]); + echo json_encode(['success' => false, 'error' => recipeText($lang, 'error_cannot_generate'), 'raw' => $text]); } } @@ -2825,6 +2921,8 @@ function generateRecipeStream(PDO $db): void { if (empty($apiKey)) { $send('error', ['error' => 'no_api_key']); return; } $input = json_decode(file_get_contents('php://input'), true) ?? []; + $lang = recipeNormalizeLang($input['lang'] ?? 'it'); + $recipeLangName = recipeLangName($lang); $mealType = $input['meal'] ?? 'pranzo'; $persons = max(1, intval($input['persons'] ?? 1)); $subType = $input['sub_type'] ?? ''; @@ -2837,7 +2935,7 @@ function generateRecipeStream(PDO $db): void { $rejectedIngredients = $input['rejected_ingredients'] ?? []; // ── AGENTE PASSO 1: Analisi dispensa ───────────────────────────────────── - $send('status', ['step' => 1, 'message' => '📦 Analizzo la dispensa...']); + $send('status', ['step' => 1, 'message' => recipeText($lang, 'status_analyze_pantry')]); $stmt = $db->query(" SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, i.opened_at, @@ -2849,7 +2947,7 @@ function generateRecipeStream(PDO $db): void { "); $items = $stmt->fetchAll(PDO::FETCH_ASSOC); - if (empty($items)) { $send('error', ['error' => 'La dispensa è vuota!']); return; } + if (empty($items)) { $send('error', ['error' => recipeText($lang, 'error_pantry_empty')]); return; } $getItemPriority = function($item): int { $daysLeft = floatval($item['days_left']); @@ -2917,13 +3015,13 @@ function generateRecipeStream(PDO $db): void { $urgentNames = array_slice(array_map( fn($l) => trim(preg_replace('/\s[\[\x{26A0}\x{1F534}\x{1F7E0}].*/u', '', explode(':', ltrim($l, '- '))[0])), $urgentRaw), 0, 3); - $send('status', ['step' => 1, 'message' => "⚠️ {$urgentCount} urgenti: " . implode(', ', $urgentNames)]); + $send('status', ['step' => 1, 'message' => recipeText($lang, 'status_urgent', ['n' => $urgentCount, 'items' => implode(', ', $urgentNames)])]); } else { - $countMsg = count($items) . ' prodotti trovati'; + $countMsg = recipeText($lang, 'status_products_found', ['n' => count($items)]); if ($hasMealPlan && $totalIngredientsSent < count($items)) { - $countMsg .= " ({$totalIngredientsSent} passati all'AI)"; + $countMsg .= recipeText($lang, 'status_passed_ai', ['n' => $totalIngredientsSent]); } elseif ($hasMealPlan) { - $countMsg .= ' — tutti passati all\'AI'; + $countMsg .= recipeText($lang, 'status_all_passed_ai'); } $send('status', ['step' => 1, 'message' => '✅ ' . $countMsg]); } @@ -3041,7 +3139,7 @@ function generateRecipeStream(PDO $db): void { // ── AGENTE PASSO 2: Selezione concetto (locale, nessuna chiamata AI) ──────── // Determina il concetto della ricetta in base agli ingredienti disponibili // e ai parametri selezionati — senza consumare quote Gemini. - $send('status', ['step' => 2, 'message' => "🧠 Valuto gli ingredienti disponibili..."]); + $send('status', ['step' => 2, 'message' => recipeText($lang, 'status_evaluate_ingredients')]); // Raccoglie i nomi degli ingredienti di maggiore priorità $conceptIngredients = []; @@ -3057,11 +3155,11 @@ function generateRecipeStream(PDO $db): void { } // Costruisce un messaggio di stato informativo basato su ciò che verrà cucinato - $conceptMsg = '👨‍🍳 Preparo la ricetta...'; + $conceptMsg = recipeText($lang, 'status_preparing_recipe'); if (!empty($mealPlanType) && isset($mealPlanTypeLabels[$mealPlanType]) && $mealPlanTypeLabels[$mealPlanType] !== '') { // Tipo di pasto dal piano settimanale — mostra la categoria $shortLabel = explode(' (', $mealPlanTypeLabels[$mealPlanType])[0]; - $conceptMsg = "🎯 Piatto a base di {$shortLabel}"; + $conceptMsg = recipeText($lang, 'status_dish_based_on', ['type' => $shortLabel]); // Aggiungi l'ingrediente principale se disponibile if (!empty($matchingItems)) { $firstMatch = ltrim(reset($matchingItems), '→ '); @@ -3071,8 +3169,10 @@ function generateRecipeStream(PDO $db): void { } elseif (!empty($conceptIngredients)) { // Mostra i primi 2 ingredienti più urgenti $shown = array_slice($conceptIngredients, 0, 2); - $conceptMsg = "🥘 Ricetta con " . implode(' e ', array_map('mb_strtolower', $shown)); - if ($variation > 0) $conceptMsg .= " — variante #{$variation}"; + $a = mb_strtolower($shown[0] ?? ''); + $b = mb_strtolower($shown[1] ?? ''); + $conceptMsg = recipeText($lang, 'status_recipe_with', ['a' => $a, 'b' => $b]); + if ($variation > 0) $conceptMsg .= recipeText($lang, 'status_variant', ['n' => $variation]); } elseif (!empty($subType) && !empty($subTypeLabels[$mealType][$subType])) { $conceptMsg = "🎨 " . explode(' (', $subTypeLabels[$mealType][$subType])[0]; } @@ -3080,10 +3180,12 @@ function generateRecipeStream(PDO $db): void { // ── AGENTE PASSO 3: Generazione ricetta (A+C: retry SSE-aware + fallback modello) ── $conceptHint = ''; - $send('status', ['step' => 3, 'message' => '✍️ Creo la ricetta completa...']); + $send('status', ['step' => 3, 'message' => recipeText($lang, 'status_creating_full_recipe')]); + $promptLanguageRule = recipeText($lang, 'prompt_lang_rule'); + $promptStepExample = recipeText($lang, 'prompt_step_example'); $prompt = << 3, 'message' => "⏳ Quota TPM esaurita ({$modelName}), attendo {$waitSec}s... (tentativo {$attempt}/{$maxRetries})"]); + $send('status', ['step' => 3, 'message' => recipeText($lang, 'status_quota_wait', ['model' => $modelName, 's' => $waitSec, 'a' => $attempt, 'm' => $maxRetries])]); sleep($waitSec); - $send('status', ['step' => 3, 'message' => '✍️ Riprovo la generazione...']); + $send('status', ['step' => 3, 'message' => recipeText($lang, 'status_retry_generation')]); } // C: se primario esaurito dopo tutti i retry, cambia modello immediatamente if ($httpCode === 429 && $modelIdx === 0) { $fallbackName = str_replace('gemini-', 'Gemini ', $models[1]); - $send('status', ['step' => 3, 'message' => "🔄 Cambio modello → {$fallbackName}..."]); + $send('status', ['step' => 3, 'message' => recipeText($lang, 'status_switch_model', ['model' => $fallbackName])]); continue; } break; @@ -3187,7 +3291,7 @@ PROMPT; if ($httpCode !== 200) { $errDetail = $result['data']['error']['message'] ?? substr($result['body'], 0, 300); - $send('error', ['error' => 'Errore API Gemini', 'http_code' => $httpCode, 'detail' => $errDetail]); + $send('error', ['error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => $errDetail]); return; } @@ -3198,7 +3302,7 @@ PROMPT; $recipe = json_decode($text, true); if (!$recipe || empty($recipe['title'])) { - $send('error', ['error' => 'Impossibile generare la ricetta', 'raw' => $text]); + $send('error', ['error' => recipeText($lang, 'error_cannot_generate'), 'raw' => $text]); return; } diff --git a/assets/js/app.js b/assets/js/app.js index 0dd2764..1c6ace2 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1634,8 +1634,8 @@ async function loadSettingsUI() { // Render legend const legend = document.querySelector('.mplan-legend'); if (legend) { - legend.innerHTML = MEAL_PLAN_TYPES.map(t => - `${t.icon} ${t.label}` + legend.innerHTML = getMealPlanTypes().map(mpt => + `${mpt.icon} ${mpt.label}` ).join(''); } // TTS settings — init defaults on first load @@ -3866,7 +3866,7 @@ async function onBarcodeDetected(barcode) { // Save to local DB const saveResult = await api('product_save', {}, 'POST', { barcode: barcode, - name: p.name || 'Prodotto sconosciuto', + name: p.name || t('product.not_recognized'), brand: p.brand || '', category: p.category || '', image_url: p.image_url || '', @@ -3880,7 +3880,7 @@ async function onBarcodeDetected(barcode) { currentProduct = { id: saveResult.id, barcode: barcode, - name: p.name || 'Prodotto sconosciuto', + name: p.name || t('product.not_recognized'), brand: p.brand || '', category: p.category || '', image_url: p.image_url || '', @@ -3909,13 +3909,13 @@ async function onBarcodeDetected(barcode) { // Not found - ask user to add manually showLoading(false); stopScanner(); - showToast('Prodotto non trovato. Inseriscilo manualmente.', 'error'); + showToast(t('error.not_found_manual'), 'error'); startManualEntry(barcode); } catch (err) { showLoading(false); console.error('Barcode lookup error:', err); - showToast('Errore nella ricerca. Riprova.', 'error'); + showToast(t('error.search'), 'error'); } } @@ -3928,7 +3928,7 @@ function submitManualBarcode() { return; } if (!/^\d{4,14}$/.test(barcode)) { - showToast('Il codice a barre deve contenere solo numeri (4-14 cifre)', 'error'); + showToast(t('error.barcode_format'), 'error'); input.focus(); return; } @@ -4002,8 +4002,8 @@ function showQuickNameResults(searchName, products) { newItem.innerHTML = `
-
Crea "${escapeHtml(searchName)}"
-
Nuovo prodotto senza barcode
+
${t('scan.create_named').replace('{name}', '"' + escapeHtml(searchName) + '"')}
+
${t('scan.new_without_barcode')}
`; newItem.onclick = () => createQuickProduct(searchName); @@ -4616,12 +4616,12 @@ function showProductAction() { banner.style.display = 'block'; banner.innerHTML = `
- 🛒 Stai cercando + 🛒 ${t('shopping.scan_target_label')} ${escapeHtml(targetName)}
- - + +
`; } else if (banner) { @@ -5629,7 +5629,7 @@ async function loadUseInventoryInfo() { const unitSwitch = document.getElementById('use-unit-switch'); if (items.length === 0) { - infoEl.innerHTML = '⚠️ Prodotto non presente nell\'inventario.'; + infoEl.innerHTML = t('use.not_in_inventory'); unitSwitch.style.display = 'none'; _useConfMode = null; document.getElementById('use-expiry-hint').style.display = 'none'; @@ -5675,7 +5675,7 @@ async function loadUseInventoryInfo() { locSelector.innerHTML = `
${locInfo.icon} ${locInfo.label} - +
`; @@ -5699,7 +5699,7 @@ async function loadUseInventoryInfo() { _useConfMode = { packageSize: pkgSize, packageUnit: pkgUnit, totalSub, totalConf, subLabel }; // Show inventory info with sub-unit total - infoEl.innerHTML = '📦 Disponibile: ' + items.map(i => { + infoEl.innerHTML = `${t('use.available')} ` + items.map(i => { const loc = LOCATIONS[i.location] || { icon: '📦', label: i.location }; const confQty = parseFloat(i.quantity); const subQty = Math.round(confQty * pkgSize); @@ -5723,7 +5723,7 @@ async function loadUseInventoryInfo() { // Trigger a live-box refresh with the latest reading if on scale if (_scaleLatestWeight) _scaleAutoFillUse(_scaleLatestWeight); - infoEl.innerHTML = '📦 Disponibile: ' + items.map(i => { + infoEl.innerHTML = `${t('use.available')} ` + items.map(i => { const loc = LOCATIONS[i.location] || { icon: '📦', label: i.location }; const qLabel = formatQuantity(parseFloat(i.quantity), i.unit, i.default_quantity, i.package_unit); return `${loc.icon} ${loc.label}: ${qLabel}`; @@ -5743,12 +5743,12 @@ async function loadUseInventoryInfo() { fracDiv.id = 'pz-fraction-btns'; fracDiv.className = 'pz-fraction-btns'; fracDiv.innerHTML = ` -

Hai usato solo una parte?

+

${t('use.partial_piece_hint')}

- - - - + + + +
`; document.querySelector('#page-use .use-partial').appendChild(fracDiv); } @@ -5772,7 +5772,7 @@ function switchUseUnit(mode) { qtyInput.value = step; qtyInput.step = step; qtyInput.min = step; - hint.textContent = `Quantità in ${_useConfMode.subLabel} (totale: ${Math.round(_useConfMode.totalSub)}${_useConfMode.subLabel})`; + hint.textContent = t('recipes.quantity_in_total', { unit: _useConfMode.subLabel, total: `${Math.round(_useConfMode.totalSub)}${_useConfMode.subLabel}` }); } else { confBtn.classList.add('active'); subBtn.classList.remove('active'); @@ -5780,7 +5780,7 @@ function switchUseUnit(mode) { qtyInput.value = 1; qtyInput.step = 0.5; qtyInput.min = 0.5; - hint.textContent = `Confezioni da ${_useConfMode.packageSize}${_useConfMode.subLabel} (hai ${_useConfMode.totalConf.toFixed(1)} conf)`; + hint.textContent = t('recipes.packs_of_have', { size: `${_useConfMode.packageSize}${_useConfMode.subLabel}`, count: _useConfMode.totalConf.toFixed(1) }); } } @@ -6062,12 +6062,12 @@ async function addLowStockToBring() { if (shoppingListUUID) payload.listUUID = shoppingListUUID; const data = await api('bring_add', {}, 'POST', payload); if (data.success && data.added > 0) { - showToast('🛒 Aggiunto alla lista della spesa!', 'success'); + showToast(t('shopping.added_to_bring').replace('{n}', data.added), 'success'); } else if (data.success && data.skipped > 0) { showToast(t('shopping.already_in_list_short'), 'info'); } } catch (e) { - showToast('Errore nell\'aggiunta a Bring!', 'error'); + showToast(t('error.bring_add'), 'error'); } const cb = window._lowStockAfterCallback; window._lowStockAfterCallback = null; @@ -6448,7 +6448,7 @@ async function selectLocalMatch(productId) { showProductAction(); } else { showLoading(false); - showToast('Prodotto non trovato', 'error'); + showToast(t('error.not_found'), 'error'); } } catch (err) { showLoading(false); @@ -6575,7 +6575,7 @@ let _pfAiStream = null; async function captureForAIFormFill() { document.getElementById('modal-content').innerHTML = `
@@ -6710,7 +6710,7 @@ async function _pfAiAnalyze(base64) { } catch (err) { statusEl.style.display = 'none'; resultEl.style.display = 'block'; - resultEl.innerHTML = `

❌ Errore di connessione

+ resultEl.innerHTML = `

❌ ${t('error.connection')}

`; } } @@ -7017,7 +7017,7 @@ async function autoAddCriticalItems() { */ async function forceSyncBring() { const btn = document.getElementById('btn-force-sync'); - if (btn) { btn.disabled = true; btn.textContent = '⏳ Sincronizzazione…'; } + if (btn) { btn.disabled = true; btn.textContent = `⏳ ${t('shopping.syncing')}`; } // Clear all guards so the next run is unconditional localStorage.removeItem('_bringPurchasedBlocklist'); localStorage.removeItem('_autoAddedCriticalTs'); @@ -7025,8 +7025,8 @@ async function forceSyncBring() { logOperation('force_sync_bring', {}); // Reload everything from scratch await loadShoppingList(); - if (btn) { btn.disabled = false; btn.textContent = '🔄 Forza sincronizzazione Bring!'; } - showToast('🔄 Sincronizzazione completata', 'success'); + if (btn) { btn.disabled = false; btn.textContent = `🔄 ${t('shopping.force_sync')}`; } + showToast(`🔄 ${t('shopping.sync_done')}`, 'success'); } /** @@ -7627,7 +7627,7 @@ async function loadShoppingList() { const suggestionsEl = document.getElementById('shopping-suggestions'); statusEl.style.display = 'block'; - statusEl.innerHTML = '
Connessione a Bring!...
'; + statusEl.innerHTML = `
${t('shopping.bring_loading')}
`; currentEl.style.display = 'none'; suggestionsEl.style.display = 'none'; @@ -7637,7 +7637,7 @@ async function loadShoppingList() { if (!data.success) { statusEl.style.display = 'block'; - statusEl.innerHTML = `
⚠️ ${escapeHtml(data.error || 'Errore connessione Bring!')}
`; + statusEl.innerHTML = `
⚠️ ${escapeHtml(data.error || t('error.bring_connection'))}
`; return; } @@ -7716,7 +7716,7 @@ async function renderShoppingItems() { if (tabCount) tabCount.textContent = shoppingItems.length; if (shoppingItems.length === 0) { - container.innerHTML = '

Lista della spesa vuota!
Usa il pulsante sotto per generare suggerimenti.

'; + container.innerHTML = `

${t('shopping.empty')}

`; updateSpesaTotal(); return; } @@ -7823,7 +7823,7 @@ async function renderShoppingItems() { let spesaBar = ''; if (hasSpesa) { if (priceData && priceData.loading) { - detailHtml = `
🔍 Cerco...
`; + detailHtml = `
🔍 ${t('shopping.price_searching')}
`; } else if (priceData && priceData.product) { const p = priceData.product; const promoHtml = p.promo @@ -7839,23 +7839,23 @@ async function renderShoppingItems() { ${promoHtml}
`; spesaBar = `
- - 🔗 Apri + + 🔗 ${t('shopping.open_action')}
`; } else if (priceData && priceData.searched && !priceData.product) { - detailHtml = `
Non trovato
`; + detailHtml = `
${t('shopping.not_found')}
`; spesaBar = `
`; } else { spesaBar = `
- +
`; } } html += ` -
+
${catIcon}
@@ -7870,8 +7870,8 @@ async function renderShoppingItems() {
${priceTag} - - + +
${spesaBar} @@ -7923,9 +7923,9 @@ function updateSpesaTotal() { banner.style.display = 'block'; valueEl.textContent = `€ ${total.toFixed(2)}`; - let detail = `${found}/${shoppingItems.length} prodotti trovati`; + let detail = t('shopping.found_count').replace('{found}', found).replace('{total}', shoppingItems.length); if (promoSaved > 0) { - detail += ` · 🏷️ Risparmi €${promoSaved.toFixed(2)} con le offerte`; + detail += ` ${t('shopping.savings_offers').replace('{amount}', promoSaved.toFixed(2))}`; } detailEl.textContent = detail; } @@ -7982,14 +7982,14 @@ async function searchAllPrices() { try { const status = await api('dupliclick_status'); if (!status.logged_in) { - showToast('Configura prima la Spesa Online nelle impostazioni', 'error'); + showToast(t('settings.spesa.configure_first'), 'error'); return; } s.spesa_logged_in = true; s.spesa_token = 'server'; saveSettings(s); } catch (e) { - showToast('Configura prima la Spesa Online nelle impostazioni', 'error'); + showToast(t('settings.spesa.configure_first'), 'error'); return; } } @@ -8001,7 +8001,7 @@ async function searchAllPrices() { }); if (toSearch.length === 0) { - showToast('Tutti i prodotti sono già stati cercati. Usa 🔄 per ricercare singoli.', 'info'); + showToast(t('shopping.all_searched'), 'info'); return; } @@ -8010,7 +8010,7 @@ async function searchAllPrices() { for (let i = 0; i < toSearch.length; i++) { const item = toSearch[i]; - btn.innerHTML = `⏳ Cerco ${i + 1}/${totalToSearch}...`; + btn.innerHTML = `⏳ ${t('shopping.searching_progress').replace('{current}', i + 1).replace('{total}', totalToSearch)}`; const priceKey = item.name.toLowerCase(); const provider = s.spesa_provider || 'dupliclick'; @@ -8041,8 +8041,8 @@ async function searchAllPrices() { } btn.disabled = false; - btn.innerHTML = '🔍 Cerca tutti i prezzi'; - showToast(`Ricerca completata: ${totalToSearch} prodotti`, 'success'); + btn.innerHTML = `🔍 ${t('shopping.search_prices')}`; + showToast(t('shopping.search_complete').replace('{count}', totalToSearch), 'success'); } async function removeBringItem(idx) { @@ -8063,7 +8063,7 @@ async function removeBringItem(idx) { loadShoppingCount(); } } catch (err) { - showToast('Errore nella rimozione', 'error'); + showToast(t('shopping.remove_error'), 'error'); } } @@ -8072,17 +8072,17 @@ async function generateSuggestions() { const suggestionsEl = document.getElementById('shopping-suggestions'); btn.disabled = true; - btn.innerHTML = '
Analisi in corso...'; + btn.innerHTML = `
${t('shopping.suggest_loading')}`; suggestionsEl.style.display = 'none'; try { const data = await api('bring_suggest', {}, 'POST', {}); btn.disabled = false; - btn.innerHTML = '🤖 Suggerisci cosa comprare'; + btn.innerHTML = `🤖 ${t('shopping.suggest_btn').replace('🤖 ', '')}`; if (!data.success) { - showToast(data.error || 'Errore nella generazione', 'error'); + showToast(data.error || t('shopping.suggest_error'), 'error'); return; } @@ -8106,7 +8106,7 @@ async function generateSuggestions() { } catch (err) { btn.disabled = false; - btn.innerHTML = '🤖 Suggerisci cosa comprare'; + btn.innerHTML = `🤖 ${t('shopping.suggest_btn').replace('🤖 ', '')}`; console.error('Suggestion error:', err); showToast(t('error.connection'), 'error'); } @@ -8121,9 +8121,9 @@ function renderSuggestions() { container.innerHTML = sorted.map((item, idx) => { const catIcon = CATEGORY_ICONS[item.category] || '🛒'; const priorityBadge = { - 'alta': 'Alta', - 'media': 'Media', - 'bassa': 'Bassa', + 'alta': `${t('shopping.priority_high')}`, + 'media': `${t('shopping.priority_medium')}`, + 'bassa': `${t('shopping.priority_low')}`, }[item.priority] || ''; return ` @@ -8157,8 +8157,7 @@ function updateSuggestionActionBtn() { const btn = document.querySelector('#suggestion-actions .btn-success'); if (btn) { const nItems = selected.length; - const prodStr = nItems === 1 ? 'prodotto' : 'prodotti'; - btn.textContent = `✅ ${t('shopping.bring_add_n').replace('{n}', nItems + ' ' + prodStr)}!`; + btn.textContent = `✅ ${nItems === 1 ? t('shopping.bring_add_one') : t('shopping.bring_add_many').replace('{n}', nItems)}`; btn.disabled = nItems === 0; } } @@ -8193,7 +8192,7 @@ async function addSelectedSuggestions() { document.getElementById('shopping-suggestions').style.display = 'none'; suggestionItems = []; } else { - showToast(data.error || 'Errore', 'error'); + showToast(data.error || t('error.generic'), 'error'); } } catch (err) { showToast(t('error.connection'), 'error'); @@ -8516,29 +8515,45 @@ async function undoTransactionEntry(id, type, name) { * All selectable meal categories per slot. * id must be URL-safe; icon + label shown in UI. */ -const MEAL_PLAN_TYPES = [ - { id: 'pasta', icon: '🍝', label: t('meal_plan_types.pasta') }, - { id: 'riso', icon: '🍚', label: t('meal_plan_types.riso') }, - { id: 'carne', icon: '🥩', label: t('meal_plan_types.carne') }, - { id: 'pesce', icon: '🐟', label: t('meal_plan_types.pesce') }, - { id: 'legumi', icon: '🫘', label: t('meal_plan_types.legumi') }, - { id: 'uova', icon: '🥚', label: t('meal_plan_types.uova') }, - { id: 'formaggio', icon: '🧀', label: t('meal_plan_types.formaggio') }, - { id: 'pizza', icon: '🍕', label: t('meal_plan_types.pizza') }, - { id: 'affettati', icon: '🥓', label: t('meal_plan_types.affettati') }, - { id: 'verdure', icon: '🥦', label: t('meal_plan_types.verdure') }, - { id: 'zuppa', icon: '🍲', label: t('meal_plan_types.zuppa') }, - { id: 'insalata', icon: '🥗', label: t('meal_plan_types.insalata') }, - { id: 'pane', icon: '🥪', label: t('meal_plan_types.pane') }, - { id: 'dolce', icon: '🍰', label: t('meal_plan_types.dolce') }, - { id: 'libero', icon: '🎲', label: t('meal_plan_types.libero') }, +const MEAL_PLAN_TYPE_DEFS = [ + { id: 'pasta', icon: '🍝', i18nKey: 'meal_plan_types.pasta' }, + { id: 'riso', icon: '🍚', i18nKey: 'meal_plan_types.riso' }, + { id: 'carne', icon: '🥩', i18nKey: 'meal_plan_types.carne' }, + { id: 'pesce', icon: '🐟', i18nKey: 'meal_plan_types.pesce' }, + { id: 'legumi', icon: '🫘', i18nKey: 'meal_plan_types.legumi' }, + { id: 'uova', icon: '🥚', i18nKey: 'meal_plan_types.uova' }, + { id: 'formaggio', icon: '🧀', i18nKey: 'meal_plan_types.formaggio' }, + { id: 'pizza', icon: '🍕', i18nKey: 'meal_plan_types.pizza' }, + { id: 'affettati', icon: '🥓', i18nKey: 'meal_plan_types.affettati' }, + { id: 'verdure', icon: '🥦', i18nKey: 'meal_plan_types.verdure' }, + { id: 'zuppa', icon: '🍲', i18nKey: 'meal_plan_types.zuppa' }, + { id: 'insalata', icon: '🥗', i18nKey: 'meal_plan_types.insalata' }, + { id: 'pane', icon: '🥪', i18nKey: 'meal_plan_types.pane' }, + { id: 'dolce', icon: '🍰', i18nKey: 'meal_plan_types.dolce' }, + { id: 'libero', icon: '🎲', i18nKey: 'meal_plan_types.libero' }, ]; -const MEAL_PLAN_TYPE_MAP = {}; -MEAL_PLAN_TYPES.forEach(mpt => { MEAL_PLAN_TYPE_MAP[mpt.id] = mpt; }); +function getMealPlanTypes() { + return MEAL_PLAN_TYPE_DEFS.map(mpt => ({ ...mpt, label: t(mpt.i18nKey) })); +} -const WEEK_DAYS = [t('days.mon'),t('days.tue'),t('days.wed'),t('days.thu'),t('days.fri'),t('days.sat'),t('days.sun')]; -const WEEK_DAYS_SHORT = [t('days.mon_short'),t('days.tue_short'),t('days.wed_short'),t('days.thu_short'),t('days.fri_short'),t('days.sat_short'),t('days.sun_short')]; +function getMealPlanTypeMap() { + const map = {}; + getMealPlanTypes().forEach(mpt => { map[mpt.id] = mpt; }); + return map; +} + +function getWeekDaysShortLabels() { + return [ + t('days.mon_short'), + t('days.tue_short'), + t('days.wed_short'), + t('days.thu_short'), + t('days.fri_short'), + t('days.sat_short'), + t('days.sun_short'), + ]; +} /** Default weekly plan as requested. */ const DEFAULT_MEAL_PLAN = { @@ -8590,6 +8605,8 @@ function renderMealPlanEditor() { // JS getDay: 0=Sun … but we display Mon-Sun (1..6,0) const dayOrder = [1,2,3,4,5,6,0]; const today = new Date().getDay(); + const mealPlanTypeMap = getMealPlanTypeMap(); + const weekDaysShort = getWeekDaysShortLabels(); const header = `
🌤️ ${t('meal_types.pranzo')} @@ -8599,11 +8616,11 @@ function renderMealPlanEditor() { const rows = dayOrder.map((dow, i) => { const pranzo = plan[dow]?.pranzo || 'libero'; const cena = plan[dow]?.cena || 'libero'; - const pt = MEAL_PLAN_TYPE_MAP[pranzo] || MEAL_PLAN_TYPE_MAP.libero; - const ct = MEAL_PLAN_TYPE_MAP[cena] || MEAL_PLAN_TYPE_MAP.libero; + const pt = mealPlanTypeMap[pranzo] || mealPlanTypeMap.libero; + const ct = mealPlanTypeMap[cena] || mealPlanTypeMap.libero; const todayClass = dow === today ? ' mplan-row-today' : ''; return `
-
${WEEK_DAYS_SHORT[i]}
+
${weekDaysShort[i]}
${pt.icon} ${pt.label} ${ct.icon} ${ct.label}
`; @@ -8621,8 +8638,8 @@ function openMealPlanPicker(dow, slot, badgeEl) { if (!picker) return; const plan = getMealPlan(); const current = plan[dow]?.[slot] || 'libero'; - picker.innerHTML = MEAL_PLAN_TYPES.map(t => - `` + picker.innerHTML = getMealPlanTypes().map(mpt => + `` ).join(''); // Position vertically near the badge, centered horizontally (CSS handles centering) const rect = badgeEl.getBoundingClientRect(); @@ -8666,43 +8683,70 @@ function resetMealPlan() { } // ===== RECIPE GENERATION ===== -const MEAL_TYPES = [ - { id: 'colazione', icon: '☀️', label: t('meal_types.colazione'), from: 6, to: 11 }, - { id: 'pranzo', icon: '🍽️', label: t('meal_types.pranzo'), from: 11, to: 14 }, - { id: 'merenda', icon: '🍪', label: t('meal_types.merenda'), from: 14, to: 17 }, - { id: 'cena', icon: '🌙', label: t('meal_types.cena'), from: 17, to: 6 }, - { id: 'dolce', icon: '🍰', label: t('meal_types.dolce'), from: -1, to: -1 }, - { id: 'succo', icon: '🧃', label: t('meal_types.succo'), from: -1, to: -1 }, +const MEAL_TYPE_DEFS = [ + { id: 'colazione', icon: '☀️', i18nKey: 'meal_types.colazione', from: 6, to: 11 }, + { id: 'pranzo', icon: '🍽️', i18nKey: 'meal_types.pranzo', from: 11, to: 14 }, + { id: 'merenda', icon: '🍪', i18nKey: 'meal_types.merenda', from: 14, to: 17 }, + { id: 'cena', icon: '🌙', i18nKey: 'meal_types.cena', from: 17, to: 6 }, + { id: 'dolce', icon: '🍰', i18nKey: 'meal_types.dolce', from: -1, to: -1 }, + { id: 'succo', icon: '🧃', i18nKey: 'meal_types.succo', from: -1, to: -1 }, ]; -const MEAL_SUB_TYPES = { - dolce: [ - { id: 'torta', icon: '🎂', label: t('meal_sub.dolce_torta') }, - { id: 'crema', icon: '🍮', label: t('meal_sub.dolce_crema') }, - { id: 'crumble', icon: '🥧', label: t('meal_sub.dolce_crumble') }, - { id: 'biscotti', icon: '🍪', label: t('meal_sub.dolce_biscotti') }, - { id: 'frutta', icon: '🍓', label: t('meal_sub.dolce_frutta') }, - ], - succo: [ - { id: 'dolce', icon: '🍑', label: t('meal_sub.succo_dolce') }, - { id: 'energizzante', icon: '⚡', label: t('meal_sub.succo_energizzante') }, - { id: 'detox', icon: '🥬', label: t('meal_sub.succo_detox') }, - { id: 'rinfrescante', icon: '🧊', label: t('meal_sub.succo_rinfrescante') }, - { id: 'vitaminico', icon: '🍊', label: t('meal_sub.succo_vitaminico') }, - ] -}; +function getMealTypes() { + return MEAL_TYPE_DEFS.map(m => ({ ...m, label: t(m.i18nKey) })); +} + +function getMealSubTypes() { + return { + dolce: [ + { id: 'torta', icon: '🎂', label: t('meal_sub.dolce_torta') }, + { id: 'crema', icon: '🍮', label: t('meal_sub.dolce_crema') }, + { id: 'crumble', icon: '🥧', label: t('meal_sub.dolce_crumble') }, + { id: 'biscotti', icon: '🍪', label: t('meal_sub.dolce_biscotti') }, + { id: 'frutta', icon: '🍓', label: t('meal_sub.dolce_frutta') }, + ], + succo: [ + { id: 'dolce', icon: '🍑', label: t('meal_sub.succo_dolce') }, + { id: 'energizzante', icon: '⚡', label: t('meal_sub.succo_energizzante') }, + { id: 'detox', icon: '🥬', label: t('meal_sub.succo_detox') }, + { id: 'rinfrescante', icon: '🧊', label: t('meal_sub.succo_rinfrescante') }, + { id: 'vitaminico', icon: '🍊', label: t('meal_sub.succo_vitaminico') }, + ] + }; +} + +function getMealLabels() { + const labels = {}; + getMealTypes().forEach(m => { labels[m.id] = `${m.icon} ${m.label}`; }); + return labels; +} function getMealType() { const hour = new Date().getHours(); - for (const m of MEAL_TYPES) { + for (const m of MEAL_TYPE_DEFS) { if (m.from < m.to) { if (hour >= m.from && hour < m.to) return m.id; } else { if (hour >= m.from || hour < m.to) return m.id; } } return 'cena'; } -const MEAL_LABELS = {}; -MEAL_TYPES.forEach(m => { MEAL_LABELS[m.id] = `${m.icon} ${m.label}`; }); +function _normalizeMealId(rawMeal) { + if (!rawMeal) return ''; + let meal = String(rawMeal).trim().toLowerCase(); + meal = meal.replace(/^meal_types?\./, ''); + if (meal === 'lunch') return 'pranzo'; + if (meal === 'dinner') return 'cena'; + return meal; +} + +function _mealLabel(rawMeal) { + const mealId = _normalizeMealId(rawMeal); + const labels = getMealLabels(); + if (labels[mealId]) return labels[mealId]; + const translated = mealId ? t(`meal_types.${mealId}`) : ''; + if (translated && translated !== `meal_types.${mealId}`) return translated; + return mealId || String(rawMeal || ''); +} function getSelectedMealType() { const checked = document.querySelector('input[name="recipe-meal"]:checked'); @@ -8778,7 +8822,7 @@ async function loadRecipeArchive() { for (const entry of entries) { const r = entry.recipe; - const mealIcon = MEAL_LABELS[r.meal] || r.meal; + const mealIcon = _mealLabel(r.meal || entry.meal); const tags = (r.tags || []).slice(0, 3).join(', '); // Find this entry's index in the flat archive array const archiveIdx = archive.indexOf(entry); @@ -8804,7 +8848,7 @@ async function loadRecipeArchive() { function viewArchivedRecipe(idx) { const entry = _recipeArchiveEntries[idx]; if (!entry) return; - _cachedRecipe = { meal: entry.meal, recipe: entry.recipe }; + _cachedRecipe = { meal: _normalizeMealId(entry.meal), recipe: entry.recipe }; renderRecipe(entry.recipe); document.getElementById('recipe-overlay').style.display = 'flex'; document.getElementById('recipe-ask').style.display = 'none'; @@ -8825,7 +8869,7 @@ function openRecipeDialog() { // Build meal selector radios const mealGrid = document.getElementById('recipe-meal-grid'); if (mealGrid) { - mealGrid.innerHTML = MEAL_TYPES.map(m => { + mealGrid.innerHTML = getMealTypes().map(m => { const checked = m.id === meal ? ' checked' : ''; return ``; }).join(''); @@ -8899,7 +8943,7 @@ async function useRecipeIngredient(idx, productId, location, qtyNumber, btn, rec const items = (data.inventory || []).filter(i => i.product_id == productId); if (items.length === 0) { - showToast('⚠️ Prodotto non trovato in inventario', 'error'); + showToast(t('error.not_in_inventory'), 'error'); return; } @@ -8946,9 +8990,9 @@ async function useRecipeIngredient(idx, productId, location, qtyNumber, btn, rec qtySection = `
- +
-

Quantità in ${subLabel} (totale: ${Math.round(totalSub)}${subLabel})

+

${t('recipes.quantity_in_total').replace('{unit}', subLabel).replace('{total}', Math.round(totalSub) + subLabel)}

Quantità da usare (${unitLabel}):

+

${t('recipes.amount_label')} (${unitLabel}):

-
Attendi 10s di stabilità per la compilazione automatica…
+
${t('recipes.scale_wait_stable')}
` : ''; document.getElementById('modal-content').innerHTML = `

${escapeHtml(items[0].name)}

- ${recipeQty ? `

📋 Ricetta: ${escapeHtml(recipeQty)}

` : ''} + ${recipeQty ? `

📋 ${t('recipes.recipe_qty_label')}: ${escapeHtml(recipeQty)}

` : ''}

📦 ${availInfo}

${scaleLiveSection}
- +
${locButtons}
- + ${qtySection}
`; @@ -9026,7 +9070,7 @@ async function useRecipeIngredient(idx, productId, location, qtyNumber, btn, rec } catch (err) { console.error('useRecipeIngredient error:', err); - showToast('Errore nel caricamento', 'error'); + showToast(t('recipes.load_error'), 'error'); } } @@ -9051,7 +9095,7 @@ function switchRecipeUseUnit(mode) { qtyInput.value = _recipeUseContext.qtyNumber || step; qtyInput.step = step; qtyInput.min = step; - hint.textContent = `Quantità in ${_recipeUseConfMode.subLabel} (totale: ${Math.round(_recipeUseConfMode.totalSub)}${_recipeUseConfMode.subLabel})`; + hint.textContent = t('recipes.quantity_in_total').replace('{unit}', _recipeUseConfMode.subLabel).replace('{total}', Math.round(_recipeUseConfMode.totalSub) + _recipeUseConfMode.subLabel); } else { confBtn.classList.add('active'); subBtn.classList.remove('active'); @@ -9059,7 +9103,7 @@ function switchRecipeUseUnit(mode) { qtyInput.value = 1; qtyInput.step = 0.5; qtyInput.min = 0.5; - hint.textContent = `Confezioni da ${_recipeUseConfMode.packageSize}${_recipeUseConfMode.subLabel} (hai ${_recipeUseConfMode.totalConf.toFixed(1)} conf)`; + hint.textContent = t('recipes.packs_of_have').replace('{size}', `${_recipeUseConfMode.packageSize}${_recipeUseConfMode.subLabel}`).replace('{count}', _recipeUseConfMode.totalConf.toFixed(1)); } } @@ -9124,9 +9168,9 @@ async function submitRecipeUse(useAll) { saveRecipeToArchive(_cachedRecipe.recipe); } - showToast('📦 Ingrediente scalato dalla dispensa!', 'success'); + showToast(t('recipes.ingredient_scaled_toast'), 'success'); if (result.added_to_bring) { - setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500); + setTimeout(() => showToast(t('recipes.finished_added_bring_toast'), 'info'), 1500); } // Check low stock → Bring! prompt, then offer move @@ -9219,8 +9263,8 @@ function renderRecipe(r) { // Meta tags html += '
'; - html += `${MEAL_LABELS[r.meal] || r.meal}`; - html += `👥 ${r.persons} pers.`; + html += `${_mealLabel(r.meal)}`; + html += `👥 ${r.persons} ${t('recipes.persons_short')}`; if (r.prep_time) html += `🔪 ${r.prep_time}`; if (r.cook_time) html += `🔥 ${r.cook_time}`; if (r.tags) r.tags.forEach(t => { html += `${t}`; }); @@ -9232,7 +9276,7 @@ function renderRecipe(r) { } // Ingredients - html += '

🧾 Ingredienti

    '; + html += `

    ${t('recipes.ingredients_title')}

      `; (r.ingredients || []).forEach((ing, idx) => { if (ing.from_pantry && ing.product_id) { const qtyNum = ing.qty_number || 0; @@ -9269,7 +9313,7 @@ function renderRecipe(r) { html += '
    '; // Steps - html += '

    👨‍🍳 Procedimento

      '; + html += `

      ${t('recipes.steps_title')}

        `; (r.steps || []).forEach(step => { const cleanStep = step.replace(/^Passo\s*\d+\s*:\s*/i, ''); html += `
      1. ${cleanStep}
      2. `; @@ -9293,7 +9337,7 @@ let _cookingVisited = new Set(); // indices of steps already seen function startCookingMode() { const recipe = _cachedRecipe && _cachedRecipe.recipe ? _cachedRecipe.recipe : null; if (!recipe || !(recipe.steps || []).length) { - showToast('Nessun procedimento disponibile', 'info'); + showToast(t('recipes.no_steps'), 'info'); return; } // Resume if same recipe; otherwise start fresh @@ -9383,7 +9427,7 @@ function renderCookingStep() { 📦 ${escapeHtml(ing.name)}: ${escapeHtml(ing.qty)}
        ${chips.join('')}
- +
`; }).join(''); ingsEl.style.display = 'flex'; @@ -9396,7 +9440,7 @@ function renderCookingStep() { const prevBtn = document.getElementById('cooking-prev'); const nextBtn = document.getElementById('cooking-next'); prevBtn.disabled = _cookingStep === 0; - nextBtn.textContent = _cookingStep === total - 1 ? '✅ Fine' : 'Successivo ▶'; + nextBtn.textContent = _cookingStep === total - 1 ? t('cooking.finish') : t('cooking.next'); // Timer: detect duration in step text and show suggestion setupCookingTimerSuggestion(cleanStep); @@ -9894,7 +9938,8 @@ function cookingUseIngredient(idx, productId, location, qtyNumber, btn) { function updateRecipeMealTitle() { const meal = getSelectedMealType(); - document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta'; + const mealLabels = getMealLabels(); + document.getElementById('recipe-meal-title').textContent = mealLabels[meal] || t('recipes.dialog_title'); _renderMealPlanHint(meal); _renderMealSubTypes(meal); } @@ -9902,7 +9947,7 @@ function updateRecipeMealTitle() { function _renderMealSubTypes(mealId) { const container = document.getElementById('recipe-subtype-group'); if (!container) return; - const subs = MEAL_SUB_TYPES[mealId]; + const subs = getMealSubTypes()[mealId]; if (!subs) { container.style.display = 'none'; container.innerHTML = ''; @@ -9944,7 +9989,7 @@ function _renderMealPlanHint(mealSlot) { if (chipWrap) chipWrap.style.display = 'none'; return; } - const mpt = MEAL_PLAN_TYPE_MAP[typeId]; + const mpt = getMealPlanTypeMap()[typeId]; if (!mpt) { if (el) el.style.display = 'none'; if (banner) banner.style.display = 'none'; @@ -10027,7 +10072,8 @@ async function generateRecipe() { const payload = { meal, persons, - sub_type: MEAL_SUB_TYPES[meal] ? getSelectedSubType() : '', + lang: _currentLang, + sub_type: getMealSubTypes()[meal] ? getSelectedSubType() : '', options, appliances: settings.appliances || [], dietary_restrictions: settings.dietary_restrictions || '', @@ -10048,9 +10094,9 @@ async function generateRecipe() { document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-ask').style.display = ''; if (data.error === 'no_api_key') { - showToast('⚠️ Chiave API Gemini non configurata', 'warning'); + showToast(t('error.no_api_key'), 'warning'); } else { - showToast(data.error || t('error.connection'), 'error'); + showToast(data.error || t('recipes.generate_error'), 'error'); } return; } @@ -10094,10 +10140,10 @@ async function generateRecipe() { document.getElementById('recipe-ask').style.display = ''; if (errorEvent) { if (errorEvent.error === 'no_api_key') { - showToast('⚠️ Chiave API Gemini non configurata', 'warning'); + showToast(t('error.no_api_key'), 'warning'); } else { const detail = errorEvent.detail ? ` (${errorEvent.detail})` : ''; - showToast((errorEvent.error || 'Errore nella generazione') + detail, 'error'); + showToast((errorEvent.error || t('recipes.generate_error')) + detail, 'error'); } } else { showToast(t('error.connection'), 'error'); @@ -10348,7 +10394,7 @@ function updateScreensaverMealPlan() { const slot = hour < 15 ? 'pranzo' : 'cena'; const typeId = getTodayMealPlanType(slot); if (!typeId || typeId === 'libero') { el.style.display = 'none'; return; } - const mpt = MEAL_PLAN_TYPE_MAP[typeId]; + const mpt = getMealPlanTypeMap()[typeId]; if (!mpt) { el.style.display = 'none'; return; } const slotLabel = slot === 'pranzo' ? '🌤️ ' + t('meal_types.pranzo') : '🌙 ' + t('meal_types.cena'); el.innerHTML = `${slotLabel} · ${mpt.icon} ${mpt.label}`; @@ -11300,7 +11346,7 @@ async function spesaLogin() { const provider = s.spesa_provider || 'dupliclick'; if (!email || !password) { - showToast('Inserisci email e password', 'error'); + showToast(t('settings.spesa.missing_credentials'), 'error'); return; } @@ -11309,7 +11355,7 @@ async function spesaLogin() { const resultEl = document.getElementById('spesa-login-result'); btn.disabled = true; - btn.innerHTML = '⏳ Accesso in corso...'; + btn.innerHTML = `⏳ ${t('settings.spesa.login_in_progress')}`; statusEl.style.display = 'none'; resultEl.style.display = 'none'; @@ -11318,10 +11364,10 @@ async function spesaLogin() { if (res.error) { statusEl.className = 'dupliclick-status error'; - statusEl.innerHTML = `❌ Errore: ${escapeHtml(res.error)}`; + statusEl.innerHTML = `❌ ${t('settings.spesa.login_error_prefix')} ${escapeHtml(res.error)}`; statusEl.style.display = 'block'; btn.disabled = false; - btn.innerHTML = '🔐 Accedi'; + btn.innerHTML = t('settings.spesa.login_btn'); return; } @@ -11339,7 +11385,7 @@ async function spesaLogin() { saveSettingsToStorage(s); statusEl.className = 'dupliclick-status success'; - const welcomeMsg = (res.infos && res.infos[0]) ? res.infos[0].info : 'Login effettuato!'; + const welcomeMsg = (res.infos && res.infos[0]) ? res.infos[0].info : t('settings.spesa.login_success_default'); statusEl.innerHTML = `✅ ${escapeHtml(welcomeMsg)}`; statusEl.style.display = 'block'; @@ -11353,10 +11399,10 @@ async function spesaLogin() { let html = '
'; html += '
'; - if (user.firstName) html += `
👤 Nome${escapeHtml(user.firstName)} ${escapeHtml(user.lastName || '')}
`; - if (user.fidelityCard) html += `
💳 Tessera${escapeHtml(user.fidelityCard)}
`; - if (shipping.addressName) html += `
🏪 Punto Ritiro${escapeHtml(shipping.addressName)}
`; - if (fidelityPts) html += `
⭐ Punti Fedeltà${fidelityPts.value || 0}
`; + if (user.firstName) html += `
👤 ${t('settings.spesa.result_name_label')}${escapeHtml(user.firstName)} ${escapeHtml(user.lastName || '')}
`; + if (user.fidelityCard) html += `
💳 ${t('settings.spesa.result_card_label')}${escapeHtml(user.fidelityCard)}
`; + if (shipping.addressName) html += `
🏪 ${t('settings.spesa.result_pickup_label')}${escapeHtml(shipping.addressName)}
`; + if (fidelityPts) html += `
⭐ ${t('settings.spesa.result_points_label')}${fidelityPts.value || 0}
`; html += '
'; resultEl.innerHTML = html; @@ -11364,12 +11410,12 @@ async function spesaLogin() { } catch (e) { statusEl.className = 'dupliclick-status error'; - statusEl.innerHTML = `❌ Errore di rete: ${escapeHtml(e.message)}`; + statusEl.innerHTML = `❌ ${t('settings.spesa.login_network_error_prefix')} ${escapeHtml(e.message)}`; statusEl.style.display = 'block'; } btn.disabled = false; - btn.innerHTML = '🔐 Accedi'; + btn.innerHTML = t('settings.spesa.login_btn'); } function loadSpesaSettings() { @@ -11388,12 +11434,12 @@ function loadSpesaSettings() { const loginBtn = document.getElementById('spesa-login-btn'); if (loginBtn) { - loginBtn.innerHTML = '✅ Connesso — Riaccedi'; + loginBtn.innerHTML = t('settings.spesa.connected_relogin'); loginBtn.className = 'btn btn-large btn-secondary full-width mt-2'; } if (statusEl) { statusEl.className = 'dupliclick-status success'; - statusEl.innerHTML = `✅ Connesso come ${escapeHtml(s.spesa_user.firstName || '')} ${escapeHtml(s.spesa_user.lastName || '')}`; + statusEl.innerHTML = `✅ ${t('settings.spesa.connected_as').replace('{name}', `${escapeHtml(s.spesa_user.firstName || '')} ${escapeHtml(s.spesa_user.lastName || '')}`.trim())}`; statusEl.style.display = 'block'; } if (resultEl) { @@ -11403,10 +11449,10 @@ function loadSpesaSettings() { const fidelityPts = Array.isArray(points) ? points[0] : points['0']; let html = '
'; - if (user.firstName) html += `
👤 Nome${escapeHtml(user.firstName)} ${escapeHtml(user.lastName || '')}
`; - if (user.fidelityCard) html += `
💳 Tessera${escapeHtml(user.fidelityCard)}
`; - if (shipping.addressName) html += `
🏪 Punto Ritiro${escapeHtml(shipping.addressName)}
`; - if (fidelityPts) html += `
⭐ Punti Fedeltà${fidelityPts.value || 0}
`; + if (user.firstName) html += `
👤 ${t('settings.spesa.result_name_label')}${escapeHtml(user.firstName)} ${escapeHtml(user.lastName || '')}
`; + if (user.fidelityCard) html += `
💳 ${t('settings.spesa.result_card_label')}${escapeHtml(user.fidelityCard)}
`; + if (shipping.addressName) html += `
🏪 ${t('settings.spesa.result_pickup_label')}${escapeHtml(shipping.addressName)}
`; + if (fidelityPts) html += `
⭐ ${t('settings.spesa.result_points_label')}${fidelityPts.value || 0}
`; html += '
'; resultEl.innerHTML = html; resultEl.style.display = 'block'; diff --git a/index.html b/index.html index 1ab8625..34d6a16 100644 --- a/index.html +++ b/index.html @@ -167,25 +167,25 @@
- +
-
oppure scrivi il nome
+
oppure scrivi il nome
- - + +
- -
-

Scansiona il barcode, scrivi il nome del prodotto, oppure usa l'AI per identificarlo

+

Scansiona il barcode, scrivi il nome del prodotto, oppure usa l'AI per identificarlo

@@ -194,8 +194,8 @@
@@ -560,10 +560,10 @@ @@ -579,31 +579,31 @@
- - -
@@ -616,32 +616,32 @@
-

🧠 Previsioni intelligenti

+

🧠 Previsioni intelligenti

0
- - - - - + + + + +
-
@@ -1161,16 +1161,16 @@