diff --git a/README.md b/README.md index 3a3d128..c6014ca 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ## ✨ Features > ⚙️ **New in v1.7.23 — Global settings tab, DB auto-cleanup, vacuum-sealed expiry** -> A new **Generali** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place. +> A new **General** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place. > Recipes older than `RECIPE_RETENTION_DAYS` and transactions older than `TRANSACTION_RETENTION_DAYS` are deleted automatically every cron cycle, followed by a SQLite `VACUUM` to keep the database small. > Vacuum-sealed products get an extended grace period (`VACUUM_EXPIRY_EXTENSION_DAYS`, default 30 days) before being flagged as expired. > Auto theme now follows **time of day** (dark 20:00–07:00) instead of the OS setting, making it server-friendly. @@ -100,7 +100,7 @@ ### 🌙 Appearance - **Dark mode** — Three modes: Light, Dark, and Auto (time-based: dark from 20:00 to 07:00, light otherwise); applies immediately without page reload; auto mode re-evaluates every 5 minutes, so night/day transitions happen automatically even on always-on kiosk displays; theme is applied before the first render to prevent a white flash -- **Global settings tab** — A dedicated **⚙️ Generali** tab groups all system-wide settings (language, currency, theme, screensaver, zero-waste tips, export) at the top of the Settings panel +- **Global settings tab** — A dedicated **⚙️ General** tab groups all system-wide settings (language, currency, theme, screensaver, zero-waste tips, export) at the top of the Settings panel ### �️ Database Maintenance - **Automatic cleanup** — Recipes older than `RECIPE_RETENTION_DAYS` (default 7) and transactions older than `TRANSACTION_RETENTION_DAYS` (default 7) are deleted automatically on every cron cycle; SQLite `VACUUM` runs after each cleanup to keep the file compact diff --git a/api/index.php b/api/index.php index 20fafd2..5749c3c 100644 --- a/api/index.php +++ b/api/index.php @@ -391,7 +391,7 @@ if (($_GET['action'] ?? '') === 'health_check') { 'ok' => $freeBytes === false || $freeBytes > 50*1048576, 'value' => $freeMB !== null ? $freeMB.' MB liberi' : null, 'optional' => true, - 'hint' => $freeBytes !== false && $freeBytes <= 50*1048576 ? 'Meno di 50 MB liberi — libera spazio sul disco' : null, + 'hint' => $freeBytes !== false && $freeBytes <= 50*1048576 ? 'Less than 50 MB free — free up disk space' : null, ]; // ── 8. SQLite database ──────────────────────────────────────────────────── @@ -419,11 +419,11 @@ if (($_GET['action'] ?? '') === 'health_check') { $checks['db_legacy'] = [ 'ok' => !$hasLegacy, 'optional' => true, - 'hint' => $hasLegacy ? 'Trovato vecchio dispensa.db — il file è ormai obsoleto, puoi eliminarlo manualmente' : null, + 'hint' => $hasLegacy ? 'Legacy dispensa.db found — the file is obsolete, you can delete it manually' : null, ]; if ($isFresh) { - $checks['db_connect'] = ['ok' => true, 'fresh' => true, 'value' => 'nuovo impianto']; + $checks['db_connect'] = ['ok' => true, 'fresh' => true, 'value' => 'fresh install']; $checks['db_tables'] = ['ok' => true, 'fresh' => true]; $checks['db_integrity'] = ['ok' => true, 'fresh' => true]; $checks['db_wal'] = ['ok' => true, 'fresh' => true, 'optional' => true]; @@ -441,7 +441,7 @@ if (($_GET['action'] ?? '') === 'health_check') { $checks['db_connect'] = ['ok' => true, 'value' => basename($dbPath)]; } catch (\Throwable $e) { $checks['db_connect'] = ['ok' => false, 'error' => $e->getMessage(), - 'hint' => 'Impossibile aprire il database — verifica permessi su data/evershelf.db']; + 'hint' => 'Cannot open the database — check permissions on data/evershelf.db']; } if ($dbConnOk && $pdo) { @@ -452,7 +452,7 @@ if (($_GET['action'] ?? '') === 'health_check') { $checks['db_tables'] = [ 'ok' => empty($missing), 'missing' => $missing, - 'hint' => !empty($missing) ? 'Tabelle mancanti: ' . implode(', ', $missing) . ' — esegui una chiamata API per auto-inizializzare il DB' : null, + 'hint' => !empty($missing) ? 'Missing tables: ' . implode(', ', $missing) . ' — call any API endpoint to auto-initialize the DB' : null, ]; // Integrity @@ -466,7 +466,7 @@ if (($_GET['action'] ?? '') === 'health_check') { // WAL $wal = $pdo->query("PRAGMA journal_mode")->fetchColumn(); $checks['db_wal'] = ['ok' => $wal === 'wal', 'value' => $wal, 'optional' => true, - 'hint' => $wal !== 'wal' ? 'Modalità journal non ottimale — sarà corretta automaticamente al primo avvio' : null]; + 'hint' => $wal !== 'wal' ? 'Journal mode not optimal — will be corrected automatically on next startup' : null]; // Size & rows $checks['db_size'] = ['ok' => true, 'value' => round(filesize($dbPath)/1024).' KB', 'optional' => true]; @@ -474,7 +474,7 @@ if (($_GET['action'] ?? '') === 'health_check') { $checks['db_row_count'] = ['ok' => true, 'value' => $cnt.' prodotti in inventario', 'optional' => true]; } else { foreach (['db_tables', 'db_integrity'] as $k) - $checks[$k] = ['ok' => false, 'hint' => 'Impossibile verificare — connessione DB fallita']; + $checks[$k] = ['ok' => false, 'hint' => 'Cannot verify — DB connection failed']; foreach (['db_wal', 'db_size', 'db_row_count'] as $k) $checks[$k] = ['ok' => false, 'optional' => true]; } @@ -492,10 +492,10 @@ if (($_GET['action'] ?? '') === 'health_check') { $geminiKey = $envGet('GEMINI_API_KEY'); if (!empty($geminiKey)) { $checks['gemini_key'] = ['ok' => strlen($geminiKey) > 20, 'optional' => true, - 'hint' => strlen($geminiKey) <= 20 ? 'Chiave Gemini AI sembra troppo corta — verifica il valore in .env' : null]; + 'hint' => strlen($geminiKey) <= 20 ? 'Gemini AI key looks too short — check the value in .env' : null]; } else { $checks['gemini_key'] = ['ok' => true, 'optional' => true, - 'value' => 'non configurata', 'hint' => 'Configura GEMINI_API_KEY in .env per abilitare le funzioni AI']; + 'value' => 'not configured', 'hint' => 'Set GEMINI_API_KEY in .env to enable AI features']; } // ── 11. Bring! — solo se EMAIL+PASSWORD sono impostate ─────────────────── @@ -511,7 +511,7 @@ if (($_GET['action'] ?? '') === 'health_check') { $checks['tts_url'] = [ 'ok' => !empty($ttsUrl), 'optional' => true, - 'hint' => empty($ttsUrl) ? 'TTS_ENABLED=true ma TTS_URL non configurata' : null, + 'hint' => empty($ttsUrl) ? 'TTS_ENABLED=true but TTS_URL not configured' : null, ]; } @@ -521,7 +521,7 @@ if (($_GET['action'] ?? '') === 'health_check') { $checks['scale_gateway'] = [ 'ok' => !empty($scaleUrl), 'optional' => true, - 'hint' => empty($scaleUrl) ? 'SCALE_ENABLED=true ma SCALE_GATEWAY_URL non configurata' : null, + 'hint' => empty($scaleUrl) ? 'SCALE_ENABLED=true but SCALE_GATEWAY_URL not configured' : null, ]; } @@ -546,7 +546,7 @@ if (($_GET['action'] ?? '') === 'health_check') { curl_close($ch); $internetOk = $httpCode > 0 || $curlErrNo === 0; $checks['internet'] = ['ok' => $internetOk, 'optional' => true, - 'hint' => !$internetOk ? 'Impossibile raggiungere i server Gemini — le funzioni AI non funzioneranno senza connessione internet' : null]; + 'hint' => !$internetOk ? 'Cannot reach Gemini servers — AI features will not work without an internet connection' : null]; } // ── Compute overall result ──────────────────────────────────────────────── @@ -2129,7 +2129,7 @@ function updateInventory(PDO $db): void { if (abs($diff) > 0.001) { $txType = $diff > 0 ? 'in' : 'out'; $txQty = abs($diff); - $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, '[Correzione manuale]')") + $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, '[Manual correction]')") ->execute([$pid, $txType, $txQty, $loc]); } } @@ -2331,7 +2331,7 @@ function undoTransaction(PDO $db): void { } } // Log counter-transaction - $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'out', ?, ?, '[Annullato]')")->execute([$productId, $quantity, $location]); + $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'out', ?, ?, '[Undone]')")->execute([$productId, $quantity, $location]); } elseif ($type === 'out' || $type === 'waste') { // Reverse a USE: add quantity back to inventory @@ -2345,7 +2345,7 @@ function undoTransaction(PDO $db): void { $db->prepare("INSERT INTO inventory (product_id, location, quantity) VALUES (?, ?, ?)")->execute([$productId, $location, $quantity]); } // Log counter-transaction - $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'in', ?, ?, '[Annullato]')")->execute([$productId, $quantity, $location]); + $db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'in', ?, ?, '[Undone]')")->execute([$productId, $quantity, $location]); } // Mark original as undone @@ -3599,7 +3599,7 @@ function geminiChat(PDO $db): void { $langName = recipeLangName($lang); if (empty($message)) { - echo json_encode(['success' => false, 'error' => 'Messaggio vuoto']); + echo json_encode(['success' => false, 'error' => 'Empty message']); return; } @@ -3703,7 +3703,7 @@ PROMPT; $httpCode = $result['http_code']; if ($httpCode !== 200) { - $errMsg = $result['data']['error']['message'] ?? 'Errore API Gemini'; + $errMsg = $result['data']['error']['message'] ?? 'Gemini API error'; echo json_encode(['success' => false, 'error' => $errMsg, 'http_code' => $httpCode]); return; } @@ -3711,7 +3711,7 @@ PROMPT; $reply = $result['data']['candidates'][0]['content']['parts'][0]['text'] ?? ''; if (empty($reply)) { - echo json_encode(['success' => false, 'error' => 'Risposta vuota da Gemini']); + echo json_encode(['success' => false, 'error' => 'Empty response from Gemini']); return; } @@ -5371,7 +5371,7 @@ PROMPT; $httpCode = $result['http_code']; if ($httpCode !== 200) { - $errMsg = $result['data']['error']['message'] ?? 'Errore API Gemini'; + $errMsg = $result['data']['error']['message'] ?? 'Gemini API error'; echo json_encode(['success' => false, 'error' => $errMsg, 'http_code' => $httpCode]); return; } @@ -5386,7 +5386,7 @@ PROMPT; $identified = json_decode($text, true); if (!$identified || empty($identified['name'])) { - echo json_encode(['success' => false, 'error' => 'Impossibile identificare il prodotto', 'raw' => $text]); + echo json_encode(['success' => false, 'error' => 'Cannot identify the product', 'raw' => $text]); return; } @@ -6290,7 +6290,7 @@ function bringGetList(): void { $auth = bringAuth(); if (!$auth) { EverLog::info('bringGetList'); - echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate. Aggiungi BRING_EMAIL e BRING_PASSWORD al file .env']); + echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured. Add BRING_EMAIL and BRING_PASSWORD to .env']); return; } @@ -6301,14 +6301,14 @@ function bringGetList(): void { if ($lists && isset($lists['lists'][0]['listUuid'])) { $listUUID = $lists['lists'][0]['listUuid']; } else { - echo json_encode(['success' => false, 'error' => 'Nessuna lista Bring! trovata']); + echo json_encode(['success' => false, 'error' => 'No Bring! list found']); return; } } $data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}"); if (!$data) { - echo json_encode(['success' => false, 'error' => 'Errore nel recupero della lista']); + echo json_encode(['success' => false, 'error' => 'Error fetching the list']); return; } @@ -6368,7 +6368,7 @@ function bringAddItems(): void { $auth = bringAuth(); if (!$auth) { EverLog::info('bringAddItems'); - echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']); + echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']); return; } @@ -6377,7 +6377,7 @@ function bringAddItems(): void { $listUUID = $input['listUUID'] ?? $auth['bringListUUID']; if (empty($listUUID)) { - echo json_encode(['success' => false, 'error' => 'Lista non trovata']); + echo json_encode(['success' => false, 'error' => 'List not found']); return; } @@ -6446,7 +6446,7 @@ function bringRemoveItem(): void { $auth = bringAuth(); if (!$auth) { EverLog::info('bringRemoveItem'); - echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']); + echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']); return; } @@ -6455,7 +6455,7 @@ function bringRemoveItem(): void { $listUUID = $input['listUUID'] ?? $auth['bringListUUID']; if (empty($name) || empty($listUUID)) { - echo json_encode(['success' => false, 'error' => 'Parametri mancanti']); + echo json_encode(['success' => false, 'error' => 'Missing parameters']); return; } @@ -6494,19 +6494,19 @@ function bringCleanSpecs(): void { $auth = bringAuth(); if (!$auth) { EverLog::info('bringCleanSpecs'); - echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']); + echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']); return; } $listUUID = $auth['bringListUUID']; if (empty($listUUID)) { - echo json_encode(['success' => false, 'error' => 'Lista non trovata']); + echo json_encode(['success' => false, 'error' => 'List not found']); return; } $data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}"); if (!$data || !isset($data['purchase'])) { - echo json_encode(['success' => false, 'error' => 'Errore nel recupero della lista']); + echo json_encode(['success' => false, 'error' => 'Error fetching the list']); return; } @@ -6605,17 +6605,17 @@ function bringMigrateNames(PDO $db): void { $auth = bringAuth(); if (!$auth) { EverLog::info('bringMigrateNames'); - echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']); + echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']); return; } $listUUID = $auth['bringListUUID']; if (empty($listUUID)) { - echo json_encode(['success' => false, 'error' => 'Lista non trovata']); + echo json_encode(['success' => false, 'error' => 'List not found']); return; } $data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}"); if (!$data || !isset($data['purchase'])) { - echo json_encode(['success' => false, 'error' => 'Errore nel recupero della lista']); + echo json_encode(['success' => false, 'error' => 'Error fetching the list']); return; } diff --git a/assets/js/app.js b/assets/js/app.js index 86dcd75..6a80d74 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -2744,7 +2744,7 @@ function _openKioskNativeSettings() { _kioskBridge.openNativeSettings(); } catch(e) { // Older APK without openNativeSettings bridge — inform user to update - showToast(t('settings.kiosk.native_update_hint') || 'Aggiorna l\'app kiosk per usare questa funzione', 'warning', 4000); + showToast(t('settings.kiosk.native_update_hint'), 'warning', 4000); } } @@ -11452,14 +11452,14 @@ async function analyzeExpiryImage(dataUrl) { if (expiryInput) { expiryInput.value = result.expiry_date; } - statusDiv.innerHTML = `

✅ Data trovata: ${formatDate(result.expiry_date)}

`; + statusDiv.innerHTML = `

✅ ${t('scanner.expiry_found')}: ${formatDate(result.expiry_date)}

`; // Close modal after delay setTimeout(() => closeExpiryScanner(), 1500); } else if (result.error === 'no_api_key') { statusDiv.innerHTML = `

${t('ai.no_api_key').replace(/\n/g, '
')}

`; } else { - statusDiv.innerHTML = `

❌ Non riesco a leggere la data. ${result.raw_text ? '
Letto: ' + escapeHtml(result.raw_text) + '' : ''}

+ statusDiv.innerHTML = `

❌ ${t('scanner.expiry_read_fail')} ${result.raw_text ? '
' + t('scanner.expiry_raw_label') + ': ' + escapeHtml(result.raw_text) + '' : ''}

`; } } catch (err) { diff --git a/translations/de.json b/translations/de.json index 4e0738e..60d8bb1 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1034,7 +1034,10 @@ "retake_btn": "🔄 Erneut aufnehmen", "camera_error_hint": "Stelle sicher, dass du HTTPS verwendest und Kameraberechtigungen erteilt hast.
Du kannst den Barcode manuell eingeben oder die KI-Identifikation verwenden.", "no_barcode": "Kein Barcode", - "save_new_btn": "🆕 Keines davon — als neu speichern" + "save_new_btn": "🆕 Keines davon — als neu speichern", + "expiry_found": "Datum gefunden", + "expiry_read_fail": "Datum konnte nicht gelesen werden.", + "expiry_raw_label": "Erkannt" }, "lowstock": { "title": "⚠️ Wird knapp!", diff --git a/translations/en.json b/translations/en.json index 4e79ea5..8eb5f89 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1034,7 +1034,10 @@ "retake_btn": "🔄 Retake", "camera_error_hint": "Ensure you use HTTPS and have granted camera permissions.
You can enter the barcode manually or use AI identification.", "no_barcode": "No barcode", - "save_new_btn": "🆕 None of these — save as new" + "save_new_btn": "🆕 None of these — save as new", + "expiry_found": "Date found", + "expiry_read_fail": "Cannot read the date.", + "expiry_raw_label": "Read" }, "lowstock": { "title": "⚠️ Running low!", diff --git a/translations/it.json b/translations/it.json index e7f885f..660fdf7 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1034,7 +1034,10 @@ "retake_btn": "🔄 Riscatta", "camera_error_hint": "Assicurati di usare HTTPS e di aver concesso i permessi della fotocamera.
Puoi inserire il barcode manualmente o usare l'identificazione AI.", "no_barcode": "Senza barcode", - "save_new_btn": "🆕 Non è nessuno di questi — salva come nuovo" + "save_new_btn": "🆕 Non è nessuno di questi — salva come nuovo", + "expiry_found": "Data trovata", + "expiry_read_fail": "Non riesco a leggere la data.", + "expiry_raw_label": "Letto" }, "lowstock": { "title": "⚠️ Sta per finire!",