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 ? `
-