diff --git a/app/static/js/app.js b/app/static/js/app.js index baec7f4..e464bbf 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -193,13 +193,13 @@ function updateMapMarkers() { }); L.marker([_selfInfo.adv_lat, _selfInfo.adv_lon], { icon: ownIcon }) .addTo(markersGroup) - .bindPopup(`${_selfInfo.name || 'This device'}
Own device`); + .bindPopup(`${_selfInfo.name || t('map.this_device')}
${tHtml('map.own_device')}`); bounds.push([_selfInfo.adv_lat, _selfInfo.adv_lon]); } filteredContacts.forEach(c => { const color = CONTACT_TYPE_COLORS[c.type] || '#2196F3'; - const typeName = CONTACT_TYPE_NAMES[c.type] || 'Unknown'; + const typeName = CONTACT_TYPE_NAMES[c.type] || t('common.unknown'); const lastSeen = c.last_advert ? formatTimeAgo(c.last_advert) : ''; L.circleMarker([c.adv_lat, c.adv_lon], { @@ -211,7 +211,7 @@ function updateMapMarkers() { fillOpacity: 0.8 }) .addTo(markersGroup) - .bindPopup(`${c.name}
${typeName}${lastSeen ? `
Last seen: ${lastSeen}` : ''}`); + .bindPopup(`${c.name}
${typeName}${lastSeen ? `
${tHtml('repeaters.map.last_seen', { time: lastSeen })}` : ''}`); bounds.push([c.adv_lat, c.adv_lon]); }); @@ -230,7 +230,7 @@ function updateMapMarkers() { fillOpacity: 0.5 }) .addTo(markersGroup) - .bindPopup(`${c.name}
${c.type_label || 'Cache'} (cached)${lastSeen ? `
Last seen: ${lastSeen}` : ''}`); + .bindPopup(`${c.name}
${tHtml('map.cached_label', { type: c.type_label || t('map.cache') })}${lastSeen ? `
${tHtml('repeaters.map.last_seen', { time: lastSeen })}` : ''}`); bounds.push([c.adv_lat, c.adv_lon]); }); @@ -423,10 +423,10 @@ async function resyncFromServer(reason, { toast = false } = {}) { loadStatus(); updatePendingContactsBadge(); checkDmUpdates(); - if (toast) showNotification('Messages refreshed', 'success'); + if (toast) showNotification(t('chat.toast.refreshed'), 'success'); } catch (error) { console.error('[resync] failed:', error); - if (toast) showNotification('Refresh failed', 'danger'); + if (toast) showNotification(t('chat.toast.refresh_failed'), 'danger'); } finally { resyncInFlight = false; } @@ -914,8 +914,8 @@ function setupEventListeners() { if (data.success) { const msg = data.already_existed - ? `Channel "${name}" already exists.` - : `Channel "${name}" created!`; + ? t('channels.toast.exists', { name }) + : t('channels.toast.created', { name }); showNotification(msg, data.already_existed ? 'info' : 'success'); // Show warning if returned (e.g., exceeding soft limit of 7 channels) @@ -932,10 +932,10 @@ function setupEventListeners() { await loadChannels(); loadChannelsList(); } else { - showNotification('Failed to create channel: ' + data.error, 'danger'); + showNotification(t('channels.toast.create_failed', { error: data.error }), 'danger'); } } catch (error) { - showNotification('Failed to create channel', 'danger'); + showNotification(t('channels.toast.create_error'), 'danger'); } finally { if (submitBtn) submitBtn.disabled = false; } @@ -953,13 +953,13 @@ function setupEventListeners() { // Validate: key is optional for channels starting with #, but required for others if (!name.startsWith('#') && !key) { - showNotification('Channel key is required for channels not starting with #', 'warning'); + showNotification(t('channels.toast.key_required'), 'warning'); return; } // Validate key format if provided if (key && !/^[a-f0-9]{32}$/.test(key)) { - showNotification('Invalid key format. Must be 32 hex characters.', 'warning'); + showNotification(t('channels.toast.key_invalid'), 'warning'); return; } @@ -982,8 +982,8 @@ function setupEventListeners() { if (data.success) { const msg = data.already_existed - ? `Already joined channel "${name}".` - : `Joined channel "${name}"!`; + ? t('channels.toast.already_joined', { name }) + : t('channels.toast.joined', { name }); showNotification(msg, data.already_existed ? 'info' : 'success'); // Show warning if returned (e.g., exceeding soft limit of 7 channels) @@ -1001,10 +1001,10 @@ function setupEventListeners() { await loadChannels(); loadChannelsList(); } else { - showNotification('Failed to join channel: ' + data.error, 'danger'); + showNotification(t('channels.toast.join_failed', { error: data.error }), 'danger'); } } catch (error) { - showNotification('Failed to join channel', 'danger'); + showNotification(t('channels.toast.join_error'), 'danger'); } finally { if (submitBtn) submitBtn.disabled = false; } @@ -1012,7 +1012,7 @@ function setupEventListeners() { // Scan QR button (placeholder) document.getElementById('scanQRBtn').addEventListener('click', function() { - showNotification('QR scanning feature coming soon! For now, manually enter the channel details.', 'info'); + showNotification(t('channels.toast.qr_soon'), 'info'); }); // Network Commands: Advert button @@ -1022,7 +1022,7 @@ function setupEventListeners() { // Network Commands: Flood Advert button (with confirmation) document.getElementById('floodadvBtn').addEventListener('click', async function() { - if (!confirm('Flood Advertisement uses high airtime and should only be used for network recovery.\n\nAre you sure you want to proceed?')) { + if (!confirm(t('menu.confirm.flood_advert'))) { return; } await executeSpecialCommand('floodadv'); @@ -1033,7 +1033,7 @@ function setupEventListeners() { await executeSpecialCommand('advert'); }); document.getElementById('fab-floodadvert')?.addEventListener('click', async () => { - if (!confirm('Flood Advertisement uses high airtime and should only be used for network recovery.\n\nAre you sure you want to proceed?')) { + if (!confirm(t('menu.confirm.flood_advert'))) { return; } await executeSpecialCommand('floodadv'); @@ -1098,7 +1098,7 @@ async function loadMessages() { updateLastRefresh(); updateRegionIndicator(); } else { - showNotification('Error loading messages: ' + data.error, 'danger'); + showNotification(t('chat.toast.load_error', { error: data.error }), 'danger'); clearLoadingSpinner(); } } catch (error) { @@ -1106,10 +1106,10 @@ async function loadMessages() { updateStatus('disconnected'); clearLoadingSpinner(); if (error.name === 'AbortError') { - showNotification('Loading messages timed out — retrying...', 'warning'); + showNotification(t('chat.toast.load_timeout'), 'warning'); setTimeout(loadMessages, 2000); } else { - showNotification('Failed to load messages', 'danger'); + showNotification(t('chat.toast.load_failed'), 'danger'); } } } @@ -1120,8 +1120,8 @@ function clearLoadingSpinner() { container.innerHTML = `
-

Could not load messages

- Will retry automatically +

${tHtml('chat.error.load_title')}

+ ${tHtml('chat.error.load_retry')}
`; } @@ -1141,8 +1141,8 @@ function displayMessages(messages) { container.innerHTML = `
-

No messages yet

- Send a message to get started! +

${tHtml('chat.empty.no_messages')}

+ ${tHtml('chat.empty.no_messages_hint')}
`; return; @@ -1255,7 +1255,7 @@ async function refreshMessagesMeta(forceIds = []) { const metaEl = wrapper.querySelector('.message-meta'); const actionsEl = wrapper.querySelector('.message-actions'); const hasRoute = metaEl && metaEl.querySelector('.path-info'); - const hasAnalyzer = actionsEl && actionsEl.querySelector('[title="View in Analyzer"]'); + const hasAnalyzer = actionsEl && actionsEl.querySelector('.btn-msg-analyzer'); if (hasRoute && hasAnalyzer) continue; } @@ -1296,7 +1296,7 @@ function updateMessageMetaDOM(wrapper, meta) { } const hopCount = meta.hop_count ?? (meta.path_len !== null && meta.path_len !== undefined ? (meta.path_len & 0x3F) : null); if (hopCount !== null) { - metaParts.push(`Hops: ${hopCount}`); + metaParts.push(tHtml('chat.route_hops', { count: hopCount })); } // Build paths from echo data @@ -1321,8 +1321,10 @@ function updateMessageMetaDOM(wrapper, meta) { ? `${segments[0]}\u2192...\u2192${segments[segments.length - 1]}` : segments.join('\u2192'); const pathsData = encodeURIComponent(JSON.stringify(paths)); - const routeLabel = paths.length > 1 ? `Route (${paths.length})` : 'Route'; - metaParts.push(`${routeLabel}: ${shortPath}`); + const routeText = paths.length > 1 + ? tHtml('chat.route_multi', { count: paths.length, route: shortPath }) + : tHtml('chat.route', { route: shortPath }); + metaParts.push(`${routeText}`); } const metaInfo = metaParts.join(' | '); @@ -1344,12 +1346,12 @@ function updateMessageMetaDOM(wrapper, meta) { // Add analyzer button if not already present if (meta.packet_hash) { const actionsEl = msgDiv.querySelector('.message-actions'); - if (actionsEl && !actionsEl.querySelector('[title="View in Analyzer"]')) { - const ignoreBtn = actionsEl.querySelector('[title^="Ignore"]'); + if (actionsEl && !actionsEl.querySelector('.btn-msg-analyzer')) { + const ignoreBtn = actionsEl.querySelector('.btn-msg-ignore'); const analyzerBtn = document.createElement('button'); - analyzerBtn.className = 'btn btn-outline-secondary btn-msg-action'; + analyzerBtn.className = 'btn btn-outline-secondary btn-msg-action btn-msg-analyzer'; analyzerBtn.setAttribute('onclick', `openMessageAnalyzer('${meta.packet_hash}')`); - analyzerBtn.title = 'View in Analyzer'; + analyzerBtn.title = t('chat.msg.analyzer_title'); analyzerBtn.innerHTML = ''; actionsEl.insertBefore(analyzerBtn, ignoreBtn); } @@ -1376,7 +1378,7 @@ function updateMessageMetaDOM(wrapper, meta) { badge.className = 'echo-badge'; actionsEl.insertBefore(badge, actionsEl.firstChild); } - badge.title = `Heard by ${echoCount} repeater(s): ${echoPaths.join(', ')}`; + badge.title = tn('chat.msg.echo_title', echoCount, { paths: echoPaths.join(', ') }); badge.innerHTML = ` ${echoCount}${pathDisplay}`; } } @@ -1384,16 +1386,15 @@ function updateMessageMetaDOM(wrapper, meta) { // Add analyzer button if (meta.packet_hash) { const actionsEl = msgDiv.querySelector('.message-actions'); - if (actionsEl && !actionsEl.querySelector('[title="View in Analyzer"]')) { - // Anchor analyzer before whichever action button comes first - // (post-rename: "Edit message"; legacy renders may still say "Resend") - const anchor = actionsEl.querySelector('[title="Edit message"]') - || actionsEl.querySelector('[title="Resend"]') - || actionsEl.querySelector('[title^="Resend"]'); + if (actionsEl && !actionsEl.querySelector('.btn-msg-analyzer')) { + // Anchor the analyzer button before the edit button. Matched on a + // class, not on the title: titles are translated, and a title + // selector would silently miss in every language but English. + const anchor = actionsEl.querySelector('.btn-msg-edit'); const analyzerBtn = document.createElement('button'); - analyzerBtn.className = 'btn btn-outline-secondary btn-msg-action'; + analyzerBtn.className = 'btn btn-outline-secondary btn-msg-action btn-msg-analyzer'; analyzerBtn.setAttribute('onclick', `openMessageAnalyzer('${meta.packet_hash}')`); - analyzerBtn.title = 'View in Analyzer'; + analyzerBtn.title = t('chat.msg.analyzer_title'); analyzerBtn.innerHTML = ''; if (anchor) actionsEl.insertBefore(analyzerBtn, anchor); else actionsEl.appendChild(analyzerBtn); @@ -1411,7 +1412,7 @@ function updateMessageMetaDOM(wrapper, meta) { const rawBtn = document.createElement('button'); rawBtn.className = 'btn btn-outline-secondary btn-msg-action btn-raw-resend'; rawBtn.setAttribute('onclick', `resendChannelMessageRaw(${msgId}, this)`); - rawBtn.title = 'Resend (rebroadcast same packet so unreached repeaters can pick it up)'; + rawBtn.title = t('chat.msg.raw_resend_title'); rawBtn.innerHTML = ''; actionsEl.appendChild(rawBtn); } @@ -1447,7 +1448,7 @@ function createMessageElement(msg) { } const msgHopCount = msg.hop_count ?? (msg.path_len !== null && msg.path_len !== undefined ? (msg.path_len & 0x3F) : null); if (msgHopCount !== null) { - metaParts.push(`Hops: ${msgHopCount}`); + metaParts.push(tHtml('chat.route_hops', { count: msgHopCount })); } if (msg.paths && msg.paths.length > 0) { // Show first path inline (shortest/first arrival) @@ -1463,8 +1464,10 @@ function createMessageElement(msg) { ? `${segments[0]}\u2192...\u2192${segments[segments.length - 1]}` : segments.join('\u2192'); const pathsData = encodeURIComponent(JSON.stringify(msg.paths)); - const routeLabel = msg.paths.length > 1 ? `Route (${msg.paths.length})` : 'Route'; - metaParts.push(`${routeLabel}: ${shortPath}`); + const routeText = msg.paths.length > 1 + ? tHtml('chat.route_multi', { count: msg.paths.length, route: shortPath }) + : tHtml('chat.route', { route: shortPath }); + metaParts.push(`${routeText}`); } const metaInfo = metaParts.join(' | '); @@ -1479,7 +1482,7 @@ function createMessageElement(msg) { const echoCount = echoPaths.length; const pathDisplay = echoPaths.length > 0 ? ` (${echoPaths.join(', ')})` : ''; const echoDisplay = echoCount > 0 - ? ` + ? ` ${echoCount}${pathDisplay} ` : ''; @@ -1495,15 +1498,15 @@ function createMessageElement(msg) {
${echoDisplay} ${msg.packet_hash ? ` - ` : ''} - ${window.deviceCaps?.supports_raw_resend && typeof msg.id === 'number' ? ` - ` : ''} @@ -1532,29 +1535,29 @@ function createMessageElement(msg) {
${processMessageContent(msg.content)}
${metaInfo ? `
${metaInfo}
` : ''}
- - ${contactsGeoCache[msg.sender] ? ` - ` : ''} ${msg.packet_hash ? ` - ` : ''} ${contactsPubkeyMap[msg.sender] && !isContactProtectedByName(msg.sender) ? ` - ` : ''} ${!isContactProtectedByName(msg.sender) ? ` - ` : ''} @@ -1607,7 +1610,7 @@ async function sendMessage() { const data = await response.json(); if (data.success) { - showNotification('Message sent', 'success'); + showNotification(t('chat.toast.sent'), 'success'); // Replace optimistic ID with real DB id so echo WebSocket updates work if (data.id) { @@ -1626,11 +1629,11 @@ async function sendMessage() { markChannelAsRead(currentChannelIdx, data.timestamp); } } else { - showNotification('Failed to send: ' + data.error, 'danger'); + showNotification(t('chat.toast.send_failed', { error: data.error }), 'danger'); } } catch (error) { console.error('Error sending message:', error); - showNotification('Failed to send message', 'danger'); + showNotification(t('chat.toast.send_error'), 'danger'); } finally { sendBtn.disabled = false; input.focus(); @@ -1761,12 +1764,12 @@ async function resendChannelMessageRaw(msgId, btn) { const resp = await fetch(`/api/messages/${msgId}/resend`, { method: 'POST' }); const data = await resp.json().catch(() => ({})); if (resp.ok && data.success) { - showNotification(`Resent (${data.bytes ?? '?'} B) — waiting for echoes…`, 'info'); + showNotification(t('chat.toast.resent', { bytes: data.bytes ?? '?' }), 'info'); } else { - showNotification(`Resend failed: ${data.error || resp.statusText}`, 'danger'); + showNotification(t('chat.toast.resend_failed', { error: data.error || resp.statusText }), 'danger'); } } catch (err) { - showNotification(`Resend network error: ${err.message || err}`, 'danger'); + showNotification(t('chat.toast.resend_network_error', { error: err.message || err }), 'danger'); } finally { if (btn) { btn.dataset.busy = '0'; @@ -1787,15 +1790,15 @@ async function ignoreContactFromChat(pubkey) { if (data.success) { showNotification(data.message, 'info'); } else { - showNotification('Failed: ' + data.error, 'danger'); + showNotification(t('common.failed_with', { error: data.error }), 'danger'); } } catch (err) { - showNotification('Network error', 'danger'); + showNotification(t('common.network_error'), 'danger'); } } async function blockContactFromChat(senderName) { - if (!confirm(`Block ${senderName}? Their messages will be hidden from chat.`)) return; + if (!confirm(t('chat.confirm.block', { name: senderName }))) return; try { const pubkey = contactsPubkeyMap[senderName]; let response; @@ -1821,11 +1824,11 @@ async function blockContactFromChat(senderName) { await loadBlockedNames(); await loadMessages(); } else { - showNotification('Failed: ' + data.error, 'danger'); + showNotification(t('common.failed_with', { error: data.error }), 'danger'); } } catch (err) { console.error('Error blocking contact from chat:', err); - showNotification('Network error', 'danger'); + showNotification(t('common.network_error'), 'danger'); } } @@ -1859,12 +1862,12 @@ function showPathsPopup(element, encodedPaths, packetHash) { const body = document.createElement('span'); body.className = 'path-route'; - body.innerHTML = `${fullRoute}SNR: ${snr} | Hops: ${hops}`; + body.innerHTML = `${fullRoute}SNR: ${snr} | ${tHtml('chat.route_hops', { count: hops })}`; entry.appendChild(body); const copyBtn = document.createElement('i'); copyBtn.className = 'bi bi-clipboard path-copy'; - copyBtn.title = 'Copy route'; + copyBtn.title = t('common.copy_route'); copyBtn.addEventListener('click', (e) => { e.stopPropagation(); navigator.clipboard.writeText(commaRoute).then(() => { @@ -1875,7 +1878,7 @@ function showPathsPopup(element, encodedPaths, packetHash) { entry.appendChild(copyBtn); if (packetHash && p.path && segments.length > 0) { - entry.title = 'Show this route on the Path Analyzer map'; + entry.title = t('chat.route_analyzer_title'); entry.addEventListener('click', (e) => { e.stopPropagation(); popup.remove(); @@ -1883,7 +1886,7 @@ function showPathsPopup(element, encodedPaths, packetHash) { }); } else { // No packet hash (or direct message): keep the copy behavior - entry.title = 'Tap to copy route'; + entry.title = t('chat.copy_route_title'); entry.addEventListener('click', (e) => { e.stopPropagation(); navigator.clipboard.writeText(commaRoute).then(() => { @@ -1981,7 +1984,7 @@ function injectRawResendButtonsForVisibleMessages() { const rawBtn = document.createElement('button'); rawBtn.className = 'btn btn-outline-secondary btn-msg-action btn-raw-resend'; rawBtn.setAttribute('onclick', `resendChannelMessageRaw(${msgId}, this)`); - rawBtn.title = 'Resend (rebroadcast same packet so unreached repeaters can pick it up)'; + rawBtn.title = t('chat.msg.raw_resend_title'); rawBtn.innerHTML = ''; actionsEl.appendChild(rawBtn); } @@ -5224,7 +5227,7 @@ function renderChannelDropdownItems(query) { const empty = document.createElement('div'); empty.className = 'channel-selector-item text-muted'; empty.style.cursor = 'default'; - empty.textContent = q ? 'No matches' : 'No channels'; + empty.textContent = q ? t('channels.dropdown.no_matches') : t('channels.dropdown.none'); dropdown.appendChild(empty); return; } @@ -5306,7 +5309,7 @@ function selectChannelFromDropdown(idx, name) { loadMessages(); updateChannelSidebarActive(); - showNotification(`Switched to channel: ${name}`, 'info'); + showNotification(t('channels.toast.switched', { name }), 'info'); } /** @@ -5325,7 +5328,7 @@ function updateChannelInputDisplay() { */ async function loadChannelsList() { const listEl = document.getElementById('channelsList'); - listEl.innerHTML = '
Loading...
'; + listEl.innerHTML = `
${tHtml('common.loading')}
`; try { const [chResp, scResp] = await Promise.all([ @@ -5339,10 +5342,10 @@ async function loadChannelsList() { if (data.success) { displayChannelsList(data.channels); } else { - listEl.innerHTML = '
Error loading channels
'; + listEl.innerHTML = `
${tHtml('channels.list.load_error')}
`; } } catch (error) { - listEl.innerHTML = '
Failed to load channels
'; + listEl.innerHTML = `
${tHtml('channels.list.load_failed')}
`; } } @@ -5353,7 +5356,7 @@ function displayChannelsList(channels) { const listEl = document.getElementById('channelsList'); if (channels.length === 0) { - listEl.innerHTML = '
No channels configured
'; + listEl.innerHTML = `
${tHtml('channels.list.empty')}
`; return; } @@ -5370,8 +5373,8 @@ function displayChannelsList(channels) { const scope = (window.channelScopes || {})[String(channel.index)]; const hasScope = !!scope; const scopeTitle = hasScope - ? `Region: ${scope.name} — click to change` - : 'Set region scope'; + ? tHtml('channels.scope_title', { name: scope.name }) + : tHtml('channels.scope_set_title'); item.innerHTML = `
${escapeHtml(channel.name)} @@ -5380,23 +5383,23 @@ function displayChannelsList(channels) {
- ${!isPublic ? ` - ` : ''} @@ -5625,10 +5628,10 @@ async function toggleChannelMute(index) { loadChannelsList(); updateUnreadBadges(); } else { - showNotification('Failed to update mute state', 'danger'); + showNotification(t('channels.toast.mute_failed'), 'danger'); } } catch (error) { - showNotification('Failed to update mute state', 'danger'); + showNotification(t('channels.toast.mute_failed'), 'danger'); } } @@ -5656,10 +5659,10 @@ async function toggleChannelFavorite(index) { loadChannelsList(); populateChannelSelector(availableChannels); } else { - showNotification('Failed to update favorite state', 'danger'); + showNotification(t('channels.toast.favorite_failed'), 'danger'); } } catch (error) { - showNotification('Failed to update favorite state', 'danger'); + showNotification(t('channels.toast.favorite_failed'), 'danger'); } } @@ -5670,7 +5673,7 @@ async function deleteChannel(index) { const channel = availableChannels.find(ch => ch.index === index); if (!channel) return; - if (!confirm(`Remove channel "${channel.name}"?`)) { + if (!confirm(t('channels.confirm.remove', { name: channel.name }))) { return; } @@ -5682,7 +5685,7 @@ async function deleteChannel(index) { const data = await response.json(); if (data.success) { - showNotification(`Channel "${channel.name}" removed`, 'success'); + showNotification(t('channels.toast.removed', { name: channel.name }), 'success'); // If deleted current channel, switch to Public if (currentChannelIdx === index) { @@ -5695,10 +5698,10 @@ async function deleteChannel(index) { await loadChannels(); loadChannelsList(); } else { - showNotification('Failed to remove channel: ' + data.error, 'danger'); + showNotification(t('channels.toast.remove_failed', { error: data.error }), 'danger'); } } catch (error) { - showNotification('Failed to remove channel', 'danger'); + showNotification(t('channels.toast.remove_error'), 'danger'); } } @@ -5712,7 +5715,7 @@ async function shareChannel(index) { if (data.success) { // Populate share modal - document.getElementById('shareChannelName').textContent = `Channel: ${data.qr_data.name}`; + document.getElementById('shareChannelName').textContent = t('channels.share_name', { name: data.qr_data.name }); document.getElementById('shareChannelQR').src = data.qr_image; document.getElementById('shareChannelKey').value = data.qr_data.key; @@ -5720,10 +5723,10 @@ async function shareChannel(index) { const modal = new bootstrap.Modal(document.getElementById('shareChannelModal')); modal.show(); } else { - showNotification('Failed to generate QR code: ' + data.error, 'danger'); + showNotification(t('channels.toast.qr_failed', { error: data.error }), 'danger'); } } catch (error) { - showNotification('Failed to generate QR code', 'danger'); + showNotification(t('channels.toast.qr_error'), 'danger'); } } @@ -5735,15 +5738,15 @@ async function copyChannelKey() { try { // Use modern Clipboard API await navigator.clipboard.writeText(input.value); - showNotification('Channel key copied to clipboard!', 'success'); + showNotification(t('channels.toast.key_copied'), 'success'); } catch (error) { // Fallback for older browsers input.select(); try { document.execCommand('copy'); - showNotification('Channel key copied to clipboard!', 'success'); + showNotification(t('channels.toast.key_copied'), 'success'); } catch (fallbackError) { - showNotification('Failed to copy to clipboard', 'danger'); + showNotification(t('channels.toast.copy_failed'), 'danger'); } } } diff --git a/app/static/js/dm.js b/app/static/js/dm.js index 599af83..6b8b59b 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -196,7 +196,7 @@ function connectChatSocket() { statusEl.className = 'bi bi-check2 dm-status delivered'; const tooltip = []; if (data.snr != null) tooltip.push(`SNR: ${data.snr}`); - if (data.route_type) tooltip.push(t('dm.route', { route: data.route_type })); + if (data.route_type) tooltip.push(t('chat.route', { route: data.route_type })); statusEl.title = tooltip.length > 0 ? tooltip.join(', ') : t('dm.status.delivered'); // Unwrap status icon from wrapper span const wrapper = statusEl.closest('[data-dm-id]'); @@ -252,7 +252,7 @@ function connectChatSocket() { const parts = []; if (data.attempt && data.max_attempts) parts.push(t('dm.attempt', { n: data.attempt, max: data.max_attempts })); const hexRoute = formatDmRoute(data.path, data.hash_size); - if (hexRoute) parts.push(t('dm.route', { route: hexRoute })); + if (hexRoute) parts.push(t('chat.route', { route: hexRoute })); if (parts.length > 0) { let metaEl = msgDiv.querySelector('.dm-delivery-meta'); if (!metaEl) { @@ -1224,7 +1224,7 @@ async function loadMessages() { } } catch (error) { console.error('Error loading messages:', error); - container.innerHTML = `
${tHtml('dm.load_messages_failed')}
`; + container.innerHTML = `
${tHtml('chat.toast.load_failed')}
`; } } @@ -1239,7 +1239,7 @@ function displayMessages(messages) { container.innerHTML = `
-

${tHtml('dm.empty.no_messages')}

+

${tHtml('chat.empty.no_messages')}

${tHtml('dm.empty.no_messages_hint')}
`; @@ -1270,7 +1270,7 @@ function displayMessages(messages) { title += ` (${msg.delivery_attempt}/${msg.delivery_max_attempts})`; } const route = formatDmRoute(msg.delivery_path, msg.delivery_path_hash_size || msg.path_hash_size); - if (route) title += `, ${t('dm.route', { route: route })}`; + if (route) title += `, ${t('chat.route', { route: route })}`; else if (msg.delivery_route) title += `, ${msg.delivery_route.replace('PATH_', '')}`; if (msg.delivery_snr !== null && msg.delivery_snr !== undefined) { title += `, SNR: ${msg.delivery_snr.toFixed(1)} dB`; @@ -1334,7 +1334,7 @@ function displayMessages(messages) { // only raw resend (arrow-repeat). const resendBtn = msg.is_own ? `
-
@@ -1399,17 +1399,17 @@ async function sendMessage() { if (data.success) { input.value = ''; updateCharCounter(); - showNotification(t('dm.toast.sent'), 'success'); + showNotification(t('chat.toast.sent'), 'success'); // Reload messages once to show sent message // ACK delivery updates arrive via SocketIO in real-time await loadMessages(); } else { - showNotification(t('dm.toast.send_failed', { error: data.error }), 'danger'); + showNotification(t('chat.toast.send_failed', { error: data.error }), 'danger'); } } catch (error) { console.error('Error sending message:', error); - showNotification(t('dm.toast.send_error'), 'danger'); + showNotification(t('chat.toast.send_error'), 'danger'); } finally { if (sendBtn) sendBtn.disabled = false; input.focus(); @@ -1542,10 +1542,10 @@ function buildDmRouteHtml(hexPath, hashSize) { const short = segments.length > 4 ? `${segments[0]}\u2192...\u2192${segments[segments.length - 1]}` : segments.join('\u2192'); - if (segments.length <= 4) return tHtml('dm.route', { route: short }); + if (segments.length <= 4) return tHtml('chat.route', { route: short }); const hs = hashSize || 1; const escaped = hexPath.replace(/'/g, "\\'"); - return `${tHtml('dm.route', { route: short })}`; + return `${tHtml('chat.route', { route: short })}`; } /** diff --git a/app/static/js/message-utils.js b/app/static/js/message-utils.js index 8de7a49..917718c 100644 --- a/app/static/js/message-utils.js +++ b/app/static/js/message-utils.js @@ -210,7 +210,7 @@ function createImageThumbnail(url) { // Escape URL for use in HTML attributes const escapedUrl = escapeHtmlAttribute(url); - return ``; + return ``; } /** @@ -253,11 +253,11 @@ function createImageModal() { @@ -360,7 +360,7 @@ async function joinAndSwitchToChannel(channelName) { const data = await response.json(); if (data.success) { - showNotification(`Joined channel "${channelName}"!`, 'success'); + showNotification(t('channels.toast.joined', { name: channelName }), 'success'); // Show warning if applicable (e.g., exceeding channel limit) if (data.warning) { @@ -373,11 +373,11 @@ async function joinAndSwitchToChannel(channelName) { await loadChannels(); switchToChannel(data.channel.index, channelName); } else { - showNotification('Failed to join channel: ' + data.error, 'danger'); + showNotification(t('channels.toast.join_failed', { error: data.error }), 'danger'); } } catch (error) { console.error('Error joining channel via link:', error); - showNotification('Failed to join channel', 'danger'); + showNotification(t('channels.toast.join_error'), 'danger'); } } diff --git a/app/translations/en.json b/app/translations/en.json index 5be3f34..70d546a 100644 --- a/app/translations/en.json +++ b/app/translations/en.json @@ -13,8 +13,12 @@ "backup.size": "Current size: {size}", "backup.title": "Database Backup", "channels.add_new": "Add New Channel", + "channels.confirm.remove": "Remove channel \"{name}\"?", "channels.create_btn": "Create & Auto-generate Key", "channels.create_title": "Create New Channel", + "channels.dropdown.no_matches": "No matches", + "channels.dropdown.none": "No channels", + "channels.fav_title": "Favorite channel", "channels.join_btn": "Join Channel", "channels.join_existing": "Join Existing", "channels.join_title": "Join Existing Channel", @@ -22,6 +26,10 @@ "channels.key_label": "Channel Key (32 hex chars)", "channels.key_optional": "- optional for channels starting with #", "channels.key_ph": "485af7e164459d280d8818d9c99fb30d (leave empty for # channels)", + "channels.list.empty": "No channels configured", + "channels.list.load_error": "Error loading channels", + "channels.list.load_failed": "Failed to load channels", + "channels.mute_title": "Mute notifications", "channels.name": "Channel Name", "channels.name_hint": "Only letters, numbers, _ and -", "channels.name_ph": "e.g., Malopolska", @@ -29,12 +37,44 @@ "channels.region_hint": "Only repeaters allowing the selected region will forward messages from this channel.", "channels.region_scope_for": "Set Region Scope for", "channels.scan_qr": "Scan QR Code", + "channels.scope_set_title": "Set region scope", + "channels.scope_title": "Region: {name} — click to change", "channels.share_hint": "Share this QR code or key with others to let them join this channel.", + "channels.share_name": "Channel: {name}", "channels.share_title": "Share Channel", + "channels.toast.already_joined": "Already joined channel \"{name}\".", + "channels.toast.copy_failed": "Failed to copy to clipboard", + "channels.toast.create_error": "Failed to create channel", + "channels.toast.create_failed": "Failed to create channel: {error}", + "channels.toast.created": "Channel \"{name}\" created!", + "channels.toast.exists": "Channel \"{name}\" already exists.", + "channels.toast.favorite_failed": "Failed to update favorite state", + "channels.toast.join_error": "Failed to join channel", + "channels.toast.join_failed": "Failed to join channel: {error}", + "channels.toast.joined": "Joined channel \"{name}\"!", + "channels.toast.key_copied": "Channel key copied to clipboard!", + "channels.toast.key_invalid": "Invalid key format. Must be 32 hex characters.", + "channels.toast.key_required": "Channel key is required for channels not starting with #", + "channels.toast.mute_failed": "Failed to update mute state", + "channels.toast.qr_error": "Failed to generate QR code", + "channels.toast.qr_failed": "Failed to generate QR code: {error}", + "channels.toast.qr_soon": "QR scanning feature coming soon! For now, manually enter the channel details.", + "channels.toast.remove_error": "Failed to remove channel", + "channels.toast.remove_failed": "Failed to remove channel: {error}", + "channels.toast.removed": "Channel \"{name}\" removed", + "channels.toast.switched": "Switched to channel: {name}", + "channels.unfav_title": "Unfavorite channel", + "channels.unmute_title": "Unmute notifications", "channels.your": "Your Channels", "chat.channels": "Channels", + "chat.confirm.block": "Block {name}? Their messages will be hidden from chat.", "chat.copy_route_title": "Tap to copy route", + "chat.edit_title": "Edit message", "chat.emoji_title": "Insert emoji", + "chat.empty.no_messages": "No messages yet", + "chat.empty.no_messages_hint": "Send a message to get started!", + "chat.error.load_retry": "Will retry automatically", + "chat.error.load_title": "Could not load messages", "chat.fab.hide": "Hide buttons", "chat.fab.search_title": "Search Messages", "chat.fab.show": "Show buttons", @@ -43,14 +83,42 @@ "chat.filter.me_title": "Filter my messages", "chat.filter.no_matches": "No messages match \"{query}\"", "chat.filter.ph": "Filter messages...", + "chat.image_alt": "Image", + "chat.image_preview": "Image Preview", + "chat.image_preview_alt": "Preview", "chat.input_ph": "Type a message...", "chat.loading_messages": "Loading messages...", + "chat.msg.analyzer_title": "View in Analyzer", + "chat.msg.block_title": "Block {name}", + "chat.msg.echo_title": { + "one": "Heard by {count} repeater: {paths}", + "other": "Heard by {count} repeaters: {paths}" + }, + "chat.msg.ignore_title": "Ignore {name}", + "chat.msg.map_title": "Show on map", + "chat.msg.quote_title": "Quote", + "chat.msg.raw_resend_title": "Resend (rebroadcast same packet so unreached repeaters can pick it up)", + "chat.msg.reply_title": "Reply", "chat.region_none": "No region", "chat.region_set_title": "Click to set a region for this channel", "chat.region_title": "Click to change region for this channel", + "chat.route": "Route: {route}", + "chat.route_analyzer_title": "Show this route on the Path Analyzer map", "chat.route_hops": "Hops: {count}", + "chat.route_multi": "Route ({count}): {route}", "chat.scroll_bottom_title": "Scroll to bottom", "chat.title": "Chat", + "chat.toast.load_error": "Error loading messages: {error}", + "chat.toast.load_failed": "Failed to load messages", + "chat.toast.load_timeout": "Loading messages timed out — retrying...", + "chat.toast.refresh_failed": "Refresh failed", + "chat.toast.refreshed": "Messages refreshed", + "chat.toast.resend_failed": "Resend failed: {error}", + "chat.toast.resend_network_error": "Resend network error: {error}", + "chat.toast.resent": "Resent ({bytes} B) — waiting for echoes…", + "chat.toast.send_error": "Failed to send message", + "chat.toast.send_failed": "Failed to send: {error}", + "chat.toast.sent": "Message sent", "chat.updated": "Updated: {time}", "common.add": "Add", "common.back": "Back", @@ -61,6 +129,7 @@ "common.connecting": "Connecting...", "common.copied": "Copied!", "common.copy": "Copy", + "common.copy_route": "Copy route", "common.days_ago": { "one": "{count}d ago", "other": "{count}d ago" @@ -103,6 +172,7 @@ "common.save_failed": "Save failed", "common.saving": "Saving...", "common.settings": "Settings", + "common.share": "Share", "common.try_again": "Try again", "common.unknown": "Unknown", "common.unknown_error": "Unknown error", @@ -292,8 +362,6 @@ "dm.dropdown.no_contacts": "No contacts available", "dm.dropdown.no_matches": "No matches", "dm.dropdown.recent": "Recent conversations", - "dm.edit_title": "Edit message", - "dm.empty.no_messages": "No messages yet", "dm.empty.no_messages_hint": "Send a message to start the conversation", "dm.empty.select": "Select a conversation", "dm.empty.select_hint": "Choose from the list or start a new chat from channel messages", @@ -301,14 +369,12 @@ "dm.keep_path": "Keep path", "dm.keep_path_title": "Keep path: don't auto-reset to FLOOD after failed retries", "dm.load_messages_error": "Error loading messages", - "dm.load_messages_failed": "Failed to load messages", "dm.msg_input_ph": "Message {name}...", "dm.no_contact_info": "No contact information available.", "dm.notify.new_messages": { "one": "New private message: {count}", "other": "New private messages: {count}" }, - "dm.route": "Route: {route}", "dm.select_chat_ph": "Select chat...", "dm.sidebar.search_ph": "Search contacts...", "dm.status.delivered": "Delivered", @@ -321,9 +387,6 @@ "dm.toast.import_failed": "Import failed", "dm.toast.keep_path_off": "Keep path disabled", "dm.toast.keep_path_on": "Keep path enabled", - "dm.toast.send_error": "Failed to send message", - "dm.toast.send_failed": "Failed to send: {error}", - "dm.toast.sent": "Message sent", "logs.clear_title": "Clear display", "logs.entries": { "one": "{count} entry", @@ -342,6 +405,10 @@ "logs.pause_title": "Pause/Resume", "logs.title": "System Log", "map.all_contacts": "All Contacts", + "map.cache": "Cache", + "map.cached_label": "{type} (cached)", + "map.own_device": "Own device", + "map.this_device": "This device", "menu.advert": "Send Advert", "menu.advert_desc": "Announce presence (normal)", "menu.advert_title": "Send single advertisement (recommended for normal operation)", @@ -349,6 +416,7 @@ "menu.backup_desc": "Database backup & restore", "menu.channels": "Manage Channels", "menu.check_updates_title": "Check for updates", + "menu.confirm.flood_advert": "Flood Advertisement uses high airtime and should only be used for network recovery.\n\nAre you sure you want to proceed?", "menu.console_desc": "Direct meshcli commands", "menu.contacts_desc": "Manage known contacts", "menu.device_info": "Device Info", diff --git a/app/translations/pl.json b/app/translations/pl.json index 0f9803e..b0007e7 100644 --- a/app/translations/pl.json +++ b/app/translations/pl.json @@ -13,8 +13,12 @@ "backup.size": "Bieżący rozmiar: {size}", "backup.title": "Kopia zapasowa bazy danych", "channels.add_new": "Dodaj nowy kanał", + "channels.confirm.remove": "Usunąć kanał \"{name}\"?", "channels.create_btn": "Utwórz i wygeneruj klucz", "channels.create_title": "Utwórz nowy kanał", + "channels.dropdown.no_matches": "Brak wyników", + "channels.dropdown.none": "Brak kanałów", + "channels.fav_title": "Dodaj kanał do ulubionych", "channels.join_btn": "Dołącz do kanału", "channels.join_existing": "Dołącz do istniejącego", "channels.join_title": "Dołącz do istniejącego kanału", @@ -22,6 +26,10 @@ "channels.key_label": "Klucz kanału (32 znaki hex)", "channels.key_optional": "- opcjonalny dla kanałów zaczynających się od #", "channels.key_ph": "485af7e164459d280d8818d9c99fb30d (zostaw puste dla kanałów #)", + "channels.list.empty": "Brak skonfigurowanych kanałów", + "channels.list.load_error": "Błąd wczytywania kanałów", + "channels.list.load_failed": "Nie udało się wczytać kanałów", + "channels.mute_title": "Wycisz powiadomienia", "channels.name": "Nazwa kanału", "channels.name_hint": "Tylko litery, cyfry, _ i -", "channels.name_ph": "np. Malopolska", @@ -29,12 +37,44 @@ "channels.region_hint": "Tylko repeatery zezwalające na wybrany region będą przekazywać wiadomości z tego kanału.", "channels.region_scope_for": "Ustaw zakres regionu dla", "channels.scan_qr": "Zeskanuj kod QR", + "channels.scope_set_title": "Ustaw zasięg regionu", + "channels.scope_title": "Region: {name} — kliknij, aby zmienić", "channels.share_hint": "Udostępnij ten kod QR lub klucz innym, aby mogli dołączyć do tego kanału.", + "channels.share_name": "Kanał: {name}", "channels.share_title": "Udostępnij kanał", + "channels.toast.already_joined": "Już dołączono do kanału \"{name}\".", + "channels.toast.copy_failed": "Nie udało się skopiować do schowka", + "channels.toast.create_error": "Nie udało się utworzyć kanału", + "channels.toast.create_failed": "Nie udało się utworzyć kanału: {error}", + "channels.toast.created": "Kanał \"{name}\" utworzony!", + "channels.toast.exists": "Kanał \"{name}\" już istnieje.", + "channels.toast.favorite_failed": "Nie udało się zmienić ulubionych", + "channels.toast.join_error": "Nie udało się dołączyć do kanału", + "channels.toast.join_failed": "Nie udało się dołączyć do kanału: {error}", + "channels.toast.joined": "Dołączono do kanału \"{name}\"!", + "channels.toast.key_copied": "Klucz kanału skopiowany do schowka!", + "channels.toast.key_invalid": "Nieprawidłowy format klucza. Wymagane 32 znaki szesnastkowe.", + "channels.toast.key_required": "Klucz kanału jest wymagany dla kanałów nierozpoczynających się od #", + "channels.toast.mute_failed": "Nie udało się zmienić wyciszenia", + "channels.toast.qr_error": "Nie udało się wygenerować kodu QR", + "channels.toast.qr_failed": "Nie udało się wygenerować kodu QR: {error}", + "channels.toast.qr_soon": "Skanowanie kodu QR będzie dostępne wkrótce! Na razie wprowadź dane kanału ręcznie.", + "channels.toast.remove_error": "Nie udało się usunąć kanału", + "channels.toast.remove_failed": "Nie udało się usunąć kanału: {error}", + "channels.toast.removed": "Kanał \"{name}\" usunięty", + "channels.toast.switched": "Przełączono na kanał: {name}", + "channels.unfav_title": "Usuń kanał z ulubionych", + "channels.unmute_title": "Włącz powiadomienia", "channels.your": "Twoje kanały", "chat.channels": "Kanały", + "chat.confirm.block": "Zablokować {name}? Wiadomości tego kontaktu będą ukryte w czacie.", "chat.copy_route_title": "Dotknij, aby skopiować trasę", + "chat.edit_title": "Edytuj wiadomość", "chat.emoji_title": "Wstaw emoji", + "chat.empty.no_messages": "Brak wiadomości", + "chat.empty.no_messages_hint": "Wyślij wiadomość, aby zacząć!", + "chat.error.load_retry": "Ponowna próba nastąpi automatycznie", + "chat.error.load_title": "Nie można wczytać wiadomości", "chat.fab.hide": "Ukryj przyciski", "chat.fab.search_title": "Szukaj w wiadomościach", "chat.fab.show": "Pokaż przyciski", @@ -43,14 +83,44 @@ "chat.filter.me_title": "Filtruj moje wiadomości", "chat.filter.no_matches": "Brak wiadomości pasujących do „{query}”", "chat.filter.ph": "Filtruj wiadomości...", + "chat.image_alt": "Obraz", + "chat.image_preview": "Podgląd obrazu", + "chat.image_preview_alt": "Podgląd", "chat.input_ph": "Napisz wiadomość...", "chat.loading_messages": "Wczytywanie wiadomości...", + "chat.msg.analyzer_title": "Pokaż w analizatorze", + "chat.msg.block_title": "Zablokuj {name}", + "chat.msg.echo_title": { + "one": "Usłyszane przez {count} repeater: {paths}", + "few": "Usłyszane przez {count} repeatery: {paths}", + "many": "Usłyszane przez {count} repeaterów: {paths}", + "other": "Usłyszane przez {count} repeatera: {paths}" + }, + "chat.msg.ignore_title": "Ignoruj {name}", + "chat.msg.map_title": "Pokaż na mapie", + "chat.msg.quote_title": "Cytuj", + "chat.msg.raw_resend_title": "Wyślij ponownie (rozgłoś ten sam pakiet, aby odebrały go repeatery, które go nie usłyszały)", + "chat.msg.reply_title": "Odpowiedz", "chat.region_none": "Bez regionu", "chat.region_set_title": "Kliknij, aby ustawić region dla tego kanału", "chat.region_title": "Kliknij, aby zmienić region dla tego kanału", + "chat.route": "Trasa: {route}", + "chat.route_analyzer_title": "Pokaż tę trasę na mapie Analizatora ścieżek", "chat.route_hops": "Hopy: {count}", + "chat.route_multi": "Trasa ({count}): {route}", "chat.scroll_bottom_title": "Przewiń na dół", "chat.title": "Czat", + "chat.toast.load_error": "Błąd wczytywania wiadomości: {error}", + "chat.toast.load_failed": "Nie udało się wczytać wiadomości", + "chat.toast.load_timeout": "Przekroczono czas wczytywania wiadomości — ponawiam...", + "chat.toast.refresh_failed": "Odświeżanie nie powiodło się", + "chat.toast.refreshed": "Wiadomości odświeżone", + "chat.toast.resend_failed": "Ponowne wysłanie nie powiodło się: {error}", + "chat.toast.resend_network_error": "Błąd sieci przy ponownym wysłaniu: {error}", + "chat.toast.resent": "Wysłano ponownie ({bytes} B) — czekam na echa…", + "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.updated": "Aktualizacja: {time}", "common.add": "Dodaj", "common.back": "Wstecz", @@ -61,6 +131,7 @@ "common.connecting": "Łączenie...", "common.copied": "Skopiowano!", "common.copy": "Kopiuj", + "common.copy_route": "Kopiuj trasę", "common.days_ago": { "one": "{count} dzień temu", "few": "{count} dni temu", @@ -113,6 +184,7 @@ "common.save_failed": "Zapis nie powiódł się", "common.saving": "Zapisywanie...", "common.settings": "Ustawienia", + "common.share": "Udostępnij", "common.try_again": "Spróbuj ponownie", "common.unknown": "Nieznany", "common.unknown_error": "Nieznany błąd", @@ -310,8 +382,6 @@ "dm.dropdown.no_contacts": "Brak dostępnych kontaktów", "dm.dropdown.no_matches": "Brak wyników", "dm.dropdown.recent": "Ostatnie rozmowy", - "dm.edit_title": "Edytuj wiadomość", - "dm.empty.no_messages": "Brak wiadomości", "dm.empty.no_messages_hint": "Wyślij wiadomość, aby rozpocząć rozmowę", "dm.empty.select": "Wybierz rozmowę", "dm.empty.select_hint": "Wybierz z listy lub rozpocznij nową rozmowę z wiadomości kanałowych", @@ -319,7 +389,6 @@ "dm.keep_path": "Zachowaj ścieżkę", "dm.keep_path_title": "Zachowaj ścieżkę: nie przywracaj automatycznie trybu FLOOD po nieudanych próbach", "dm.load_messages_error": "Błąd wczytywania wiadomości", - "dm.load_messages_failed": "Nie udało się wczytać wiadomości", "dm.msg_input_ph": "Wiadomość do {name}...", "dm.no_contact_info": "Brak informacji o kontakcie.", "dm.notify.new_messages": { @@ -328,7 +397,6 @@ "many": "Nowych wiadomości prywatnych: {count}", "other": "Nowe wiadomości prywatne: {count}" }, - "dm.route": "Trasa: {route}", "dm.select_chat_ph": "Wybierz rozmowę...", "dm.sidebar.search_ph": "Szukaj kontaktów...", "dm.status.delivered": "Dostarczono", @@ -341,9 +409,6 @@ "dm.toast.import_failed": "Import nieudany", "dm.toast.keep_path_off": "Zachowywanie ścieżki wyłączone", "dm.toast.keep_path_on": "Zachowywanie ścieżki włączone", - "dm.toast.send_error": "Nie udało się wysłać wiadomości", - "dm.toast.send_failed": "Nie udało się wysłać: {error}", - "dm.toast.sent": "Wiadomość wysłana", "logs.clear_title": "Wyczyść widok", "logs.entries": { "one": "{count} wpis", @@ -366,6 +431,10 @@ "logs.pause_title": "Wstrzymaj/Wznów", "logs.title": "Dziennik systemowy", "map.all_contacts": "Wszystkie kontakty", + "map.cache": "Pamięć", + "map.cached_label": "{type} (z pamięci)", + "map.own_device": "Własne urządzenie", + "map.this_device": "To urządzenie", "menu.advert": "Wyślij advert", "menu.advert_desc": "Ogłoś obecność (normalnie)", "menu.advert_title": "Wyślij pojedynczy advert (zalecane przy normalnej pracy)", @@ -373,6 +442,7 @@ "menu.backup_desc": "Kopia i przywracanie bazy danych", "menu.channels": "Zarządzaj kanałami", "menu.check_updates_title": "Sprawdź aktualizacje", + "menu.confirm.flood_advert": "Flood advert zużywa dużo czasu antenowego i powinien być używany wyłącznie do odbudowy sieci.\n\nCzy na pewno chcesz kontynuować?", "menu.console_desc": "Bezpośrednie komendy meshcli", "menu.contacts_desc": "Zarządzaj znanymi kontaktami", "menu.device_info": "Informacje o urządzeniu", diff --git a/docs/whatsnew.md b/docs/whatsnew.md index 3fc0dd7..1d32ef2 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, and its dialogs — plus every panel that opens in its own window: **System Log**, **Console**, **My Repeaters**, **Path Analyzer**, **Contacts** and **Direct Messages**. The **Settings** dialog and many 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. 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. - **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.