feat: i18n — translate all hardcoded Italian strings (nutrition, facts, kiosk, gemini, scanner, shopping)
- Added 106 new translation keys across all 3 languages (it/en/de): - nutrition.* (11 keys): card title, score labels, health/variety/fresh bars, source - facts.* (70 keys): screensaver facts — greetings, expiry, shopping, categories, tips - kiosk.* (12 keys): update check, install flow, exit/refresh button titles - update.* (2 keys): badge label and button - gemini.* (2 keys): chat button title, not-configured tooltip - dashboard.banner_explain_title/btn/analyzing (3 keys): anomaly explain button - add.history_badge_tip (1 key): history badge tooltip - shopping.smart_last_update, names_already_updated (2 keys) - appliances.empty (1 key) - scanner.save_new_btn (1 key) - app.js: replaced all remaining hardcoded Italian strings with t() calls - api/index.php: fixed Frutta/Früchte Bring! loop (Pass 2 genericQualifiers) - index.html: asset version bumped to v=20260511b
This commit is contained in:
+38
-21
@@ -1744,7 +1744,8 @@ function getInventoryAnomalies(PDO $db): void {
|
||||
$rows = $db->query("
|
||||
SELECT p.id AS product_id, p.name, p.brand, p.unit,
|
||||
p.default_quantity, p.package_unit,
|
||||
i.id AS inventory_id, i.quantity AS inv_qty, i.location,
|
||||
MIN(i.id) AS inventory_id,
|
||||
SUM(i.quantity) AS inv_qty,
|
||||
COALESCE(tx_in.tot, 0) AS total_in,
|
||||
COALESCE(tx_out.tot, 0) AS total_out
|
||||
FROM inventory i
|
||||
@@ -1758,6 +1759,8 @@ function getInventoryAnomalies(PDO $db): void {
|
||||
FROM transactions WHERE type IN ('out','waste') AND undone = 0 GROUP BY product_id
|
||||
) tx_out ON tx_out.product_id = p.id
|
||||
WHERE i.quantity > 0
|
||||
GROUP BY p.id, p.name, p.brand, p.unit, p.default_quantity, p.package_unit,
|
||||
tx_in.tot, tx_out.tot
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Anomaly dismissed keys stored in a simple JSON file
|
||||
@@ -1783,14 +1786,13 @@ function getInventoryAnomalies(PDO $db): void {
|
||||
// so it stays dismissed until the user explicitly resets or the direction changes.
|
||||
// An inventory correction (bringing qty closer to expected) will flip the direction
|
||||
// or drop below threshold — naturally clearing the dismissed state.
|
||||
$key = 'a_' . $r['product_id'] . '_' . $direction;
|
||||
if (!empty($dismissed[$key])) continue;
|
||||
|
||||
$direction = $diff > 0 ? 'phantom' : 'missing';
|
||||
// Special case: expected is negative — more consumption recorded than entries.
|
||||
// The real qty vs tx comparison is meaningless; what we actually know is that
|
||||
// "initial stock was never formally registered as an 'in' transaction".
|
||||
if ($expected <= 0) $direction = 'untracked';
|
||||
$key = 'a_' . $r['product_id'] . '_' . $direction;
|
||||
if (!empty($dismissed[$key])) continue;
|
||||
$anomalies[] = [
|
||||
'inventory_id' => (int)$r['inventory_id'],
|
||||
'product_id' => (int)$r['product_id'],
|
||||
@@ -3076,8 +3078,8 @@ function generateRecipe(PDO $db): void {
|
||||
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 ($isOpen) return 3; // opened items: same priority as expiring this week — must be used soon
|
||||
if (!empty($item['expiry_date'])) return 4;
|
||||
if ($isOpen) return 5;
|
||||
return 6;
|
||||
};
|
||||
|
||||
@@ -3132,13 +3134,12 @@ function generateRecipe(PDO $db): void {
|
||||
$priorityHeaders = [
|
||||
1 => 'SCADUTI — usa subito',
|
||||
2 => 'SCADENZA ≤3gg — priorità alta',
|
||||
3 => 'SCADENZA ≤7gg',
|
||||
3 => 'SCADENZA ≤7gg / APERTI — usa presto',
|
||||
4 => 'ALTRI CON SCADENZA',
|
||||
5 => 'APERTI',
|
||||
6 => 'DISPENSA',
|
||||
];
|
||||
// Limit groups to keep prompt compact:
|
||||
// 1-3 (urgent): all items; 4 (has expiry): max 40; 5 (opened): all; 6 (pantry): max 20
|
||||
// 1-3 (urgent+opened): all items; 4 (has expiry): max 40; 6 (pantry): max 20
|
||||
foreach ($priorityHeaders as $g => $header) {
|
||||
if (empty($priorityGroups[$g])) continue;
|
||||
$groupItems = $priorityGroups[$g];
|
||||
@@ -4014,8 +4015,8 @@ function generateRecipeStream(PDO $db): void {
|
||||
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 ($isOpen) return 3; // opened items: same priority as expiring this week — must be used soon
|
||||
if (!empty($item['expiry_date'])) return 4;
|
||||
if ($isOpen) return 5;
|
||||
return 6;
|
||||
};
|
||||
|
||||
@@ -4050,7 +4051,7 @@ function generateRecipeStream(PDO $db): void {
|
||||
// Senza piano pasto: limiti moderati per ridurre token (ora safe grazie a thinkingBudget:0)
|
||||
$hasMealPlan = !empty($mealPlanType);
|
||||
$ingredientSections = [];
|
||||
$priorityHeaders = [1=>'SCADUTI — usa subito',2=>'SCADENZA ≤3gg — priorità alta',3=>'SCADENZA ≤7gg',4=>'ALTRI CON SCADENZA',5=>'APERTI',6=>'DISPENSA'];
|
||||
$priorityHeaders = [1=>'SCADUTI — usa subito',2=>'SCADENZA ≤3gg — priorità alta',3=>'SCADENZA ≤7gg / APERTI — usa presto',4=>'ALTRI CON SCADENZA',6=>'DISPENSA'];
|
||||
$totalIngredientsSent = 0;
|
||||
foreach ($priorityHeaders as $g => $header) {
|
||||
if (empty($priorityGroups[$g])) continue;
|
||||
@@ -4854,7 +4855,12 @@ function italianToBring(string $italianName): string {
|
||||
|
||||
$genericQualifiers = [
|
||||
'dolce','salato','light','bio','classico','original','naturale','fresco','fresca',
|
||||
'intero','intera','magro','magra','piccolo','piccola','grande','rosso','bianco'
|
||||
'intero','intera','magro','magra','piccolo','piccola','grande','rosso','bianco',
|
||||
// Generic descriptors that appear inside multi-word product names (e.g. "succo e polpa frutta",
|
||||
// "muesli frutta secca") but do NOT represent the item category on their own.
|
||||
// Pass 1 (exact match on shopping_name) still works correctly for truly generic items
|
||||
// like shopping_name='Frutta' → it2de['frutta'] = 'Früchte'.
|
||||
'frutta','verdura','frutti',
|
||||
];
|
||||
$candidates = [];
|
||||
foreach ($catalog['it2de'] as $itLower => $deKey) {
|
||||
@@ -5292,14 +5298,16 @@ function bringCleanupObsolete(PDO $db): array {
|
||||
return array_values(array_unique(array_filter($toks, fn($t) => mb_strlen($t) > 2 && !in_array($t, $stopwords))));
|
||||
};
|
||||
|
||||
// Build smart map: ONLY shopping_name tokens → item.
|
||||
// Deliberately NOT indexing by product name tokens — product names like
|
||||
// "Pera Italiana Succo e polpa frutta" contain words ("succo", "frutta") that
|
||||
// would wrongly keep unrelated Bring! items ("Succo", "Frutta") on the list.
|
||||
// The shopping_name (e.g. "Pere") is the canonical generic name used in Bring!.
|
||||
// Build smart map by shopping_name tokens AND by exact name.
|
||||
// Exact match is tried first to prevent loose token collisions like
|
||||
// 'Panna' (Bring! item, in stock) matching 'Panna da cucina' (depleted, critical)
|
||||
// because they share the 'panna' token.
|
||||
$smartByTok = [];
|
||||
$smartByExactName = [];
|
||||
foreach ($smartItems as $si) {
|
||||
$sName = !empty($si['shopping_name']) ? $si['shopping_name'] : $si['name'];
|
||||
$sNameNorm = strtolower(trim($sName));
|
||||
if ($sNameNorm !== '') $smartByExactName[$sNameNorm] = $si;
|
||||
foreach ($ntFn($sName) as $tok) {
|
||||
if (!isset($smartByTok[$tok])) $smartByTok[$tok] = $si;
|
||||
}
|
||||
@@ -5321,10 +5329,15 @@ function bringCleanupObsolete(PDO $db): array {
|
||||
}
|
||||
if (!$isAppAdded) continue;
|
||||
|
||||
// Match against smart items using shopping_name-priority tokens
|
||||
// Match against smart items: exact shopping_name first, then first-token fallback.
|
||||
// Exact match prevents e.g. 'Panna' → 'Panna da cucina' via shared token 'panna'.
|
||||
$nameToks = $ntFn($name);
|
||||
$firstTok = $nameToks[0] ?? '';
|
||||
$smartSi = $firstTok ? ($smartByTok[$firstTok] ?? null) : null;
|
||||
$exactKey = strtolower(trim($name));
|
||||
$smartSi = $smartByExactName[$exactKey] ?? null;
|
||||
if ($smartSi === null) {
|
||||
$firstTok = $nameToks[0] ?? '';
|
||||
$smartSi = $firstTok ? ($smartByTok[$firstTok] ?? null) : null;
|
||||
}
|
||||
|
||||
if ($smartSi !== null) {
|
||||
// Still in smart_shopping with critical or high urgency → keep
|
||||
@@ -5985,8 +5998,12 @@ function smartShopping(PDO $db): void {
|
||||
// first purchase → now, so idle periods after last use don't deflate the rate.
|
||||
// Example: Aglio bought 60 days ago but last used 34 days ago → use 34-day window.
|
||||
$lastActivity = max($lastIn ?? 0, $lastOut ?? 0);
|
||||
$effectiveDays = ($firstIn && $lastActivity > $firstIn)
|
||||
? max(1, ($lastActivity - $firstIn) / 86400)
|
||||
$activitySpan = ($firstIn && $lastActivity > $firstIn) ? ($lastActivity - $firstIn) : 0;
|
||||
// Guard: if all activity fits within 24h (e.g. bought & consumed same day / seconds apart),
|
||||
// effectiveDays would collapse to 1 → wildly inflated daily rate (e.g. Pizza: in+out 9s apart).
|
||||
// Fall back to daysSinceFirst (first purchase → now) for a conservative estimate.
|
||||
$effectiveDays = ($activitySpan >= 86400)
|
||||
? max(1, $activitySpan / 86400)
|
||||
: $daysSinceFirst;
|
||||
$dailyRate = $effectiveDays < 999 && $totalUsed > 0 ? $totalUsed / $effectiveDays : 0;
|
||||
|
||||
|
||||
+107
-111
@@ -103,10 +103,10 @@ function _updateGeminiButtonState() {
|
||||
if (_geminiAvailable) {
|
||||
btn.classList.remove('header-btn-no-ai');
|
||||
btn.removeAttribute('title');
|
||||
btn.setAttribute('title', 'Chat con Gemini');
|
||||
btn.setAttribute('title', t('gemini.chat_title'));
|
||||
} else {
|
||||
btn.classList.add('header-btn-no-ai');
|
||||
btn.setAttribute('title', '🤖 Gemini non configurato — imposta GEMINI_API_KEY nelle impostazioni');
|
||||
btn.setAttribute('title', t('gemini.not_configured'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +168,8 @@ function _checkWebappUpdate() {
|
||||
if (!badge) return;
|
||||
|
||||
const versionLabel = deployChanged
|
||||
? (serverVer ? `v${serverVer}` : 'Nuova versione')
|
||||
: (latestTag ? `v${latestTag}` : 'Nuova versione');
|
||||
? (serverVer ? `v${serverVer}` : t('update.new_version'))
|
||||
: (latestTag ? `v${latestTag}` : t('update.new_version'));
|
||||
|
||||
const hideBadge = () => {
|
||||
badge.style.display = 'none';
|
||||
@@ -179,7 +179,7 @@ function _checkWebappUpdate() {
|
||||
|
||||
badge.innerHTML =
|
||||
`<span class="header-update-badge-label">⬆️ ${versionLabel}</span>` +
|
||||
`<button class="header-update-btn" onclick="window.location.reload()">Aggiorna</button>` +
|
||||
`<button class="header-update-btn" onclick="window.location.reload()">${t('update.btn')}</button>` +
|
||||
`<button class="header-update-close" id="_header_update_close">✕</button>`;
|
||||
badge.style.display = 'inline-flex';
|
||||
|
||||
@@ -2152,7 +2152,7 @@ window._kioskUpdateResult = function(result) {
|
||||
const verLabel = document.getElementById('kiosk-update-version-label');
|
||||
if (!status) return;
|
||||
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔍 Cerca aggiornamenti'; }
|
||||
if (btn) { btn.disabled = false; btn.textContent = t('kiosk.check_btn'); }
|
||||
|
||||
if (result.error && !result.has_update) {
|
||||
status.style.display = '';
|
||||
@@ -2165,7 +2165,7 @@ window._kioskUpdateResult = function(result) {
|
||||
|
||||
const current = result.current || '?';
|
||||
const latest = result.latest || '?';
|
||||
if (verLabel) verLabel.textContent = `Installata: ${current}`;
|
||||
if (verLabel) verLabel.textContent = t('kiosk.version_installed').replace('{v}', current);
|
||||
|
||||
if (result.has_update) {
|
||||
_kioskPendingApkUrl = result.apk_url || '';
|
||||
@@ -2173,7 +2173,7 @@ window._kioskUpdateResult = function(result) {
|
||||
status.style.background = 'rgba(245,158,11,0.1)';
|
||||
status.style.border = '1px solid rgba(245,158,11,0.35)';
|
||||
status.style.color = '';
|
||||
status.innerHTML = `⬆️ Nuova versione disponibile: <strong>${latest}</strong> (installata: ${current})`;
|
||||
status.innerHTML = t('kiosk.update_available').replace('{latest}', latest).replace('{current}', current);
|
||||
if (installBtn) installBtn.style.display = '';
|
||||
} else {
|
||||
_kioskPendingApkUrl = '';
|
||||
@@ -2181,7 +2181,7 @@ window._kioskUpdateResult = function(result) {
|
||||
status.style.background = 'rgba(52,211,153,0.1)';
|
||||
status.style.border = '1px solid rgba(52,211,153,0.3)';
|
||||
status.style.color = '';
|
||||
status.innerHTML = `✅ Sei già aggiornato — versione <strong>${current}</strong>`;
|
||||
status.innerHTML = t('kiosk.up_to_date').replace('{v}', current);
|
||||
if (installBtn) installBtn.style.display = 'none';
|
||||
}
|
||||
};
|
||||
@@ -2195,8 +2195,7 @@ function _kioskCheckForUpdates() {
|
||||
status.style.display = '';
|
||||
status.style.background = 'rgba(245,158,11,0.1)';
|
||||
status.style.border = '1px solid rgba(245,158,11,0.35)';
|
||||
status.innerHTML = `⚠️ Il kiosk installato è troppo vecchio per il controllo automatico.<br>
|
||||
Premi il pulsante qui sotto per scaricare e installare la v1.7.0 direttamente.`;
|
||||
status.innerHTML = t('kiosk.too_old');
|
||||
}
|
||||
// Pre-set the pending URL and show the install button (installUpdate works in old APKs too)
|
||||
_kioskPendingApkUrl = 'https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk';
|
||||
@@ -2206,13 +2205,13 @@ function _kioskCheckForUpdates() {
|
||||
const btn = document.getElementById('btn-kiosk-check-update');
|
||||
const status = document.getElementById('kiosk-update-status');
|
||||
const installBtn = document.getElementById('btn-kiosk-install-update');
|
||||
if (btn) { btn.disabled = true; btn.textContent = '⏳ Controllo…'; }
|
||||
if (btn) { btn.disabled = true; btn.textContent = t('kiosk.checking'); }
|
||||
if (status) { status.style.display = 'none'; }
|
||||
if (installBtn) { installBtn.style.display = 'none'; }
|
||||
_kioskPendingApkUrl = '';
|
||||
try { _kioskBridge.checkForUpdates(); } catch(e) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔍 Cerca aggiornamenti'; }
|
||||
showToast('❌ Errore durante il controllo', 'error');
|
||||
if (btn) { btn.disabled = false; btn.textContent = t('kiosk.check_btn'); }
|
||||
showToast('❌ ' + t('kiosk.error_check'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2226,22 +2225,18 @@ function _kioskInstallUpdate() {
|
||||
status.style.display = '';
|
||||
status.style.background = 'rgba(239,68,68,0.1)';
|
||||
status.style.border = '1px solid rgba(239,68,68,0.3)';
|
||||
status.innerHTML = `⚠️ Questo kiosk non supporta l'installazione automatica.<br>
|
||||
<strong>Procedura manuale:</strong><br>
|
||||
1. Esci dal kiosk (tasto ✕ in alto a sinistra)<br>
|
||||
2. Disinstalla l'app EverShelf Kiosk<br>
|
||||
3. Scarica e installa la nuova APK da GitHub:<br>
|
||||
<code style="font-size:0.75rem;word-break:break-all">
|
||||
https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk
|
||||
status.innerHTML = t('kiosk.manual_install') +
|
||||
`<br><code style="font-size:0.75rem;word-break:break-all">
|
||||
https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk
|
||||
</code>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const installBtn = document.getElementById('btn-kiosk-install-update');
|
||||
if (installBtn) { installBtn.disabled = true; installBtn.textContent = '⏳ Avvio download…'; }
|
||||
if (installBtn) { installBtn.disabled = true; installBtn.textContent = t('kiosk.starting_download'); }
|
||||
try { _kioskBridge.installUpdate(_kioskPendingApkUrl); } catch(e) {
|
||||
if (installBtn) { installBtn.disabled = false; installBtn.textContent = '⬇️ Installa aggiornamento'; }
|
||||
showToast('❌ Errore avvio installazione', 'error');
|
||||
if (installBtn) { installBtn.disabled = false; installBtn.textContent = t('kiosk.install_btn'); }
|
||||
showToast('❌ ' + t('kiosk.error_start_install'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2271,7 +2266,7 @@ function _injectKioskOverlay() {
|
||||
const exitBtn = document.createElement('button');
|
||||
exitBtn.id = '_kiosk_exit_btn';
|
||||
exitBtn.textContent = '\u2715';
|
||||
exitBtn.title = 'Esci dal kiosk';
|
||||
exitBtn.title = t('kiosk.exit_title');
|
||||
exitBtn.style.cssText = btnStyle;
|
||||
exitBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -2282,7 +2277,7 @@ function _injectKioskOverlay() {
|
||||
const refBtn = document.createElement('button');
|
||||
refBtn.id = '_kiosk_refresh_btn';
|
||||
refBtn.textContent = '\u21bb';
|
||||
refBtn.title = 'Aggiorna pagina';
|
||||
refBtn.title = t('kiosk.refresh_title');
|
||||
refBtn.style.cssText = btnStyle.replace('font-size:15px', 'font-size:18px');
|
||||
refBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -2297,7 +2292,7 @@ function _injectKioskOverlay() {
|
||||
function renderAppliances(appliances) {
|
||||
const container = document.getElementById('appliances-list');
|
||||
if (!appliances || appliances.length === 0) {
|
||||
container.innerHTML = '<p style="color:var(--text-muted);font-size:0.85rem;padding:8px 0">Nessun elettrodomestico aggiunto</p>';
|
||||
container.innerHTML = `<p style="color:var(--text-muted);font-size:0.85rem;padding:8px 0">${t('appliances.empty')}</p>`;
|
||||
return;
|
||||
}
|
||||
container.innerHTML = appliances.map((a, i) => `
|
||||
@@ -3101,14 +3096,14 @@ function _renderNutritionSection(inventory) {
|
||||
|
||||
// Score colour
|
||||
const scoreColor = healthScore >= 70 ? '#4ade80' : healthScore >= 45 ? '#fbbf24' : '#f87171';
|
||||
const scoreLabel = healthScore >= 70 ? '😄 Ottimo' : healthScore >= 45 ? '🙂 Discreto' : '😬 Migliorabile';
|
||||
const scoreLabel = healthScore >= 70 ? t('nutrition.score_excellent') : healthScore >= 45 ? t('nutrition.score_good') : t('nutrition.score_improve');
|
||||
|
||||
section.innerHTML = `
|
||||
<div class="nutr-card">
|
||||
<div class="aw-header">
|
||||
<div class="aw-title-row">
|
||||
<span class="aw-live-dot aw-live-on"></span>
|
||||
<h3 class="aw-title">🥗 Analisi Alimentare</h3>
|
||||
<h3 class="aw-title">${t('nutrition.title')}</h3>
|
||||
</div>
|
||||
<span class="aw-grade" style="background:${scoreColor};font-size:.75rem;padding:4px 10px">${scoreLabel}</span>
|
||||
</div>
|
||||
@@ -3119,7 +3114,7 @@ function _renderNutritionSection(inventory) {
|
||||
<div class="nutr-pie-3d" id="nutr-pie" style="background:${gradient}"></div>
|
||||
<div class="nutr-pie-center">
|
||||
<span class="nutr-pie-total">${total}</span>
|
||||
<span class="nutr-pie-label">prodotti</span>
|
||||
<span class="nutr-pie-label">${t('nutrition.products_count')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3137,12 +3132,12 @@ function _renderNutritionSection(inventory) {
|
||||
|
||||
<!-- Score bar row -->
|
||||
<div class="nutr-scores">
|
||||
${_nutrScoreBar('🌿 Salute', healthScore, '#4ade80')}
|
||||
${_nutrScoreBar('🎨 Varietà', varietyScore, '#60a5fa')}
|
||||
${_nutrScoreBar('❄️ Freschi', fresh_pct, '#22d3ee')}
|
||||
${_nutrScoreBar(t('nutrition.label_health'), healthScore, '#4ade80')}
|
||||
${_nutrScoreBar(t('nutrition.label_variety'), varietyScore, '#60a5fa')}
|
||||
${_nutrScoreBar(t('nutrition.label_fresh'), fresh_pct, '#22d3ee')}
|
||||
</div>
|
||||
|
||||
<div class="aw-source">Basato su ${total} prodotti in dispensa · EverShelf</div>
|
||||
<div class="aw-source">${t('nutrition.source').replace('{n}', total)}</div>
|
||||
</div>`;
|
||||
|
||||
// Trigger pie animation after render
|
||||
@@ -3889,7 +3884,7 @@ function renderBannerItem() {
|
||||
let btns = `<button class="btn-banner btn-banner-edit" onclick="editBannerAnomaly()">${t('dashboard.banner_anomaly_action_edit')}</button>`;
|
||||
btns += `<button class="btn-banner btn-banner-ok" onclick="dismissBannerAnomaly()">${t('dashboard.banner_anomaly_action_dismiss')} (${an.inv_qty} ${an.unit})</button>`;
|
||||
if (_geminiAvailable) {
|
||||
btns += `<button class="btn-banner btn-banner-ai" onclick="explainBannerAnomaly()" title="Chiedi a Gemini una spiegazione">\ud83e\udd16 Spiega</button>`;
|
||||
btns += `<button class="btn-banner btn-banner-ai" onclick="explainBannerAnomaly()" title="${t('dashboard.banner_explain_title')}">\ud83e\udd16 ${t('dashboard.banner_explain_btn')}</button>`;
|
||||
}
|
||||
actionsEl.innerHTML = btns;
|
||||
|
||||
@@ -3981,7 +3976,7 @@ async function explainBannerAnomaly() {
|
||||
const detailEl = document.getElementById('alert-banner-detail');
|
||||
if (!detailEl) return;
|
||||
const originalHtml = detailEl.innerHTML;
|
||||
detailEl.innerHTML = '<em style="opacity:0.7">\ud83e\udd16 Analizzo\u2026</em>';
|
||||
detailEl.innerHTML = `<em style="opacity:0.7">${t('dashboard.banner_analyzing')}</em>`;
|
||||
|
||||
// Disable the Spiega button to prevent double calls
|
||||
const explainBtn = document.querySelector('#alert-banner .btn-banner-ai');
|
||||
@@ -5569,7 +5564,7 @@ function startManualEntry(barcode = '') {
|
||||
document.getElementById('pf-barcode').value = barcode || '';
|
||||
document.getElementById('pf-image').value = '';
|
||||
document.getElementById('pf-image-preview').style.display = 'none';
|
||||
document.getElementById('product-form-title').textContent = 'Nuovo Prodotto';
|
||||
document.getElementById('product-form-title').textContent = t('product.title_new');
|
||||
const pfAiRow = document.getElementById('pf-ai-fill-row');
|
||||
if (pfAiRow) pfAiRow.style.display = 'block';
|
||||
|
||||
@@ -6775,7 +6770,7 @@ async function _fetchExpiryHistoryAndUpdate(productId) {
|
||||
let days = isVacuum ? getVacuumExpiryDays(data.avg_days) : data.avg_days;
|
||||
const newDate = addDays(days);
|
||||
const newLabel = formatEstimatedExpiry(days);
|
||||
const suffix = ` <span class="history-badge" title="Media da ${data.count} insertiment${data.count === 1 ? 'o' : 'i'} precedent${data.count === 1 ? 'e' : 'i'}">📊 storico</span>`;
|
||||
const suffix = ` <span class="history-badge" title="${t('add.history_badge_tip').replace('{n}', data.count)}">📊 storico</span>`;
|
||||
const expiryInput = document.getElementById('add-expiry');
|
||||
const estimateEl = document.querySelector('.expiry-estimate-label');
|
||||
const dateEl = document.querySelector('.expiry-estimate-date');
|
||||
@@ -6976,7 +6971,7 @@ function selectPurchaseType(btn, type) {
|
||||
const estimatedDate = addDays(days);
|
||||
const estimateLabel = formatEstimatedExpiry(days);
|
||||
let suffix = '';
|
||||
if (window._historyExpiryDays) suffix = ` <span class="history-badge" title="Media da ${window._historyExpiryCount} inserimento/i precedente/i">📊 storico</span>`;
|
||||
if (window._historyExpiryDays) suffix = ` <span class="history-badge" title="${t('add.history_badge_tip').replace('{n}', window._historyExpiryCount)}">📊 storico</span>`;
|
||||
else if (loc === 'freezer' && isVacuum) suffix = ' ' + t('add.suffix_freezer_vacuum');
|
||||
else if (loc === 'freezer') suffix = ' ' + t('add.suffix_freezer');
|
||||
else if (isVacuum) suffix = ' ' + t('add.suffix_vacuum');
|
||||
@@ -7572,7 +7567,7 @@ function setPzFraction(frac) {
|
||||
function isLowStock(totalRemaining, unit, defaultQty) {
|
||||
if (totalRemaining <= 0) return true; // fully depleted → definitely needs restocking
|
||||
if (unit === 'pz') return totalRemaining <= 1; // only 1 piece left
|
||||
if (unit === 'conf') return totalRemaining < 1; // only warn when less than 1 full pack remains (opened/partial)
|
||||
if (unit === 'conf') return totalRemaining < 0.25; // warn when less than 25% of a package remains
|
||||
// Weight/volume: use percentage of default_qty or fixed threshold
|
||||
if (defaultQty > 0) return totalRemaining <= defaultQty * 0.25;
|
||||
// Fallback fixed thresholds
|
||||
@@ -8320,7 +8315,7 @@ async function analyzeWithAI() {
|
||||
|
||||
// Option to save as-is without barcode
|
||||
html += `<div style="margin-top:16px; border-top: 1px solid var(--bg-light); padding-top: 12px">`;
|
||||
html += `<button class="btn btn-secondary full-width" onclick="saveAIProductDirect()">🆕 Non è nessuno di questi — salva come nuovo</button>`;
|
||||
html += `<button class="btn btn-secondary full-width" onclick="saveAIProductDirect()">${t('scanner.save_new_btn')}</button>`;
|
||||
html += `</div>`;
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
@@ -9488,7 +9483,7 @@ function _renderSmartLastUpdate() {
|
||||
const el = document.getElementById('smart-last-update');
|
||||
if (!el || !_smartShoppingLastFetch) return;
|
||||
const d = new Date(_smartShoppingLastFetch);
|
||||
el.textContent = `Aggiornato ${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}`;
|
||||
el.textContent = t('shopping.smart_last_update').replace('{time}', `${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}`);
|
||||
}
|
||||
|
||||
function startBgShoppingRefresh() {
|
||||
@@ -9708,7 +9703,7 @@ async function migrateBringNames(btn) {
|
||||
showToast(`🔄 ${data.migrated} nomi generalizzati in Bring!`, 'success');
|
||||
loadShoppingList(); // refresh the shopping list view
|
||||
} else {
|
||||
showToast('Tutti i nomi sono già aggiornati', 'info');
|
||||
showToast(t('shopping.names_already_updated'), 'info');
|
||||
}
|
||||
} else {
|
||||
if (statusEl) statusEl.textContent = '❌ ' + (data.error || 'Errore');
|
||||
@@ -12931,21 +12926,21 @@ function _renderScreensaverNutrition() {
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="ss-nutr-wrap">
|
||||
<div class="ss-nutr-title">🥗 La tua dispensa oggi</div>
|
||||
<div class="ss-nutr-title">${t('nutrition.today_title')}</div>
|
||||
<div class="ss-nutr-charts">
|
||||
<!-- Main category pie -->
|
||||
<div class="ss-nutr-chart-block">
|
||||
<div class="ss-pie3d" id="ss-pie-main" style="--pie-bg:${gradient}"></div>
|
||||
<div class="ss-nutr-chart-label">${total} prodotti</div>
|
||||
<div class="ss-nutr-chart-label">${t('nutrition.products_n').replace('{n}', total)}</div>
|
||||
<div class="ss-nutr-legend">
|
||||
${top4.map(s => `<div class="ss-leg-row"><span style="background:${s.color}" class="ss-leg-dot"></span><span>${s.icon} ${s.cat}</span><span class="ss-leg-pct">${s.pct}%</span></div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Score donuts -->
|
||||
<div class="ss-nutr-scores-col">
|
||||
${_ssDonut('❤️ Salute', healthScore, healthColor)}
|
||||
${_ssDonut('🎨 Varietà', varietyScore, varColor)}
|
||||
${_ssDonut('❄️ Freschi', fresh_pct, freshColor)}
|
||||
${_ssDonut(t('nutrition.label_health'), healthScore, healthColor)}
|
||||
${_ssDonut(t('nutrition.label_variety'), varietyScore, varColor)}
|
||||
${_ssDonut(t('nutrition.label_fresh'), fresh_pct, freshColor)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -13050,7 +13045,7 @@ function generateScreensaverFact() {
|
||||
}
|
||||
|
||||
// Greeting based on time
|
||||
const greeting = hour < 12 ? 'Buongiorno' : hour < 18 ? 'Buon pomeriggio' : 'Buonasera';
|
||||
const greeting = hour < 12 ? t('facts.greeting_morning') : hour < 18 ? t('facts.greeting_afternoon') : t('facts.greeting_evening');
|
||||
|
||||
// Random item picker
|
||||
const rItem = (arr) => arr.length ? arr[Math.floor(Math.random() * arr.length)] : null;
|
||||
@@ -13060,10 +13055,11 @@ function generateScreensaverFact() {
|
||||
|
||||
// --- Expired items facts ---
|
||||
if (expired.length > 0) {
|
||||
facts.push(() => `Hai ${expired.length} ${expired.length === 1 ? 'prodotto scaduto' : 'prodotti scaduti'} in dispensa. Controlla!`);
|
||||
facts.push(() => expired.length === 1 ? t('facts.expired_one') : t('facts.expired_many').replace('{n}', expired.length));
|
||||
facts.push(() => {
|
||||
const names = expired.slice(0, 3).map(i => i.name);
|
||||
return `Prodotti scaduti: ${names.join(', ')}${expired.length > 3 ? ` e altri ${expired.length - 3}` : ''}`;
|
||||
const extra = expired.length > 3 ? ` ${t('facts.expired_list_more').replace('{n}', expired.length - 3)}` : '';
|
||||
return t('facts.expired_list').replace('{names}', names.join(', ') + extra);
|
||||
});
|
||||
const freezerExpired = expired.filter(i => i.location === 'freezer');
|
||||
if (freezerExpired.length > 0) {
|
||||
@@ -13071,14 +13067,14 @@ function generateScreensaverFact() {
|
||||
const item = rItem(freezerExpired);
|
||||
const safety = getExpiredSafety(item, Math.abs(daysUntilExpiry(item.expiry_date)));
|
||||
if (safety.level === 'ok' || safety.level === 'warning') {
|
||||
return `${item.name} è scaduto, ma essendo in freezer potrebbe essere ancora buono! Controlla.`;
|
||||
return t('facts.freezer_expired_ok').replace('{name}', item.name);
|
||||
}
|
||||
return `${item.name} in freezer è scaduto da troppo tempo. Meglio buttarlo.`;
|
||||
return t('facts.freezer_expired_old').replace('{name}', item.name);
|
||||
});
|
||||
}
|
||||
const frigoExpired = expired.filter(i => i.location === 'frigo');
|
||||
if (frigoExpired.length > 0) {
|
||||
facts.push(() => `Hai ${frigoExpired.length} ${frigoExpired.length === 1 ? 'prodotto scaduto' : 'prodotti scaduti'} in frigo!`);
|
||||
facts.push(() => frigoExpired.length === 1 ? t('facts.fridge_expired_one') : t('facts.fridge_expired_many').replace('{n}', frigoExpired.length));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13087,48 +13083,48 @@ function generateScreensaverFact() {
|
||||
facts.push(() => {
|
||||
const item = expiringSoon[0];
|
||||
const days = daysUntilExpiry(item.expiry_date);
|
||||
if (days === 0) return `${item.name} scade oggi! Usalo subito.`;
|
||||
if (days === 1) return `${item.name} scade domani. Pensaci!`;
|
||||
return `${item.name} scade tra ${days} giorni.`;
|
||||
if (days === 0) return t('facts.expiring_today').replace('{name}', item.name);
|
||||
if (days === 1) return t('facts.expiring_tomorrow').replace('{name}', item.name);
|
||||
return t('facts.expiring_days').replace('{name}', item.name).replace('{days}', days);
|
||||
});
|
||||
if (expiringSoon.length > 1) {
|
||||
facts.push(() => `Hai ${expiringSoon.length} prodotti in scadenza ravvicinata.`);
|
||||
facts.push(() => t('facts.expiring_many').replace('{n}', expiringSoon.length));
|
||||
}
|
||||
}
|
||||
if (expiringThisWeek.length > 0) {
|
||||
facts.push(() => `Questa settimana scadono ${expiringThisWeek.length} prodotti. Pianifica i pasti di conseguenza!`);
|
||||
facts.push(() => t('facts.expiring_this_week').replace('{n}', expiringThisWeek.length));
|
||||
facts.push(() => {
|
||||
const item = rItem(expiringThisWeek);
|
||||
const days = daysUntilExpiry(item.expiry_date);
|
||||
const locLabel = LOCATIONS[item.location]?.label || item.location;
|
||||
return `${item.name} (${locLabel}) scade tra ${days} ${days === 1 ? 'giorno' : 'giorni'}.`;
|
||||
return t('facts.expiring_item_loc').replace('{name}', item.name).replace('{loc}', locLabel).replace('{days}', days).replace('{dayslabel}', days === 1 ? t('facts.day') : t('facts.days'));
|
||||
});
|
||||
}
|
||||
if (expiringThisMonth.length > 0) {
|
||||
facts.push(() => `In questo mese scadranno ${expiringThisMonth.length} prodotti.`);
|
||||
facts.push(() => t('facts.expiring_this_month').replace('{n}', expiringThisMonth.length));
|
||||
}
|
||||
|
||||
// --- Shopping list facts (skip count/names — already shown in the shopping panel) ---
|
||||
if (shop.length > 0) {
|
||||
const names = shop.slice(0, 3).map(i => i.name).join(', ');
|
||||
const extra = shop.length > 3 ? ` e altri ${shop.length - 3}` : '';
|
||||
facts.push(() => `Metti in lista: ${names}${extra} 🛒`);
|
||||
const extra = shop.length > 3 ? ` ${t('facts.shopping_more').replace('{n}', shop.length - 3)}` : '';
|
||||
facts.push(() => t('facts.shopping_add').replace('{names}', names + extra));
|
||||
}
|
||||
if (shop.length === 0) {
|
||||
facts.push(() => `Lista della spesa vuota. Tutto rifornito! ✅`);
|
||||
facts.push(() => t('facts.shopping_empty'));
|
||||
}
|
||||
|
||||
// --- Location-based facts ---
|
||||
if (inFrigo.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = rItem(inFrigo);
|
||||
return `In frigo c'è: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}.`;
|
||||
return t('facts.in_fridge').replace('{name}', item.name + (item.brand ? ' (' + item.brand + ')' : ''));
|
||||
});
|
||||
}
|
||||
if (inFreezer.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = rItem(inFreezer);
|
||||
return `Nel freezer c'è: ${item.name}. Non dimenticartelo!`;
|
||||
return t('facts.in_freezer').replace('{name}', item.name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13140,37 +13136,37 @@ function generateScreensaverFact() {
|
||||
const top = sorted[0];
|
||||
const catLabel = top[0];
|
||||
const icon = CATEGORY_ICONS[catLabel] || '📦';
|
||||
return `La categoria più presente è ${icon} ${catLabel} con ${top[1].length} prodotti.`;
|
||||
return t('facts.top_category').replace('{icon}', icon).replace('{cat}', t('categories.' + catLabel) || catLabel).replace('{n}', top[1].length);
|
||||
});
|
||||
if (byCategory['carne'] && byCategory['carne'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['carne'].length} prodotti di carne. 🥩`);
|
||||
facts.push(() => t('facts.cat_meat').replace('{n}', byCategory['carne'].length));
|
||||
}
|
||||
if (byCategory['latticini'] && byCategory['latticini'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['latticini'].length} latticini in casa. 🥛`);
|
||||
facts.push(() => t('facts.cat_dairy').replace('{n}', byCategory['latticini'].length));
|
||||
}
|
||||
if (byCategory['verdura'] && byCategory['verdura'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['verdura'].length} tipi di verdura. Ottimo per la salute! 🥬`);
|
||||
facts.push(() => t('facts.cat_veggies').replace('{n}', byCategory['verdura'].length));
|
||||
}
|
||||
if (byCategory['frutta'] && byCategory['frutta'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['frutta'].length} tipi di frutta. 🍎`);
|
||||
facts.push(() => t('facts.cat_fruit').replace('{n}', byCategory['frutta'].length));
|
||||
}
|
||||
if (byCategory['bevande'] && byCategory['bevande'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['bevande'].length} bevande disponibili. 🥤`);
|
||||
facts.push(() => t('facts.cat_drinks').replace('{n}', byCategory['bevande'].length));
|
||||
}
|
||||
if (byCategory['surgelati'] && byCategory['surgelati'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['surgelati'].length} surgelati nel freezer. ❄️`);
|
||||
facts.push(() => t('facts.cat_frozen').replace('{n}', byCategory['surgelati'].length));
|
||||
}
|
||||
if (byCategory['pasta'] && byCategory['pasta'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['pasta'].length} tipi di pasta. 🍝 Che ne dici di una carbonara?`);
|
||||
facts.push(() => t('facts.cat_pasta').replace('{n}', byCategory['pasta'].length));
|
||||
}
|
||||
if (byCategory['conserve'] && byCategory['conserve'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['conserve'].length} conserve in dispensa. 🥫`);
|
||||
facts.push(() => t('facts.cat_canned').replace('{n}', byCategory['conserve'].length));
|
||||
}
|
||||
if (byCategory['snack'] && byCategory['snack'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['snack'].length} snack. Resisti alla tentazione! 🍪`);
|
||||
facts.push(() => t('facts.cat_snacks').replace('{n}', byCategory['snack'].length));
|
||||
}
|
||||
if (byCategory['condimenti'] && byCategory['condimenti'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['condimenti'].length} condimenti a disposizione. 🧂`);
|
||||
facts.push(() => t('facts.cat_condiments').replace('{n}', byCategory['condimenti'].length));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13178,16 +13174,16 @@ function generateScreensaverFact() {
|
||||
if (inv.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = rItem(inv);
|
||||
return `Lo sapevi? Hai ${item.name} in ${LOCATIONS[item.location]?.label || item.location}.`;
|
||||
return t('facts.item_random').replace('{name}', item.name).replace('{loc}', LOCATIONS[item.location]?.label || item.location);
|
||||
});
|
||||
facts.push(() => {
|
||||
const item = rItem(inv);
|
||||
const qty = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||
return `${item.name}: ne hai ${qty}.`;
|
||||
return t('facts.item_qty').replace('{name}', item.name).replace('{qty}', qty);
|
||||
});
|
||||
}
|
||||
if (noExpiry.length > 0) {
|
||||
facts.push(() => `${noExpiry.length} prodotti non hanno una data di scadenza impostata.`);
|
||||
facts.push(() => t('facts.no_expiry_count').replace('{n}', noExpiry.length));
|
||||
}
|
||||
if (withExpiry.length > 0) {
|
||||
// Find the one expiring furthest away
|
||||
@@ -13196,7 +13192,7 @@ function generateScreensaverFact() {
|
||||
return d > (best.d || 0) ? { item, d } : best;
|
||||
}, { d: 0 });
|
||||
if (furthest.item && furthest.d > 30) {
|
||||
facts.push(() => `Il prodotto con scadenza più lontana è ${furthest.item.name}: ${Math.round(furthest.d / 30)} mesi.`);
|
||||
facts.push(() => t('facts.furthest_expiry').replace('{name}', furthest.item.name).replace('{months}', Math.round(furthest.d / 30)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13206,56 +13202,56 @@ function generateScreensaverFact() {
|
||||
facts.push(() => {
|
||||
const item = rItem(highQtyItems);
|
||||
const qty = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||
return `Hai una bella scorta di ${item.name}: ${qty}!`;
|
||||
return t('facts.high_qty').replace('{name}', item.name).replace('{qty}', qty);
|
||||
});
|
||||
}
|
||||
const lowQtyItems = inv.filter(i => parseFloat(i.quantity) <= 1 && parseFloat(i.quantity) > 0);
|
||||
if (lowQtyItems.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = rItem(lowQtyItems);
|
||||
return `${item.name} sta per finire. Aggiungilo alla spesa?`;
|
||||
return t('facts.low_qty_item').replace('{name}', item.name);
|
||||
});
|
||||
facts.push(() => `Ci sono ${lowQtyItems.length} prodotti quasi finiti.`);
|
||||
facts.push(() => t('facts.low_qty_count').replace('{n}', lowQtyItems.length));
|
||||
}
|
||||
|
||||
// --- Time-of-day greetings & suggestions ---
|
||||
if (hour >= 6 && hour < 10) {
|
||||
if (byCategory['pane']) facts.push(() => `Buongiorno! Hai del pane per la colazione. 🍞`);
|
||||
if (byCategory['latticini']) facts.push(() => `C'è del latte in frigo per il cappuccino? ☕🥛`);
|
||||
if (byCategory['frutta']) facts.push(() => `Buongiorno! Una bella frutta fresca per iniziare bene. 🍎`);
|
||||
if (byCategory['pane']) facts.push(() => t('facts.morning_bread'));
|
||||
if (byCategory['latticini']) facts.push(() => t('facts.morning_milk'));
|
||||
if (byCategory['frutta']) facts.push(() => t('facts.morning_fruit'));
|
||||
}
|
||||
if (hour >= 11 && hour < 14) {
|
||||
if (byCategory['pasta']) facts.push(() => `Ora di pranzo… Un bel piatto di pasta? 🍝`);
|
||||
if (byCategory['verdura']) facts.push(() => `Un'insalata fresca per pranzo? Hai ${byCategory['verdura'].length} verdure! 🥗`);
|
||||
if (byCategory['pasta']) facts.push(() => t('facts.noon_pasta'));
|
||||
if (byCategory['verdura']) facts.push(() => t('facts.noon_salad').replace('{n}', byCategory['verdura'].length));
|
||||
}
|
||||
if (hour >= 17 && hour < 21) {
|
||||
if (byCategory['carne']) facts.push(() => `Per cena potresti usare la carne che hai. 🥩`);
|
||||
if (byCategory['pesce']) facts.push(() => `Che ne dici di pesce per cena? 🐟`);
|
||||
if (expiringThisWeek.length > 0) facts.push(() => `Hai ${expiringThisWeek.length} prodotti in scadenza questa settimana — usali stasera!`);
|
||||
if (byCategory['carne']) facts.push(() => t('facts.evening_meat'));
|
||||
if (byCategory['pesce']) facts.push(() => t('facts.evening_fish'));
|
||||
if (expiringThisWeek.length > 0) facts.push(() => t('facts.evening_expiring').replace('{n}', expiringThisWeek.length));
|
||||
}
|
||||
if (hour >= 21 || hour < 6) {
|
||||
if (expiringSoon.length > 0) facts.push(() => `Buonanotte! Domani ricordati di usare: ${expiringSoon.slice(0,2).map(i=>i.name).join(', ')}.`);
|
||||
if (expiringSoon.length > 0) facts.push(() => t('facts.night_reminder').replace('{names}', expiringSoon.slice(0,2).map(i=>i.name).join(', ')));
|
||||
}
|
||||
|
||||
// --- Weekly stats ---
|
||||
const recentIn = stats.recent_in || 0;
|
||||
const recentOut = stats.recent_out || 0;
|
||||
if (recentIn > 0 && recentOut > 0) {
|
||||
facts.push(() => `Bilancio settimana: +${recentIn} aggiunti, −${recentOut} consumati.`);
|
||||
facts.push(() => t('facts.weekly_balance').replace('{in}', recentIn).replace('{out}', recentOut));
|
||||
} else if (recentIn > 0) {
|
||||
facts.push(() => `Questa settimana hai aggiunto ${recentIn} prodotti.`);
|
||||
facts.push(() => t('facts.weekly_added').replace('{n}', recentIn));
|
||||
} else if (recentOut > 0) {
|
||||
facts.push(() => `Questa settimana hai consumato ${recentOut} prodotti. Ottimo!`);
|
||||
facts.push(() => t('facts.weekly_consumed').replace('{n}', recentOut));
|
||||
}
|
||||
|
||||
// --- Tips & curiosità (statici ma ruotano) ---
|
||||
facts.push(() => `💡 I prodotti in freezer durano molto più a lungo della data di scadenza.`);
|
||||
facts.push(() => `💡 Il pane congelato mantiene la fragranza per settimane.`);
|
||||
facts.push(() => `💡 Per evitare sprechi, usa prima i prodotti con scadenza più vicina (FIFO).`);
|
||||
facts.push(() => `💡 La carne in freezer può durare fino a 6 mesi senza problemi.`);
|
||||
facts.push(() => `💡 Non ricongelare mai un alimento già scongelato. Cucinalo subito!`);
|
||||
facts.push(() => `💡 Un frigo ordinato ti fa risparmiare tempo e denaro.`);
|
||||
facts.push(() => `💡 Le conserve aperte vanno in frigo e consumate in pochi giorni.`);
|
||||
facts.push(() => t('facts.tip_freezer'));
|
||||
facts.push(() => t('facts.tip_bread'));
|
||||
facts.push(() => t('facts.tip_fifo'));
|
||||
facts.push(() => t('facts.tip_meat'));
|
||||
facts.push(() => t('facts.tip_no_refreeze'));
|
||||
facts.push(() => t('facts.tip_fridge'));
|
||||
facts.push(() => t('facts.tip_canned'));
|
||||
|
||||
// --- Brand-based facts ---
|
||||
const brands = inv.filter(i => i.brand).map(i => i.brand);
|
||||
@@ -13263,24 +13259,24 @@ function generateScreensaverFact() {
|
||||
const brandCount = {};
|
||||
brands.forEach(b => { brandCount[b] = (brandCount[b] || 0) + 1; });
|
||||
const topBrand = Object.entries(brandCount).sort((a, b) => b[1] - a[1])[0];
|
||||
facts.push(() => `Il marca più presente nella tua dispensa è ${topBrand[0]} con ${topBrand[1]} prodotti.`);
|
||||
facts.push(() => t('facts.top_brand').replace('{brand}', topBrand[0]).replace('{n}', topBrand[1]));
|
||||
}
|
||||
|
||||
// --- Specific food combo facts ---
|
||||
if (byCategory['pasta'] && byCategory['condimenti']) {
|
||||
facts.push(() => `Hai pasta e condimenti: sei pronto per un primo piatto! 🍝`);
|
||||
facts.push(() => t('facts.combo_pasta'));
|
||||
}
|
||||
if (byCategory['pane'] && byCategory['carne']) {
|
||||
facts.push(() => `Pane e carne: un panino veloce è sempre una buona idea! 🥪`);
|
||||
facts.push(() => t('facts.combo_sandwich'));
|
||||
}
|
||||
if (byCategory['verdura'] && byCategory['carne']) {
|
||||
facts.push(() => `Verdura e carne: hai tutto per un piatto equilibrato! 🥗🥩`);
|
||||
facts.push(() => t('facts.combo_balanced'));
|
||||
}
|
||||
|
||||
// --- Empty states ---
|
||||
if (inv.length === 0) {
|
||||
facts.push(() => `La dispensa è vuota! Fai una bella spesa. 🛒`);
|
||||
facts.push(() => `Nessun prodotto registrato. Scansiona qualcosa per iniziare!`);
|
||||
facts.push(() => t('facts.pantry_empty'));
|
||||
facts.push(() => t('facts.pantry_empty_scan'));
|
||||
}
|
||||
|
||||
// --- Location distribution ---
|
||||
@@ -13290,7 +13286,7 @@ function generateScreensaverFact() {
|
||||
const parts = Object.entries(byLocation).map(([loc, items]) =>
|
||||
`${LOCATIONS[loc]?.icon || '📦'} ${items.length}`
|
||||
);
|
||||
return `Distribuzione: ${parts.join(' · ')}`;
|
||||
return t('facts.location_distribution').replace('{parts}', parts.join(' · '));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13300,7 +13296,7 @@ function generateScreensaverFact() {
|
||||
|
||||
// Pick a random fact
|
||||
if (facts.length === 0) {
|
||||
return `${greeting}! La tua Dispensa ti aspetta.`;
|
||||
return t('facts.pantry_waiting').replace('{greeting}', greeting);
|
||||
}
|
||||
return facts[Math.floor(Math.random() * facts.length)]();
|
||||
}
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
<title>EverShelf</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260511a">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260511b">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||||
@@ -1469,6 +1469,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260511a"></script>
|
||||
<script src="assets/js/app.js?v=20260511b"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1046
-928
File diff suppressed because it is too large
Load Diff
+1046
-928
File diff suppressed because it is too large
Load Diff
+1046
-928
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user