chore: auto-merge develop → main
Triggered by: a21b54d feat: i18n — translate all hardcoded Italian strings (nutrition, facts, kiosk, gemini, scanner, shopping)
This commit is contained in:
+36
-19
@@ -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);
|
||||
$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>
|
||||
|
||||
+122
-4
@@ -141,7 +141,10 @@
|
||||
"consumed": "Verbraucht: {n} ({pct}%)",
|
||||
"wasted": "Weggeworfen: {n} ({pct}%)",
|
||||
"more_opened": "und {n} weitere geöffnet...",
|
||||
"banner_expired_detail": "{when} · du hast noch <strong>{qty}</strong>."
|
||||
"banner_expired_detail": "{when} · du hast noch <strong>{qty}</strong>.",
|
||||
"banner_explain_title": "Gemini um eine Erklärung bitten",
|
||||
"banner_explain_btn": "Erklären",
|
||||
"banner_analyzing": "🤖 Analysiere…"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Vorrat",
|
||||
@@ -218,7 +221,8 @@
|
||||
"hint_modify": "📝 Du kannst das Datum ändern oder mit der Kamera scannen",
|
||||
"scan_expiry_title": "📷 Ablaufdatum scannen",
|
||||
"product_added": "✅ {name} hinzugefügt!{qty}",
|
||||
"suffix_freezer_vacuum": "(Tiefkühler + vakuumversiegelt)"
|
||||
"suffix_freezer_vacuum": "(Tiefkühler + vakuumversiegelt)",
|
||||
"history_badge_tip": "Durchschnitt aus {n} früheren Einträgen"
|
||||
},
|
||||
"use": {
|
||||
"title": "Verwenden / Verbrauchen",
|
||||
@@ -427,7 +431,9 @@
|
||||
"suggest_error": "Fehler bei der Vorschlagserstellung",
|
||||
"priority_high": "Hoch",
|
||||
"priority_medium": "Mittel",
|
||||
"priority_low": "Niedrig"
|
||||
"priority_low": "Niedrig",
|
||||
"smart_last_update": "Aktualisiert {time}",
|
||||
"names_already_updated": "Alle Namen sind bereits aktuell"
|
||||
},
|
||||
"ai": {
|
||||
"title": "🤖 KI-Identifikation",
|
||||
@@ -873,7 +879,8 @@
|
||||
"capture_photo_btn": "📸 Foto aufnehmen",
|
||||
"retake_btn": "🔄 Erneut aufnehmen",
|
||||
"camera_error_hint": "Stelle sicher, dass du HTTPS verwendest und Kameraberechtigungen erteilt hast.<br>Du kannst den Barcode manuell eingeben oder die KI-Identifikation verwenden.",
|
||||
"no_barcode": "Kein Barcode"
|
||||
"no_barcode": "Kein Barcode",
|
||||
"save_new_btn": "🆕 Keines davon — als neu speichern"
|
||||
},
|
||||
"lowstock": {
|
||||
"title": "⚠️ Wird knapp!",
|
||||
@@ -932,6 +939,91 @@
|
||||
"not_available": "nicht im Vorrat verfügbar",
|
||||
"suggested_by": "vom Wochenplan vorgeschlagen"
|
||||
},
|
||||
"nutrition": {
|
||||
"title": "🥗 Ernährungsanalyse",
|
||||
"score_excellent": "😄 Ausgezeichnet",
|
||||
"score_good": "🙂 Gut",
|
||||
"score_improve": "😬 Verbesserbar",
|
||||
"label_health": "🌿 Gesundheit",
|
||||
"label_variety": "🎨 Vielfalt",
|
||||
"label_fresh": "❄️ Frisch",
|
||||
"source": "Basierend auf {n} Produkten in deiner Vorratskammer · EverShelf",
|
||||
"products_count": "Produkte",
|
||||
"today_title": "🥗 Deine Vorratskammer heute",
|
||||
"products_n": "{n} Produkte"
|
||||
},
|
||||
"facts": {
|
||||
"greeting_morning": "Guten Morgen",
|
||||
"greeting_afternoon": "Guten Tag",
|
||||
"greeting_evening": "Guten Abend",
|
||||
"pantry_waiting": "{greeting}! Deine Vorratskammer wartet.",
|
||||
"expired_one": "Du hast 1 abgelaufenes Produkt in der Vorratskammer. Bitte überprüfen!",
|
||||
"expired_many": "Du hast {n} abgelaufene Produkte in der Vorratskammer. Bitte überprüfen!",
|
||||
"expired_list": "Abgelaufene Produkte: {names}",
|
||||
"expired_list_more": "und {n} weitere",
|
||||
"freezer_expired_ok": "{name} ist abgelaufen, aber im Gefrierschrank könnte es noch gut sein! Überprüfe es.",
|
||||
"freezer_expired_old": "{name} im Gefrierschrank ist zu lange abgelaufen. Besser wegwerfen.",
|
||||
"fridge_expired_one": "Du hast 1 abgelaufenes Produkt im Kühlschrank!",
|
||||
"fridge_expired_many": "Du hast {n} abgelaufene Produkte im Kühlschrank!",
|
||||
"expiring_today": "{name} läuft heute ab! Sofort verbrauchen.",
|
||||
"expiring_tomorrow": "{name} läuft morgen ab. Denk daran!",
|
||||
"expiring_days": "{name} läuft in {days} Tagen ab.",
|
||||
"expiring_many": "Du hast {n} Produkte, die bald ablaufen.",
|
||||
"expiring_this_week": "{n} Produkte laufen diese Woche ab. Plane deine Mahlzeiten entsprechend!",
|
||||
"expiring_item_loc": "{name} ({loc}) läuft in {days} {dayslabel} ab.",
|
||||
"expiring_this_month": "{n} Produkte laufen diesen Monat ab.",
|
||||
"shopping_add": "Zur Liste: {names} 🛒",
|
||||
"shopping_more": "und {n} weitere",
|
||||
"shopping_empty": "Einkaufsliste leer. Alles aufgefüllt! ✅",
|
||||
"in_fridge": "Im Kühlschrank: {name}.",
|
||||
"in_freezer": "Im Gefrierschrank: {name}. Vergiss es nicht!",
|
||||
"top_category": "Häufigste Kategorie: {icon} {cat} mit {n} Produkten.",
|
||||
"cat_meat": "Du hast {n} Fleischprodukte. 🥩",
|
||||
"cat_dairy": "Du hast {n} Milchprodukte zu Hause. 🥛",
|
||||
"cat_veggies": "Du hast {n} Gemüsesorten. Super für die Gesundheit! 🥬",
|
||||
"cat_fruit": "Du hast {n} Obstsorten. 🍎",
|
||||
"cat_drinks": "Du hast {n} Getränke verfügbar. 🥤",
|
||||
"cat_frozen": "Du hast {n} Tiefkühlprodukte. ❄️",
|
||||
"cat_pasta": "Du hast {n} Nudelsorten. 🍝 Wie wäre es mit einer Carbonara?",
|
||||
"cat_canned": "Du hast {n} Konserven in der Vorratskammer. 🥫",
|
||||
"cat_snacks": "Du hast {n} Snacks. Widerstand leisten! 🍪",
|
||||
"cat_condiments": "Du hast {n} Gewürze zur Verfügung. 🧂",
|
||||
"item_random": "Wusstest du? Du hast {name} in {loc}.",
|
||||
"item_qty": "{name}: du hast {qty}.",
|
||||
"no_expiry_count": "{n} Produkte haben kein Ablaufdatum.",
|
||||
"furthest_expiry": "Das Produkt mit dem spätesten Ablaufdatum ist {name}: {months} Monate.",
|
||||
"high_qty": "Du hast einen guten Vorrat von {name}: {qty}!",
|
||||
"low_qty_item": "{name} geht zur Neige. Auf die Einkaufsliste?",
|
||||
"low_qty_count": "{n} Produkte sind fast aufgebraucht.",
|
||||
"morning_bread": "Guten Morgen! Du hast Brot für das Frühstück. 🍞",
|
||||
"morning_milk": "Gibt es Milch im Kühlschrank für den Cappuccino? ☕🥛",
|
||||
"morning_fruit": "Guten Morgen! Frisches Obst ist ein guter Start. 🍎",
|
||||
"noon_pasta": "Mittagszeit… Wie wäre es mit Pasta? 🍝",
|
||||
"noon_salad": "Ein frischer Salat zum Mittagessen? Du hast {n} Gemüsesorten! 🥗",
|
||||
"evening_meat": "Zum Abendessen könntest du das Fleisch verwenden. 🥩",
|
||||
"evening_fish": "Wie wäre es mit Fisch zum Abendessen? 🐟",
|
||||
"evening_expiring": "Du hast {n} Produkte, die diese Woche ablaufen — heute Abend verwenden!",
|
||||
"night_reminder": "Gute Nacht! Denk morgen daran zu verwenden: {names}.",
|
||||
"weekly_balance": "Wochenbilanz: +{in} hinzugefügt, −{out} verbraucht.",
|
||||
"weekly_added": "Du hast diese Woche {n} Produkte hinzugefügt.",
|
||||
"weekly_consumed": "Du hast diese Woche {n} Produkte verbraucht. Gut gemacht!",
|
||||
"tip_freezer": "💡 Tiefkühlprodukte halten viel länger als das Ablaufdatum.",
|
||||
"tip_bread": "💡 Gefrorenes Brot behält seinen Duft wochenlang.",
|
||||
"tip_fifo": "💡 Um Verschwendung zu vermeiden, zuerst Produkte mit nahem Ablaufdatum verwenden (FIFO).",
|
||||
"tip_meat": "💡 Fleisch im Gefrierschrank hält bis zu 6 Monate problemlos.",
|
||||
"tip_no_refreeze": "💡 Niemals ein aufgetautes Lebensmittel wieder einfrieren. Sofort zubereiten!",
|
||||
"tip_fridge": "💡 Ein ordentlicher Kühlschrank spart Zeit und Geld.",
|
||||
"tip_canned": "💡 Geöffnete Konserven in den Kühlschrank und in wenigen Tagen verbrauchen.",
|
||||
"top_brand": "Die häufigste Marke in deiner Vorratskammer ist {brand} mit {n} Produkten.",
|
||||
"combo_pasta": "Du hast Pasta und Gewürze: bereit für ein Erstgericht! 🍝",
|
||||
"combo_sandwich": "Brot und Fleisch: ein schnelles Sandwich ist immer eine gute Idee! 🥪",
|
||||
"combo_balanced": "Gemüse und Fleisch: du hast alles für eine ausgewogene Mahlzeit! 🥗🥩",
|
||||
"pantry_empty": "Die Vorratskammer ist leer! Zeit zum Einkaufen. 🛒",
|
||||
"pantry_empty_scan": "Keine Produkte erfasst. Scanne etwas um zu beginnen!",
|
||||
"location_distribution": "Verteilung: {parts}",
|
||||
"day": "Tag",
|
||||
"days": "Tage"
|
||||
},
|
||||
"kiosk_session": {
|
||||
"first_item": "Erstes Produkt: {name}!",
|
||||
"items_two_four": "{n} Artikel — Trägheit überwinden 🚀",
|
||||
@@ -942,5 +1034,31 @@
|
||||
"duplicates_many": "{n} Duplikate (mehrfach genommen)",
|
||||
"top_category": "Top-Kategorie: {cat} ({count}×)",
|
||||
"items_fallback": "{n} Artikel hinzugefügt"
|
||||
},
|
||||
"kiosk": {
|
||||
"check_btn": "🔍 Nach Updates suchen",
|
||||
"checking": "⏳ Prüfe…",
|
||||
"error_check": "Fehler bei der Update-Prüfung",
|
||||
"error_start_install": "Fehler beim Starten der Installation",
|
||||
"version_installed": "Installiert: {v}",
|
||||
"update_available": "⬆️ Neue Version verfügbar: <strong>{latest}</strong> (installiert: {current})",
|
||||
"up_to_date": "✅ Du bist auf dem neuesten Stand — Version <strong>{v}</strong>",
|
||||
"too_old": "⚠️ Der installierte Kiosk ist zu alt für die automatische Update-Prüfung.<br>Drücke den Knopf unten, um die neue Version direkt herunterzuladen.",
|
||||
"manual_install": "⚠️ Dieser Kiosk unterstützt keine automatische Installation.<br><strong>Manuelle Vorgehensweise:</strong><br>1. Kiosk verlassen (✕ oben links)<br>2. EverShelf Kiosk App deinstallieren<br>3. Neue APK von GitHub herunterladen und installieren:",
|
||||
"starting_download": "⏳ Download startet…",
|
||||
"install_btn": "⬇️ Update installieren",
|
||||
"exit_title": "Kiosk beenden",
|
||||
"refresh_title": "Seite aktualisieren"
|
||||
},
|
||||
"update": {
|
||||
"new_version": "Neue Version",
|
||||
"btn": "Aktualisieren"
|
||||
},
|
||||
"gemini": {
|
||||
"chat_title": "Mit Gemini chatten",
|
||||
"not_configured": "🤖 Gemini nicht konfiguriert — GEMINI_API_KEY in den Einstellungen setzen"
|
||||
},
|
||||
"appliances": {
|
||||
"empty": "Kein Haushaltsgerät hinzugefügt"
|
||||
}
|
||||
}
|
||||
+122
-4
@@ -141,7 +141,10 @@
|
||||
"consumed": "Consumed: {n} ({pct}%)",
|
||||
"wasted": "Wasted: {n} ({pct}%)",
|
||||
"more_opened": "and {n} more opened...",
|
||||
"banner_expired_detail": "{when} · you still have <strong>{qty}</strong>."
|
||||
"banner_expired_detail": "{when} · you still have <strong>{qty}</strong>.",
|
||||
"banner_explain_title": "Ask Gemini for an explanation",
|
||||
"banner_explain_btn": "Explain",
|
||||
"banner_analyzing": "🤖 Analyzing…"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Pantry",
|
||||
@@ -218,7 +221,8 @@
|
||||
"hint_modify": "📝 You can change the date or scan it with the camera",
|
||||
"scan_expiry_title": "📷 Scan Expiry Date",
|
||||
"product_added": "✅ {name} added!{qty}",
|
||||
"suffix_freezer_vacuum": "(freezer + vacuum sealed)"
|
||||
"suffix_freezer_vacuum": "(freezer + vacuum sealed)",
|
||||
"history_badge_tip": "Average from {n} previous entries"
|
||||
},
|
||||
"use": {
|
||||
"title": "Use / Consume",
|
||||
@@ -427,7 +431,9 @@
|
||||
"suggest_error": "Suggestion generation error",
|
||||
"priority_high": "High",
|
||||
"priority_medium": "Medium",
|
||||
"priority_low": "Low"
|
||||
"priority_low": "Low",
|
||||
"smart_last_update": "Updated {time}",
|
||||
"names_already_updated": "All names are already up to date"
|
||||
},
|
||||
"ai": {
|
||||
"title": "🤖 AI Identification",
|
||||
@@ -873,7 +879,8 @@
|
||||
"capture_photo_btn": "📸 Take Photo",
|
||||
"retake_btn": "🔄 Retake",
|
||||
"camera_error_hint": "Ensure you use HTTPS and have granted camera permissions.<br>You can enter the barcode manually or use AI identification.",
|
||||
"no_barcode": "No barcode"
|
||||
"no_barcode": "No barcode",
|
||||
"save_new_btn": "🆕 None of these — save as new"
|
||||
},
|
||||
"lowstock": {
|
||||
"title": "⚠️ Running low!",
|
||||
@@ -932,6 +939,91 @@
|
||||
"not_available": "not available in pantry",
|
||||
"suggested_by": "suggested by weekly plan"
|
||||
},
|
||||
"nutrition": {
|
||||
"title": "🥗 Food Analysis",
|
||||
"score_excellent": "😄 Excellent",
|
||||
"score_good": "🙂 Good",
|
||||
"score_improve": "😬 Improvable",
|
||||
"label_health": "🌿 Health",
|
||||
"label_variety": "🎨 Variety",
|
||||
"label_fresh": "❄️ Fresh",
|
||||
"source": "Based on {n} products in your pantry · EverShelf",
|
||||
"products_count": "products",
|
||||
"today_title": "🥗 Your pantry today",
|
||||
"products_n": "{n} products"
|
||||
},
|
||||
"facts": {
|
||||
"greeting_morning": "Good morning",
|
||||
"greeting_afternoon": "Good afternoon",
|
||||
"greeting_evening": "Good evening",
|
||||
"pantry_waiting": "{greeting}! Your Pantry awaits.",
|
||||
"expired_one": "You have 1 expired product in your pantry. Check it!",
|
||||
"expired_many": "You have {n} expired products in your pantry. Check them!",
|
||||
"expired_list": "Expired products: {names}",
|
||||
"expired_list_more": "and {n} more",
|
||||
"freezer_expired_ok": "{name} is expired, but being in the freezer it may still be fine! Check it.",
|
||||
"freezer_expired_old": "{name} in the freezer has been expired too long. Better to discard it.",
|
||||
"fridge_expired_one": "You have 1 expired product in the fridge!",
|
||||
"fridge_expired_many": "You have {n} expired products in the fridge!",
|
||||
"expiring_today": "{name} expires today! Use it right away.",
|
||||
"expiring_tomorrow": "{name} expires tomorrow. Plan ahead!",
|
||||
"expiring_days": "{name} expires in {days} days.",
|
||||
"expiring_many": "You have {n} products expiring soon.",
|
||||
"expiring_this_week": "{n} products expire this week. Plan your meals accordingly!",
|
||||
"expiring_item_loc": "{name} ({loc}) expires in {days} {dayslabel}.",
|
||||
"expiring_this_month": "{n} products will expire this month.",
|
||||
"shopping_add": "Add to list: {names} 🛒",
|
||||
"shopping_more": "and {n} more",
|
||||
"shopping_empty": "Shopping list empty. All stocked up! ✅",
|
||||
"in_fridge": "In the fridge: {name}.",
|
||||
"in_freezer": "In the freezer: {name}. Don't forget it!",
|
||||
"top_category": "Top category is {icon} {cat} with {n} products.",
|
||||
"cat_meat": "You have {n} meat products. 🥩",
|
||||
"cat_dairy": "You have {n} dairy products at home. 🥛",
|
||||
"cat_veggies": "You have {n} types of vegetables. Great for your health! 🥬",
|
||||
"cat_fruit": "You have {n} types of fruit. 🍎",
|
||||
"cat_drinks": "You have {n} drinks available. 🥤",
|
||||
"cat_frozen": "You have {n} frozen items. ❄️",
|
||||
"cat_pasta": "You have {n} types of pasta. 🍝 How about a carbonara?",
|
||||
"cat_canned": "You have {n} canned goods in your pantry. 🥫",
|
||||
"cat_snacks": "You have {n} snacks. Resist the temptation! 🍪",
|
||||
"cat_condiments": "You have {n} condiments available. 🧂",
|
||||
"item_random": "Did you know? You have {name} in {loc}.",
|
||||
"item_qty": "{name}: you have {qty}.",
|
||||
"no_expiry_count": "{n} products have no expiry date set.",
|
||||
"furthest_expiry": "The product with the furthest expiry is {name}: {months} months.",
|
||||
"high_qty": "You have a great stock of {name}: {qty}!",
|
||||
"low_qty_item": "{name} is running low. Add it to your shopping list?",
|
||||
"low_qty_count": "{n} products are almost out.",
|
||||
"morning_bread": "Good morning! You have bread for breakfast. 🍞",
|
||||
"morning_milk": "Is there milk in the fridge for a cappuccino? ☕🥛",
|
||||
"morning_fruit": "Good morning! Some fresh fruit is a great way to start. 🍎",
|
||||
"noon_pasta": "Lunchtime… How about a nice bowl of pasta? 🍝",
|
||||
"noon_salad": "A fresh salad for lunch? You have {n} vegetables! 🥗",
|
||||
"evening_meat": "For dinner you could use the meat you have. 🥩",
|
||||
"evening_fish": "How about fish for dinner? 🐟",
|
||||
"evening_expiring": "You have {n} products expiring this week — use them tonight!",
|
||||
"night_reminder": "Good night! Remember to use tomorrow: {names}.",
|
||||
"weekly_balance": "Weekly balance: +{in} added, −{out} consumed.",
|
||||
"weekly_added": "You added {n} products this week.",
|
||||
"weekly_consumed": "You consumed {n} products this week. Well done!",
|
||||
"tip_freezer": "💡 Frozen products last much longer than the expiry date.",
|
||||
"tip_bread": "💡 Frozen bread keeps its freshness for weeks.",
|
||||
"tip_fifo": "💡 To avoid waste, use products closest to expiry first (FIFO).",
|
||||
"tip_meat": "💡 Meat in the freezer can last up to 6 months safely.",
|
||||
"tip_no_refreeze": "💡 Never refreeze a thawed product. Cook it right away!",
|
||||
"tip_fridge": "💡 A tidy fridge saves you time and money.",
|
||||
"tip_canned": "💡 Opened canned goods should go in the fridge and be consumed within a few days.",
|
||||
"top_brand": "The most common brand in your pantry is {brand} with {n} products.",
|
||||
"combo_pasta": "You have pasta and condiments: ready for a first course! 🍝",
|
||||
"combo_sandwich": "Bread and meat: a quick sandwich is always a good idea! 🥪",
|
||||
"combo_balanced": "Vegetables and meat: you have everything for a balanced meal! 🥗🥩",
|
||||
"pantry_empty": "The pantry is empty! Time to go shopping. 🛒",
|
||||
"pantry_empty_scan": "No products registered. Scan something to start!",
|
||||
"location_distribution": "Distribution: {parts}",
|
||||
"day": "day",
|
||||
"days": "days"
|
||||
},
|
||||
"kiosk_session": {
|
||||
"first_item": "First item: {name}!",
|
||||
"items_two_four": "{n} items — warming up 🚀",
|
||||
@@ -942,5 +1034,31 @@
|
||||
"duplicates_many": "{n} duplicates (picked multiple times)",
|
||||
"top_category": "Top category: {cat} ({count}×)",
|
||||
"items_fallback": "{n} item{plural} added"
|
||||
},
|
||||
"kiosk": {
|
||||
"check_btn": "🔍 Check for updates",
|
||||
"checking": "⏳ Checking…",
|
||||
"error_check": "Error during update check",
|
||||
"error_start_install": "Error starting installation",
|
||||
"version_installed": "Installed: {v}",
|
||||
"update_available": "⬆️ New version available: <strong>{latest}</strong> (installed: {current})",
|
||||
"up_to_date": "✅ You are up to date — version <strong>{v}</strong>",
|
||||
"too_old": "⚠️ The installed kiosk is too old for automatic update checking.<br>Press the button below to download and install the new version directly.",
|
||||
"manual_install": "⚠️ This kiosk does not support automatic installation.<br><strong>Manual procedure:</strong><br>1. Exit the kiosk (✕ button top left)<br>2. Uninstall the EverShelf Kiosk app<br>3. Download and install the new APK from GitHub:",
|
||||
"starting_download": "⏳ Starting download…",
|
||||
"install_btn": "⬇️ Install update",
|
||||
"exit_title": "Exit kiosk",
|
||||
"refresh_title": "Refresh page"
|
||||
},
|
||||
"update": {
|
||||
"new_version": "New version",
|
||||
"btn": "Update"
|
||||
},
|
||||
"gemini": {
|
||||
"chat_title": "Chat with Gemini",
|
||||
"not_configured": "🤖 Gemini not configured — set GEMINI_API_KEY in settings"
|
||||
},
|
||||
"appliances": {
|
||||
"empty": "No appliances added"
|
||||
}
|
||||
}
|
||||
+122
-4
@@ -141,7 +141,10 @@
|
||||
"consumed": "Consumati: {n} ({pct}%)",
|
||||
"wasted": "Buttati: {n} ({pct}%)",
|
||||
"more_opened": "e altri {n} prodotti aperti...",
|
||||
"banner_expired_detail": "{when} · hai ancora <strong>{qty}</strong>."
|
||||
"banner_expired_detail": "{when} · hai ancora <strong>{qty}</strong>.",
|
||||
"banner_explain_title": "Chiedi a Gemini una spiegazione",
|
||||
"banner_explain_btn": "Spiega",
|
||||
"banner_analyzing": "🤖 Analizzo…"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Dispensa",
|
||||
@@ -218,7 +221,8 @@
|
||||
"hint_modify": "📝 Puoi modificare la data o scansionarla con la fotocamera",
|
||||
"scan_expiry_title": "📷 Scansiona Data Scadenza",
|
||||
"product_added": "✅ {name} aggiunto!{qty}",
|
||||
"suffix_freezer_vacuum": "(freezer + sotto vuoto)"
|
||||
"suffix_freezer_vacuum": "(freezer + sotto vuoto)",
|
||||
"history_badge_tip": "Media da {n} inserimenti precedenti"
|
||||
},
|
||||
"use": {
|
||||
"title": "Usa / Consuma",
|
||||
@@ -427,7 +431,9 @@
|
||||
"suggest_error": "Errore nella generazione",
|
||||
"priority_high": "Alta",
|
||||
"priority_medium": "Media",
|
||||
"priority_low": "Bassa"
|
||||
"priority_low": "Bassa",
|
||||
"smart_last_update": "Aggiornato {time}",
|
||||
"names_already_updated": "Tutti i nomi sono già aggiornati"
|
||||
},
|
||||
"ai": {
|
||||
"title": "🤖 Identificazione AI",
|
||||
@@ -873,7 +879,8 @@
|
||||
"capture_photo_btn": "📸 Scatta Foto",
|
||||
"retake_btn": "🔄 Riscatta",
|
||||
"camera_error_hint": "Assicurati di usare HTTPS e di aver concesso i permessi della fotocamera.<br>Puoi inserire il barcode manualmente o usare l'identificazione AI.",
|
||||
"no_barcode": "Senza barcode"
|
||||
"no_barcode": "Senza barcode",
|
||||
"save_new_btn": "🆕 Non è nessuno di questi — salva come nuovo"
|
||||
},
|
||||
"lowstock": {
|
||||
"title": "⚠️ Sta per finire!",
|
||||
@@ -942,5 +949,116 @@
|
||||
"duplicates_many": "{n} bis (roba presa più volte)",
|
||||
"top_category": "Categoria top: {cat} ({count}×)",
|
||||
"items_fallback": "{n} prodott{n} aggiunti"
|
||||
},
|
||||
"nutrition": {
|
||||
"title": "🥗 Analisi Alimentare",
|
||||
"score_excellent": "😄 Ottimo",
|
||||
"score_good": "🙂 Discreto",
|
||||
"score_improve": "😬 Migliorabile",
|
||||
"label_health": "🌿 Salute",
|
||||
"label_variety": "🎨 Varietà",
|
||||
"label_fresh": "❄️ Freschi",
|
||||
"source": "Basato su {n} prodotti in dispensa · EverShelf",
|
||||
"products_count": "prodotti",
|
||||
"today_title": "🥗 La tua dispensa oggi",
|
||||
"products_n": "{n} prodotti"
|
||||
},
|
||||
"facts": {
|
||||
"greeting_morning": "Buongiorno",
|
||||
"greeting_afternoon": "Buon pomeriggio",
|
||||
"greeting_evening": "Buonasera",
|
||||
"pantry_waiting": "{greeting}! La tua Dispensa ti aspetta.",
|
||||
"expired_one": "Hai 1 prodotto scaduto in dispensa. Controlla!",
|
||||
"expired_many": "Hai {n} prodotti scaduti in dispensa. Controlla!",
|
||||
"expired_list": "Prodotti scaduti: {names}",
|
||||
"expired_list_more": "e altri {n}",
|
||||
"freezer_expired_ok": "{name} è scaduto, ma essendo in freezer potrebbe essere ancora buono! Controlla.",
|
||||
"freezer_expired_old": "{name} in freezer è scaduto da troppo tempo. Meglio buttarlo.",
|
||||
"fridge_expired_one": "Hai 1 prodotto scaduto in frigo!",
|
||||
"fridge_expired_many": "Hai {n} prodotti scaduti in frigo!",
|
||||
"expiring_today": "{name} scade oggi! Usalo subito.",
|
||||
"expiring_tomorrow": "{name} scade domani. Pensaci!",
|
||||
"expiring_days": "{name} scade tra {days} giorni.",
|
||||
"expiring_many": "Hai {n} prodotti in scadenza ravvicinata.",
|
||||
"expiring_this_week": "Questa settimana scadono {n} prodotti. Pianifica i pasti di conseguenza!",
|
||||
"expiring_item_loc": "{name} ({loc}) scade tra {days} {dayslabel}.",
|
||||
"expiring_this_month": "In questo mese scadranno {n} prodotti.",
|
||||
"shopping_add": "Metti in lista: {names} 🛒",
|
||||
"shopping_more": "e altri {n}",
|
||||
"shopping_empty": "Lista della spesa vuota. Tutto rifornito! ✅",
|
||||
"in_fridge": "In frigo c'è: {name}.",
|
||||
"in_freezer": "Nel freezer c'è: {name}. Non dimenticartelo!",
|
||||
"top_category": "La categoria più presente è {icon} {cat} con {n} prodotti.",
|
||||
"cat_meat": "Hai {n} prodotti di carne. 🥩",
|
||||
"cat_dairy": "Hai {n} latticini in casa. 🥛",
|
||||
"cat_veggies": "Hai {n} tipi di verdura. Ottimo per la salute! 🥬",
|
||||
"cat_fruit": "Hai {n} tipi di frutta. 🍎",
|
||||
"cat_drinks": "Hai {n} bevande disponibili. 🥤",
|
||||
"cat_frozen": "Hai {n} surgelati nel freezer. ❄️",
|
||||
"cat_pasta": "Hai {n} tipi di pasta. 🍝 Che ne dici di una carbonara?",
|
||||
"cat_canned": "Hai {n} conserve in dispensa. 🥫",
|
||||
"cat_snacks": "Hai {n} snack. Resisti alla tentazione! 🍪",
|
||||
"cat_condiments": "Hai {n} condimenti a disposizione. 🧂",
|
||||
"item_random": "Lo sapevi? Hai {name} in {loc}.",
|
||||
"item_qty": "{name}: ne hai {qty}.",
|
||||
"no_expiry_count": "{n} prodotti non hanno una data di scadenza impostata.",
|
||||
"furthest_expiry": "Il prodotto con scadenza più lontana è {name}: {months} mesi.",
|
||||
"high_qty": "Hai una bella scorta di {name}: {qty}!",
|
||||
"low_qty_item": "{name} sta per finire. Aggiungilo alla spesa?",
|
||||
"low_qty_count": "Ci sono {n} prodotti quasi finiti.",
|
||||
"morning_bread": "Buongiorno! Hai del pane per la colazione. 🍞",
|
||||
"morning_milk": "C'è del latte in frigo per il cappuccino? ☕🥛",
|
||||
"morning_fruit": "Buongiorno! Una bella frutta fresca per iniziare bene. 🍎",
|
||||
"noon_pasta": "Ora di pranzo… Un bel piatto di pasta? 🍝",
|
||||
"noon_salad": "Un'insalata fresca per pranzo? Hai {n} verdure! 🥗",
|
||||
"evening_meat": "Per cena potresti usare la carne che hai. 🥩",
|
||||
"evening_fish": "Che ne dici di pesce per cena? 🐟",
|
||||
"evening_expiring": "Hai {n} prodotti in scadenza questa settimana — usali stasera!",
|
||||
"night_reminder": "Buonanotte! Domani ricordati di usare: {names}.",
|
||||
"weekly_balance": "Bilancio settimana: +{in} aggiunti, −{out} consumati.",
|
||||
"weekly_added": "Questa settimana hai aggiunto {n} prodotti.",
|
||||
"weekly_consumed": "Questa settimana hai consumato {n} prodotti. Ottimo!",
|
||||
"tip_freezer": "💡 I prodotti in freezer durano molto più a lungo della data di scadenza.",
|
||||
"tip_bread": "💡 Il pane congelato mantiene la fragranza per settimane.",
|
||||
"tip_fifo": "💡 Per evitare sprechi, usa prima i prodotti con scadenza più vicina (FIFO).",
|
||||
"tip_meat": "💡 La carne in freezer può durare fino a 6 mesi senza problemi.",
|
||||
"tip_no_refreeze": "💡 Non ricongelare mai un alimento già scongelato. Cucinalo subito!",
|
||||
"tip_fridge": "💡 Un frigo ordinato ti fa risparmiare tempo e denaro.",
|
||||
"tip_canned": "💡 Le conserve aperte vanno in frigo e consumate in pochi giorni.",
|
||||
"top_brand": "Il marchio più presente nella tua dispensa è {brand} con {n} prodotti.",
|
||||
"combo_pasta": "Hai pasta e condimenti: sei pronto per un primo piatto! 🍝",
|
||||
"combo_sandwich": "Pane e carne: un panino veloce è sempre una buona idea! 🥪",
|
||||
"combo_balanced": "Verdura e carne: hai tutto per un piatto equilibrato! 🥗🥩",
|
||||
"pantry_empty": "La dispensa è vuota! Fai una bella spesa. 🛒",
|
||||
"pantry_empty_scan": "Nessun prodotto registrato. Scansiona qualcosa per iniziare!",
|
||||
"location_distribution": "Distribuzione: {parts}",
|
||||
"day": "giorno",
|
||||
"days": "giorni"
|
||||
},
|
||||
"kiosk": {
|
||||
"check_btn": "🔍 Cerca aggiornamenti",
|
||||
"checking": "⏳ Controllo…",
|
||||
"error_check": "Errore durante il controllo",
|
||||
"error_start_install": "Errore avvio installazione",
|
||||
"version_installed": "Installata: {v}",
|
||||
"update_available": "⬆️ Nuova versione disponibile: <strong>{latest}</strong> (installata: {current})",
|
||||
"up_to_date": "✅ Sei già aggiornato — versione <strong>{v}</strong>",
|
||||
"too_old": "⚠️ Il kiosk installato è troppo vecchio per il controllo automatico.<br>Premi il pulsante qui sotto per scaricare e installare la nuova versione direttamente.",
|
||||
"manual_install": "⚠️ 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:",
|
||||
"starting_download": "⏳ Avvio download…",
|
||||
"install_btn": "⬇️ Installa aggiornamento",
|
||||
"exit_title": "Esci dal kiosk",
|
||||
"refresh_title": "Aggiorna pagina"
|
||||
},
|
||||
"update": {
|
||||
"new_version": "Nuova versione",
|
||||
"btn": "Aggiorna"
|
||||
},
|
||||
"gemini": {
|
||||
"chat_title": "Chat con Gemini",
|
||||
"not_configured": "🤖 Gemini non configurato — imposta GEMINI_API_KEY nelle impostazioni"
|
||||
},
|
||||
"appliances": {
|
||||
"empty": "Nessun elettrodomestico aggiunto"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user