diff --git a/app/static/js/app.js b/app/static/js/app.js index e464bbf..73f32f3 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -2010,7 +2010,7 @@ async function copyToClipboard(text, btnElement) { */ async function loadDeviceInfo() { const container = document.getElementById('deviceInfoContent'); - container.innerHTML = '
Loading...
'; + container.innerHTML = `
${tHtml('common.loading')}
`; try { const response = await fetch('/api/device/info'); @@ -2024,13 +2024,13 @@ async function loadDeviceInfo() { // API returns info as a dict directly (v2 DeviceManager) const info = data.info; if (!info || typeof info !== 'object') { - container.innerHTML = `
No device info available
`; + container.innerHTML = `
${tHtml('device.info.none')}
`; return; } // Type mapping const typeNames = { 1: 'Companion', 2: 'Repeater', 3: 'Room Server', 4: 'Sensor' }; - const typeName = typeNames[info.adv_type] || `Unknown (${info.adv_type})`; + const typeName = typeNames[info.adv_type] || t('device.info.type_unknown', { code: info.adv_type }); // Shorten public key for display const pubKey = info.public_key || ''; @@ -2038,22 +2038,22 @@ async function loadDeviceInfo() { // Location const hasLocation = info.adv_lat && info.adv_lon && (info.adv_lat !== 0 || info.adv_lon !== 0); - const coords = hasLocation ? `${info.adv_lat.toFixed(6)}, ${info.adv_lon.toFixed(6)}` : 'Not available'; + const coords = hasLocation ? `${info.adv_lat.toFixed(6)}, ${info.adv_lon.toFixed(6)}` : tHtml('device.info.location_none'); // Build table rows const rows = [ - { label: 'Name', value: escapeHtml(info.name || 'Unknown'), copyValue: info.name }, - { label: 'Type', value: typeName }, - { label: 'Public Key', value: `${escapeHtml(shortKey)}`, copyValue: pubKey }, - { label: 'Location', value: coords, showMap: hasLocation, lat: info.adv_lat, lon: info.adv_lon, name: info.name }, - { label: 'TX Power', value: `${info.tx_power || 0} / ${info.max_tx_power || 0} dBm` }, - { label: 'Frequency', value: `${info.radio_freq || 0} MHz` }, - { label: 'Bandwidth', value: `${info.radio_bw || 0} kHz` }, - { label: 'Spreading Factor', value: info.radio_sf || 0 }, - { label: 'Coding Rate', value: `4/${info.radio_cr || 0}` }, - { label: 'Multi Acks', value: info.multi_acks ? 'Enabled' : 'Disabled' }, - { label: 'Location Sharing', value: info.adv_loc_policy ? 'Enabled' : 'Disabled' }, - { label: 'Manual Add Contacts', value: info.manual_add_contacts ? 'Yes' : 'No' } + { label: tHtml('device.info.name'), value: escapeHtml(info.name || t('common.unknown')), copyValue: info.name }, + { label: tHtml('device.info.type'), value: typeName }, + { label: tHtml('device.info.pubkey'), value: `${escapeHtml(shortKey)}`, copyValue: pubKey }, + { label: tHtml('device.info.location'), value: coords, showMap: hasLocation, lat: info.adv_lat, lon: info.adv_lon, name: info.name }, + { label: tHtml('device.info.tx_power'), value: `${info.tx_power || 0} / ${info.max_tx_power || 0} dBm` }, + { label: tHtml('device.info.freq'), value: `${info.radio_freq || 0} MHz` }, + { label: tHtml('device.info.bw'), value: `${info.radio_bw || 0} kHz` }, + { label: tHtml('device.info.sf'), value: info.radio_sf || 0 }, + { label: tHtml('device.info.cr'), value: `4/${info.radio_cr || 0}` }, + { label: tHtml('device.info.multi_acks'), value: tHtml(info.multi_acks ? 'common.enabled' : 'common.disabled') }, + { label: tHtml('device.info.loc_sharing'), value: tHtml(info.adv_loc_policy ? 'common.enabled' : 'common.disabled') }, + { label: tHtml('device.info.manual_add'), value: tHtml(info.manual_add_contacts ? 'common.yes' : 'common.no') } ]; let html = ''; @@ -2067,12 +2067,12 @@ async function loadDeviceInfo() { // Copy button if (row.copyValue) { - html += ` `; + html += ` `; } // Map button if (row.showMap) { - html += ` `; + html += ` `; } html += ''; @@ -2084,7 +2084,7 @@ async function loadDeviceInfo() { } catch (error) { console.error('Error loading device info:', error); - container.innerHTML = '
Failed to load device info
'; + container.innerHTML = `
${tHtml('device.info.load_failed')}
`; } } @@ -2095,7 +2095,7 @@ async function loadDeviceStats() { const container = document.getElementById('deviceStatsContent'); if (!container) return; - container.innerHTML = '
Loading...
'; + container.innerHTML = `
${tHtml('common.loading')}
`; try { const response = await fetch('/api/device/stats'); @@ -2112,9 +2112,9 @@ async function loadDeviceStats() { // Battery (from dedicated get_bat or from core stats) if (bat && typeof bat === 'object' && bat.voltage) { - html += ``; + html += ``; } else if (stats.core && stats.core.battery_mv) { - html += ``; + html += ``; } // Core stats @@ -2124,58 +2124,58 @@ async function loadDeviceStats() { const d = Math.floor(c.uptime / 86400); const h = Math.floor((c.uptime % 86400) / 3600); const m = Math.floor((c.uptime % 3600) / 60); - html += ``; + html += ``; } if (c.queue_length !== undefined) - html += ``; + html += ``; if (c.errors !== undefined) - html += ``; + html += ``; } // Radio stats if (stats.radio) { const r = stats.radio; if (r.tx_air_time !== undefined) - html += ``; + html += ``; if (r.rx_air_time !== undefined) - html += ``; + html += ``; } // Packet stats if (stats.packets) { const p = stats.packets; if (p.sent !== undefined) - html += ``; + html += ``; if (p.received !== undefined) - html += ``; + html += ``; } // DB stats (included in same response) if (data.db_stats) { const db = data.db_stats; if (db.contacts !== undefined) - html += ``; + html += ``; if (db.channel_messages !== undefined) - html += ``; + html += ``; if (db.direct_messages !== undefined) - html += ``; + html += ``; if (db.db_size_bytes !== undefined) { const sizeMB = (db.db_size_bytes / (1024 * 1024)).toFixed(1); - html += ``; + html += ``; } } html += '
Battery${bat.voltage}V
${tHtml('device.stats.battery')}${bat.voltage}V
Battery${(stats.core.battery_mv / 1000).toFixed(2)}V
${tHtml('device.stats.battery')}${(stats.core.battery_mv / 1000).toFixed(2)}V
Uptime${d}d ${h}h ${m}m
${tHtml('device.stats.uptime')}${d}d ${h}h ${m}m
Queue${c.queue_length}
${tHtml('device.stats.queue')}${c.queue_length}
Errors${c.errors}
${tHtml('device.stats.errors')}${c.errors}
TX Air Time${r.tx_air_time.toFixed(1)} min
${tHtml('device.stats.tx_air')}${r.tx_air_time.toFixed(1)} min
RX Air Time${r.rx_air_time.toFixed(1)} min
${tHtml('device.stats.rx_air')}${r.rx_air_time.toFixed(1)} min
Packets TX${p.sent.toLocaleString()}
${tHtml('device.stats.packets_tx')}${p.sent.toLocaleString()}
Packets RX${p.received.toLocaleString()}
${tHtml('device.stats.packets_rx')}${p.received.toLocaleString()}
Contacts (DB)${db.contacts}
${tHtml('device.stats.contacts_db')}${db.contacts}
Channel Msgs${db.channel_messages.toLocaleString()}
${tHtml('device.stats.channel_msgs')}${db.channel_messages.toLocaleString()}
Direct Msgs${db.direct_messages.toLocaleString()}
${tHtml('device.stats.direct_msgs')}${db.direct_messages.toLocaleString()}
DB Size${sizeMB} MB
${tHtml('device.stats.db_size')}${sizeMB} MB
'; if (html === '
') { - container.innerHTML = '
No statistics available
'; + container.innerHTML = `
${tHtml('device.stats.none')}
`; } else { container.innerHTML = html; } } catch (error) { console.error('Error loading device stats:', error); - container.innerHTML = '
Failed to load stats
'; + container.innerHTML = `
${tHtml('device.stats.load_failed')}
`; } } @@ -2192,7 +2192,7 @@ async function loadDeviceShare() { const container = document.getElementById('deviceShareContent'); if (!container) return; - container.innerHTML = '
Loading...
'; + container.innerHTML = `
${tHtml('common.loading')}
`; try { const response = await fetch('/api/device/info'); @@ -2205,7 +2205,7 @@ async function loadDeviceShare() { const info = data.info; if (!info || !info.public_key || !info.name) { - container.innerHTML = '
Device info not available
'; + container.innerHTML = `
${tHtml('device.share.unavailable')}
`; return; } @@ -2215,17 +2215,17 @@ async function loadDeviceShare() { const typeNames = { 1: 'Companion', 2: 'Repeater', 3: 'Room Server', 4: 'Sensor' }; let html = '
'; - html += '

Share this QR code or URI so others can add your device as a contact.

'; + html += `

${tHtml('device.share.hint')}

`; html += '
'; html += '
' + escapeHtml(info.name) + '
'; - html += '
' + escapeHtml(typeNames[contactType] || 'Unknown') + '
'; + html += '
' + escapeHtml(typeNames[contactType] || t('common.unknown')) + '
'; html += '
'; html += '
'; - html += ''; + html += ``; html += '
'; html += ''; - html += ''; + html += ''; html += '
'; html += '
'; @@ -2246,7 +2246,7 @@ async function loadDeviceShare() { } catch (error) { console.error('Error loading device share:', error); - container.innerHTML = '
Failed to load device info
'; + container.innerHTML = `
${tHtml('device.info.load_failed')}
`; } } @@ -2322,7 +2322,7 @@ async function loadDeviceConfig() { async function saveDevicePublicInfo() { const name = document.getElementById('settDeviceName').value.trim(); if (!name) { - showNotification('Device name cannot be empty', 'danger'); + showNotification(t('settings.device.toast.name_empty'), 'danger'); document.getElementById('settDeviceName').focus(); return; } @@ -2350,14 +2350,14 @@ async function saveDevicePublicInfo() { }); const data = await resp.json(); if (data.success) { - showNotification('Public info saved', 'success'); + showNotification(t('settings.device.toast.info_saved'), 'success'); _selfInfo = null; if (phmSel) phmSel.dataset.initial = phmSel.value; } else { - showNotification(data.error || 'Failed to save', 'danger'); + showNotification(data.error || t('common.save_failed'), 'danger'); } } catch (e) { - showNotification('Failed to save public info', 'danger'); + showNotification(t('settings.device.toast.info_save_failed'), 'danger'); } } @@ -2369,23 +2369,23 @@ async function saveDeviceRadioSettings() { const txPower = parseInt(document.getElementById('settRadioTxPower').value, 10); if (isNaN(freq) || freq < 100 || freq > 1000) { - showNotification('Invalid frequency', 'danger'); + showNotification(t('settings.device.toast.freq_invalid'), 'danger'); return; } if (isNaN(sf) || sf < 5 || sf > 12) { - showNotification('Spreading factor must be 5-12', 'danger'); + showNotification(t('settings.device.toast.sf_invalid'), 'danger'); return; } if (isNaN(cr) || cr < 5 || cr > 8) { - showNotification('Coding rate must be 5-8', 'danger'); + showNotification(t('settings.device.toast.cr_invalid'), 'danger'); return; } if (isNaN(txPower) || txPower < 0 || txPower > 30) { - showNotification('TX power must be 0-30 dBm', 'danger'); + showNotification(t('settings.device.toast.tx_invalid'), 'danger'); return; } - if (!confirm('Changing radio settings will disconnect from the mesh network. Continue?')) return; + if (!confirm(t('settings.device.confirm.radio'))) return; try { const resp = await fetch('/api/device/config', { @@ -2401,12 +2401,12 @@ async function saveDeviceRadioSettings() { }); const data = await resp.json(); if (data.success) { - showNotification('Radio settings saved', 'success'); + showNotification(t('settings.device.toast.radio_saved'), 'success'); } else { - showNotification(data.error || 'Failed to save', 'danger'); + showNotification(data.error || t('common.save_failed'), 'danger'); } } catch (e) { - showNotification('Failed to save radio settings', 'danger'); + showNotification(t('settings.device.toast.radio_save_failed'), 'danger'); } } @@ -2580,13 +2580,13 @@ async function saveChatSettings() { const data = await resp.json(); chatSettingsCache = { ...CHAT_SETTINGS_DEFAULTS, ...data }; window.chatSettingsCache = chatSettingsCache; - showNotification('Settings saved', 'success'); + showNotification(t('settings.toast.saved'), 'success'); } else { const err = await resp.json(); - showNotification(err.error || 'Failed to save', 'danger'); + showNotification(err.error || t('common.save_failed'), 'danger'); } } catch (e) { - showNotification('Failed to save settings', 'danger'); + showNotification(t('settings.toast.save_failed'), 'danger'); } } @@ -2650,7 +2650,7 @@ async function saveUiSettings() { const timeoutEl = document.getElementById('settToastTimeout'); const timeout = parseFloat(timeoutEl.value); if (isNaN(timeout) || timeout < 1 || timeout > 60) { - showNotification('Invalid auto-close duration', 'danger'); + showNotification(t('settings.iface.toast.autoclose_invalid'), 'danger'); timeoutEl.focus(); return; } @@ -2670,13 +2670,13 @@ async function saveUiSettings() { uiSettingsCache = { ...UI_SETTINGS_DEFAULTS, ...data }; window.uiSettingsCache = uiSettingsCache; applyToastPosition(uiSettingsCache.toast_position); - showNotification('Settings saved', 'success'); + showNotification(t('settings.toast.saved'), 'success'); } else { const err = await resp.json(); - showNotification(err.error || 'Failed to save', 'danger'); + showNotification(err.error || t('common.save_failed'), 'danger'); } } catch (e) { - showNotification('Failed to save settings', 'danger'); + showNotification(t('settings.toast.save_failed'), 'danger'); } } @@ -2723,13 +2723,13 @@ async function reloadTranslations() { const data = await resp.json(); if (resp.ok && data.success) { const names = Object.values(data.languages || {}).join(', '); - showNotification(`Translations reloaded: ${names}`, 'success'); + showNotification(t('settings.appear.toast.reloaded', { names }), 'success'); setTimeout(() => location.reload(), 800); } else { - showNotification(data.error || 'Failed to reload translations', 'danger'); + showNotification(data.error || t('settings.appear.toast.reload_failed'), 'danger'); } } catch (e) { - showNotification('Failed to reload translations', 'danger'); + showNotification(t('settings.appear.toast.reload_failed'), 'danger'); } } @@ -2791,13 +2791,13 @@ async function saveDmRetrySettings() { body: JSON.stringify(payload) }); if (resp.ok) { - showNotification('Settings saved', 'success'); + showNotification(t('settings.toast.saved'), 'success'); } else { const err = await resp.json(); - showNotification(err.error || 'Failed to save', 'danger'); + showNotification(err.error || t('common.save_failed'), 'danger'); } } catch (e) { - showNotification('Failed to save settings', 'danger'); + showNotification(t('settings.toast.save_failed'), 'danger'); } } @@ -2879,7 +2879,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('observerIataInput')?.addEventListener('change', (e) => { const iata = (e.target.value || '').trim(); if (iata && !/^[A-Za-z]{3}$/.test(iata)) { - showNotification('Location code must be empty or exactly 3 letters', 'warning'); + showNotification(t('observer.iata_invalid'), 'warning'); return; } saveObserverSettings({ iata }); @@ -2986,7 +2986,7 @@ document.addEventListener('DOMContentLoaded', () => { async function cleanupContacts() { const hours = parseInt(document.getElementById('inactiveHours').value); - if (!confirm(`Remove all contacts inactive for more than ${hours} hours?`)) { + if (!confirm(t('contacts.cleanup.confirm', { hours }))) { return; } @@ -3007,11 +3007,11 @@ async function cleanupContacts() { if (data.success) { showNotification(data.message, 'success'); } else { - showNotification('Cleanup failed: ' + data.error, 'danger'); + showNotification(t('contacts.cleanup.failed', { error: data.error }), 'danger'); } } catch (error) { console.error('Error cleaning contacts:', error); - showNotification('Cleanup failed', 'danger'); + showNotification(t('contacts.cleanup.error'), 'danger'); } finally { btn.disabled = false; } @@ -3041,9 +3041,9 @@ async function executeSpecialCommand(command) { const data = await response.json(); if (data.success) { - showNotification(data.message || `${command} sent successfully`, 'success'); + showNotification(data.message || t('device.cmd.sent', { command }), 'success'); } else { - showNotification(`Command failed: ${data.error}`, 'danger'); + showNotification(t('device.cmd.failed', { error: data.error }), 'danger'); } // Close offcanvas menu after command execution @@ -3072,7 +3072,7 @@ async function executeSpecialCommand(command) { */ async function requestNotificationPermission() { if (!('Notification' in window)) { - showNotification('Notifications are not supported in this browser', 'warning'); + showNotification(t('settings.notif.toast.unsupported'), 'warning'); return false; } @@ -3082,17 +3082,17 @@ async function requestNotificationPermission() { if (permission === 'granted') { localStorage.setItem('mc_notifications_enabled', 'true'); updateNotificationToggleUI(); - showNotification('Notifications enabled', 'success'); + showNotification(t('settings.notif.toast.enabled'), 'success'); return true; } else if (permission === 'denied') { localStorage.setItem('mc_notifications_enabled', 'false'); updateNotificationToggleUI(); - showNotification('Notifications blocked. Change browser settings to enable them.', 'warning'); + showNotification(t('settings.notif.toast.denied'), 'warning'); return false; } } catch (error) { console.error('Error requesting notification permission:', error); - showNotification('Error enabling notifications', 'danger'); + showNotification(t('settings.notif.toast.error'), 'danger'); return false; } } @@ -3161,16 +3161,16 @@ async function handleNotificationToggle() { // Turn OFF localStorage.setItem('mc_notifications_enabled', 'false'); updateNotificationToggleUI(); - showNotification('Notifications disabled', 'info'); + showNotification(t('settings.notif.toast.disabled'), 'info'); } else { // Turn ON localStorage.setItem('mc_notifications_enabled', 'true'); updateNotificationToggleUI(); - showNotification('Notifications enabled', 'success'); + showNotification(t('settings.notif.toast.enabled'), 'success'); } } else if (permission === 'denied') { // Blocked - show help message - showNotification('Notifications are blocked. Change browser settings: Settings → Site Settings → Notifications', 'warning'); + showNotification(t('settings.notif.toast.blocked_help'), 'warning'); } else { // Not yet requested - ask for permission await requestNotificationPermission(); @@ -3240,7 +3240,7 @@ async function saveContactsSetting(key, value, inputEl) { const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { if (inputEl) inputEl.checked = !value; - showNotification(data.error || 'Failed to save setting', 'danger'); + showNotification(data.error || t('settings.toast.setting_save_failed'), 'danger'); return; } window.contactsSettings[key] = !!value; @@ -3257,7 +3257,7 @@ async function saveContactsSetting(key, value, inputEl) { } catch (e) { console.error('Error saving contacts setting:', e); if (inputEl) inputEl.checked = !value; - showNotification('Network error saving setting', 'danger'); + showNotification(t('settings.toast.setting_network_error'), 'danger'); } } @@ -3292,7 +3292,7 @@ async function loadRegions() { renderRegionsList(); } catch (e) { console.error('Error loading regions:', e); - listEl.innerHTML = '
Failed to load regions
'; + listEl.innerHTML = `
${tHtml('settings.regions.load_failed')}
`; } } @@ -3301,7 +3301,7 @@ function renderRegionsList() { if (!listEl) return; const regions = window.regionRegistry || []; if (regions.length === 0) { - listEl.innerHTML = '
No regions defined. Add one below.
'; + listEl.innerHTML = `
${tHtml('settings.regions.empty')}
`; return; } const noDefault = !regions.some(r => r.is_default); @@ -3313,7 +3313,7 @@ function renderRegionsList() { onchange="clearDefaultRegion()">
- None — use firmware default + ${tHtml('settings.regions.none_row')}
`; @@ -3333,7 +3333,7 @@ function renderRegionsList() { @@ -3344,7 +3344,7 @@ function renderRegionsList() { async function addRegion(name, inputEl) { if (!isValidRegionName(name)) { - showNotification('Invalid region name. Allowed: letters, digits, - $ # (max 30 bytes, no spaces).', 'warning'); + showNotification(t('settings.regions.toast.name_invalid'), 'warning'); return; } try { @@ -3355,14 +3355,14 @@ async function addRegion(name, inputEl) { }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to add region', 'danger'); + showNotification(data.error || t('settings.regions.toast.add_failed'), 'danger'); return; } if (inputEl) inputEl.value = ''; await loadRegions(); } catch (e) { console.error('Error adding region:', e); - showNotification('Network error adding region', 'danger'); + showNotification(t('settings.regions.toast.add_error'), 'danger'); } } @@ -3372,13 +3372,13 @@ async function deleteRegion(id, name) { const resp = await fetch(`/api/regions/${id}`, { method: 'DELETE' }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to delete region', 'danger'); + showNotification(data.error || t('settings.regions.toast.delete_failed'), 'danger'); return; } await loadRegions(); } catch (e) { console.error('Error deleting region:', e); - showNotification('Network error deleting region', 'danger'); + showNotification(t('settings.regions.toast.delete_error'), 'danger'); } } @@ -3387,7 +3387,7 @@ async function setDefaultRegion(id) { const resp = await fetch(`/api/regions/${id}/default`, { method: 'POST' }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to set default region', 'danger'); + showNotification(data.error || t('settings.regions.toast.default_failed'), 'danger'); await loadRegions(); // snap UI back to server truth return; } @@ -3398,7 +3398,7 @@ async function setDefaultRegion(id) { (window.regionRegistry || []).forEach(r => { r.is_default = (r.id === id) ? 1 : 0; }); } catch (e) { console.error('Error setting default region:', e); - showNotification('Network error setting default', 'danger'); + showNotification(t('settings.regions.toast.default_error'), 'danger'); await loadRegions(); } } @@ -3408,7 +3408,7 @@ async function clearDefaultRegion() { const resp = await fetch('/api/regions/default', { method: 'DELETE' }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to clear default region', 'danger'); + showNotification(data.error || t('settings.regions.toast.clear_failed'), 'danger'); await loadRegions(); // snap UI back to server truth return; } @@ -3418,7 +3418,7 @@ async function clearDefaultRegion() { (window.regionRegistry || []).forEach(r => { r.is_default = 0; }); } catch (e) { console.error('Error clearing default region:', e); - showNotification('Network error clearing default', 'danger'); + showNotification(t('settings.regions.toast.clear_error'), 'danger'); await loadRegions(); } } @@ -3460,7 +3460,7 @@ async function loadAnalyzers() { console.error('Error loading analyzers:', e); const listEl = document.getElementById('analyzersList'); if (listEl) { - listEl.innerHTML = '
Failed to load analyzers
'; + listEl.innerHTML = `
${tHtml('analyzer.load_failed')}
`; } } } @@ -3472,7 +3472,7 @@ function renderAnalyzersList() { if (analyzers.length === 0) { listEl.innerHTML = - '
No analyzers configured. Click "Add analyzer" to add one.
'; + `
${tHtml('analyzer.empty')}
`; return; } @@ -3482,31 +3482,31 @@ function renderAnalyzersList() { const isDefault = !!a.is_default; const starIcon = isDefault ? 'bi-star-fill text-warning' : 'bi-star'; const disabledBadge = disabled - ? 'Disabled' : ''; + ? `${tHtml('common.disabled')}` : ''; const nameClass = disabled ? 'text-muted text-decoration-line-through' : ''; const safeName = escapeHtml(a.name); return `
${safeName}${disabledBadge}
${escapeHtml(a.url_template)}
-
+
@@ -3562,11 +3562,11 @@ async function saveAnalyzerFromForm() { const is_disabled = !enabledEl.checked; if (!name) { - showAnalyzerFormError('Name is required'); + showAnalyzerFormError(t('analyzer.name_required')); return; } if (!url_template.startsWith('http://') && !url_template.startsWith('https://')) { - showAnalyzerFormError('URL must start with http:// or https://'); + showAnalyzerFormError(t('analyzer.url_invalid')); return; } if (!url_template.includes(ANALYZER_PLACEHOLDER)) { @@ -3585,7 +3585,7 @@ async function saveAnalyzerFromForm() { }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showAnalyzerFormError(data.error || 'Failed to save analyzer'); + showAnalyzerFormError(data.error || t('analyzer.toast.save_failed')); return; } // If creating a new analyzer with disabled=true, push the flag in a follow-up PUT. @@ -3601,7 +3601,7 @@ async function saveAnalyzerFromForm() { await loadAnalyzers(); } catch (e) { console.error('Error saving analyzer:', e); - showAnalyzerFormError('Network error saving analyzer'); + showAnalyzerFormError(t('analyzer.toast.save_error')); } } @@ -3618,13 +3618,13 @@ async function deleteAnalyzer(id, name) { const resp = await fetch(`/api/analyzers/${id}`, { method: 'DELETE' }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to delete analyzer', 'danger'); + showNotification(data.error || t('analyzer.toast.delete_failed'), 'danger'); return; } await loadAnalyzers(); } catch (e) { console.error('Error deleting analyzer:', e); - showNotification('Network error deleting analyzer', 'danger'); + showNotification(t('analyzer.toast.delete_error'), 'danger'); } } @@ -3635,14 +3635,14 @@ async function toggleAnalyzerDefault(id, currentlyDefault) { const resp = await fetch(url, { method }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to update default', 'danger'); + showNotification(data.error || t('analyzer.toast.default_failed'), 'danger'); await loadAnalyzers(); return; } await loadAnalyzers(); } catch (e) { console.error('Error toggling analyzer default:', e); - showNotification('Network error updating default', 'danger'); + showNotification(t('analyzer.toast.default_error'), 'danger'); await loadAnalyzers(); } } @@ -3656,14 +3656,14 @@ async function toggleAnalyzerDisabled(id, disabled) { }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to update analyzer', 'danger'); + showNotification(data.error || t('analyzer.toast.update_failed'), 'danger'); await loadAnalyzers(); return; } await loadAnalyzers(); } catch (e) { console.error('Error toggling analyzer disabled:', e); - showNotification('Network error updating analyzer', 'danger'); + showNotification(t('analyzer.toast.update_error'), 'danger'); await loadAnalyzers(); } } @@ -3686,7 +3686,7 @@ async function openMessageAnalyzer(packetHash) { // Nothing enabled — user has deliberately turned everything off or deleted it. if (enabled.length === 0) { - showNotification('No analyzer configured. Add one in Settings → Analyzer.', 'warning'); + showNotification(t('analyzer.toast.none_configured'), 'warning'); return; } @@ -3760,7 +3760,7 @@ async function loadObserverTab() { console.error('Error loading observer status:', e); const listEl = document.getElementById('observerBrokersList'); if (listEl) { - listEl.innerHTML = '
Failed to load observer status
'; + listEl.innerHTML = `
${tHtml('observer.load_failed')}
`; } } } @@ -3780,20 +3780,22 @@ function renderObserverStatusLine(status) { const el = document.getElementById('observerStatusLine'); if (!el || !status) return; if (!status.enabled) { - el.innerHTML = 'Observer is off.'; + el.innerHTML = tHtml('observer.off'); return; } const state = status.running - ? 'running' - : `waiting ${escapeHtml(status.reason || '')}`; - el.innerHTML = `${state} — packets captured: ${status.packets_seen ?? 0},` - + ` published: ${status.packets_published ?? 0}`; + ? `${tHtml('observer.state.running')}` + : `${tHtml('observer.state.waiting')} ${escapeHtml(status.reason || '')}`; + el.innerHTML = `${state} — ` + tHtml('observer.counters', { + seen: status.packets_seen ?? 0, + published: status.packets_published ?? 0, + }); } function observerBrokerBadgeParts(b) { - if (b.connected) return { cls: 'bg-success', txt: 'connected', title: '' }; - if (b.last_error) return { cls: 'bg-danger', txt: 'error', title: b.last_error }; - return { cls: 'bg-secondary', txt: 'offline', title: '' }; + if (b.connected) return { cls: 'bg-success', txt: tHtml('observer.state.connected'), title: '' }; + if (b.last_error) return { cls: 'bg-danger', txt: tHtml('observer.state.error'), title: b.last_error }; + return { cls: 'bg-secondary', txt: tHtml('observer.state.offline'), title: '' }; } function renderObserverBrokers() { @@ -3803,7 +3805,7 @@ function renderObserverBrokers() { if (brokers.length === 0) { listEl.innerHTML = - '
No brokers configured. Click "Add broker" to add one.
'; + `
${tHtml('observer.empty')}
`; return; } @@ -3813,7 +3815,7 @@ function renderObserverBrokers() { const safeName = escapeHtml(b.name); const tlsBadge = b.use_tls ? 'TLS' : ''; const badge = b.is_disabled - ? 'Disabled' + ? `${tHtml('common.disabled')}` : (() => { const p = observerBrokerBadgeParts(b); return `${p.txt}`; @@ -3825,16 +3827,16 @@ function renderObserverBrokers() {
${safeName}${tlsBadge}${badge}
${userInfo}${escapeHtml(b.host)}:${b.port}
-
+
@@ -3927,9 +3929,9 @@ async function saveObserverBrokerFromForm() { const tls_verify = document.getElementById('observerBrokerEditTlsVerify').checked; const is_disabled = !document.getElementById('observerBrokerEditEnabled').checked; - if (!name) { showObserverBrokerFormError('Name is required'); return; } - if (!host) { showObserverBrokerFormError('Host is required'); return; } - if (!(port >= 1 && port <= 65535)) { showObserverBrokerFormError('Port must be 1-65535'); return; } + if (!name) { showObserverBrokerFormError(t('observer.name_required')); return; } + if (!host) { showObserverBrokerFormError(t('observer.host_required')); return; } + if (!(port >= 1 && port <= 65535)) { showObserverBrokerFormError(t('observer.port_invalid')); return; } const body = { name, host, port, username, use_tls, tls_verify, is_disabled }; // Edit mode: an empty password field means "keep the stored password" @@ -3944,7 +3946,7 @@ async function saveObserverBrokerFromForm() { }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showObserverBrokerFormError(data.error || 'Failed to save broker'); + showObserverBrokerFormError(data.error || t('observer.toast.save_failed')); return; } bootstrap.Modal.getInstance(document.getElementById('observerBrokerEditModal'))?.hide(); @@ -3953,7 +3955,7 @@ async function saveObserverBrokerFromForm() { setTimeout(loadObserverTab, 1500); } catch (e) { console.error('Error saving observer broker:', e); - showObserverBrokerFormError('Network error saving broker'); + showObserverBrokerFormError(t('observer.toast.save_error')); } } @@ -3963,13 +3965,13 @@ async function deleteObserverBroker(id, name) { const resp = await fetch(`/api/observer/brokers/${id}`, { method: 'DELETE' }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to delete broker', 'danger'); + showNotification(data.error || t('observer.toast.delete_failed'), 'danger'); return; } await loadObserverTab(); } catch (e) { console.error('Error deleting observer broker:', e); - showNotification('Network error deleting broker', 'danger'); + showNotification(t('observer.toast.delete_error'), 'danger'); } } @@ -3982,11 +3984,11 @@ async function toggleObserverBrokerDisabled(id, disabled) { }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to update broker', 'danger'); + showNotification(data.error || t('observer.toast.update_failed'), 'danger'); } } catch (e) { console.error('Error toggling observer broker:', e); - showNotification('Network error updating broker', 'danger'); + showNotification(t('observer.toast.update_error'), 'danger'); } await loadObserverTab(); setTimeout(loadObserverTab, 1500); @@ -4001,7 +4003,7 @@ async function saveObserverSettings(patch) { }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to save observer settings', 'danger'); + showNotification(data.error || t('observer.toast.settings_failed'), 'danger'); await loadObserverTab(); return; } @@ -4009,7 +4011,7 @@ async function saveObserverSettings(patch) { setTimeout(loadObserverTab, 1500); } catch (e) { console.error('Error saving observer settings:', e); - showNotification('Network error saving observer settings', 'danger'); + showNotification(t('observer.toast.settings_error'), 'danger'); } } @@ -4075,9 +4077,9 @@ function renderRegionPickerList() { listEl.innerHTML = `
-

No regions defined yet.

+

${tHtml('settings.regions.picker_empty')}

`; @@ -4099,7 +4101,7 @@ function renderRegionPickerList() { `]; for (const r of regions) { @@ -4109,7 +4111,7 @@ function renderRegionPickerList() { ${_regionPickerPending === r.id ? 'checked' : ''}> ${escapeHtml(r.name)} - ${r.is_default ? 'default' : ''} + ${r.is_default ? `${tHtml('settings.regions.is_default')}` : ''} `); @@ -4170,7 +4172,7 @@ async function saveChannelScope() { }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.success) { - showNotification(data.error || 'Failed to save region scope', 'danger'); + showNotification(data.error || t('settings.regions.toast.scope_failed'), 'danger'); return; } // Update the local cache + re-render the channels list. @@ -4190,7 +4192,7 @@ async function saveChannelScope() { } } catch (e) { console.error('Error saving channel scope:', e); - showNotification('Network error saving region scope', 'danger'); + showNotification(t('settings.regions.toast.scope_error'), 'danger'); } } @@ -7029,10 +7031,10 @@ async function loadDatabaseSize() { if (data.success) { statusEl.textContent = t('backup.size', { size: _formatBytes(data.size) }); } else { - statusEl.textContent = 'Size: unknown'; + statusEl.textContent = t('backup.size_unknown'); } } catch (error) { - statusEl.textContent = 'Size: unknown'; + statusEl.textContent = t('backup.size_unknown'); } } @@ -7042,12 +7044,12 @@ async function optimizeDatabase() { if (!btn) return; btn.disabled = true; - btn.innerHTML = '
Optimizing…'; - if (statusEl) statusEl.textContent = 'Running VACUUM…'; + btn.innerHTML = `
${tHtml('backup.optimizing')}`; + if (statusEl) statusEl.textContent = t('backup.vacuum_running'); const restoreButton = () => { btn.disabled = false; - btn.innerHTML = ' Optimize now'; + btn.innerHTML = ` ${tHtml('backup.optimize_now')}`; }; try { @@ -7055,7 +7057,7 @@ async function optimizeDatabase() { const kickoffData = await kickoff.json().catch(() => ({})); if (!kickoff.ok && kickoff.status !== 409) { - showNotification('Optimize failed: ' + (kickoffData.error || `HTTP ${kickoff.status}`), 'danger'); + showNotification(t('backup.optimize_failed', { error: kickoffData.error || `HTTP ${kickoff.status}` }), 'danger'); loadDatabaseSize(); restoreButton(); return; @@ -7077,17 +7079,19 @@ async function optimizeDatabase() { } if (status.running) { - if (statusEl) statusEl.textContent = `Running VACUUM… (${status.elapsed_seconds || 0}s)`; + if (statusEl) statusEl.textContent = t('backup.vacuum_running_for', { seconds: status.elapsed_seconds || 0 }); continue; } // Done — either success or error. if (status.success === true && status.size_after !== undefined) { - const freed = status.freed > 0 ? `freed ${_formatBytes(status.freed)}` : 'no space to reclaim'; - showNotification(`Optimized: ${freed} in ${status.elapsed_seconds}s`, 'success'); + const freed = status.freed > 0 + ? t('backup.freed', { size: _formatBytes(status.freed) }) + : t('backup.freed_none'); + showNotification(t('backup.optimized', { freed, seconds: status.elapsed_seconds }), 'success'); if (statusEl) statusEl.textContent = t('backup.size', { size: _formatBytes(status.size_after) }); } else if (status.error) { - showNotification('Optimize failed: ' + status.error, 'danger'); + showNotification(t('backup.optimize_failed', { error: status.error }), 'danger'); loadDatabaseSize(); } else { // No result, no error, not running — odd, just refresh size @@ -7097,12 +7101,12 @@ async function optimizeDatabase() { return; } - showNotification('Optimize is still running after 10 minutes — check container logs', 'warning'); + showNotification(t('backup.optimize_stuck'), 'warning'); loadDatabaseSize(); restoreButton(); } catch (error) { console.error('Error running VACUUM:', error); - showNotification('Optimize failed', 'danger'); + showNotification(t('backup.optimize_error'), 'danger'); loadDatabaseSize(); restoreButton(); } @@ -7113,7 +7117,7 @@ async function loadBackupList() { const statusEl = document.getElementById('backupAutoStatus'); if (!container) return; - container.innerHTML = '
Loading...
'; + container.innerHTML = `
${tHtml('common.loading')}
`; try { const response = await fetch('/api/backup/list'); @@ -7127,12 +7131,15 @@ async function loadBackupList() { // Show auto-backup status if (statusEl) { statusEl.textContent = data.auto_backup_enabled - ? `Auto: daily at ${String(data.backup_hour).padStart(2, '0')}:00, keep ${data.retention_days}d` - : 'Auto-backup disabled'; + ? t('backup.auto_on', { + time: `${String(data.backup_hour).padStart(2, '0')}:00`, + days: data.retention_days, + }) + : t('backup.auto_off'); } if (data.backups.length === 0) { - container.innerHTML = '

No backups yet

'; + container.innerHTML = `

${tHtml('backup.empty')}

`; return; } @@ -7148,7 +7155,7 @@ async function loadBackupList() { ${escapeHtml(b.filename)} ${b.size_display} - + `; @@ -7160,7 +7167,7 @@ async function loadBackupList() { } catch (error) { console.error('Error loading backups:', error); - container.innerHTML = '
Failed to load backups
'; + container.innerHTML = `
${tHtml('backup.load_failed')}
`; } } @@ -7169,24 +7176,24 @@ async function createBackup() { if (!btn) return; btn.disabled = true; - btn.innerHTML = '
Creating...'; + btn.innerHTML = `
${tHtml('backup.creating')}`; try { const response = await fetch('/api/backup/create', { method: 'POST' }); const data = await response.json(); if (data.success) { - showNotification(`Backup created: ${data.filename}`, 'success'); + showNotification(t('backup.created', { filename: data.filename }), 'success'); loadBackupList(); } else { - showNotification('Backup failed: ' + data.error, 'danger'); + showNotification(t('backup.failed', { error: data.error }), 'danger'); } } catch (error) { console.error('Error creating backup:', error); - showNotification('Backup failed', 'danger'); + showNotification(t('backup.error'), 'danger'); } finally { btn.disabled = false; - btn.innerHTML = ' Create Backup'; + btn.innerHTML = ` ${tHtml('backup.create')}`; } } diff --git a/app/translations/en.json b/app/translations/en.json index 70d546a..6daf95f 100644 --- a/app/translations/en.json +++ b/app/translations/en.json @@ -1,17 +1,51 @@ { "analyzer.add": "Add analyzer", "analyzer.choose": "Choose analyzer", + "analyzer.clear_default_title": "Clear default", "analyzer.edit": "Edit analyzer", + "analyzer.empty": "No analyzers configured. Click \"Add analyzer\" to add one.", + "analyzer.load_failed": "Failed to load analyzers", "analyzer.name_ph": "e.g. MarWoj Analyzer", + "analyzer.name_required": "Name is required", + "analyzer.set_default_title": "Mark as default", + "analyzer.toast.default_error": "Network error updating default", + "analyzer.toast.default_failed": "Failed to update default", + "analyzer.toast.delete_error": "Network error deleting analyzer", + "analyzer.toast.delete_failed": "Failed to delete analyzer", + "analyzer.toast.none_configured": "No analyzer configured. Add one in Settings → Analyzer.", + "analyzer.toast.save_error": "Network error saving analyzer", + "analyzer.toast.save_failed": "Failed to save analyzer", + "analyzer.toast.update_error": "Network error updating analyzer", + "analyzer.toast.update_failed": "Failed to update analyzer", "analyzer.url": "URL template", "analyzer.url_hint": "Must include {packetHash}.", + "analyzer.url_invalid": "URL must start with http:// or https://", + "backup.auto_off": "Auto-backup disabled", + "backup.auto_on": "Auto: daily at {time}, keep {days}d", "backup.create": "Create Backup", + "backup.created": "Backup created: {filename}", + "backup.creating": "Creating...", "backup.current_size": "Current size: …", + "backup.download_title": "Download", + "backup.empty": "No backups yet", + "backup.error": "Backup failed", + "backup.failed": "Backup failed: {error}", + "backup.freed": "freed {size}", + "backup.freed_none": "no space to reclaim", + "backup.load_failed": "Failed to load backups", + "backup.optimize_error": "Optimize failed", + "backup.optimize_failed": "Optimize failed: {error}", "backup.optimize_hint": "Reclaim space freed by message retention. Runs SQLite VACUUM; readers keep working but writes are paused for a few seconds.", "backup.optimize_now": "Optimize now", + "backup.optimize_stuck": "Optimize is still running after 10 minutes — check container logs", "backup.optimize_title": "Optimize database", + "backup.optimized": "Optimized: {freed} in {seconds}s", + "backup.optimizing": "Optimizing…", "backup.size": "Current size: {size}", + "backup.size_unknown": "Size: unknown", "backup.title": "Database Backup", + "backup.vacuum_running": "Running VACUUM…", + "backup.vacuum_running_for": "Running VACUUM… ({seconds}s)", "channels.add_new": "Add New Channel", "channels.confirm.remove": "Remove channel \"{name}\"?", "channels.create_btn": "Create & Auto-generate Key", @@ -142,6 +176,7 @@ "common.deleting": "Deleting...", "common.disabled": "Disabled", "common.disconnected": "Disconnected", + "common.edit": "Edit", "common.enabled": "Enabled", "common.failed_with": "Failed: {error}", "common.hours_ago": "{count}h ago", @@ -166,6 +201,7 @@ "common.network_error": "Network error", "common.network_error_detail": "Network error: {error}", "common.never": "Never", + "common.no": "No", "common.refresh": "Refresh", "common.remove": "Remove", "common.save": "Save", @@ -181,6 +217,7 @@ "one": "{count} year ago", "other": "{count} years ago" }, + "common.yes": "Yes", "common.yesterday": "Yesterday", "console.clear_title": "Clear output history", "console.connect_failed": "Failed to connect: {error}", @@ -234,6 +271,7 @@ "contacts.card.add_desc": "Add from URI, QR code, or manual entry", "contacts.card.existing_desc": "Manage all stored contacts", "contacts.card.pending_desc": "Contacts awaiting manual approval", + "contacts.cleanup.confirm": "Remove all contacts inactive for more than {hours} hours?", "contacts.cleanup.confirm_title": "Confirm Contact Cleanup", "contacts.cleanup.date_field": "Date Field:", "contacts.cleanup.days": "Days of Inactivity (0 = ignore):", @@ -242,6 +280,8 @@ "contacts.cleanup.delete_confirm": "The following 0 contact(s) will be permanently deleted:", "contacts.cleanup.enable": "Enable Auto-Cleanup", "contacts.cleanup.enable_hint": "When enabled, contacts matching the above criteria will be automatically deleted daily at the specified hour", + "contacts.cleanup.error": "Cleanup failed", + "contacts.cleanup.failed": "Cleanup failed: {error}", "contacts.cleanup.last_advert": "Last Advert", "contacts.cleanup.last_modified": "Last Modified", "contacts.cleanup.name_filter": "Name Filter (optional):", @@ -344,7 +384,44 @@ "coord.confirm": "Confirm", "coord.hint": "Click on the map to select coordinates", "coord.title": "Pick Coordinates", + "device.cmd.failed": "Command failed: {error}", + "device.cmd.sent": "{command} sent successfully", + "device.info.bw": "Bandwidth", + "device.info.copy_title": "Copy to clipboard", + "device.info.cr": "Coding Rate", + "device.info.freq": "Frequency", + "device.info.load_failed": "Failed to load device info", + "device.info.loc_sharing": "Location Sharing", + "device.info.location": "Location", + "device.info.location_none": "Not available", + "device.info.manual_add": "Manual Add Contacts", + "device.info.multi_acks": "Multi Acks", + "device.info.name": "Name", + "device.info.none": "No device info available", + "device.info.pubkey": "Public Key", + "device.info.sf": "Spreading Factor", + "device.info.tx_power": "TX Power", + "device.info.type": "Type", + "device.info.type_unknown": "Unknown ({code})", + "device.share.copy_uri": "Copy URI", + "device.share.hint": "Share this QR code or URI so others can add your device as a contact.", + "device.share.unavailable": "Device info not available", + "device.share.uri": "Contact URI:", "device.share_hint": "Click to generate share code", + "device.stats.battery": "Battery", + "device.stats.channel_msgs": "Channel Msgs", + "device.stats.contacts_db": "Contacts (DB)", + "device.stats.db_size": "DB Size", + "device.stats.direct_msgs": "Direct Msgs", + "device.stats.errors": "Errors", + "device.stats.load_failed": "Failed to load stats", + "device.stats.none": "No statistics available", + "device.stats.packets_rx": "Packets RX", + "device.stats.packets_tx": "Packets TX", + "device.stats.queue": "Queue", + "device.stats.rx_air": "RX Air Time", + "device.stats.tx_air": "TX Air Time", + "device.stats.uptime": "Uptime", "device.stats_hint": "Click to load stats", "device.tab.info": "Info", "device.tab.share": "Share", @@ -453,13 +530,34 @@ "nav.select_channel_title": "Select channel", "nav.tcp_title": "TCP connection", "observer.add_broker": "Add broker", + "observer.counters": "packets captured: {seen}, published: {published}", "observer.edit_broker": "Edit broker", + "observer.empty": "No brokers configured. Click \"Add broker\" to add one.", "observer.host": "Host", + "observer.host_required": "Host is required", + "observer.iata_invalid": "Location code must be empty or exactly 3 letters", + "observer.load_failed": "Failed to load observer status", "observer.name_ph": "e.g. My MQTT server", + "observer.name_required": "Name is required", + "observer.off": "Observer is off.", "observer.password": "Password", "observer.password_hint": "Leave the password blank to keep the current one.", "observer.port": "Port", + "observer.port_invalid": "Port must be 1-65535", "observer.ports_hint": "Typical ports: 1883 (plain), 8883 (TLS).", + "observer.state.connected": "connected", + "observer.state.error": "error", + "observer.state.offline": "offline", + "observer.state.running": "running", + "observer.state.waiting": "waiting", + "observer.toast.delete_error": "Network error deleting broker", + "observer.toast.delete_failed": "Failed to delete broker", + "observer.toast.save_error": "Network error saving broker", + "observer.toast.save_failed": "Failed to save broker", + "observer.toast.settings_error": "Network error saving observer settings", + "observer.toast.settings_failed": "Failed to save observer settings", + "observer.toast.update_error": "Network error updating broker", + "observer.toast.update_failed": "Failed to update broker", "observer.use_tls": "Use TLS", "observer.username": "Username", "observer.verify_tls": "Verify TLS certificate", @@ -840,6 +938,8 @@ "settings.appear.theme_dark_desc": "Easy on the eyes, deep navy palette", "settings.appear.theme_light": "Light", "settings.appear.theme_light_desc": "Classic bright interface", + "settings.appear.toast.reload_failed": "Failed to reload translations", + "settings.appear.toast.reloaded": "Translations reloaded: {names}", "settings.chat.autoclose": "Auto-close after (s)", "settings.chat.autoclose_help": "Seconds before the route popup closes automatically", "settings.chat.no_autoclose": "Don't close automatically", @@ -856,6 +956,7 @@ "settings.contacts.suppress_notifs": "Suppress new advert notifications", "settings.contacts.suppress_notifs_help": "Hide the badge over Contact Management and the browser notification when new pending contacts arrive. The Pending Contacts list itself still shows them. Requires Manual approval ON.", "settings.device.bw": "Bandwidth (kHz)", + "settings.device.confirm.radio": "Changing radio settings will disconnect from the mesh network. Continue?", "settings.device.cr": "Coding Rate", "settings.device.freq": "Frequency (MHz)", "settings.device.hash_1b": "1 byte (default)", @@ -870,6 +971,15 @@ "settings.device.sf": "Spreading Factor", "settings.device.share_pos": "Share position in advert", "settings.device.share_pos_help": "Include GPS coordinates in device advertisement", + "settings.device.toast.cr_invalid": "Coding rate must be 5-8", + "settings.device.toast.freq_invalid": "Invalid frequency", + "settings.device.toast.info_save_failed": "Failed to save public info", + "settings.device.toast.info_saved": "Public info saved", + "settings.device.toast.name_empty": "Device name cannot be empty", + "settings.device.toast.radio_save_failed": "Failed to save radio settings", + "settings.device.toast.radio_saved": "Radio settings saved", + "settings.device.toast.sf_invalid": "Spreading factor must be 5-12", + "settings.device.toast.tx_invalid": "TX power must be 0-30 dBm", "settings.device.tx_power": "TX Power (dBm)", "settings.iface.breakpoint": "Sidebar breakpoint (px)", "settings.iface.breakpoint_help": "Default: 992. Range: 600-2000.", @@ -882,6 +992,7 @@ "settings.iface.pos_top_left": "Top left", "settings.iface.pos_top_right": "Top right", "settings.iface.position": "Position on screen", + "settings.iface.toast.autoclose_invalid": "Invalid auto-close duration", "settings.iface.toast_autoclose_help": "Seconds before a notification closes automatically", "settings.iface.toast_desc": "Controls the small toasts shown after actions (e.g. \"Advert Sent\", errors).", "settings.iface.toast_no_autoclose_help": "Notifications stay until dismissed via their close button", @@ -902,6 +1013,12 @@ "settings.msg.other": "Other", "settings.notif.blocked": "Blocked", "settings.notif.desc": "Browser notifications appear when the app is hidden or in the background.", + "settings.notif.toast.blocked_help": "Notifications are blocked. Change browser settings: Settings → Site Settings → Notifications", + "settings.notif.toast.denied": "Notifications blocked. Change browser settings to enable them.", + "settings.notif.toast.disabled": "Notifications disabled", + "settings.notif.toast.enabled": "Notifications enabled", + "settings.notif.toast.error": "Error enabling notifications", + "settings.notif.toast.unsupported": "Notifications are not supported in this browser", "settings.notif.unavailable": "Unavailable", "settings.notif.works_hidden": "Works when app is hidden", "settings.observer.advert_interval": "Flood advert interval (h)", @@ -912,10 +1029,28 @@ "settings.observer.iata": "Location code (IATA)", "settings.observer.iata_ph": "e.g. KRK", "settings.observer.intro": "Publish every mesh packet this device overhears to MQTT brokers, in the meshcore-packet-capture format (topics meshcore/IATA/PUBKEY/packets). Compatible with letsmesh-style packet analyzers.", + "settings.regions.delete_title": "Delete region", + "settings.regions.empty": "No regions defined. Add one below.", "settings.regions.intro": "Only repeaters allowing a region will forward messages tagged with it. Find standardised region names at regions.meshcore.nz.", + "settings.regions.is_default": "default", + "settings.regions.load_failed": "Failed to load regions", "settings.regions.name_ph": "Region name (e.g. pl, pl-ma)", + "settings.regions.none_row": "None — use firmware default", + "settings.regions.picker_empty": "No regions defined yet.", + "settings.regions.picker_manage": "Manage Regions", "settings.regions.registry": "Region Registry", "settings.regions.tip": "Tip: pick the default region via the radio button, or select None to fall back to the firmware default. The chosen region is also pushed to the firmware so any untagged channel uses it.", + "settings.regions.toast.add_error": "Network error adding region", + "settings.regions.toast.add_failed": "Failed to add region", + "settings.regions.toast.clear_error": "Network error clearing default", + "settings.regions.toast.clear_failed": "Failed to clear default region", + "settings.regions.toast.default_error": "Network error setting default", + "settings.regions.toast.default_failed": "Failed to set default region", + "settings.regions.toast.delete_error": "Network error deleting region", + "settings.regions.toast.delete_failed": "Failed to delete region", + "settings.regions.toast.name_invalid": "Invalid region name. Allowed: letters, digits, - $ # (max 30 bytes, no spaces).", + "settings.regions.toast.scope_error": "Network error saving region scope", + "settings.regions.toast.scope_failed": "Failed to save region scope", "settings.reset_defaults": "Reset to defaults", "settings.tab.analyzer": "Analyzer", "settings.tab.appearance": "Appearance", @@ -926,6 +1061,10 @@ "settings.tab.notifications": "Notifications", "settings.tab.observer": "Observer", "settings.tab.regions": "Regions", + "settings.toast.save_failed": "Failed to save settings", + "settings.toast.saved": "Settings saved", + "settings.toast.setting_network_error": "Network error saving setting", + "settings.toast.setting_save_failed": "Failed to save setting", "update.checking": "Checking for updates...", "update.now": "Update Now", "update.reload": "Reload Page", diff --git a/app/translations/pl.json b/app/translations/pl.json index b0007e7..5ad7858 100644 --- a/app/translations/pl.json +++ b/app/translations/pl.json @@ -1,17 +1,51 @@ { "analyzer.add": "Dodaj analizator", "analyzer.choose": "Wybierz analizator", + "analyzer.clear_default_title": "Wyczyść domyślny", "analyzer.edit": "Edytuj analizator", + "analyzer.empty": "Brak skonfigurowanych analizatorów. Kliknij \"Dodaj analizator\", aby dodać.", + "analyzer.load_failed": "Nie udało się wczytać analizatorów", "analyzer.name_ph": "np. Mój analizator", + "analyzer.name_required": "Nazwa jest wymagana", + "analyzer.set_default_title": "Ustaw jako domyślny", + "analyzer.toast.default_error": "Błąd sieci przy zmianie domyślnego analizatora", + "analyzer.toast.default_failed": "Nie udało się zmienić domyślnego analizatora", + "analyzer.toast.delete_error": "Błąd sieci przy usuwaniu analizatora", + "analyzer.toast.delete_failed": "Nie udało się usunąć analizatora", + "analyzer.toast.none_configured": "Brak skonfigurowanego analizatora. Dodaj go w Ustawienia → Analyzer.", + "analyzer.toast.save_error": "Błąd sieci przy zapisie analizatora", + "analyzer.toast.save_failed": "Nie udało się zapisać analizatora", + "analyzer.toast.update_error": "Błąd sieci przy aktualizacji analizatora", + "analyzer.toast.update_failed": "Nie udało się zaktualizować analizatora", "analyzer.url": "Szablon URL", "analyzer.url_hint": "Musi zawierać {packetHash}.", + "analyzer.url_invalid": "URL musi zaczynać się od http:// lub https://", + "backup.auto_off": "Automatyczna kopia wyłączona", + "backup.auto_on": "Automatycznie: codziennie o {time}, przechowywanie {days} dni", "backup.create": "Utwórz kopię", + "backup.created": "Utworzono kopię: {filename}", + "backup.creating": "Tworzenie...", "backup.current_size": "Bieżący rozmiar: …", + "backup.download_title": "Pobierz", + "backup.empty": "Brak kopii zapasowych", + "backup.error": "Kopia zapasowa nie powiodła się", + "backup.failed": "Kopia zapasowa nie powiodła się: {error}", + "backup.freed": "zwolniono {size}", + "backup.freed_none": "brak miejsca do odzyskania", + "backup.load_failed": "Nie udało się wczytać kopii zapasowych", + "backup.optimize_error": "Optymalizacja nie powiodła się", + "backup.optimize_failed": "Optymalizacja nie powiodła się: {error}", "backup.optimize_hint": "Odzyskaj miejsce zwolnione przez retencję wiadomości. Uruchamia SQLite VACUUM; odczyty działają, ale zapisy są wstrzymane na kilka sekund.", "backup.optimize_now": "Optymalizuj teraz", + "backup.optimize_stuck": "Optymalizacja trwa już ponad 10 minut — sprawdź logi kontenera", "backup.optimize_title": "Optymalizuj bazę danych", + "backup.optimized": "Zoptymalizowano: {freed} w {seconds}s", + "backup.optimizing": "Optymalizowanie…", "backup.size": "Bieżący rozmiar: {size}", + "backup.size_unknown": "Rozmiar: nieznany", "backup.title": "Kopia zapasowa bazy danych", + "backup.vacuum_running": "Trwa VACUUM…", + "backup.vacuum_running_for": "Trwa VACUUM… ({seconds}s)", "channels.add_new": "Dodaj nowy kanał", "channels.confirm.remove": "Usunąć kanał \"{name}\"?", "channels.create_btn": "Utwórz i wygeneruj klucz", @@ -148,6 +182,7 @@ "common.deleting": "Usuwanie...", "common.disabled": "Wyłączone", "common.disconnected": "Rozłączono", + "common.edit": "Edytuj", "common.enabled": "Włączone", "common.failed_with": "Niepowodzenie: {error}", "common.hours_ago": "{count} godz. temu", @@ -178,6 +213,7 @@ "common.network_error": "Błąd sieci", "common.network_error_detail": "Błąd sieci: {error}", "common.never": "Nigdy", + "common.no": "Nie", "common.refresh": "Odśwież", "common.remove": "Usuń", "common.save": "Zapisz", @@ -195,6 +231,7 @@ "many": "{count} lat temu", "other": "{count} roku temu" }, + "common.yes": "Tak", "common.yesterday": "Wczoraj", "console.clear_title": "Wyczyść zapis konsoli", "console.connect_failed": "Nie udało się połączyć: {error}", @@ -248,6 +285,7 @@ "contacts.card.add_desc": "Dodaj z URI, kodu QR lub ręcznie", "contacts.card.existing_desc": "Zarządzaj wszystkimi zapisanymi kontaktami", "contacts.card.pending_desc": "Kontakty oczekujące na ręczne zatwierdzenie", + "contacts.cleanup.confirm": "Usunąć wszystkie kontakty nieaktywne dłużej niż {hours} godz.?", "contacts.cleanup.confirm_title": "Potwierdź czyszczenie kontaktów", "contacts.cleanup.date_field": "Pole daty:", "contacts.cleanup.days": "Dni nieaktywności (0 = pomiń):", @@ -256,6 +294,8 @@ "contacts.cleanup.delete_confirm": "Następujące kontakty (0) zostaną trwale usunięte:", "contacts.cleanup.enable": "Włącz automatyczne czyszczenie", "contacts.cleanup.enable_hint": "Gdy włączone, kontakty spełniające powyższe kryteria będą usuwane codziennie o wskazanej godzinie", + "contacts.cleanup.error": "Czyszczenie nie powiodło się", + "contacts.cleanup.failed": "Czyszczenie nie powiodło się: {error}", "contacts.cleanup.last_advert": "Ostatni advert", "contacts.cleanup.last_modified": "Ostatnia zmiana", "contacts.cleanup.name_filter": "Filtr nazwy (opcjonalnie):", @@ -364,7 +404,44 @@ "coord.confirm": "Zatwierdź", "coord.hint": "Kliknij na mapie, aby wybrać współrzędne", "coord.title": "Wybierz współrzędne", + "device.cmd.failed": "Polecenie nie powiodło się: {error}", + "device.cmd.sent": "Wysłano: {command}", + "device.info.bw": "Bandwidth", + "device.info.copy_title": "Kopiuj do schowka", + "device.info.cr": "Coding Rate", + "device.info.freq": "Częstotliwość", + "device.info.load_failed": "Nie udało się wczytać informacji o urządzeniu", + "device.info.loc_sharing": "Udostępnianie lokalizacji", + "device.info.location": "Lokalizacja", + "device.info.location_none": "Niedostępna", + "device.info.manual_add": "Ręczne dodawanie kontaktów", + "device.info.multi_acks": "Multi Acks", + "device.info.name": "Nazwa", + "device.info.none": "Brak informacji o urządzeniu", + "device.info.pubkey": "Klucz publiczny", + "device.info.sf": "Spreading Factor", + "device.info.tx_power": "Moc TX", + "device.info.type": "Typ", + "device.info.type_unknown": "Nieznany ({code})", + "device.share.copy_uri": "Kopiuj URI", + "device.share.hint": "Udostępnij ten kod QR lub URI, aby inni mogli dodać Twoje urządzenie jako kontakt.", + "device.share.unavailable": "Informacje o urządzeniu niedostępne", + "device.share.uri": "URI kontaktu:", "device.share_hint": "Kliknij, aby wygenerować kod udostępniania", + "device.stats.battery": "Bateria", + "device.stats.channel_msgs": "Wiadomości kanałowe", + "device.stats.contacts_db": "Kontakty (DB)", + "device.stats.db_size": "Rozmiar bazy", + "device.stats.direct_msgs": "Wiadomości prywatne", + "device.stats.errors": "Błędy", + "device.stats.load_failed": "Nie udało się wczytać statystyk", + "device.stats.none": "Brak statystyk", + "device.stats.packets_rx": "Pakiety RX", + "device.stats.packets_tx": "Pakiety TX", + "device.stats.queue": "Kolejka", + "device.stats.rx_air": "Czas antenowy RX", + "device.stats.tx_air": "Czas antenowy TX", + "device.stats.uptime": "Czas pracy", "device.stats_hint": "Kliknij, aby wczytać statystyki", "device.tab.info": "Informacje", "device.tab.share": "Udostępnij", @@ -479,13 +556,34 @@ "nav.select_channel_title": "Wybierz kanał", "nav.tcp_title": "Połączenie TCP", "observer.add_broker": "Dodaj brokera", + "observer.counters": "przechwycone pakiety: {seen}, opublikowane: {published}", "observer.edit_broker": "Edytuj brokera", + "observer.empty": "Brak skonfigurowanych brokerów. Kliknij \"Dodaj brokera\", aby dodać.", "observer.host": "Host", + "observer.host_required": "Host jest wymagany", + "observer.iata_invalid": "Kod lokalizacji musi być pusty albo mieć dokładnie 3 litery", + "observer.load_failed": "Nie udało się wczytać statusu obserwatora", "observer.name_ph": "np. Mój serwer MQTT", + "observer.name_required": "Nazwa jest wymagana", + "observer.off": "Obserwator jest wyłączony.", "observer.password": "Hasło", "observer.password_hint": "Zostaw hasło puste, aby zachować bieżące.", "observer.port": "Port", + "observer.port_invalid": "Port musi być z zakresu 1-65535", "observer.ports_hint": "Typowe porty: 1883 (bez szyfrowania), 8883 (TLS).", + "observer.state.connected": "połączony", + "observer.state.error": "błąd", + "observer.state.offline": "offline", + "observer.state.running": "działa", + "observer.state.waiting": "czeka", + "observer.toast.delete_error": "Błąd sieci przy usuwaniu brokera", + "observer.toast.delete_failed": "Nie udało się usunąć brokera", + "observer.toast.save_error": "Błąd sieci przy zapisie brokera", + "observer.toast.save_failed": "Nie udało się zapisać brokera", + "observer.toast.settings_error": "Błąd sieci przy zapisie ustawień obserwatora", + "observer.toast.settings_failed": "Nie udało się zapisać ustawień obserwatora", + "observer.toast.update_error": "Błąd sieci przy aktualizacji brokera", + "observer.toast.update_failed": "Nie udało się zaktualizować brokera", "observer.use_tls": "Użyj TLS", "observer.username": "Nazwa użytkownika", "observer.verify_tls": "Weryfikuj certyfikat TLS", @@ -898,6 +996,8 @@ "settings.appear.theme_dark_desc": "Łagodny dla oczu, głęboki granat", "settings.appear.theme_light": "Jasny", "settings.appear.theme_light_desc": "Klasyczny jasny interfejs", + "settings.appear.toast.reload_failed": "Nie udało się przeładować tłumaczeń", + "settings.appear.toast.reloaded": "Przeładowano tłumaczenia: {names}", "settings.chat.autoclose": "Zamknij automatycznie po (s)", "settings.chat.autoclose_help": "Ile sekund do automatycznego zamknięcia popupu trasy", "settings.chat.no_autoclose": "Nie zamykaj automatycznie", @@ -914,6 +1014,7 @@ "settings.contacts.suppress_notifs": "Wycisz powiadomienia o nowych advertach", "settings.contacts.suppress_notifs_help": "Ukryj plakietkę nad Zarządzaniem kontaktami oraz powiadomienie przeglądarki, gdy pojawiają się nowe oczekujące kontakty. Sama lista oczekujących kontaktów nadal je pokazuje. Wymaga włączonego zatwierdzania ręcznego.", "settings.device.bw": "Bandwidth (kHz)", + "settings.device.confirm.radio": "Zmiana ustawień radia rozłączy urządzenie z siecią mesh. Kontynuować?", "settings.device.cr": "Coding Rate", "settings.device.freq": "Częstotliwość (MHz)", "settings.device.hash_1b": "1 bajt (domyślnie)", @@ -928,6 +1029,15 @@ "settings.device.sf": "Spreading Factor", "settings.device.share_pos": "Udostępniaj pozycję w advert", "settings.device.share_pos_help": "Dołącz współrzędne GPS do advertu urządzenia", + "settings.device.toast.cr_invalid": "Coding Rate musi być z zakresu 5-8", + "settings.device.toast.freq_invalid": "Nieprawidłowa częstotliwość", + "settings.device.toast.info_save_failed": "Nie udało się zapisać informacji publicznych", + "settings.device.toast.info_saved": "Informacje publiczne zapisane", + "settings.device.toast.name_empty": "Nazwa urządzenia nie może być pusta", + "settings.device.toast.radio_save_failed": "Nie udało się zapisać ustawień radia", + "settings.device.toast.radio_saved": "Ustawienia radia zapisane", + "settings.device.toast.sf_invalid": "Spreading Factor musi być z zakresu 5-12", + "settings.device.toast.tx_invalid": "Moc TX musi być z zakresu 0-30 dBm", "settings.device.tx_power": "Moc TX (dBm)", "settings.iface.breakpoint": "Próg szerokości paska bocznego (px)", "settings.iface.breakpoint_help": "Domyślnie: 992. Zakres: 600-2000.", @@ -940,6 +1050,7 @@ "settings.iface.pos_top_left": "Lewy górny", "settings.iface.pos_top_right": "Prawy górny", "settings.iface.position": "Pozycja na ekranie", + "settings.iface.toast.autoclose_invalid": "Nieprawidłowy czas automatycznego zamknięcia", "settings.iface.toast_autoclose_help": "Ile sekund do automatycznego zamknięcia powiadomienia", "settings.iface.toast_desc": "Steruje małymi powiadomieniami po akcjach (np. \"Advert wysłany\", błędy).", "settings.iface.toast_no_autoclose_help": "Powiadomienia pozostają, dopóki nie zamkniesz ich przyciskiem", @@ -960,6 +1071,12 @@ "settings.msg.other": "Inne", "settings.notif.blocked": "Zablokowane", "settings.notif.desc": "Powiadomienia przeglądarki pojawiają się, gdy aplikacja jest ukryta lub w tle.", + "settings.notif.toast.blocked_help": "Powiadomienia są zablokowane. Zmień ustawienia przeglądarki: Ustawienia → Ustawienia witryn → Powiadomienia", + "settings.notif.toast.denied": "Powiadomienia zablokowane. Zmień ustawienia przeglądarki, aby je włączyć.", + "settings.notif.toast.disabled": "Powiadomienia wyłączone", + "settings.notif.toast.enabled": "Powiadomienia włączone", + "settings.notif.toast.error": "Błąd włączania powiadomień", + "settings.notif.toast.unsupported": "Powiadomienia nie są obsługiwane w tej przeglądarce", "settings.notif.unavailable": "Niedostępne", "settings.notif.works_hidden": "Działa, gdy aplikacja jest ukryta", "settings.observer.advert_interval": "Interwał flood advert (h)", @@ -970,10 +1087,28 @@ "settings.observer.iata": "Kod lokalizacji (IATA)", "settings.observer.iata_ph": "np. KRK", "settings.observer.intro": "Publikuj każdy pakiet mesh usłyszany przez to urządzenie do brokerów MQTT, w formacie meshcore-packet-capture (tematy meshcore/IATA/PUBKEY/packets). Zgodne z analizatorami pakietów w stylu letsmesh.", + "settings.regions.delete_title": "Usuń region", + "settings.regions.empty": "Brak zdefiniowanych regionów. Dodaj poniżej.", "settings.regions.intro": "Tylko repeatery zezwalające na dany region będą przekazywać oznaczone nim wiadomości. Standardowe nazwy regionów znajdziesz na regions.meshcore.nz.", + "settings.regions.is_default": "domyślny", + "settings.regions.load_failed": "Nie udało się wczytać regionów", "settings.regions.name_ph": "Nazwa regionu (np. pl, pl-ma)", + "settings.regions.none_row": "Brak — użyj domyślnego z firmware", + "settings.regions.picker_empty": "Nie zdefiniowano jeszcze żadnego regionu.", + "settings.regions.picker_manage": "Zarządzaj regionami", "settings.regions.registry": "Rejestr regionów", "settings.regions.tip": "Wskazówka: wybierz domyślny region przyciskiem radiowym albo wybierz Brak, aby wrócić do domyślnego ustawienia firmware. Wybrany region jest też wysyłany do firmware, więc użyje go każdy nieoznaczony kanał.", + "settings.regions.toast.add_error": "Błąd sieci przy dodawaniu regionu", + "settings.regions.toast.add_failed": "Nie udało się dodać regionu", + "settings.regions.toast.clear_error": "Błąd sieci przy czyszczeniu domyślnego regionu", + "settings.regions.toast.clear_failed": "Nie udało się wyczyścić domyślnego regionu", + "settings.regions.toast.default_error": "Błąd sieci przy ustawianiu domyślnego regionu", + "settings.regions.toast.default_failed": "Nie udało się ustawić domyślnego regionu", + "settings.regions.toast.delete_error": "Błąd sieci przy usuwaniu regionu", + "settings.regions.toast.delete_failed": "Nie udało się usunąć regionu", + "settings.regions.toast.name_invalid": "Nieprawidłowa nazwa regionu. Dozwolone: litery, cyfry, - $ # (maks. 30 bajtów, bez spacji).", + "settings.regions.toast.scope_error": "Błąd sieci przy zapisie zasięgu regionu", + "settings.regions.toast.scope_failed": "Nie udało się zapisać zasięgu regionu", "settings.reset_defaults": "Przywróć domyślne", "settings.tab.analyzer": "Analizator", "settings.tab.appearance": "Wygląd", @@ -984,6 +1119,10 @@ "settings.tab.notifications": "Powiadomienia", "settings.tab.observer": "Observer", "settings.tab.regions": "Regiony", + "settings.toast.save_failed": "Nie udało się zapisać ustawień", + "settings.toast.saved": "Ustawienia zapisane", + "settings.toast.setting_network_error": "Błąd sieci przy zapisie ustawienia", + "settings.toast.setting_save_failed": "Nie udało się zapisać ustawienia", "update.checking": "Sprawdzanie aktualizacji...", "update.now": "Aktualizuj teraz", "update.reload": "Przeładuj stronę",