mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-06 16:53:21 +02:00
feat(i18n): translate the Path Analyzer panel (stage 4)
~90 keys across path-analyzer.html and path-analyzer.js. Catalog now 333 keys, pl at 100%. Covers the four view buttons, the whole filter bar, all four table layouts with their explanatory column tooltips, the map sidebar, and the per-view counters. The counters were the fiddly part: four of them, each combining a filtered and an unfiltered form, and each mixing two independently pluralised nouns — "12 repeaters (340 of 977 messages)". They are now composed from tn() parts, so Polish gets "12 repeaterów (340 z 977 wiadomości)" with the right form on both. Not translated, deliberately: message text, sender names, channel names, packet and repeater hashes, SNR figures, and the hop chains — all device or user data. The diff tool's exclusions for this page name the four table bodies that carry it. The diff caught four filter options I had skipped (1-byte / 2-byte / 3-byte / 2/3-byte) because my template edit only replaced the "Any HB" line above them. Its one remaining hit is "1 hop", which is correct: Polish "one" is identical to the English singular here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -196,15 +196,15 @@ function paFormatTime(msg) {
|
||||
|
||||
function paCopyText(text, label) {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => showNotification(`${label} copied to clipboard`, 'success'),
|
||||
() => showNotification(`Failed to copy ${label}`, 'danger')
|
||||
() => showNotification(t('pa.toast.copied', { what: label }), 'success'),
|
||||
() => showNotification(t('pa.toast.copy_failed', { what: label }), 'danger')
|
||||
);
|
||||
}
|
||||
|
||||
function paBuildEchoLine(msg, echo, echoIdx) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'pa-echo-line';
|
||||
line.title = 'Click to copy route';
|
||||
line.title = t('pa.copy_route_title');
|
||||
|
||||
const dirBadge = document.createElement('span');
|
||||
dirBadge.className = 'badge ' + (echo.direction === 'outgoing' ? 'text-bg-primary' : 'text-bg-secondary');
|
||||
@@ -214,7 +214,7 @@ function paBuildEchoLine(msg, echo, echoIdx) {
|
||||
if (echo.hops === 0) {
|
||||
const direct = document.createElement('span');
|
||||
direct.className = 'pa-direct';
|
||||
direct.textContent = 'Direct (flood, 0 hops)';
|
||||
direct.textContent = t('pa.direct_flood');
|
||||
line.appendChild(direct);
|
||||
} else {
|
||||
echo.tokens.forEach((tok, i) => {
|
||||
@@ -229,7 +229,7 @@ function paBuildEchoLine(msg, echo, echoIdx) {
|
||||
chip.title = `Copy repeater hash ${tok}`;
|
||||
chip.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
paCopyText(tok, 'Repeater hash');
|
||||
paCopyText(tok, t('pa.repeater_hash'));
|
||||
});
|
||||
line.appendChild(chip);
|
||||
});
|
||||
@@ -244,7 +244,7 @@ function paBuildEchoLine(msg, echo, echoIdx) {
|
||||
if (echo.hops > 0) {
|
||||
const mapBtn = document.createElement('i');
|
||||
mapBtn.className = 'bi bi-map pa-echo-mapbtn';
|
||||
mapBtn.title = 'Show this path on the map';
|
||||
mapBtn.title = t('pa.show_on_map_title');
|
||||
mapBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
paMapSelection = { msgId: msg.id, echoIdx: echoIdx };
|
||||
@@ -286,7 +286,9 @@ function paRenderTable() {
|
||||
tr.appendChild(tdChannel);
|
||||
|
||||
const tdSender = document.createElement('td');
|
||||
tdSender.textContent = msg.is_own ? `${msg.sender || 'Me'} (own)` : (msg.sender || '—');
|
||||
tdSender.textContent = msg.is_own
|
||||
? t('pa.sender_own', { name: msg.sender || t('pa.me') })
|
||||
: (msg.sender || '—');
|
||||
tr.appendChild(tdSender);
|
||||
|
||||
const tdContent = document.createElement('td');
|
||||
@@ -303,14 +305,14 @@ function paRenderTable() {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'pa-hash';
|
||||
span.textContent = msg.packet_hash;
|
||||
span.title = 'Click to copy';
|
||||
span.title = t('pa.copy_title');
|
||||
span.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
paCopyText(msg.packet_hash, 'Packet hash');
|
||||
paCopyText(msg.packet_hash, t('pa.packet_hash'));
|
||||
});
|
||||
tdHash.appendChild(span);
|
||||
} else {
|
||||
tdHash.innerHTML = '<span class="text-muted small">no path data</span>';
|
||||
tdHash.innerHTML = `<span class="text-muted small">${tHtml('pa.no_path_data')}</span>`;
|
||||
}
|
||||
tr.appendChild(tdHash);
|
||||
|
||||
@@ -345,10 +347,10 @@ function paRenderTable() {
|
||||
const hashSpan = document.createElement('span');
|
||||
hashSpan.className = 'pa-hash';
|
||||
hashSpan.textContent = msg.packet_hash;
|
||||
hashSpan.title = 'Click to copy';
|
||||
hashSpan.title = t('pa.copy_title');
|
||||
hashSpan.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
paCopyText(msg.packet_hash, 'Packet hash');
|
||||
paCopyText(msg.packet_hash, t('pa.packet_hash'));
|
||||
});
|
||||
hashLine.append('Hash: ');
|
||||
hashLine.appendChild(hashSpan);
|
||||
@@ -370,13 +372,13 @@ function paRenderTable() {
|
||||
|
||||
const counter = document.getElementById('paCounter');
|
||||
counter.textContent = paFiltersActive()
|
||||
? `${filtered.length} of ${paMessages.length} messages`
|
||||
: `${paMessages.length} message${paMessages.length === 1 ? '' : 's'}`;
|
||||
? t('pa.counter.filtered', { shown: filtered.length, total: paMessages.length })
|
||||
: tn('pa.counter.messages', paMessages.length);
|
||||
|
||||
// Empty state when filters exclude everything
|
||||
if (paMessages.length > 0) {
|
||||
if (filtered.length === 0) {
|
||||
document.getElementById('paEmptyText').textContent = 'No messages match the current filters.';
|
||||
document.getElementById('paEmptyText').textContent = t('pa.empty_filtered');
|
||||
paSetView('empty');
|
||||
} else {
|
||||
paSetView('table');
|
||||
@@ -495,13 +497,13 @@ function paRenderStats() {
|
||||
|
||||
const counter = document.getElementById('paCounter');
|
||||
counter.textContent = paFiltersActive()
|
||||
? `${rows.length} repeaters (${filtered.length} of ${paMessages.length} messages)`
|
||||
: `${rows.length} repeaters (${paMessages.length} messages)`;
|
||||
? `${tn('pa.counter.repeaters', rows.length)} (${t('pa.counter.filtered', { shown: filtered.length, total: paMessages.length })})`
|
||||
: `${tn('pa.counter.repeaters', rows.length)} (${tn('pa.counter.messages', paMessages.length)})`;
|
||||
|
||||
if (rows.length === 0) {
|
||||
document.getElementById('paEmptyText').textContent =
|
||||
filtered.length === 0 ? 'No messages match the current filters.'
|
||||
: 'No routed echoes in the current selection.';
|
||||
filtered.length === 0 ? t('pa.empty_filtered')
|
||||
: t('pa.empty_no_echoes');
|
||||
paSetView('empty');
|
||||
} else {
|
||||
paSetView('stats');
|
||||
@@ -561,7 +563,7 @@ function paRenderRoutes() {
|
||||
for (const r of rows) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'pa-stats-row';
|
||||
tr.title = 'Filter messages by this segment';
|
||||
tr.title = t('pa.filter_by_segment_title');
|
||||
|
||||
const tdRoute = document.createElement('td');
|
||||
const hashLine = document.createElement('div');
|
||||
@@ -602,12 +604,12 @@ function paRenderRoutes() {
|
||||
|
||||
const counter = document.getElementById('paCounter');
|
||||
counter.textContent = paFiltersActive()
|
||||
? `${rows.length} segments (${filtered.length} of ${paMessages.length} messages)`
|
||||
: `${rows.length} segments (${paMessages.length} messages)`;
|
||||
? `${tn('pa.counter.segments', rows.length)} (${t('pa.counter.filtered', { shown: filtered.length, total: paMessages.length })})`
|
||||
: `${tn('pa.counter.segments', rows.length)} (${tn('pa.counter.messages', paMessages.length)})`;
|
||||
|
||||
if (rows.length === 0) {
|
||||
document.getElementById('paEmptyText').textContent =
|
||||
filtered.length === 0 ? 'No messages match the current filters.'
|
||||
filtered.length === 0 ? t('pa.empty_filtered')
|
||||
: `No paths with at least ${n} hops in the current selection.`;
|
||||
paSetView('empty');
|
||||
} else {
|
||||
@@ -850,8 +852,8 @@ function paBuildLegend(echo) {
|
||||
resetRow.className = 'text-end';
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.className = 'btn btn-sm btn-outline-warning py-0 pa-reset-picks';
|
||||
resetBtn.innerHTML = '<i class="bi bi-arrow-counterclockwise"></i> Reset picks';
|
||||
resetBtn.title = 'Clear all manual repeater assignments on this path';
|
||||
resetBtn.innerHTML = `<i class="bi bi-arrow-counterclockwise"></i> ${tHtml('pa.reset_picks')}`;
|
||||
resetBtn.title = t('pa.reset_picks_title');
|
||||
resetBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
echo.tokens.forEach(tok => { delete paPicks[tok]; });
|
||||
@@ -876,7 +878,7 @@ function paBuildLegend(echo) {
|
||||
// Manually assigned - allow undoing just this hop
|
||||
const undo = document.createElement('i');
|
||||
undo.className = 'bi bi-x-circle pa-unpick';
|
||||
undo.title = 'Undo this manual assignment';
|
||||
undo.title = t('pa.undo_pick_title');
|
||||
undo.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
delete paPicks[tok];
|
||||
@@ -887,13 +889,13 @@ function paBuildLegend(echo) {
|
||||
} else if (candidates.length > 1) {
|
||||
const amb = document.createElement('span');
|
||||
amb.className = 'pa-ambiguous';
|
||||
amb.textContent = `— ${candidates.length} candidates:`;
|
||||
amb.textContent = '— ' + tn('pa.candidates', candidates.length) + ':';
|
||||
row.appendChild(amb);
|
||||
candidates.forEach(c => {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'pa-pick-chip' + (paPicks[tok] === c.public_key ? ' pa-picked' : '');
|
||||
chip.textContent = c.name || c.public_key.slice(0, 8);
|
||||
chip.title = 'Use this contact for the hop';
|
||||
chip.title = t('pa.use_contact_title');
|
||||
chip.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
paPicks[tok] = c.public_key;
|
||||
@@ -904,7 +906,7 @@ function paBuildLegend(echo) {
|
||||
} else {
|
||||
const unk = document.createElement('span');
|
||||
unk.className = 'pa-ambiguous';
|
||||
unk.textContent = '— unknown / no position';
|
||||
unk.textContent = t('pa.unknown_position');
|
||||
row.appendChild(unk);
|
||||
}
|
||||
legend.appendChild(row);
|
||||
@@ -944,7 +946,7 @@ function paRenderMapView(fit = true) {
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '<div class="small text-muted p-2">No messages with routed echoes match the current filters.</div>';
|
||||
list.innerHTML = `<div class="small text-muted p-2">${tHtml('pa.map.no_routed')}</div>`;
|
||||
}
|
||||
|
||||
for (const msg of filtered) {
|
||||
@@ -1032,8 +1034,8 @@ function paRenderMapView(fit = true) {
|
||||
|
||||
const counter = document.getElementById('paCounter');
|
||||
counter.textContent = paFiltersActive()
|
||||
? `${filtered.length} of ${paMessages.length} messages`
|
||||
: `${filtered.length} routed message${filtered.length === 1 ? '' : 's'}`;
|
||||
? t('pa.counter.filtered', { shown: filtered.length, total: paMessages.length })
|
||||
: tn('pa.counter.routed', filtered.length);
|
||||
}
|
||||
|
||||
function paClearMapSelection() {
|
||||
@@ -1086,12 +1088,12 @@ async function paLoadMessages() {
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load messages:', e);
|
||||
showNotification(`Failed to load messages: ${e.message}`, 'danger');
|
||||
showNotification(t('pa.toast.load_failed', { error: e.message }), 'danger');
|
||||
paMessages = [];
|
||||
}
|
||||
|
||||
if (paMessages.length === 0) {
|
||||
document.getElementById('paEmptyText').textContent = 'No messages in the selected time range.';
|
||||
document.getElementById('paEmptyText').textContent = t('pa.empty_range');
|
||||
paSetView('empty');
|
||||
} else {
|
||||
paRender();
|
||||
@@ -1118,7 +1120,7 @@ async function paApplyDeepLink() {
|
||||
return;
|
||||
}
|
||||
paDeepLink = null;
|
||||
showNotification('This message is no longer in the analyzer data (max 7 days).', 'warning');
|
||||
showNotification(t('pa.toast.msg_gone'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1129,7 +1131,7 @@ async function paApplyDeepLink() {
|
||||
e.hops > 0 && (e.path || '').toLowerCase() === (dl.path || '').toLowerCase());
|
||||
if (echoIdx === -1) echoIdx = paShortestEchoIdx(msg);
|
||||
if (echoIdx === null) {
|
||||
showNotification('This message has no routed echoes to draw.', 'warning');
|
||||
showNotification(t('pa.toast.no_echoes'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Path Analyzer - mc-webui</title>
|
||||
<title>{{ t('pa.title') }} - mc-webui</title>
|
||||
|
||||
<!-- Theme: apply saved preference before CSS loads to prevent flash -->
|
||||
<script>
|
||||
@@ -550,64 +550,64 @@
|
||||
<body>
|
||||
<!-- Toolbar -->
|
||||
<div class="pa-toolbar">
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="View">
|
||||
<button id="paViewMessagesBtn" class="btn btn-primary" title="Message list">
|
||||
<i class="bi bi-list-ul"></i><span class="d-none d-sm-inline"> Messages</span>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('pa.view_aria') }}">
|
||||
<button id="paViewMessagesBtn" class="btn btn-primary" title="{{ t('pa.view.messages_title') }}">
|
||||
<i class="bi bi-list-ul"></i><span class="d-none d-sm-inline"> {{ t('pa.view.messages') }}</span>
|
||||
</button>
|
||||
<button id="paViewStatsBtn" class="btn btn-outline-primary" title="Per-repeater statistics">
|
||||
<i class="bi bi-bar-chart"></i><span class="d-none d-sm-inline"> Repeaters</span>
|
||||
<button id="paViewStatsBtn" class="btn btn-outline-primary" title="{{ t('pa.view.repeaters_title') }}">
|
||||
<i class="bi bi-bar-chart"></i><span class="d-none d-sm-inline"> {{ t('pa.view.repeaters') }}</span>
|
||||
</button>
|
||||
<button id="paViewRoutesBtn" class="btn btn-outline-primary" title="Route segment statistics">
|
||||
<i class="bi bi-signpost-2"></i><span class="d-none d-sm-inline"> Routes</span>
|
||||
<button id="paViewRoutesBtn" class="btn btn-outline-primary" title="{{ t('pa.view.routes_title') }}">
|
||||
<i class="bi bi-signpost-2"></i><span class="d-none d-sm-inline"> {{ t('pa.view.routes') }}</span>
|
||||
</button>
|
||||
<button id="paViewMapBtn" class="btn btn-outline-primary" title="Map view">
|
||||
<i class="bi bi-map"></i><span class="d-none d-sm-inline"> Map</span>
|
||||
<button id="paViewMapBtn" class="btn btn-outline-primary" title="{{ t('pa.view.map_title') }}">
|
||||
<i class="bi bi-map"></i><span class="d-none d-sm-inline"> {{ t('pa.view.map') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button id="paFiltersToggleBtn" class="btn btn-sm btn-outline-secondary d-md-none" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#paFiltersPanel"
|
||||
aria-expanded="false" aria-controls="paFiltersPanel" title="Show/hide filters">
|
||||
<i class="bi bi-funnel"></i> Filters
|
||||
aria-expanded="false" aria-controls="paFiltersPanel" title="{{ t('pa.filters_toggle_title') }}">
|
||||
<i class="bi bi-funnel"></i> {{ t('pa.filters') }}
|
||||
<span id="paFiltersBadge" class="badge text-bg-primary d-none"></span>
|
||||
</button>
|
||||
<div id="paFiltersPanel" class="pa-filters collapse">
|
||||
<div class="vr d-none d-sm-block"></div>
|
||||
<label for="paDaysSelect" class="form-label mb-0 small text-muted">Time range:</label>
|
||||
<label for="paDaysSelect" class="form-label mb-0 small text-muted">{{ t('pa.time_range') }}</label>
|
||||
<select id="paDaysSelect" class="form-select form-select-sm" style="width: auto;">
|
||||
<option value="1">Last 1 day</option>
|
||||
<option value="3" selected>Last 3 days</option>
|
||||
<option value="5">Last 5 days</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="1">{{ tn('pa.last_days', 1) }}</option>
|
||||
<option value="3" selected>{{ tn('pa.last_days', 3) }}</option>
|
||||
<option value="5">{{ tn('pa.last_days', 5) }}</option>
|
||||
<option value="7">{{ tn('pa.last_days', 7) }}</option>
|
||||
</select>
|
||||
<button id="paRefreshBtn" class="btn btn-sm btn-outline-secondary" title="Reload">
|
||||
<button id="paRefreshBtn" class="btn btn-sm btn-outline-secondary" title="{{ t('pa.reload_title') }}">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
<div class="vr d-none d-sm-block"></div>
|
||||
<select id="paHopsFilter" class="form-select form-select-sm" style="width: auto;" title="Filter by hop count (any echo)">
|
||||
<option value="any" selected>Any hops</option>
|
||||
<option value="0">0 hops</option>
|
||||
<option value="1">1 hop</option>
|
||||
<option value="2">2 hops</option>
|
||||
<option value="3">3 hops</option>
|
||||
<option value="4+">4+ hops</option>
|
||||
<select id="paHopsFilter" class="form-select form-select-sm" style="width: auto;" title="{{ t('pa.hops_filter_title') }}">
|
||||
<option value="any" selected>{{ t('pa.any_hops') }}</option>
|
||||
<option value="0">{{ tn('pa.n_hops', 0) }}</option>
|
||||
<option value="1">{{ tn('pa.n_hops', 1) }}</option>
|
||||
<option value="2">{{ tn('pa.n_hops', 2) }}</option>
|
||||
<option value="3">{{ tn('pa.n_hops', 3) }}</option>
|
||||
<option value="4+">{{ t('pa.hops_4plus') }}</option>
|
||||
</select>
|
||||
<select id="paHashSizeFilter" class="form-select form-select-sm" style="width: auto;"
|
||||
title="Filter by path hash size (bytes per hop, any routed echo)">
|
||||
<option value="any" selected>Any HB</option>
|
||||
<option value="1">1-byte</option>
|
||||
<option value="2">2-byte</option>
|
||||
<option value="3">3-byte</option>
|
||||
<option value="2,3">2/3-byte</option>
|
||||
title="{{ t('pa.hb_filter_title') }}">
|
||||
<option value="any" selected>{{ t('pa.any_hb') }}</option>
|
||||
<option value="1">{{ tn('pa.n_byte', 1) }}</option>
|
||||
<option value="2">{{ tn('pa.n_byte', 2) }}</option>
|
||||
<option value="3">{{ tn('pa.n_byte', 3) }}</option>
|
||||
<option value="2,3">{{ t('pa.byte_2_3') }}</option>
|
||||
</select>
|
||||
<input id="paTokenFilter" type="text" class="form-control form-control-sm" style="width: 10.5rem;"
|
||||
placeholder="Repeater (3B or name)"
|
||||
title="Match a repeater anywhere in a path: hash prefix (hex) or contact name. Chain with '>' for a consecutive sequence, e.g. AFE6>6E9A">
|
||||
placeholder="{{ t('pa.token_ph') }}"
|
||||
title="{{ t('pa.token_title') }}">
|
||||
<input id="paSenderFilter" type="text" class="form-control form-control-sm" style="width: 9.5rem;"
|
||||
placeholder="Sender" title="Filter by sender name">
|
||||
placeholder="{{ t('pa.sender_ph') }}" title="{{ t('pa.sender_title') }}">
|
||||
<input id="paContentFilter" type="text" class="form-control form-control-sm" style="width: 11rem;"
|
||||
placeholder="Message text" title="Filter by message content">
|
||||
<button id="paClearFiltersBtn" class="btn btn-sm btn-outline-secondary d-none" title="Clear filters">
|
||||
<i class="bi bi-x-lg"></i> Clear
|
||||
placeholder="{{ t('pa.content_ph') }}" title="{{ t('pa.content_title') }}">
|
||||
<button id="paClearFiltersBtn" class="btn btn-sm btn-outline-secondary d-none" title="{{ t('pa.clear_filters_title') }}">
|
||||
<i class="bi bi-x-lg"></i> {{ t('pa.clear') }}
|
||||
</button>
|
||||
</div>
|
||||
<span id="paCounter" class="small text-muted ms-auto"></span>
|
||||
@@ -617,25 +617,25 @@
|
||||
<div class="pa-content">
|
||||
<div id="paLoading" class="empty-state">
|
||||
<div class="spinner-border text-secondary" role="status"></div>
|
||||
<div class="mt-2">Loading messages…</div>
|
||||
<div class="mt-2">{{ t('pa.loading') }}</div>
|
||||
</div>
|
||||
<div id="paEmpty" class="empty-state d-none">
|
||||
<i class="bi bi-signpost-split"></i>
|
||||
<span id="paEmptyText">No messages in the selected time range.</span>
|
||||
<span id="paEmptyText">{{ t('pa.empty_range') }}</span>
|
||||
</div>
|
||||
<div id="paTableWrap" class="pa-table-wrap d-none">
|
||||
<table class="table table-hover pa-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1.5rem;"></th>
|
||||
<th>Time</th>
|
||||
<th>Channel</th>
|
||||
<th>Sender</th>
|
||||
<th>Message</th>
|
||||
<th class="pa-col-hash">Hash</th>
|
||||
<th class="text-end">Hops</th>
|
||||
<th class="text-end pa-col-hb" title="Path hash size in bytes per hop (from routed echoes)">HB</th>
|
||||
<th class="text-end pa-col-echoes">Echoes</th>
|
||||
<th>{{ t('pa.col.time') }}</th>
|
||||
<th>{{ t('pa.col.channel') }}</th>
|
||||
<th>{{ t('pa.col.sender') }}</th>
|
||||
<th>{{ t('pa.col.message') }}</th>
|
||||
<th class="pa-col-hash">{{ t('pa.col.hash') }}</th>
|
||||
<th class="text-end">{{ t('pa.col.hops') }}</th>
|
||||
<th class="text-end pa-col-hb" title="{{ t('pa.col.hb_title') }}">{{ t('pa.col.hb') }}</th>
|
||||
<th class="text-end pa-col-echoes">{{ t('pa.col.echoes') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="paTableBody"></tbody>
|
||||
@@ -645,16 +645,16 @@
|
||||
<table class="table table-hover pa-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Repeater</th>
|
||||
<th>Contact</th>
|
||||
<th>{{ t('pa.col.repeater') }}</th>
|
||||
<th>{{ t('pa.col.contact') }}</th>
|
||||
<th class="text-end pa-sortable" data-sort="relayed"
|
||||
title="Echoes whose path contains this hash (anywhere)">Relayed</th>
|
||||
title="{{ t('pa.col.relayed_title') }}">{{ t('pa.col.relayed') }}</th>
|
||||
<th class="text-end pa-sortable pa-col-msgs" data-sort="messages"
|
||||
title="Distinct messages relayed through this hash">Messages</th>
|
||||
title="{{ t('pa.col.msgs_hash_title') }}">{{ t('pa.col.message_plural') }}</th>
|
||||
<th class="text-end pa-sortable pa-col-lasthop" data-sort="lastHop"
|
||||
title="Echoes where this hash is the final hop (heard directly by us)">As last hop</th>
|
||||
title="{{ t('pa.col.lasthop_title') }}">{{ t('pa.col.lasthop') }}</th>
|
||||
<th class="text-end pa-sortable" data-sort="avgSnr"
|
||||
title="Average echo SNR, counted only when this hash is the final hop - SNR is measured at our receiver for the last hop">Avg SNR (last hop)</th>
|
||||
title="{{ t('pa.col.avgsnr_title') }}">{{ t('pa.col.avgsnr') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="paStatsBody"></tbody>
|
||||
@@ -662,25 +662,25 @@
|
||||
</div>
|
||||
<div id="paRoutesWrap" class="d-none">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<label for="paSegLenSelect" class="form-label small text-muted mb-0">Segment length:</label>
|
||||
<label for="paSegLenSelect" class="form-label small text-muted mb-0">{{ t('pa.segment_length') }}</label>
|
||||
<select id="paSegLenSelect" class="form-select form-select-sm" style="width: auto;"
|
||||
title="Number of consecutive hops per counted segment">
|
||||
<option value="2" selected>2 hops</option>
|
||||
<option value="3">3 hops</option>
|
||||
<option value="4">4 hops</option>
|
||||
title="{{ t('pa.seglen_title') }}">
|
||||
<option value="2" selected>{{ tn('pa.n_hops', 2) }}</option>
|
||||
<option value="3">{{ tn('pa.n_hops', 3) }}</option>
|
||||
<option value="4">{{ tn('pa.n_hops', 4) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pa-table-wrap">
|
||||
<table class="table table-hover pa-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Route</th>
|
||||
<th>{{ t('pa.col.route') }}</th>
|
||||
<th class="text-end pa-sortable" data-sort="echoes"
|
||||
title="Echoes whose path contains this segment (anywhere)">Echoes</th>
|
||||
title="{{ t('pa.col.echoes_seg_title') }}">{{ t('pa.col.echoes') }}</th>
|
||||
<th class="text-end pa-sortable pa-col-msgs" data-sort="messages"
|
||||
title="Distinct messages whose path contains this segment">Messages</th>
|
||||
title="{{ t('pa.col.msgs_seg_title') }}">{{ t('pa.col.message_plural') }}</th>
|
||||
<th class="text-end pa-sortable" data-sort="pathEnd"
|
||||
title="Echoes where this segment is the end of the path - the route the message took to reach us">As path end</th>
|
||||
title="{{ t('pa.col.pathend_title') }}">{{ t('pa.col.pathend') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="paRoutesBody"></tbody>
|
||||
@@ -690,8 +690,8 @@
|
||||
<div id="paMapWrap" class="d-none">
|
||||
<div id="paMapSidebar">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<span class="small text-muted">Click a message, then an echo to draw its path.</span>
|
||||
<button id="paMapClearBtn" class="btn btn-sm btn-outline-secondary d-none" title="Clear drawn path">
|
||||
<span class="small text-muted">{{ t('pa.map.hint') }}</span>
|
||||
<button id="paMapClearBtn" class="btn btn-sm btn-outline-secondary d-none" title="{{ t('pa.map.clear_title') }}">
|
||||
<i class="bi bi-eraser"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -705,7 +705,7 @@
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3" data-toast-container>
|
||||
<div id="notificationToast" class="toast" role="alert">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">Path Analyzer</strong>
|
||||
<strong class="me-auto">{{ t('pa.title') }}</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
<div class="toast-body"></div>
|
||||
|
||||
@@ -51,6 +51,120 @@
|
||||
"meta.language_english_name": "English",
|
||||
"meta.language_name": "English",
|
||||
"meta.translator": "mc-webui",
|
||||
"pa.any_hb": "Any HB",
|
||||
"pa.any_hops": "Any hops",
|
||||
"pa.byte_2_3": "2/3-byte",
|
||||
"pa.candidates": {
|
||||
"one": "{count} candidate",
|
||||
"other": "{count} candidates"
|
||||
},
|
||||
"pa.clear": "Clear",
|
||||
"pa.clear_filters_title": "Clear filters",
|
||||
"pa.col.avgsnr": "Avg SNR (last hop)",
|
||||
"pa.col.avgsnr_title": "Average echo SNR, counted only when this hash is the final hop - SNR is measured at our receiver for the last hop",
|
||||
"pa.col.channel": "Channel",
|
||||
"pa.col.contact": "Contact",
|
||||
"pa.col.echoes": "Echoes",
|
||||
"pa.col.echoes_seg_title": "Echoes whose path contains this segment (anywhere)",
|
||||
"pa.col.hash": "Hash",
|
||||
"pa.col.hb": "HB",
|
||||
"pa.col.hb_title": "Path hash size in bytes per hop (from routed echoes)",
|
||||
"pa.col.hops": "Hops",
|
||||
"pa.col.lasthop": "As last hop",
|
||||
"pa.col.lasthop_title": "Echoes where this hash is the final hop (heard directly by us)",
|
||||
"pa.col.message": "Message",
|
||||
"pa.col.message_plural": "Messages",
|
||||
"pa.col.msgs_hash_title": "Distinct messages relayed through this hash",
|
||||
"pa.col.msgs_seg_title": "Distinct messages whose path contains this segment",
|
||||
"pa.col.pathend": "As path end",
|
||||
"pa.col.pathend_title": "Echoes where this segment is the end of the path - the route the message took to reach us",
|
||||
"pa.col.relayed": "Relayed",
|
||||
"pa.col.relayed_title": "Echoes whose path contains this hash (anywhere)",
|
||||
"pa.col.repeater": "Repeater",
|
||||
"pa.col.route": "Route",
|
||||
"pa.col.sender": "Sender",
|
||||
"pa.col.time": "Time",
|
||||
"pa.content_ph": "Message text",
|
||||
"pa.content_title": "Filter by message content",
|
||||
"pa.copy_route_title": "Click to copy route",
|
||||
"pa.copy_title": "Click to copy",
|
||||
"pa.counter.filtered": "{shown} of {total} messages",
|
||||
"pa.counter.messages": {
|
||||
"one": "{count} message",
|
||||
"other": "{count} messages"
|
||||
},
|
||||
"pa.counter.repeaters": {
|
||||
"one": "{count} repeater",
|
||||
"other": "{count} repeaters"
|
||||
},
|
||||
"pa.counter.routed": {
|
||||
"one": "{count} routed message",
|
||||
"other": "{count} routed messages"
|
||||
},
|
||||
"pa.counter.segments": {
|
||||
"one": "{count} segment",
|
||||
"other": "{count} segments"
|
||||
},
|
||||
"pa.direct_flood": "Direct (flood, 0 hops)",
|
||||
"pa.empty_filtered": "No messages match the current filters.",
|
||||
"pa.empty_no_echoes": "No routed echoes in the current selection.",
|
||||
"pa.empty_range": "No messages in the selected time range.",
|
||||
"pa.filter_by_segment_title": "Filter messages by this segment",
|
||||
"pa.filters": "Filters",
|
||||
"pa.filters_toggle_title": "Show/hide filters",
|
||||
"pa.hb_filter_title": "Filter by path hash size (bytes per hop, any routed echo)",
|
||||
"pa.hops_4plus": "4+ hops",
|
||||
"pa.hops_filter_title": "Filter by hop count (any echo)",
|
||||
"pa.last_days": {
|
||||
"one": "Last {count} day",
|
||||
"other": "Last {count} days"
|
||||
},
|
||||
"pa.loading": "Loading messages…",
|
||||
"pa.map.clear_title": "Clear drawn path",
|
||||
"pa.map.hint": "Click a message, then an echo to draw its path.",
|
||||
"pa.map.no_routed": "No messages with routed echoes match the current filters.",
|
||||
"pa.me": "Me",
|
||||
"pa.n_byte": {
|
||||
"one": "{count}-byte",
|
||||
"other": "{count}-byte"
|
||||
},
|
||||
"pa.n_hops": {
|
||||
"one": "{count} hop",
|
||||
"other": "{count} hops"
|
||||
},
|
||||
"pa.no_path_data": "no path data",
|
||||
"pa.packet_hash": "Packet hash",
|
||||
"pa.reload_title": "Reload",
|
||||
"pa.repeater_hash": "Repeater hash",
|
||||
"pa.reset_picks": "Reset picks",
|
||||
"pa.reset_picks_title": "Clear all manual repeater assignments on this path",
|
||||
"pa.seglen_title": "Number of consecutive hops per counted segment",
|
||||
"pa.segment_length": "Segment length:",
|
||||
"pa.sender_own": "{name} (own)",
|
||||
"pa.sender_ph": "Sender",
|
||||
"pa.sender_title": "Filter by sender name",
|
||||
"pa.show_on_map_title": "Show this path on the map",
|
||||
"pa.time_range": "Time range:",
|
||||
"pa.title": "Path Analyzer",
|
||||
"pa.toast.copied": "{what} copied to clipboard",
|
||||
"pa.toast.copy_failed": "Failed to copy {what}",
|
||||
"pa.toast.load_failed": "Failed to load messages: {error}",
|
||||
"pa.toast.msg_gone": "This message is no longer in the analyzer data (max 7 days).",
|
||||
"pa.toast.no_echoes": "This message has no routed echoes to draw.",
|
||||
"pa.token_ph": "Repeater (3B or name)",
|
||||
"pa.token_title": "Match a repeater anywhere in a path: hash prefix (hex) or contact name. Chain with '>' for a consecutive sequence, e.g. AFE6>6E9A",
|
||||
"pa.undo_pick_title": "Undo this manual assignment",
|
||||
"pa.unknown_position": "— unknown / no position",
|
||||
"pa.use_contact_title": "Use this contact for the hop",
|
||||
"pa.view.map": "Map",
|
||||
"pa.view.map_title": "Map view",
|
||||
"pa.view.messages": "Messages",
|
||||
"pa.view.messages_title": "Message list",
|
||||
"pa.view.repeaters": "Repeaters",
|
||||
"pa.view.repeaters_title": "Per-repeater statistics",
|
||||
"pa.view.routes": "Routes",
|
||||
"pa.view.routes_title": "Route segment statistics",
|
||||
"pa.view_aria": "View",
|
||||
"repeaters.add": "Add repeater",
|
||||
"repeaters.add_loading": "Loading device contacts...",
|
||||
"repeaters.add_modal_title": "Add Repeater",
|
||||
|
||||
@@ -57,6 +57,136 @@
|
||||
"meta.language_english_name": "Polish",
|
||||
"meta.language_name": "Polski",
|
||||
"meta.translator": "mc-webui",
|
||||
"pa.any_hb": "Dowolne HB",
|
||||
"pa.any_hops": "Dowolne hopy",
|
||||
"pa.byte_2_3": "2/3 bajty",
|
||||
"pa.candidates": {
|
||||
"one": "{count} kandydat",
|
||||
"few": "{count} kandydaci",
|
||||
"many": "{count} kandydatów",
|
||||
"other": "{count} kandydata"
|
||||
},
|
||||
"pa.clear": "Wyczyść",
|
||||
"pa.clear_filters_title": "Wyczyść filtry",
|
||||
"pa.col.avgsnr": "Śr. SNR (ostatni hop)",
|
||||
"pa.col.avgsnr_title": "Średni SNR echa, liczony tylko gdy ten hash jest ostatnim hopem — SNR mierzymy na naszym odbiorniku dla ostatniego hopa",
|
||||
"pa.col.channel": "Kanał",
|
||||
"pa.col.contact": "Kontakt",
|
||||
"pa.col.echoes": "Echa",
|
||||
"pa.col.echoes_seg_title": "Echa, których ścieżka zawiera ten segment (w dowolnym miejscu)",
|
||||
"pa.col.hash": "Hash",
|
||||
"pa.col.hb": "HB",
|
||||
"pa.col.hb_title": "Rozmiar hasha ścieżki w bajtach na hop (z ech z trasą)",
|
||||
"pa.col.hops": "Hopy",
|
||||
"pa.col.lasthop": "Jako ostatni hop",
|
||||
"pa.col.lasthop_title": "Echa, w których ten hash jest ostatnim hopem (usłyszane bezpośrednio przez nas)",
|
||||
"pa.col.message": "Wiadomość",
|
||||
"pa.col.message_plural": "Wiadomości",
|
||||
"pa.col.msgs_hash_title": "Odrębne wiadomości przekazane przez ten hash",
|
||||
"pa.col.msgs_seg_title": "Odrębne wiadomości, których ścieżka zawiera ten segment",
|
||||
"pa.col.pathend": "Jako koniec ścieżki",
|
||||
"pa.col.pathend_title": "Echa, w których ten segment kończy ścieżkę — trasa, którą wiadomość do nas dotarła",
|
||||
"pa.col.relayed": "Przekazane",
|
||||
"pa.col.relayed_title": "Echa, których ścieżka zawiera ten hash (w dowolnym miejscu)",
|
||||
"pa.col.repeater": "Repeater",
|
||||
"pa.col.route": "Trasa",
|
||||
"pa.col.sender": "Nadawca",
|
||||
"pa.col.time": "Czas",
|
||||
"pa.content_ph": "Treść wiadomości",
|
||||
"pa.content_title": "Filtruj po treści wiadomości",
|
||||
"pa.copy_route_title": "Kliknij, aby skopiować trasę",
|
||||
"pa.copy_title": "Kliknij, aby skopiować",
|
||||
"pa.counter.filtered": "{shown} z {total} wiadomości",
|
||||
"pa.counter.messages": {
|
||||
"one": "{count} wiadomość",
|
||||
"few": "{count} wiadomości",
|
||||
"many": "{count} wiadomości",
|
||||
"other": "{count} wiadomości"
|
||||
},
|
||||
"pa.counter.repeaters": {
|
||||
"one": "{count} repeater",
|
||||
"few": "{count} repeatery",
|
||||
"many": "{count} repeaterów",
|
||||
"other": "{count} repeatera"
|
||||
},
|
||||
"pa.counter.routed": {
|
||||
"one": "{count} wiadomość z trasą",
|
||||
"few": "{count} wiadomości z trasą",
|
||||
"many": "{count} wiadomości z trasą",
|
||||
"other": "{count} wiadomości z trasą"
|
||||
},
|
||||
"pa.counter.segments": {
|
||||
"one": "{count} segment",
|
||||
"few": "{count} segmenty",
|
||||
"many": "{count} segmentów",
|
||||
"other": "{count} segmentu"
|
||||
},
|
||||
"pa.direct_flood": "Bezpośrednio (flood, 0 hopów)",
|
||||
"pa.empty_filtered": "Żadna wiadomość nie pasuje do bieżących filtrów.",
|
||||
"pa.empty_no_echoes": "Brak ech z trasą w bieżącym wyborze.",
|
||||
"pa.empty_range": "Brak wiadomości w wybranym zakresie czasu.",
|
||||
"pa.filter_by_segment_title": "Filtruj wiadomości po tym segmencie",
|
||||
"pa.filters": "Filtry",
|
||||
"pa.filters_toggle_title": "Pokaż/ukryj filtry",
|
||||
"pa.hb_filter_title": "Filtruj po rozmiarze hasha ścieżki (bajtów na hop, dowolne echo z trasą)",
|
||||
"pa.hops_4plus": "4+ hopów",
|
||||
"pa.hops_filter_title": "Filtruj po liczbie hopów (dowolne echo)",
|
||||
"pa.last_days": {
|
||||
"one": "Ostatni {count} dzień",
|
||||
"few": "Ostatnie {count} dni",
|
||||
"many": "Ostatnie {count} dni",
|
||||
"other": "Ostatnie {count} dnia"
|
||||
},
|
||||
"pa.loading": "Wczytywanie wiadomości…",
|
||||
"pa.map.clear_title": "Wyczyść narysowaną ścieżkę",
|
||||
"pa.map.hint": "Kliknij wiadomość, a potem echo, aby narysować jej ścieżkę.",
|
||||
"pa.map.no_routed": "Żadna wiadomość z echami z trasą nie pasuje do bieżących filtrów.",
|
||||
"pa.me": "Ja",
|
||||
"pa.n_byte": {
|
||||
"one": "{count} bajt",
|
||||
"few": "{count} bajty",
|
||||
"many": "{count} bajtów",
|
||||
"other": "{count} bajta"
|
||||
},
|
||||
"pa.n_hops": {
|
||||
"one": "{count} hop",
|
||||
"few": "{count} hopy",
|
||||
"many": "{count} hopów",
|
||||
"other": "{count} hopa"
|
||||
},
|
||||
"pa.no_path_data": "brak danych o ścieżce",
|
||||
"pa.packet_hash": "Hash pakietu",
|
||||
"pa.reload_title": "Odśwież",
|
||||
"pa.repeater_hash": "Hash repeatera",
|
||||
"pa.reset_picks": "Resetuj wybory",
|
||||
"pa.reset_picks_title": "Wyczyść wszystkie ręczne przypisania repeaterów na tej ścieżce",
|
||||
"pa.seglen_title": "Liczba kolejnych hopów w liczonym segmencie",
|
||||
"pa.segment_length": "Długość segmentu:",
|
||||
"pa.sender_own": "{name} (własna)",
|
||||
"pa.sender_ph": "Nadawca",
|
||||
"pa.sender_title": "Filtruj po nazwie nadawcy",
|
||||
"pa.show_on_map_title": "Pokaż tę ścieżkę na mapie",
|
||||
"pa.time_range": "Zakres czasu:",
|
||||
"pa.title": "Analizator ścieżek",
|
||||
"pa.toast.copied": "Skopiowano do schowka: {what}",
|
||||
"pa.toast.copy_failed": "Nie udało się skopiować: {what}",
|
||||
"pa.toast.load_failed": "Nie udało się wczytać wiadomości: {error}",
|
||||
"pa.toast.msg_gone": "Tej wiadomości nie ma już w danych analizatora (maks. 7 dni).",
|
||||
"pa.toast.no_echoes": "Ta wiadomość nie ma ech z trasą do narysowania.",
|
||||
"pa.token_ph": "Repeater (3B lub nazwa)",
|
||||
"pa.token_title": "Dopasuj repeater w dowolnym miejscu ścieżki: prefiks hasha (hex) lub nazwa kontaktu. Połącz znakiem '>', aby wskazać kolejność, np. AFE6>6E9A",
|
||||
"pa.undo_pick_title": "Cofnij to ręczne przypisanie",
|
||||
"pa.unknown_position": "— nieznany / brak pozycji",
|
||||
"pa.use_contact_title": "Użyj tego kontaktu dla hopa",
|
||||
"pa.view.map": "Mapa",
|
||||
"pa.view.map_title": "Widok mapy",
|
||||
"pa.view.messages": "Wiadomości",
|
||||
"pa.view.messages_title": "Lista wiadomości",
|
||||
"pa.view.repeaters": "Repeatery",
|
||||
"pa.view.repeaters_title": "Statystyki per repeater",
|
||||
"pa.view.routes": "Trasy",
|
||||
"pa.view.routes_title": "Statystyki segmentów tras",
|
||||
"pa.view_aria": "Widok",
|
||||
"repeaters.add": "Dodaj repeater",
|
||||
"repeaters.add_loading": "Wczytywanie kontaktów z urządzenia...",
|
||||
"repeaters.add_modal_title": "Dodaj repeater",
|
||||
|
||||
Reference in New Issue
Block a user