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:
dadaloop82
2026-05-11 15:49:55 +00:00
parent da62647089
commit a21b54deaa
6 changed files with 3285 additions and 2918 deletions
+36 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}