diff --git a/app/static/js/app.js b/app/static/js/app.js index 73f32f3..52e51f3 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -2560,7 +2560,7 @@ async function saveChatSettings() { const el = document.getElementById(elId); const val = parseInt(el.value, 10); if (isNaN(val) || val < parseInt(el.min) || val > parseInt(el.max)) { - showNotification(`Invalid value for ${el.previousElementSibling?.textContent || key}`, 'danger'); + showNotification(t('settings.toast.invalid_value', { field: el.previousElementSibling?.textContent || key }), 'danger'); el.focus(); return; } @@ -2778,7 +2778,7 @@ async function saveDmRetrySettings() { const el = document.getElementById(elId); const val = parseInt(el.value, 10); if (isNaN(val) || val < parseInt(el.min) || val > parseInt(el.max)) { - showNotification(`Invalid value for ${el.previousElementSibling?.textContent || key}`, 'danger'); + showNotification(t('settings.toast.invalid_value', { field: el.previousElementSibling?.textContent || key }), 'danger'); el.focus(); return; } @@ -3054,7 +3054,7 @@ async function executeSpecialCommand(command) { } catch (error) { console.error(`Error executing ${command}:`, error); - showNotification(`Failed to execute ${command}`, 'danger'); + showNotification(t('device.cmd.exec_failed', { command }), 'danger'); } finally { if (btn) { btn.disabled = false; @@ -3367,7 +3367,7 @@ async function addRegion(name, inputEl) { } async function deleteRegion(id, name) { - if (!confirm(`Delete region "${name}"?\nChannels using this region will revert to no scope.`)) return; + if (!confirm(t('settings.regions.confirm.delete', { name }))) return; try { const resp = await fetch(`/api/regions/${id}`, { method: 'DELETE' }); const data = await resp.json().catch(() => ({})); @@ -3570,7 +3570,7 @@ async function saveAnalyzerFromForm() { return; } if (!url_template.includes(ANALYZER_PLACEHOLDER)) { - showAnalyzerFormError(`URL must contain the ${ANALYZER_PLACEHOLDER} placeholder`); + showAnalyzerFormError(t('analyzer.url_placeholder_missing', { placeholder: ANALYZER_PLACEHOLDER })); return; } @@ -3613,7 +3613,7 @@ function showAnalyzerFormError(msg) { } async function deleteAnalyzer(id, name) { - if (!confirm(`Delete analyzer "${name}"?`)) return; + if (!confirm(t('analyzer.confirm.delete', { name }))) return; try { const resp = await fetch(`/api/analyzers/${id}`, { method: 'DELETE' }); const data = await resp.json().catch(() => ({})); @@ -3960,7 +3960,7 @@ async function saveObserverBrokerFromForm() { } async function deleteObserverBroker(id, name) { - if (!confirm(`Delete broker "${name}"?`)) return; + if (!confirm(t('observer.confirm.delete', { name }))) return; try { const resp = await fetch(`/api/observer/brokers/${id}`, { method: 'DELETE' }); const data = await resp.json().catch(() => ({})); @@ -4212,18 +4212,18 @@ function sendBrowserNotification(channelCount, dmCount, pendingCount) { const parts = []; if (channelCount > 0) { - parts.push(`${channelCount} ${channelCount === 1 ? 'channel' : 'channels'}`); + parts.push(tn('notify.channels', channelCount)); } if (dmCount > 0) { - parts.push(`${dmCount} ${dmCount === 1 ? 'private message' : 'private messages'}`); + parts.push(tn('notify.dms', dmCount)); } if (pendingCount > 0) { - parts.push(`${pendingCount} ${pendingCount === 1 ? 'pending contact' : 'pending contacts'}`); + parts.push(tn('notify.pending', pendingCount)); } if (parts.length === 0) return; - message = `New: ${parts.join(', ')}`; + message = t('notify.new', { parts: parts.join(', ') }); try { const notification = new Notification('mc-webui', { @@ -4407,22 +4407,22 @@ async function checkForAppUpdates() { if (updaterStatus.available) { // Show "Update Now" link below version if (updateLinkContainer) { - updateLinkContainer.innerHTML = ` Update now`; + updateLinkContainer.innerHTML = ` ${tHtml('update.link_now')}`; updateLinkContainer.classList.remove('d-none'); } } else { // Show link to GitHub (no remote update available) if (updateLinkContainer) { - updateLinkContainer.innerHTML = ` Update available`; + updateLinkContainer.innerHTML = ` ${tHtml('update.link_available')}`; updateLinkContainer.classList.remove('d-none'); } } icon.className = 'bi bi-check-circle-fill text-success'; - showNotification(`Update available: ${data.latest_date}+${data.latest_commit}`, 'success'); + showNotification(t('update.toast.available', { version: `${data.latest_date}+${data.latest_commit}` }), 'success'); } else { // Up to date icon.className = 'bi bi-check-circle text-success'; - showNotification('You are running the latest version', 'success'); + showNotification(t('update.toast.latest'), 'success'); // Reset icon after 3 seconds setTimeout(() => { icon.className = 'bi bi-arrow-repeat'; @@ -4431,7 +4431,7 @@ async function checkForAppUpdates() { } else { // Error icon.className = 'bi bi-exclamation-triangle text-warning'; - showNotification(data.error || 'Failed to check for updates', 'warning'); + showNotification(data.error || t('update.toast.check_failed'), 'warning'); setTimeout(() => { icon.className = 'bi bi-arrow-repeat'; }, 3000); @@ -4439,7 +4439,7 @@ async function checkForAppUpdates() { } catch (error) { console.error('Error checking for updates:', error); icon.className = 'bi bi-exclamation-triangle text-danger'; - showNotification('Network error checking for updates', 'danger'); + showNotification(t('update.toast.check_error'), 'danger'); setTimeout(() => { icon.className = 'bi bi-arrow-repeat'; }, 3000); @@ -4468,7 +4468,7 @@ function openUpdateModal(newVersion, githubUrl) { document.getElementById('updateCancelBtn').classList.remove('d-none'); document.getElementById('updateConfirmBtn').classList.remove('d-none'); document.getElementById('updateReloadBtn').classList.add('d-none'); - document.getElementById('updateMessage').textContent = `New version available: ${newVersion}`; + document.getElementById('updateMessage').textContent = t('update.new_version', { version: newVersion }); // Set up "What's new" link const whatsNewEl = document.getElementById('updateWhatsNew'); @@ -4500,7 +4500,7 @@ async function performRemoteUpdate() { document.getElementById('updateProgress').classList.remove('d-none'); document.getElementById('updateCancelBtn').classList.add('d-none'); document.getElementById('updateConfirmBtn').classList.add('d-none'); - document.getElementById('updateProgressMessage').textContent = 'Starting update...'; + document.getElementById('updateProgressMessage').textContent = t('update.starting'); try { // Trigger update @@ -4508,11 +4508,11 @@ async function performRemoteUpdate() { const data = await response.json(); if (!data.success) { - showUpdateResult(false, data.error || 'Failed to start update'); + showUpdateResult(false, data.error || t('update.start_failed')); return; } - document.getElementById('updateProgressMessage').textContent = 'Update started. Waiting for server to restart...'; + document.getElementById('updateProgressMessage').textContent = t('update.waiting'); // Poll for server to come back up with new version let attempts = 0; @@ -4534,20 +4534,20 @@ async function performRemoteUpdate() { // Check if version changed if (newVersion !== currentVersion) { - showUpdateResult(true, `Updated to ${newVersion}`); + showUpdateResult(true, t('update.done', { version: newVersion })); return; } } } catch (e) { // Server not responding yet - this is expected during restart document.getElementById('updateProgressMessage').textContent = - `Rebuilding containers... (${attempts}/${maxAttempts})`; + t('update.rebuilding', { attempt: attempts, max: maxAttempts }); } if (attempts < maxAttempts) { setTimeout(pollForCompletion, pollInterval); } else { - showUpdateResult(false, 'Update timed out. Please check server manually.'); + showUpdateResult(false, t('update.timed_out')); } }; @@ -4556,7 +4556,7 @@ async function performRemoteUpdate() { } catch (error) { console.error('Update error:', error); - showUpdateResult(false, 'Network error during update'); + showUpdateResult(false, t('update.network_error')); } } @@ -4666,7 +4666,7 @@ function populateDateSelector(archives) { archives.forEach(archive => { const option = document.createElement('option'); option.value = archive.date; - option.textContent = `${archive.date} (${archive.message_count} msgs)`; + option.textContent = t('menu.archive_option', { date: archive.date, count: archive.message_count }); selector.appendChild(option); }); @@ -4859,7 +4859,7 @@ async function markAllChannelsRead() { for (const [idx, count] of Object.entries(unreadCounts)) { if (count > 0) { const channel = availableChannels.find(ch => ch.index === parseInt(idx)); - const name = channel ? channel.name : `Channel ${idx}`; + const name = channel ? channel.name : t('chat.unnamed_channel', { index: idx }); unreadChannels.push({ idx, count, name }); } } @@ -4868,7 +4868,7 @@ async function markAllChannelsRead() { // Show confirmation dialog with list of unread channels const channelList = unreadChannels.map(ch => ` - ${ch.name} (${ch.count})`).join('\n'); - if (!confirm(`Mark all messages as read?\n\nUnread channels:\n${channelList}`)) return; + if (!confirm(t('chat.confirm.mark_all_read', { channels: channelList }))) return; // Collect latest timestamps const now = Math.floor(Date.now() / 1000); @@ -6090,7 +6090,7 @@ function showMentionsPopup(query) { const filtered = filterContacts(query); if (filtered.length === 0) { - list.innerHTML = '
Type at least 2 characters to search
${tHtml('search.min_chars')}
No results for "${escapeHtml(query)}"
${tHtml('search.no_results', { query })}
{packetHash}.",
"analyzer.url_invalid": "URL must start with http:// or https://",
+ "analyzer.url_placeholder_missing": "URL must contain the {placeholder} placeholder",
"backup.auto_off": "Auto-backup disabled",
"backup.auto_on": "Auto: daily at {time}, keep {days}d",
"backup.create": "Create Backup",
@@ -102,6 +104,7 @@
"channels.your": "Your Channels",
"chat.channels": "Channels",
"chat.confirm.block": "Block {name}? Their messages will be hidden from chat.",
+ "chat.confirm.mark_all_read": "Mark all messages as read?\n\nUnread channels:\n{channels}",
"chat.copy_route_title": "Tap to copy route",
"chat.edit_title": "Edit message",
"chat.emoji_title": "Insert emoji",
@@ -122,6 +125,7 @@
"chat.image_preview_alt": "Preview",
"chat.input_ph": "Type a message...",
"chat.loading_messages": "Loading messages...",
+ "chat.mentions.none": "No contacts found",
"chat.msg.analyzer_title": "View in Analyzer",
"chat.msg.block_title": "Block {name}",
"chat.msg.echo_title": {
@@ -153,6 +157,7 @@
"chat.toast.send_error": "Failed to send message",
"chat.toast.send_failed": "Failed to send: {error}",
"chat.toast.sent": "Message sent",
+ "chat.unnamed_channel": "Channel {index}",
"chat.updated": "Updated: {time}",
"common.add": "Add",
"common.back": "Back",
@@ -384,6 +389,7 @@
"coord.confirm": "Confirm",
"coord.hint": "Click on the map to select coordinates",
"coord.title": "Pick Coordinates",
+ "device.cmd.exec_failed": "Failed to execute {command}",
"device.cmd.failed": "Command failed: {error}",
"device.cmd.sent": "{command} sent successfully",
"device.info.bw": "Bandwidth",
@@ -489,6 +495,7 @@
"menu.advert": "Send Advert",
"menu.advert_desc": "Announce presence (normal)",
"menu.advert_title": "Send single advertisement (recommended for normal operation)",
+ "menu.archive_option": "{date} ({count} msgs)",
"menu.backup": "Backup",
"menu.backup_desc": "Database backup & restore",
"menu.channels": "Manage Channels",
@@ -529,7 +536,21 @@
"nav.mark_all_read_title": "Mark all as read",
"nav.select_channel_title": "Select channel",
"nav.tcp_title": "TCP connection",
+ "notify.channels": {
+ "one": "{count} channel",
+ "other": "{count} channels"
+ },
+ "notify.dms": {
+ "one": "{count} private message",
+ "other": "{count} private messages"
+ },
+ "notify.new": "New: {parts}",
+ "notify.pending": {
+ "one": "{count} pending contact",
+ "other": "{count} pending contacts"
+ },
"observer.add_broker": "Add broker",
+ "observer.confirm.delete": "Delete broker \"{name}\"?",
"observer.counters": "packets captured: {seen}, published: {published}",
"observer.edit_broker": "Edit broker",
"observer.empty": "No brokers configured. Click \"Add broker\" to add one.",
@@ -904,10 +925,20 @@
"rptmgmt.tools.telemetry_desc": "Sensor channels (Cayenne LPP)",
"rptmgmt.tools_heading": "Management Tools",
"rptmgmt.updated_just_now": "Updated just now",
+ "search.count": {
+ "one": "{count} result",
+ "other": "{count} results"
+ },
+ "search.dm_received": "(received)",
+ "search.dm_sent": "(sent)",
"search.empty": "Search across all channel and direct messages",
+ "search.failed": "Search failed. Please try again.",
"search.fts5_ref": "Full FTS5 syntax reference",
"search.help_title": "Search syntax help",
+ "search.min_chars": "Type at least 2 characters to search",
+ "search.no_results": "No results for \"{query}\"",
"search.ph": "Search all messages...",
+ "search.searching": "Searching...",
"search.special_chars": "Special characters (. , - :) should be wrapped in quotes.",
"search.tip_and": "messages containing both words",
"search.tip_not": "hello but not world",
@@ -1029,6 +1060,7 @@
"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.confirm.delete": "Delete region \"{name}\"?\nChannels using this region will revert to no scope.",
"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.",
@@ -1061,14 +1093,31 @@
"settings.tab.notifications": "Notifications",
"settings.tab.observer": "Observer",
"settings.tab.regions": "Regions",
+ "settings.toast.invalid_value": "Invalid value for {field}",
"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.done": "Updated to {version}",
+ "update.link_available": "Update available",
+ "update.link_available_title": "Update available: {version}",
+ "update.link_now": "Update now",
+ "update.link_now_title": "Click to update",
+ "update.network_error": "Network error during update",
+ "update.new_version": "New version available: {version}",
"update.now": "Update Now",
+ "update.rebuilding": "Rebuilding containers... ({attempt}/{max})",
"update.reload": "Reload Page",
+ "update.start_failed": "Failed to start update",
+ "update.starting": "Starting update...",
+ "update.timed_out": "Update timed out. Please check server manually.",
"update.title": "Update mc-webui",
+ "update.toast.available": "Update available: {version}",
+ "update.toast.check_error": "Network error checking for updates",
+ "update.toast.check_failed": "Failed to check for updates",
+ "update.toast.latest": "You are running the latest version",
"update.updating": "Updating...",
+ "update.waiting": "Update started. Waiting for server to restart...",
"update.whats_new": "What's new?"
}
diff --git a/app/translations/pl.json b/app/translations/pl.json
index 5ad7858..975001c 100644
--- a/app/translations/pl.json
+++ b/app/translations/pl.json
@@ -2,6 +2,7 @@
"analyzer.add": "Dodaj analizator",
"analyzer.choose": "Wybierz analizator",
"analyzer.clear_default_title": "Wyczyść domyślny",
+ "analyzer.confirm.delete": "Usunąć analizator \"{name}\"?",
"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",
@@ -20,6 +21,7 @@
"analyzer.url": "Szablon URL",
"analyzer.url_hint": "Musi zawierać {packetHash}.",
"analyzer.url_invalid": "URL musi zaczynać się od http:// lub https://",
+ "analyzer.url_placeholder_missing": "URL musi zawierać symbol zastępczy {placeholder}",
"backup.auto_off": "Automatyczna kopia wyłączona",
"backup.auto_on": "Automatycznie: codziennie o {time}, przechowywanie {days} dni",
"backup.create": "Utwórz kopię",
@@ -102,6 +104,7 @@
"channels.your": "Twoje kanały",
"chat.channels": "Kanały",
"chat.confirm.block": "Zablokować {name}? Wiadomości tego kontaktu będą ukryte w czacie.",
+ "chat.confirm.mark_all_read": "Oznaczyć wszystkie wiadomości jako przeczytane?\n\nKanały z nieprzeczytanymi:\n{channels}",
"chat.copy_route_title": "Dotknij, aby skopiować trasę",
"chat.edit_title": "Edytuj wiadomość",
"chat.emoji_title": "Wstaw emoji",
@@ -122,6 +125,7 @@
"chat.image_preview_alt": "Podgląd",
"chat.input_ph": "Napisz wiadomość...",
"chat.loading_messages": "Wczytywanie wiadomości...",
+ "chat.mentions.none": "Nie znaleziono kontaktów",
"chat.msg.analyzer_title": "Pokaż w analizatorze",
"chat.msg.block_title": "Zablokuj {name}",
"chat.msg.echo_title": {
@@ -155,6 +159,7 @@
"chat.toast.send_error": "Nie udało się wysłać wiadomości",
"chat.toast.send_failed": "Nie udało się wysłać: {error}",
"chat.toast.sent": "Wiadomość wysłana",
+ "chat.unnamed_channel": "Kanał {index}",
"chat.updated": "Aktualizacja: {time}",
"common.add": "Dodaj",
"common.back": "Wstecz",
@@ -404,6 +409,7 @@
"coord.confirm": "Zatwierdź",
"coord.hint": "Kliknij na mapie, aby wybrać współrzędne",
"coord.title": "Wybierz współrzędne",
+ "device.cmd.exec_failed": "Nie udało się wykonać: {command}",
"device.cmd.failed": "Polecenie nie powiodło się: {error}",
"device.cmd.sent": "Wysłano: {command}",
"device.info.bw": "Bandwidth",
@@ -515,6 +521,7 @@
"menu.advert": "Wyślij advert",
"menu.advert_desc": "Ogłoś obecność (normalnie)",
"menu.advert_title": "Wyślij pojedynczy advert (zalecane przy normalnej pracy)",
+ "menu.archive_option": "{date} ({count} wiad.)",
"menu.backup": "Kopia zapasowa",
"menu.backup_desc": "Kopia i przywracanie bazy danych",
"menu.channels": "Zarządzaj kanałami",
@@ -555,7 +562,27 @@
"nav.mark_all_read_title": "Oznacz wszystko jako przeczytane",
"nav.select_channel_title": "Wybierz kanał",
"nav.tcp_title": "Połączenie TCP",
+ "notify.channels": {
+ "one": "{count} kanał",
+ "few": "{count} kanały",
+ "many": "{count} kanałów",
+ "other": "{count} kanału"
+ },
+ "notify.dms": {
+ "one": "{count} wiadomość prywatna",
+ "few": "{count} wiadomości prywatne",
+ "many": "{count} wiadomości prywatnych",
+ "other": "{count} wiadomości prywatnej"
+ },
+ "notify.new": "Nowe: {parts}",
+ "notify.pending": {
+ "one": "{count} oczekujący kontakt",
+ "few": "{count} oczekujące kontakty",
+ "many": "{count} oczekujących kontaktów",
+ "other": "{count} oczekującego kontaktu"
+ },
"observer.add_broker": "Dodaj brokera",
+ "observer.confirm.delete": "Usunąć brokera \"{name}\"?",
"observer.counters": "przechwycone pakiety: {seen}, opublikowane: {published}",
"observer.edit_broker": "Edytuj brokera",
"observer.empty": "Brak skonfigurowanych brokerów. Kliknij \"Dodaj brokera\", aby dodać.",
@@ -962,10 +989,22 @@
"rptmgmt.tools.telemetry_desc": "Kanały czujników (Cayenne LPP)",
"rptmgmt.tools_heading": "Narzędzia zarządzania",
"rptmgmt.updated_just_now": "Zaktualizowano przed chwilą",
+ "search.count": {
+ "one": "{count} wynik",
+ "few": "{count} wyniki",
+ "many": "{count} wyników",
+ "other": "{count} wyniku"
+ },
+ "search.dm_received": "(odebrana)",
+ "search.dm_sent": "(wysłana)",
"search.empty": "Szukaj we wszystkich wiadomościach kanałowych i prywatnych",
+ "search.failed": "Wyszukiwanie nie powiodło się. Spróbuj ponownie.",
"search.fts5_ref": "Pełna dokumentacja składni FTS5",
"search.help_title": "Pomoc do składni wyszukiwania",
+ "search.min_chars": "Wpisz co najmniej 2 znaki, aby wyszukać",
+ "search.no_results": "Brak wyników dla „{query}”",
"search.ph": "Szukaj we wszystkich wiadomościach...",
+ "search.searching": "Wyszukiwanie...",
"search.special_chars": "Znaki specjalne (. , - :) należy ująć w cudzysłów.",
"search.tip_and": "wiadomości zawierające oba słowa",
"search.tip_not": "hello, ale bez world",
@@ -1087,6 +1126,7 @@
"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.confirm.delete": "Usunąć region \"{name}\"?\nKanały używające tego regionu wrócą do braku zasięgu.",
"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.",
@@ -1119,14 +1159,31 @@
"settings.tab.notifications": "Powiadomienia",
"settings.tab.observer": "Observer",
"settings.tab.regions": "Regiony",
+ "settings.toast.invalid_value": "Nieprawidłowa wartość dla: {field}",
"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.done": "Zaktualizowano do {version}",
+ "update.link_available": "Dostępna aktualizacja",
+ "update.link_available_title": "Dostępna aktualizacja: {version}",
+ "update.link_now": "Aktualizuj teraz",
+ "update.link_now_title": "Kliknij, aby zaktualizować",
+ "update.network_error": "Błąd sieci podczas aktualizacji",
+ "update.new_version": "Dostępna nowa wersja: {version}",
"update.now": "Aktualizuj teraz",
+ "update.rebuilding": "Przebudowa kontenerów... ({attempt}/{max})",
"update.reload": "Przeładuj stronę",
+ "update.start_failed": "Nie udało się rozpocząć aktualizacji",
+ "update.starting": "Rozpoczynanie aktualizacji...",
+ "update.timed_out": "Przekroczono czas aktualizacji. Sprawdź serwer ręcznie.",
"update.title": "Aktualizacja mc-webui",
+ "update.toast.available": "Dostępna aktualizacja: {version}",
+ "update.toast.check_error": "Błąd sieci przy sprawdzaniu aktualizacji",
+ "update.toast.check_failed": "Nie udało się sprawdzić aktualizacji",
+ "update.toast.latest": "Masz najnowszą wersję",
"update.updating": "Aktualizowanie...",
+ "update.waiting": "Aktualizacja rozpoczęta. Czekam na restart serwera...",
"update.whats_new": "Co nowego?"
}
diff --git a/docs/whatsnew.md b/docs/whatsnew.md
index 1d32ef2..f1c9c15 100644
--- a/docs/whatsnew.md
+++ b/docs/whatsnew.md
@@ -12,7 +12,7 @@ For deep technical notes, see [architecture.md](architecture.md). For the full g
### Features
-- **The interface can be translated, and Polish has started.** mc-webui was written English-only, with every label and message baked into the code. There is now a translation system behind it, and a **Language** setting at the top of Settings → Appearance. So far it covers the main window — menu, chat, its dialogs and **Settings** — plus every panel that opens in its own window: **System Log**, **Console**, **My Repeaters**, **Path Analyzer**, **Contacts** and **Direct Messages**. Message bubbles now translate too: the button tooltips under a message, the SNR/hops/route line, the route pop-up, and the channel-management dialog. Some of the short messages the app shows while it works are still English; they follow next. Your choice applies to the browser you set it in and also becomes the default for anyone else opening the server without a preference of their own.
+- **The interface can be translated, and Polish has started.** mc-webui was written English-only, with every label and message baked into the code. There is now a translation system behind it, and a **Language** setting at the top of Settings → Appearance. It covers the whole interface: the main window — menu, chat, message bubbles, its dialogs and **Settings** — every panel that opens in its own window (**System Log**, **Console**, **My Repeaters**, **Path Analyzer**, **Contacts**, **Direct Messages**), and the running commentary too: the small toasts after every action, the confirmation dialogs, the search results, the update flow and the notifications your phone shows when the app is in the background. Error text that comes back from the server itself is still English — that is the one part left, and it is a separate job. Your choice applies to the browser you set it in and also becomes the default for anyone else opening the server without a preference of their own.
- **You can add a language yourself, without waiting for a release.** A language is a single file. Copy `en.json` from `app/translations/`, translate the values, and drop it into a `translations` folder inside your config directory — the same place the database lives. Refresh the page and it appears in the Language list, named however you named it, with no rebuild and no restart. Anything you leave untranslated falls back to English, so a half-finished translation is perfectly usable. A file you drop in also overrides one that ships with the app, so you can correct the built-in Polish on your own server. English and Polish ship in the box; everything else is open to whoever wants to write it. See [translations.md](translations.md), which explains the format and — importantly — which words to leave alone: mesh terms like flood, hop, advert, RSSI and the repeater roles stay English in every language, because that is what the firmware, the CLI and the forums all use. The Console and the log lines themselves stay English for the same reason.
- **Times and dates behave the same in every language.** Only the words are translated — "Yesterday" becomes "Wczoraj", "5 min ago" becomes "5 min temu". The clock and the number formatting keep following your browser's own settings, so switching the menus to English will not suddenly turn your 24-hour clock into "9:53 AM". One display of large numbers on the repeater statistics page had been hard-coded to American thousands separators; it now follows your locale like everything else.