feat(i18n): translate Settings - Appearance, Contacts, Regions, Analyzer,

Observer, Notifications (stage 8b)

Finishes the Settings modal, and with it every template in the project.

The Quick Access placement table is 13 rows that differ only in the item
they name, so it is driven by one template key plus the item labels the
menu and FAB column already have - t('settings.appear.placement_aria',
item=t('dm.title')) - instead of 13 near-duplicate keys and 26 copies of
"Quick Access"/"Main Menu".

That does normalise two aria-labels: "DM placement" and "Contacts
placement" become the full item names, matching what the row already
reads and what the other eleven already did. They are screen-reader-only,
so nothing visible changes; the alternative was keeping two keys alive
forever to preserve an abbreviation.

Paragraphs with a link or <code> inside stay whole rather than being cut
around the markup - the Regions and Observer intros are single t_html
values, so a translator moves the link where their grammar wants it
instead of translating two half-sentences.

Also in app.js: updateNotificationToggleUI() writes all four badge states
(Unavailable / Blocked / Enabled / Disabled) over the server-rendered one.

Catalog: 715 -> 760 keys, pl at 100%. The Settings diff now reports two
identical strings across all ten tabs, both correct: docs/translations.md
and meshcore-packet-capture, a path and a format name. No horizontal
overflow at 360px on any tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-08-01 10:15:41 +02:00
parent 0a99a7b6e7
commit 55737fa8c6
4 changed files with 192 additions and 113 deletions
+4 -4
View File
@@ -3126,20 +3126,20 @@ function updateNotificationToggleUI() {
if (permission === 'unsupported') {
statusBadge.className = 'badge bg-secondary';
statusBadge.textContent = 'Unavailable';
statusBadge.textContent = t('settings.notif.unavailable');
toggleBtn.disabled = true;
} else if (permission === 'denied') {
statusBadge.className = 'badge bg-danger';
statusBadge.textContent = 'Blocked';
statusBadge.textContent = t('settings.notif.blocked');
toggleBtn.disabled = false;
} else if (permission === 'granted' && isEnabled) {
statusBadge.className = 'badge bg-success';
statusBadge.textContent = 'Enabled';
statusBadge.textContent = t('common.enabled');
toggleBtn.disabled = false;
} else {
// permission === 'default' OR (permission === 'granted' AND !isEnabled)
statusBadge.className = 'badge bg-secondary';
statusBadge.textContent = 'Disabled';
statusBadge.textContent = t('common.disabled');
toggleBtn.disabled = false;
}
}
+98 -109
View File
@@ -768,7 +768,7 @@
</form>
</div>
<div class="tab-pane fade" id="tabSettingsAppearance">
<h6 class="text-muted mb-3">Language</h6>
<h6 class="text-muted mb-3">{{ t('settings.appear.language') }}</h6>
<div class="input-group input-group-sm mb-2">
<select class="form-select form-select-sm" id="settLanguage">
{% for code, name in available_languages.items() %}
@@ -776,26 +776,24 @@
{% endfor %}
</select>
<button class="btn btn-outline-secondary" type="button" id="reloadTranslationsBtn"
title="Re-scan the translations folder">
title="{{ t('settings.appear.reload_title') }}">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<small class="d-block text-muted mb-3">
Applies to this browser and becomes the server default for others.
Add your own by dropping a catalog into the translations folder — see
<code>docs/translations.md</code>.
{{ t_html('settings.appear.language_hint') }}
</small>
<hr>
<h6 class="text-muted mb-3">Theme</h6>
<h6 class="text-muted mb-3">{{ t('settings.appear.theme') }}</h6>
<div class="d-flex flex-column gap-2">
<div class="theme-option active" data-theme-value="light" onclick="setTheme('light')">
<div class="theme-option-preview light">
<i class="bi bi-sun"></i>
</div>
<div>
<div class="theme-option-label">Light</div>
<div class="theme-option-desc">Classic bright interface</div>
<div class="theme-option-label">{{ t('settings.appear.theme_light') }}</div>
<div class="theme-option-desc">{{ t('settings.appear.theme_light_desc') }}</div>
</div>
</div>
<div class="theme-option" data-theme-value="dark" onclick="setTheme('dark')">
@@ -803,168 +801,168 @@
<i class="bi bi-moon-stars" style="color: #60a5fa;"></i>
</div>
<div>
<div class="theme-option-label">Dark</div>
<div class="theme-option-desc">Easy on the eyes, deep navy palette</div>
<div class="theme-option-label">{{ t('settings.appear.theme_dark') }}</div>
<div class="theme-option-desc">{{ t('settings.appear.theme_dark_desc') }}</div>
</div>
</div>
</div>
<hr>
<h6 class="text-muted mb-3">Quick Access Buttons</h6>
<h6 class="text-muted mb-3">{{ t('settings.appear.quick_access') }}</h6>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="settHideFab">
<label class="form-check-label" for="settHideFab">Hide Quick Access buttons</label>
<small class="d-block text-muted">When hidden, all items appear in the Main Menu.</small>
<label class="form-check-label" for="settHideFab">{{ t('settings.appear.hide_fab') }}</label>
<small class="d-block text-muted">{{ t('settings.appear.hide_fab_hint') }}</small>
</div>
<div id="fabAppearanceControls">
<table class="table table-sm align-middle mb-3" id="settItemPlacementTable">
<thead>
<tr>
<th class="ps-0 fw-normal text-muted small">Item</th>
<th class="pe-0 text-end fw-normal text-muted small">Placement</th>
<th class="ps-0 fw-normal text-muted small">{{ t('settings.appear.item') }}</th>
<th class="pe-0 text-end fw-normal text-muted small">{{ t('settings.appear.placement') }}</th>
</tr>
</thead>
<tbody>
<tr data-placement-key="filter">
<td class="ps-0">Filter messages</td>
<td class="ps-0">{{ t('menu.filter') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Filter placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.filter')) }}">
<input type="radio" class="btn-check" name="place-filter" id="place-filter-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-filter-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-filter-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-filter" id="place-filter-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-filter-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-filter-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="search">
<td class="ps-0">Search messages</td>
<td class="ps-0">{{ t('menu.search') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Search placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.search')) }}">
<input type="radio" class="btn-check" name="place-search" id="place-search-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-search-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-search-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-search" id="place-search-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-search-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-search-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="dm">
<td class="ps-0">Direct Messages</td>
<td class="ps-0">{{ t('dm.title') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="DM placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('dm.title')) }}">
<input type="radio" class="btn-check" name="place-dm" id="place-dm-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-dm-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-dm-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-dm" id="place-dm-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-dm-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-dm-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="contacts">
<td class="ps-0">Contact Management</td>
<td class="ps-0">{{ t('contacts.title') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Contacts placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('contacts.title')) }}">
<input type="radio" class="btn-check" name="place-contacts" id="place-contacts-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-contacts-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-contacts-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-contacts" id="place-contacts-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-contacts-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-contacts-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="settings">
<td class="ps-0">Settings</td>
<td class="ps-0">{{ t('common.settings') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Settings placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('common.settings')) }}">
<input type="radio" class="btn-check" name="place-settings" id="place-settings-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-settings-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-settings-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-settings" id="place-settings-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-settings-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-settings-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="advert">
<td class="ps-0">Send Advert</td>
<td class="ps-0">{{ t('menu.advert') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Advert placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.advert')) }}">
<input type="radio" class="btn-check" name="place-advert" id="place-advert-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-advert-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-advert-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-advert" id="place-advert-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-advert-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-advert-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="floodadvert">
<td class="ps-0">Flood Advert</td>
<td class="ps-0">{{ t('menu.flood_advert') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Flood Advert placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.flood_advert')) }}">
<input type="radio" class="btn-check" name="place-floodadvert" id="place-floodadvert-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-floodadvert-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-floodadvert-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-floodadvert" id="place-floodadvert-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-floodadvert-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-floodadvert-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="map">
<td class="ps-0">Map</td>
<td class="ps-0">{{ t('menu.map') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Map placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.map')) }}">
<input type="radio" class="btn-check" name="place-map" id="place-map-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-map-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-map-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-map" id="place-map-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-map-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-map-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="console">
<td class="ps-0">Console</td>
<td class="ps-0">{{ t('console.title') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Console placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('console.title')) }}">
<input type="radio" class="btn-check" name="place-console" id="place-console-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-console-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-console-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-console" id="place-console-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-console-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-console-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="repeaters">
<td class="ps-0">My Repeaters</td>
<td class="ps-0">{{ t('repeaters.title') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="My Repeaters placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('repeaters.title')) }}">
<input type="radio" class="btn-check" name="place-repeaters" id="place-repeaters-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-repeaters-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-repeaters-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-repeaters" id="place-repeaters-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-repeaters-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-repeaters-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="pathanalyzer">
<td class="ps-0">Path Analyzer</td>
<td class="ps-0">{{ t('pa.title') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Path Analyzer placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('pa.title')) }}">
<input type="radio" class="btn-check" name="place-pathanalyzer" id="place-pathanalyzer-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-pathanalyzer-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-pathanalyzer-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-pathanalyzer" id="place-pathanalyzer-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-pathanalyzer-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-pathanalyzer-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="deviceinfo">
<td class="ps-0">Device Info</td>
<td class="ps-0">{{ t('menu.device_info') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="Device Info placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.device_info')) }}">
<input type="radio" class="btn-check" name="place-deviceinfo" id="place-deviceinfo-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-deviceinfo-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-deviceinfo-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-deviceinfo" id="place-deviceinfo-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-deviceinfo-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-deviceinfo-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
<tr data-placement-key="syslog">
<td class="ps-0">System Log</td>
<td class="ps-0">{{ t('logs.title') }}</td>
<td class="pe-0 text-end">
<div class="btn-group btn-group-sm" role="group" aria-label="System Log placement">
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('logs.title')) }}">
<input type="radio" class="btn-check" name="place-syslog" id="place-syslog-fab" value="fab" autocomplete="off">
<label class="btn btn-outline-primary" for="place-syslog-fab">Quick Access</label>
<label class="btn btn-outline-primary" for="place-syslog-fab">{{ t('settings.appear.place_fab') }}</label>
<input type="radio" class="btn-check" name="place-syslog" id="place-syslog-menu" value="menu" autocomplete="off">
<label class="btn btn-outline-primary" for="place-syslog-menu">Main Menu</label>
<label class="btn btn-outline-primary" for="place-syslog-menu">{{ t('settings.appear.place_menu') }}</label>
</div>
</td>
</tr>
@@ -973,23 +971,23 @@
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0">Button size (px)</td>
<td class="ps-0">{{ t('settings.appear.btn_size') }}</td>
<td class="pe-0 d-flex align-items-center gap-2" style="width:12rem">
<input type="range" class="form-range flex-grow-1" id="settFabSize" min="28" max="72" step="2" value="56">
<span class="text-muted small" id="settFabSizeVal" style="min-width:2.5rem;text-align:right">56</span>
</td>
</tr>
<tr>
<td class="ps-0">Spacing (px)</td>
<td class="ps-0">{{ t('settings.appear.spacing') }}</td>
<td class="pe-0 d-flex align-items-center gap-2">
<input type="range" class="form-range flex-grow-1" id="settFabGap" min="2" max="24" step="1" value="12">
<span class="text-muted small" id="settFabGapVal" style="min-width:2.5rem;text-align:right">12</span>
</td>
</tr>
<tr>
<td class="ps-0">Position</td>
<td class="ps-0">{{ t('settings.appear.position') }}</td>
<td class="pe-0">
<button type="button" class="btn btn-outline-secondary btn-sm" id="settFabResetPos">Reset to default</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="settFabResetPos">{{ t('settings.appear.reset_default') }}</button>
</td>
</tr>
</tbody>
@@ -1000,9 +998,9 @@
<table class="table table-sm table-borderless mb-0 align-middle">
<tbody>
<tr>
<td class="ps-0">Manual approval enabled
<td class="ps-0">{{ t('settings.contacts.manual_approval') }}
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="When enabled, new contacts must be manually approved before they can communicate with your node"><i class="bi bi-info-circle"></i></span>
title="{{ t('settings.contacts.manual_approval_help') }}"><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
@@ -1011,9 +1009,9 @@
</td>
</tr>
<tr>
<td class="ps-0" id="settSuppressAdvertNotifsLabel">Suppress new advert notifications
<td class="ps-0" id="settSuppressAdvertNotifsLabel">{{ t('settings.contacts.suppress_notifs') }}
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="Hide the badge over Contact Management and the browser notification when new pending contacts arrive. The Pending Contacts list itself still shows them. Requires Manual approval ON."><i class="bi bi-info-circle"></i></span>
title="{{ t('settings.contacts.suppress_notifs_help') }}"><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
@@ -1022,9 +1020,9 @@
</td>
</tr>
<tr>
<td class="ps-0" id="settAutoIgnoreAdvertsLabel">Automatically add new contacts to "Ignored"
<td class="ps-0" id="settAutoIgnoreAdvertsLabel">{{ t('settings.contacts.auto_ignore') }}
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
title="Every new advert is automatically marked as Ignored — no notifications, no badge. Contacts still appear under Existing Contacts (Cache) and can be promoted with 'To Device'. Requires Manual approval ON."><i class="bi bi-info-circle"></i></span>
title="{{ t('settings.contacts.auto_ignore_help') }}"><i class="bi bi-info-circle"></i></span>
</td>
<td class="pe-0">
<div class="form-check form-switch mb-0">
@@ -1037,11 +1035,9 @@
</div>
<div class="tab-pane fade" id="tabSettingsRegions">
<div class="alert alert-info py-2 small mb-3 mt-2">
<i class="bi bi-info-circle"></i> Only repeaters allowing a region will forward messages tagged with it.
Find standardised region names at
<a href="https://regions.meshcore.nz/" target="_blank" rel="noopener">regions.meshcore.nz</a>.
<i class="bi bi-info-circle"></i> {{ t_html('settings.regions.intro') }}
</div>
<h6 class="mb-2">Region Registry</h6>
<h6 class="mb-2">{{ t('settings.regions.registry') }}</h6>
<div id="regionsList" class="list-group mb-3">
<div class="text-center text-muted py-3 small">
<div class="spinner-border spinner-border-sm"></div> Loading...
@@ -1050,88 +1046,81 @@
<form id="addRegionForm" class="row g-2 mb-0">
<div class="col-8">
<input type="text" class="form-control form-control-sm" id="newRegionName"
placeholder="Region name (e.g. pl, pl-ma)" required maxlength="30" autocomplete="off">
placeholder="{{ t('settings.regions.name_ph') }}" required maxlength="30" autocomplete="off">
</div>
<div class="col-4">
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-plus-circle"></i> Add
<i class="bi bi-plus-circle"></i> {{ t('common.add') }}
</button>
</div>
</form>
<div class="form-text small">
Tip: pick the default region via the radio button, or select <em>None</em> to fall back to the firmware default. The chosen region is also pushed to the firmware so any untagged channel uses it.
{{ t_html('settings.regions.tip') }}
</div>
</div>
<div class="tab-pane fade" id="tabSettingsAnalyzer">
<div class="alert alert-info py-2 small mb-3 mt-2">
<i class="bi bi-info-circle"></i> Add MeshCore Analyzer services to choose from when clicking
<i class="bi bi-clipboard-data"></i> under a group chat message. The URL must contain the
<code>{packetHash}</code> placeholder &mdash; it is replaced with the message's packet hash.
<i class="bi bi-info-circle"></i> {{ t_html('settings.analyzer.intro') }}
</div>
<h6 class="mb-2">Analyzer Services</h6>
<h6 class="mb-2">{{ t('settings.analyzer.services') }}</h6>
<div id="analyzersList" class="list-group mb-3">
<div class="text-center text-muted py-3 small">
<div class="spinner-border spinner-border-sm"></div> Loading...
</div>
</div>
<button type="button" class="btn btn-primary btn-sm" id="addAnalyzerBtn">
<i class="bi bi-plus-circle"></i> Add analyzer
<i class="bi bi-plus-circle"></i> {{ t('analyzer.add') }}
</button>
<div class="form-text small mt-2">
Tip: star one analyzer to use it without being asked. Clear the star to be prompted on every click.
Disabled analyzers stay in the list but are hidden from the chooser.
{{ t('settings.analyzer.tip') }}
</div>
</div>
<div class="tab-pane fade" id="tabSettingsObserver">
<div class="alert alert-info py-2 small mb-3 mt-2">
<i class="bi bi-info-circle"></i> Publish every mesh packet this device overhears to
MQTT brokers, in the <code>meshcore-packet-capture</code> format
(topics <code>meshcore/IATA/PUBKEY/packets</code>). Compatible with
letsmesh-style packet analyzers.
<i class="bi bi-info-circle"></i> {{ t_html('settings.observer.intro') }}
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="observerEnabledToggle">
<label class="form-check-label" for="observerEnabledToggle">Enable observer</label>
<label class="form-check-label" for="observerEnabledToggle">{{ t('settings.observer.enable') }}</label>
</div>
<div class="row g-2 mb-1">
<div class="col-6">
<label class="form-label small mb-1" for="observerIataInput">Location code (IATA)</label>
<label class="form-label small mb-1" for="observerIataInput">{{ t('settings.observer.iata') }}</label>
<input type="text" class="form-control form-control-sm" id="observerIataInput"
maxlength="3" placeholder="e.g. KRK" autocomplete="off"
maxlength="3" placeholder="{{ t('settings.observer.iata_ph') }}" autocomplete="off"
style="text-transform: uppercase;">
</div>
<div class="col-6">
<label class="form-label small mb-1" for="observerAdvertIntervalInput">Flood advert interval (h)</label>
<label class="form-label small mb-1" for="observerAdvertIntervalInput">{{ t('settings.observer.advert_interval') }}</label>
<input type="number" class="form-control form-control-sm" id="observerAdvertIntervalInput"
min="0" max="8760" step="1" placeholder="0 = off">
min="0" max="8760" step="1" placeholder="{{ t('settings.observer.advert_interval_ph') }}">
</div>
</div>
<div class="form-text small mb-2">
The 3-letter location code is used in MQTT topics; leave it empty for the flat
<code>meshcore/packets</code> topics. Advert interval 0 disables scheduled flood adverts.
{{ t_html('settings.observer.hint') }}
</div>
<div id="observerStatusLine" class="small text-muted mb-3"></div>
<h6 class="mb-2">MQTT Brokers</h6>
<h6 class="mb-2">{{ t('settings.observer.brokers') }}</h6>
<div id="observerBrokersList" class="list-group mb-3">
<div class="text-center text-muted py-3 small">
<div class="spinner-border spinner-border-sm"></div> Loading...
</div>
</div>
<button type="button" class="btn btn-primary btn-sm" id="addObserverBrokerBtn">
<i class="bi bi-plus-circle"></i> Add broker
<i class="bi bi-plus-circle"></i> {{ t('observer.add_broker') }}
</button>
</div>
<div class="tab-pane fade" id="tabSettingsNotifications">
<p class="text-muted small mb-3">Browser notifications appear when the app is hidden or in the background.</p>
<p class="text-muted small mb-3">{{ t('settings.notif.desc') }}</p>
<button id="notificationsToggle" class="list-group-item list-group-item-action d-flex align-items-center gap-3 border rounded" type="button" style="width: 100%;">
<i class="bi bi-bell" style="font-size: 1.5rem;"></i>
<div class="flex-grow-1 text-start">
<div class="d-flex justify-content-between align-items-center">
<span>Notifications</span>
<span id="notificationStatus" class="badge bg-secondary">Disabled</span>
<span>{{ t('settings.tab.notifications') }}</span>
<span id="notificationStatus" class="badge bg-secondary">{{ t('common.disabled') }}</span>
</div>
<small class="text-muted d-block mt-1" style="font-size: 0.75rem;">
Works when app is hidden
{{ t('settings.notif.works_hidden') }}
</small>
</div>
</button>
@@ -1139,7 +1128,7 @@
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ t('common.close') }}</button>
</div>
</div>
</div>
+45
View File
@@ -728,6 +728,29 @@
"search.tip_phrase": "exact phrase",
"search.tip_prefix": "prefix match (hello,hellas...)",
"search.tips": "Search tips:",
"settings.analyzer.intro": "Add MeshCore Analyzer services to choose from when clicking <i class=\"bi bi-clipboard-data\"></i> under a group chat message. The URL must contain the <code>{packetHash}</code> placeholder — it is replaced with the message's packet hash.",
"settings.analyzer.services": "Analyzer Services",
"settings.analyzer.tip": "Tip: star one analyzer to use it without being asked. Clear the star to be prompted on every click. Disabled analyzers stay in the list but are hidden from the chooser.",
"settings.appear.btn_size": "Button size (px)",
"settings.appear.hide_fab": "Hide Quick Access buttons",
"settings.appear.hide_fab_hint": "When hidden, all items appear in the Main Menu.",
"settings.appear.item": "Item",
"settings.appear.language": "Language",
"settings.appear.language_hint": "Applies to this browser and becomes the server default for others. Add your own by dropping a catalog into the translations folder — see <code>docs/translations.md</code>.",
"settings.appear.place_fab": "Quick Access",
"settings.appear.place_menu": "Main Menu",
"settings.appear.placement": "Placement",
"settings.appear.placement_aria": "{item} placement",
"settings.appear.position": "Position",
"settings.appear.quick_access": "Quick Access Buttons",
"settings.appear.reload_title": "Re-scan the translations folder",
"settings.appear.reset_default": "Reset to default",
"settings.appear.spacing": "Spacing (px)",
"settings.appear.theme": "Theme",
"settings.appear.theme_dark": "Dark",
"settings.appear.theme_dark_desc": "Easy on the eyes, deep navy palette",
"settings.appear.theme_light": "Light",
"settings.appear.theme_light_desc": "Classic bright interface",
"settings.chat.autoclose": "Auto-close after (s)",
"settings.chat.autoclose_help": "Seconds before the route popup closes automatically",
"settings.chat.no_autoclose": "Don't close automatically",
@@ -737,6 +760,12 @@
"settings.chat.quote_len_help": "Default max UTF-8 bytes for truncated quotes",
"settings.chat.route_popup": "Route popup",
"settings.chat.route_popup_desc": "The popup shown when tapping \"SNR | Hops\" under a message. Also applies to DMs.",
"settings.contacts.auto_ignore": "Automatically add new contacts to \"Ignored\"",
"settings.contacts.auto_ignore_help": "Every new advert is automatically marked as Ignored — no notifications, no badge. Contacts still appear under Existing Contacts (Cache) and can be promoted with 'To Device'. Requires Manual approval ON.",
"settings.contacts.manual_approval": "Manual approval enabled",
"settings.contacts.manual_approval_help": "When enabled, new contacts must be manually approved before they can communicate with your node",
"settings.contacts.suppress_notifs": "Suppress new advert notifications",
"settings.contacts.suppress_notifs_help": "Hide the badge over Contact Management and the browser notification when new pending contacts arrive. The Pending Contacts list itself still shows them. Requires Manual approval ON.",
"settings.device.bw": "Bandwidth (kHz)",
"settings.device.cr": "Coding Rate",
"settings.device.freq": "Frequency (MHz)",
@@ -782,6 +811,22 @@
"settings.msg.max_retries": "Max retries",
"settings.msg.max_retries_help": "Flood retry attempts (also used after path rotation)",
"settings.msg.other": "Other",
"settings.notif.blocked": "Blocked",
"settings.notif.desc": "Browser notifications appear when the app is hidden or in the background.",
"settings.notif.unavailable": "Unavailable",
"settings.notif.works_hidden": "Works when app is hidden",
"settings.observer.advert_interval": "Flood advert interval (h)",
"settings.observer.advert_interval_ph": "0 = off",
"settings.observer.brokers": "MQTT Brokers",
"settings.observer.enable": "Enable observer",
"settings.observer.hint": "The 3-letter location code is used in MQTT topics; leave it empty for the flat <code>meshcore/packets</code> topics. Advert interval 0 disables scheduled flood adverts.",
"settings.observer.iata": "Location code (IATA)",
"settings.observer.iata_ph": "e.g. KRK",
"settings.observer.intro": "Publish every mesh packet this device overhears to MQTT brokers, in the <code>meshcore-packet-capture</code> format (topics <code>meshcore/IATA/PUBKEY/packets</code>). Compatible with letsmesh-style packet analyzers.",
"settings.regions.intro": "Only repeaters allowing a region will forward messages tagged with it. Find standardised region names at <a href=\"https://regions.meshcore.nz/\" target=\"_blank\" rel=\"noopener\">regions.meshcore.nz</a>.",
"settings.regions.name_ph": "Region name (e.g. pl, pl-ma)",
"settings.regions.registry": "Region Registry",
"settings.regions.tip": "Tip: pick the default region via the radio button, or select <em>None</em> to fall back to the firmware default. The chosen region is also pushed to the firmware so any untagged channel uses it.",
"settings.reset_defaults": "Reset to defaults",
"settings.tab.analyzer": "Analyzer",
"settings.tab.appearance": "Appearance",
+45
View File
@@ -784,6 +784,29 @@
"search.tip_phrase": "dokładna fraza",
"search.tip_prefix": "dopasowanie przedrostka (hello, hellas...)",
"search.tips": "Wskazówki wyszukiwania:",
"settings.analyzer.intro": "Dodaj usługi MeshCore Analyzer do wyboru po kliknięciu <i class=\"bi bi-clipboard-data\"></i> pod wiadomością z czatu grupowego. URL musi zawierać znacznik <code>{packetHash}</code> — jest zastępowany hashem pakietu wiadomości.",
"settings.analyzer.services": "Usługi analizatorów",
"settings.analyzer.tip": "Wskazówka: oznacz gwiazdką jeden analizator, aby używać go bez pytania. Usuń gwiazdkę, aby pytać przy każdym kliknięciu. Wyłączone analizatory zostają na liście, ale są ukryte w oknie wyboru.",
"settings.appear.btn_size": "Rozmiar przycisku (px)",
"settings.appear.hide_fab": "Ukryj przyciski szybkiego dostępu",
"settings.appear.hide_fab_hint": "Gdy ukryte, wszystkie pozycje pojawiają się w menu głównym.",
"settings.appear.item": "Pozycja",
"settings.appear.language": "Język",
"settings.appear.language_hint": "Dotyczy tej przeglądarki i staje się domyślnym ustawieniem serwera dla pozostałych. Dodaj własny, wrzucając katalog tłumaczeń do folderu translations — zobacz <code>docs/translations.md</code>.",
"settings.appear.place_fab": "Szybki dostęp",
"settings.appear.place_menu": "Menu główne",
"settings.appear.placement": "Umieszczenie",
"settings.appear.placement_aria": "Umieszczenie: {item}",
"settings.appear.position": "Położenie",
"settings.appear.quick_access": "Przyciski szybkiego dostępu",
"settings.appear.reload_title": "Przeskanuj ponownie folder tłumaczeń",
"settings.appear.reset_default": "Przywróć domyślne",
"settings.appear.spacing": "Odstęp (px)",
"settings.appear.theme": "Motyw",
"settings.appear.theme_dark": "Ciemny",
"settings.appear.theme_dark_desc": "Łagodny dla oczu, głęboki granat",
"settings.appear.theme_light": "Jasny",
"settings.appear.theme_light_desc": "Klasyczny jasny interfejs",
"settings.chat.autoclose": "Zamknij automatycznie po (s)",
"settings.chat.autoclose_help": "Ile sekund do automatycznego zamknięcia popupu trasy",
"settings.chat.no_autoclose": "Nie zamykaj automatycznie",
@@ -793,6 +816,12 @@
"settings.chat.quote_len_help": "Domyślna maks. liczba bajtów UTF-8 dla skróconych cytatów",
"settings.chat.route_popup": "Popup trasy",
"settings.chat.route_popup_desc": "Popup pokazywany po dotknięciu \"SNR | Hops\" pod wiadomością. Dotyczy także DM-ów.",
"settings.contacts.auto_ignore": "Automatycznie dodawaj nowe kontakty do \"Ignorowanych\"",
"settings.contacts.auto_ignore_help": "Każdy nowy advert jest automatycznie oznaczany jako ignorowany — bez powiadomień, bez plakietki. Kontakty nadal pojawiają się w Istniejących kontaktach (pamięć) i można je przenieść przyciskiem \"Na urządzenie\". Wymaga włączonego zatwierdzania ręcznego.",
"settings.contacts.manual_approval": "Zatwierdzanie ręczne włączone",
"settings.contacts.manual_approval_help": "Gdy włączone, nowe kontakty muszą zostać ręcznie zatwierdzone, zanim będą mogły komunikować się z Twoim węzłem",
"settings.contacts.suppress_notifs": "Wycisz powiadomienia o nowych advertach",
"settings.contacts.suppress_notifs_help": "Ukryj plakietkę nad Zarządzaniem kontaktami oraz powiadomienie przeglądarki, gdy pojawiają się nowe oczekujące kontakty. Sama lista oczekujących kontaktów nadal je pokazuje. Wymaga włączonego zatwierdzania ręcznego.",
"settings.device.bw": "Bandwidth (kHz)",
"settings.device.cr": "Coding Rate",
"settings.device.freq": "Częstotliwość (MHz)",
@@ -838,6 +867,22 @@
"settings.msg.max_retries": "Maks. prób",
"settings.msg.max_retries_help": "Próby flood (używane także po rotacji ścieżki)",
"settings.msg.other": "Inne",
"settings.notif.blocked": "Zablokowane",
"settings.notif.desc": "Powiadomienia przeglądarki pojawiają się, gdy aplikacja jest ukryta lub w tle.",
"settings.notif.unavailable": "Niedostępne",
"settings.notif.works_hidden": "Działa, gdy aplikacja jest ukryta",
"settings.observer.advert_interval": "Interwał flood advert (h)",
"settings.observer.advert_interval_ph": "0 = wył.",
"settings.observer.brokers": "Brokery MQTT",
"settings.observer.enable": "Włącz observera",
"settings.observer.hint": "Trzyliterowy kod lokalizacji jest używany w tematach MQTT; zostaw pusty, aby użyć płaskich tematów <code>meshcore/packets</code>. Interwał advert równy 0 wyłącza zaplanowane flood adverty.",
"settings.observer.iata": "Kod lokalizacji (IATA)",
"settings.observer.iata_ph": "np. KRK",
"settings.observer.intro": "Publikuj każdy pakiet mesh usłyszany przez to urządzenie do brokerów MQTT, w formacie <code>meshcore-packet-capture</code> (tematy <code>meshcore/IATA/PUBKEY/packets</code>). Zgodne z analizatorami pakietów w stylu letsmesh.",
"settings.regions.intro": "Tylko repeatery zezwalające na dany region będą przekazywać oznaczone nim wiadomości. Standardowe nazwy regionów znajdziesz na <a href=\"https://regions.meshcore.nz/\" target=\"_blank\" rel=\"noopener\">regions.meshcore.nz</a>.",
"settings.regions.name_ph": "Nazwa regionu (np. pl, pl-ma)",
"settings.regions.registry": "Rejestr regionów",
"settings.regions.tip": "Wskazówka: wybierz domyślny region przyciskiem radiowym albo wybierz <em>Brak</em>, aby wrócić do domyślnego ustawienia firmware. Wybrany region jest też wysyłany do firmware, więc użyje go każdy nieoznaczony kanał.",
"settings.reset_defaults": "Przywróć domyślne",
"settings.tab.analyzer": "Analizator",
"settings.tab.appearance": "Wygląd",