`;
@@ -328,7 +331,7 @@
// Tabs drawn from the bulk load; the other tabs fetch their own data.
var BULK_TABS = new Set(['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions']);
function loadErrorHtml(msg) {
- return `
Failed to load: ${esc(msg)}
`;
+ return `
${tr('Failed to load: {message}', { message: esc(msg) })}
`;
}
async function loadAnalytics() {
@@ -382,7 +385,7 @@
// (or error) state instead of drawing from missing data (#2200).
if (BULK_TABS.has(tab) && !d.rfData) {
el.innerHTML = _loadError ? loadErrorHtml(_loadError)
- : '
Loading analytics…
';
+ : '
' + tr('Loading analytics…') + '
';
return;
}
switch (tab) {
@@ -437,45 +440,45 @@
${(rf.totalTransmissions || rf.totalAllPackets || rf.totalPackets).toLocaleString()}
-
Total Transmissions
+
${tr('Total Transmissions')}
${sparkSvg(rf.packetsPerHour.map(h=>h.count), 'var(--accent)')}
${rf.totalPackets.toLocaleString()}
-
Observations with Signal
+
${tr('Observations with Signal')}
${topo.uniqueNodes}
-
Unique Nodes
+
${tr('Unique Nodes')}
${sf(rf.snr.avg, 1)} dB
-
Avg SNR
-
${sf(rf.snr.min, 1)} to ${sf(rf.snr.max, 1)}
+
${tr('Avg SNR')}
+
${tr('{min} to {max}', { min: sf(rf.snr.min, 1), max: sf(rf.snr.max, 1) })}
${sf(rf.rssi.avg, 0)} dBm
-
Avg RSSI
-
${rf.rssi.min} to ${rf.rssi.max}
+
${tr('Avg RSSI')}
+
${tr('{min} to {max}', { min: rf.rssi.min, max: rf.rssi.max })}
${sf(topo.avgHops, 1)}
-
Avg Hops
-
max ${topo.maxHops}
+
${tr('Avg Hops')}
+
${tr('max {count}', { count: topo.maxHops })}
${ch.activeChannels}
-
Active Channels
-
${ch.decryptable} decryptable
+
${tr('Active Channels')}
+
${tr('{count} decryptable', { count: ch.decryptable })}
${rf.avgPacketSize} B
-
Avg Packet Size
+
${tr('Avg Packet Size')}
${rf.minPacketSize}–${rf.maxPacketSize} B
${((rf.timeSpanHours || 1)).toFixed(1)}h
-
Data Span
+
${tr('Data Span')}
@@ -485,19 +488,19 @@
-
Payload Type Mix
+ ${tr('Payload Type Mix')}
${renderPayloadPie(rf.payloadTypes)}
-
Hop Count Distribution
+ ${tr('Hop Count Distribution')}
${barChart(topo.hopDistribution.map(h=>h.count), topo.hopDistribution.map(h=>h.hops), ['#3b82f6'])}
-
Relay Airtime Share
-
Score = LoRa Time-on-Air × distinct repeaters that forwarded the packet (issue #1768). Counts relay re-transmissions; originator TX excluded. Not comparable across meshes — see preset caption below.
+
${tr('Relay Airtime Share')}
+
${tr('Score = LoRa Time-on-Air × distinct repeaters that forwarded the packet (issue #1768). Counts relay re-transmissions; originator TX excluded. Not comparable across meshes — see preset caption below.')}
${renderRelayAirtimeDumbbell(d.airtimeData)}
@@ -518,16 +521,16 @@
var widget = document.createElement('div');
widget.className = 'analytics-row';
widget.innerHTML = '
' +
- '
Neighbor Affinity Graph
' +
+ '
' + tr('Neighbor Affinity Graph') + '
' +
'
' +
- '
' + s.totalEdges + '
Total Edges
' +
- '
' + s.totalNodes + '
Total Nodes
' +
- '
' + s.resolvedCount + ' (' + resolvedPct + '%)
Resolved Prefixes
' +
- '
' + s.ambiguousCount + ' (' + ambiguousPct + '%)
Ambiguous Prefixes
' +
- '
' + (s.avgConfidence || 0).toFixed(3) + '
Avg Confidence
' +
- '
' + (s.coldStartCoverage || 0).toFixed(1) + '%
Cold-Start Coverage
' +
- '
' + (s.cacheAge || 'N/A') + '
Cache Age
' +
- '
' + (s.lastRebuild ? s.lastRebuild.substring(0, 19) : 'N/A') + '
Last Rebuild
' +
+ '
' + s.totalEdges + '
' + tr('Total Edges') + '
' +
+ '
' + s.totalNodes + '
' + tr('Total Nodes') + '
' +
+ '
' + s.resolvedCount + ' (' + resolvedPct + '%)
' + tr('Resolved Prefixes') + '
' +
+ '
' + s.ambiguousCount + ' (' + ambiguousPct + '%)
' + tr('Ambiguous Prefixes') + '
' +
+ '
' + (s.avgConfidence || 0).toFixed(3) + '
' + tr('Avg Confidence') + '
' +
+ '
' + (s.coldStartCoverage || 0).toFixed(1) + '%
' + tr('Cold-Start Coverage') + '
' +
+ '
' + (s.cacheAge || tr('N/A')) + '
' + tr('Cache Age') + '
' +
+ '
' + (s.lastRebuild ? s.lastRebuild.substring(0, 19) : tr('N/A')) + '
' + tr('Last Rebuild') + '
' +
'
';
el.appendChild(widget);
})
@@ -559,11 +562,11 @@
function renderRelayAirtimeDumbbell(data) {
var rows = (data && Array.isArray(data.rows)) ? data.rows : [];
if (!rows.length) {
- return '
No relay-airtime data in this window.
';
+ return '
' + tr('No relay-airtime data in this window.') + '
';
}
var totalScore = (data && typeof data.total_score === 'number') ? data.total_score : 0;
if (totalScore <= 0) {
- return '
No relay activity observed in this window.
';
+ return '
' + tr('No relay activity observed in this window.') + '
';
}
// Issue #1768 — surface the LoRa preset baked into the ToA score. Share
// numbers are only meaningful relative to one PHY preset; operators must
@@ -580,12 +583,12 @@
var bwStr = bwKhz ? bwKhz.toFixed(1).replace(/\.0$/, '') : '0';
presetCaption =
'
' +
- 'Assumed LoRa preset: ' +
+ tr('Assumed LoRa preset:') + ' ' +
(freqMHz ? esc(freqMHz.toFixed(3)) + ' MHz / ' : '') +
'BW ' + esc(bwStr) + ' kHz / ' +
'SF ' + esc(String(Number(preset.sf || 0))) + ' / ' +
'CR 4/' + esc(String(Number(preset.cr || 0))) +
- ' (preamble ' + esc(String(Number(preset.preamble || 0))) + ' sym)' +
+ ' (' + tr('preamble {count} sym', { count: esc(String(Number(preset.preamble || 0))) }) + ')' +
'
';
}
// Layout: per row → label | track 0..100% | values
@@ -593,15 +596,15 @@
var html = '
';
if (presetCaption) html += presetCaption;
if (rows.some(function (r) { return r.payload_type === 'ADVERT'; })) {
- html += '
Known route evidence: Mixed adverts were observed as both flood and direct with an empty remaining path. An empty path does not prove an original zero-hop send. Other adverts have no classified evidence yet. Background backfill improves classification automatically. Available history is a lower bound; older overwritten observations cannot be recovered, and failed evidence writes may leave gaps.
';
+ html += '
' + tr('Known route evidence: Mixed adverts were observed as both flood and direct with an empty remaining path. An empty path does not prove an original zero-hop send. Other adverts have no classified evidence yet. Background backfill improves classification automatically. Available history is a lower bound; older overwritten observations cannot be recovered, and failed evidence writes may leave gaps.') + '
';
}
rows.forEach(function (r, i) {
var name = r.payload_type || 'UNK';
if (name === 'ADVERT') {
- if (r.advert_kind === 'flood') name = 'Flood adverts';
- else if (r.advert_kind === 'zero_hop') name = 'Direct adverts (empty path)';
- else if (r.advert_kind === 'mixed') name = 'Mixed adverts';
- else if (r.advert_kind === 'other') name = 'Other adverts';
+ if (r.advert_kind === 'flood') name = tr('Flood adverts');
+ else if (r.advert_kind === 'zero_hop') name = tr('Direct adverts (empty path)');
+ else if (r.advert_kind === 'mixed') name = tr('Mixed adverts');
+ else if (r.advert_kind === 'other') name = tr('Other adverts');
}
var cnt = Number(r.count || 0);
var cpct = Number(r.count_pct || 0);
@@ -620,20 +623,20 @@
: scoreMs.toFixed(2) + ' ms';
var tip =
name + '\n' +
- 'Count: ' + cnt.toLocaleString() + ' (' + cpct.toFixed(2) + '%)\n' +
- 'Airtime: ' + apct.toFixed(2) + '% (score ' + scoreStr + ' · airtime × repeaters)\n' +
- 'Score = LoRa Time-on-Air × distinct repeaters. Within-mesh only.';
+ tr('Count: {count} ({pct}%)', { count: cnt.toLocaleString(), pct: cpct.toFixed(2) }) + '\n' +
+ tr('Airtime: {pct}% (score {score} · airtime × repeaters)', { pct: apct.toFixed(2), score: scoreStr }) + '\n' +
+ tr('Score = LoRa Time-on-Air × distinct repeaters. Within-mesh only.');
html += '
' +
'
' + esc(name) + '
' +
'
' +
'
' +
- '
' +
- '
' +
+ '
' +
+ '
' +
'
' +
'
' +
- 'cnt ' + cpct.toFixed(1) + '%' +
+ '' + tr('cnt {pct}%', { pct: cpct.toFixed(1) }) + '' +
' · ' +
- 'air ' + apct.toFixed(1) + '%' +
+ '' + tr('air {pct}%', { pct: apct.toFixed(1) }) + '' +
'
' +
'
';
});
@@ -643,7 +646,7 @@
'
0%50%100%
' +
'
' +
'
';
- html += '
count % airtime %
';
+ html += '
' + tr('count %') + ' ' + tr('airtime %') + '
';
html += '
';
return html;
}
@@ -656,26 +659,26 @@
el.innerHTML = `
-
SNR Distribution
-
Signal-to-Noise Ratio (higher = cleaner signal)
+
${tr('SNR Distribution')}
+
${tr('Signal-to-Noise Ratio (higher = cleaner signal)')}
${snrHist.svg}
- Min: ${sf(rf.snr.min, 1)} dB
- Mean: ${sf(rf.snr.avg, 1)} dB
- Median: ${sf(rf.snr.median, 1)} dB
- Max: ${sf(rf.snr.max, 1)} dB
+ ${tr('Min:')} ${sf(rf.snr.min, 1)} dB
+ ${tr('Mean:')} ${sf(rf.snr.avg, 1)} dB
+ ${tr('Median:')} ${sf(rf.snr.median, 1)} dB
+ ${tr('Max:')} ${sf(rf.snr.max, 1)} dB
σ: ${sf(rf.snr.stddev, 1)} dB
-
RSSI Distribution
-
Received Signal Strength (closer to 0 = stronger)
+
${tr('RSSI Distribution')}
+
${tr('Received Signal Strength (closer to 0 = stronger)')}
${rssiHist.svg}
- Min: ${rf.rssi.min} dBm
- Mean: ${sf(rf.rssi.avg, 0)} dBm
- Median: ${rf.rssi.median} dBm
- Max: ${rf.rssi.max} dBm
+ ${tr('Min:')} ${rf.rssi.min} dBm
+ ${tr('Mean:')} ${sf(rf.rssi.avg, 0)} dBm
+ ${tr('Median:')} ${rf.rssi.median} dBm
+ ${tr('Max:')} ${rf.rssi.max} dBm
σ: ${sf(rf.rssi.stddev, 1)} dBm
@@ -683,31 +686,31 @@
-
SNR vs RSSI Scatter
-
Each dot = one packet. Cluster position reveals link quality.
+
${tr('SNR vs RSSI Scatter')}
+
${tr('Each dot = one packet. Cluster position reveals link quality.')}
${renderScatter(rf.scatterData)}
-
SNR by Payload Type
+ ${tr('SNR by Payload Type')}
${renderSNRByType(rf.snrByType)}
-
Signal Quality Over Time
+ ${tr('Signal Quality Over Time')}
${renderSignalTimeline(rf.signalOverTime)}
-
Packet Size Distribution
-
Raw packet length in bytes
+
${tr('Packet Size Distribution')}
+
${tr('Raw packet length in bytes')}
${histogram(rf.packetSizes, 25, '#8b5cf6').svg}
- Min: ${rf.minPacketSize} B
- Avg: ${rf.avgPacketSize} B
- Max: ${rf.maxPacketSize} B
+ ${tr('Min:')} ${rf.minPacketSize} B
+ ${tr('Avg:')} ${rf.avgPacketSize} B
+ ${tr('Max:')} ${rf.maxPacketSize} B
`;
@@ -716,7 +719,7 @@
function renderScatter(data) {
const w = 600, h = 300, pad = 40;
const snrMin = -12, snrMax = 15, rssiMin = -130, rssiMax = -5;
- let svg = `