diff --git a/app/static/js/repeater-manage.js b/app/static/js/repeater-manage.js
index 6f84e7a..f14479d 100644
--- a/app/static/js/repeater-manage.js
+++ b/app/static/js/repeater-manage.js
@@ -88,19 +88,20 @@ function esc(s) {
// Tools configuration
// ================================================================
+// title/desc hold catalog KEYS, resolved with t() at render time.
const TOOLS = [
- { key: 'status', icon: 'bi-bar-chart-line', title: 'Status',
- desc: 'Battery, radio and packet statistics', adminOnly: false },
- { key: 'telemetry', icon: 'bi-activity', title: 'Telemetry',
- desc: 'Sensor channels (Cayenne LPP)', adminOnly: false },
- { key: 'neighbors', icon: 'bi-people', title: 'Neighbors',
- desc: 'Zero-hop repeaters heard', adminOnly: false },
- { key: 'cli', icon: 'bi-terminal', title: 'CLI',
- desc: 'Send text commands to the repeater', adminOnly: true },
- { key: 'settings', icon: 'bi-gear', title: 'Settings',
- desc: 'Configure repeater parameters', adminOnly: true },
- { key: 'actions', icon: 'bi-lightning', title: 'Actions',
- desc: 'Advert, clock sync, reboot', adminOnly: true },
+ { key: 'status', icon: 'bi-bar-chart-line', title: 'rptmgmt.tools.status',
+ desc: 'rptmgmt.tools.status_desc', adminOnly: false },
+ { key: 'telemetry', icon: 'bi-activity', title: 'rptmgmt.tools.telemetry',
+ desc: 'rptmgmt.tools.telemetry_desc', adminOnly: false },
+ { key: 'neighbors', icon: 'bi-people', title: 'rptmgmt.tools.neighbors',
+ desc: 'rptmgmt.tools.neighbors_desc', adminOnly: false },
+ { key: 'cli', icon: 'bi-terminal', title: 'rptmgmt.tools.cli',
+ desc: 'rptmgmt.tools.cli_desc', adminOnly: true },
+ { key: 'settings', icon: 'bi-gear', title: 'rptmgmt.tools.settings',
+ desc: 'rptmgmt.tools.settings_desc', adminOnly: true },
+ { key: 'actions', icon: 'bi-lightning', title: 'rptmgmt.tools.actions',
+ desc: 'rptmgmt.tools.actions_desc', adminOnly: true },
];
// ================================================================
@@ -118,7 +119,7 @@ let _passwordModal = null;
function showLoading(text) {
document.getElementById('loadingState').style.display = '';
- document.getElementById('loadingText').textContent = text || 'Loading…';
+ document.getElementById('loadingText').textContent = text || t('common.loading');
document.getElementById('errorState').style.display = 'none';
document.getElementById('panelContent').style.display = 'none';
}
@@ -126,7 +127,7 @@ function showLoading(text) {
function showError(text) {
document.getElementById('loadingState').style.display = 'none';
document.getElementById('errorState').style.display = '';
- document.getElementById('errorText').textContent = text || 'Something went wrong.';
+ document.getElementById('errorText').textContent = text || t('rptmgmt.error_generic');
document.getElementById('panelContent').style.display = 'none';
}
@@ -188,8 +189,8 @@ function renderTools() {
${locked ? 'title="Admin login required"' : ''}>
-
${esc(tool.title)}${locked ? ' ' : ''}
-
${esc(tool.desc)}
+
${esc(t(tool.title))}${locked ? ' ' : ''}
+
${esc(t(tool.desc))}
@@ -197,7 +198,7 @@ function renderTools() {
const tile = col.querySelector('.tool-tile');
tile.addEventListener('click', () => {
if (locked) {
- showNotification('Admin login required for this tool', 'warning');
+ showNotification(t('rptmgmt.toast.admin_required'), 'warning');
return;
}
openToolPane(tool);
@@ -226,7 +227,7 @@ function openToolPane(tool) {
icon.style.height = '32px';
icon.style.fontSize = '1rem';
icon.innerHTML = ` `;
- document.getElementById('paneTitle').textContent = tool.title;
+ document.getElementById('paneTitle').textContent = t(tool.title);
const body = document.getElementById('paneBody');
if (tool.key === 'status') {
@@ -256,7 +257,7 @@ function openToolPane(tool) {
body.innerHTML = `
-
The ${esc(tool.title)} tool is coming in a later stage.
+
${tHtml('rptmgmt.tool_coming', { tool: t(tool.title) })}
`;
}
@@ -322,7 +323,7 @@ function setStatusUpdatedLabel() {
const label = document.getElementById('statusUpdated');
if (!label) { clearInterval(_statusTimer); _statusTimer = null; return; }
const secs = Math.floor((Date.now() - _statusUpdatedAt) / 1000);
- if (secs < 2) label.textContent = 'Updated just now';
+ if (secs < 2) label.textContent = t('rptmgmt.updated_just_now');
else if (secs < 60) label.textContent = `Updated ${secs}s ago`;
else label.textContent = `Updated ${fmtDuration(secs)} ago`;
};
@@ -347,7 +348,7 @@ async function loadStatus() {
const resp = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}/status`);
data = await resp.json();
} catch (e) {
- data = { success: false, error: 'Request failed' };
+ data = { success: false, error: t('rptmgmt.request_failed') };
}
if (btn) btn.disabled = false;
@@ -355,7 +356,7 @@ async function loadStatus() {
container.innerHTML = `
-
${esc((data && data.error) || 'Failed to get status')}
+
${esc((data && data.error) || t('rptmgmt.status_failed'))}
Try again
@@ -494,7 +495,7 @@ function setTelemetryUpdatedLabel() {
const label = document.getElementById('telemetryUpdated');
if (!label) { clearInterval(_telemetryTimer); _telemetryTimer = null; return; }
const secs = Math.floor((Date.now() - _telemetryUpdatedAt) / 1000);
- if (secs < 2) label.textContent = 'Updated just now';
+ if (secs < 2) label.textContent = t('rptmgmt.updated_just_now');
else if (secs < 60) label.textContent = `Updated ${secs}s ago`;
else label.textContent = `Updated ${fmtDuration(secs)} ago`;
};
@@ -520,7 +521,7 @@ async function loadTelemetry() {
const resp = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}/telemetry`);
data = await resp.json();
} catch (e) {
- data = { success: false, error: 'Request failed' };
+ data = { success: false, error: t('rptmgmt.request_failed') };
}
if (btn) btn.disabled = false;
@@ -528,7 +529,7 @@ async function loadTelemetry() {
container.innerHTML = `
-
${esc((data && data.error) || 'Failed to get telemetry')}
+
${esc((data && data.error) || t('rptmgmt.telemetry_failed'))}
Try again
@@ -546,7 +547,7 @@ async function loadTelemetry() {
function renderTelemetryCards(container, lpp) {
if (!lpp.length) {
- container.innerHTML = '
No telemetry data reported.
';
+ container.innerHTML = `
${tHtml('rptmgmt.telemetry_none')}
`;
return;
}
@@ -710,7 +711,7 @@ async function sendCliCommand(raw) {
});
data = await resp.json();
} catch (e) {
- data = { success: false, error: 'Request failed' };
+ data = { success: false, error: t('rptmgmt.request_failed') };
}
if (pendingLine) pendingLine.remove();
@@ -720,7 +721,7 @@ async function sendCliCommand(raw) {
cliAppend('meta', `(${(data.elapsed_ms / 1000).toFixed(1)} s)`);
}
} else {
- cliAppend('error', (data && data.error) || 'Command failed');
+ cliAppend('error', (data && data.error) || t('rptmgmt.command_failed'));
}
_cliPending = false;
@@ -786,7 +787,7 @@ async function loadNeighbors() {
const resp = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}/neighbours`);
data = await resp.json();
} catch (e) {
- data = { success: false, error: 'Request failed' };
+ data = { success: false, error: t('rptmgmt.request_failed') };
}
if (btn) btn.disabled = false;
@@ -796,7 +797,7 @@ async function loadNeighbors() {
container.innerHTML = `
-
${esc((data && data.error) || 'Failed to get neighbours')}
+
${esc((data && data.error) || t('rptmgmt.neighbours_failed'))}
Try again
@@ -835,7 +836,7 @@ function renderNeighborsList() {
if (toggle) toggle.style.display = mappable ? '' : 'none';
if (!entries.length) {
- container.innerHTML = '
No neighbours reported yet.Neighbours are learned from zero-hop repeater adverts.
';
+ container.innerHTML = `
${tHtml('rptmgmt.neighbours_none')}${tHtml('rptmgmt.neighbours_none_hint')}
`;
return;
}
@@ -852,8 +853,8 @@ function renderNeighborsList() {
container.innerHTML = `
- Repeater
- Heard
+ ${tHtml('rptmgmt.neigh.repeater')}
+ ${tHtml('rptmgmt.neigh.heard')}
SNR
${rows}
@@ -910,7 +911,7 @@ function renderNeighborsMap() {
opacity: 1,
fillOpacity: 0.9
}).addTo(_nbMapLayers);
- centerMarker.bindPopup(`${esc(_repeater.name || 'This repeater')} managed repeater`);
+ centerMarker.bindPopup(`${esc(_repeater.name || t('rptmgmt.this_repeater'))} ${tHtml('rptmgmt.managed_repeater')}`);
bounds.push(center);
}
@@ -997,52 +998,54 @@ async function loadClock() {
// Sections and fields mirror _REPEATER_SETTINGS_FIELDS in api.py.
// Values travel as raw CLI strings (`get X` → `> value`, `set X value`).
+// title/label/help/note hold catalog KEYS, resolved with t() at render time.
+// `key` is the firmware CLI parameter and is never translated.
const SETTINGS_SECTIONS = [
- { key: 'basic', title: 'Basic', icon: 'bi-tag', fields: [
- { key: 'name', label: 'Repeater name', type: 'text' },
- { key: 'password', label: 'Admin password', type: 'password', writeOnly: true,
- help: 'Write-only — the current password is never shown. Changing it does not log out already active sessions.' },
- { key: 'guest.password', label: 'Guest password', type: 'text',
- help: 'Password for read-only guest logins.' },
+ { key: 'basic', title: 'rptmgmt.set.basic', icon: 'bi-tag', fields: [
+ { key: 'name', label: 'rptmgmt.set.name', type: 'text' },
+ { key: 'password', label: 'rptmgmt.set.password', type: 'password', writeOnly: true,
+ help: 'rptmgmt.set.password_help' },
+ { key: 'guest.password', label: 'rptmgmt.set.guest_password', type: 'text',
+ help: 'rptmgmt.set.guest_password_help' },
]},
- { key: 'radio', title: 'Radio', icon: 'bi-broadcast',
- note: 'Frequency, bandwidth, SF and CR are applied after a reboot. TX power applies immediately.',
+ { key: 'radio', title: 'rptmgmt.set.radio', icon: 'bi-broadcast',
+ note: 'rptmgmt.set.radio_note',
fields: [
- { key: 'radio', label: 'Radio parameters', type: 'radio4' },
- { key: 'tx', label: 'TX power (dBm)', type: 'number', min: 0, max: 30, step: 1 },
- { key: 'radio.rxgain', label: 'RX boosted gain', type: 'onoff' },
+ { key: 'radio', label: 'rptmgmt.set.radio_params', type: 'radio4' },
+ { key: 'tx', label: 'rptmgmt.set.tx', type: 'number', min: 0, max: 30, step: 1 },
+ { key: 'radio.rxgain', label: 'rptmgmt.set.rxgain', type: 'onoff' },
]},
- { key: 'location', title: 'Location', icon: 'bi-geo-alt', fields: [
- { key: 'lat', label: 'Latitude', type: 'text' },
- { key: 'lon', label: 'Longitude', type: 'text' },
+ { key: 'location', title: 'rptmgmt.set.location', icon: 'bi-geo-alt', fields: [
+ { key: 'lat', label: 'rptmgmt.set.lat', type: 'text' },
+ { key: 'lon', label: 'rptmgmt.set.lon', type: 'text' },
]},
- { key: 'features', title: 'Features', icon: 'bi-toggles', fields: [
- { key: 'repeat', label: 'Repeat packets', type: 'onoff' },
- { key: 'allow.read.only', label: 'Allow read-only (guest) access', type: 'onoff' },
- { key: 'multi.acks', label: 'Send multiple ACKs', type: 'zeroone' },
+ { key: 'features', title: 'rptmgmt.set.features', icon: 'bi-toggles', fields: [
+ { key: 'repeat', label: 'rptmgmt.set.repeat', type: 'onoff' },
+ { key: 'allow.read.only', label: 'rptmgmt.set.allow_read_only', type: 'onoff' },
+ { key: 'multi.acks', label: 'rptmgmt.set.multi_acks', type: 'zeroone' },
]},
- { key: 'network', title: 'Network health', icon: 'bi-heart-pulse', fields: [
- { key: 'loop.detect', label: 'Loop detection', type: 'select',
+ { key: 'network', title: 'rptmgmt.set.network', icon: 'bi-heart-pulse', fields: [
+ { key: 'loop.detect', label: 'rptmgmt.set.loop_detect', type: 'select',
options: ['off', 'minimal', 'moderate', 'strict'] },
- { key: 'dutycycle', label: 'Duty cycle (%)', type: 'number', min: 1, max: 100, step: 1 },
+ { key: 'dutycycle', label: 'rptmgmt.set.dutycycle', type: 'number', min: 1, max: 100, step: 1 },
]},
- { key: 'advert', title: 'Advertisement', icon: 'bi-megaphone', fields: [
- { key: 'advert.interval', label: 'Local advert interval (minutes)', type: 'number', min: 0, max: 240, step: 1,
- help: 'Firmware accepts 60–240 minutes, or 0 to disable.' },
- { key: 'flood.advert.interval', label: 'Flood advert interval (hours)', type: 'number', min: 0, step: 1 },
- { key: 'flood.max', label: 'Max flood hops', type: 'number', min: 0, max: 64, step: 1 },
+ { key: 'advert', title: 'rptmgmt.set.advert', icon: 'bi-megaphone', fields: [
+ { key: 'advert.interval', label: 'rptmgmt.set.advert_interval', type: 'number', min: 0, max: 240, step: 1,
+ help: 'rptmgmt.set.advert_interval_help' },
+ { key: 'flood.advert.interval', label: 'rptmgmt.set.flood_advert_interval', type: 'number', min: 0, step: 1 },
+ { key: 'flood.max', label: 'rptmgmt.set.flood_max', type: 'number', min: 0, max: 64, step: 1 },
]},
- { key: 'operator', title: 'Operator info', icon: 'bi-person-vcard', fields: [
- { key: 'owner.info', label: 'Owner info', type: 'textarea',
- help: 'Free-form operator / contact info shown to clients. Multiple lines allowed.' },
+ { key: 'operator', title: 'rptmgmt.set.operator', icon: 'bi-person-vcard', fields: [
+ { key: 'owner.info', label: 'rptmgmt.set.owner_info', type: 'textarea',
+ help: 'rptmgmt.set.owner_info_help' },
]},
- { key: 'advanced', title: 'Advanced', icon: 'bi-sliders', fields: [
- { key: 'path.hash.mode', label: 'Path hash mode (0–2)', type: 'number', min: 0, max: 2, step: 1 },
- { key: 'txdelay', label: 'TX delay factor (0–2)', type: 'number', min: 0, max: 2, step: 'any' },
- { key: 'direct.txdelay', label: 'Direct TX delay factor (0–2)', type: 'number', min: 0, max: 2, step: 'any' },
- { key: 'int.thresh', label: 'Interference threshold', type: 'number', step: 1 },
- { key: 'agc.reset.interval', label: 'AGC reset interval', type: 'number', min: 0, step: 4,
- help: 'Multiple of 4; 0 disables periodic AGC resets.' },
+ { key: 'advanced', title: 'rptmgmt.set.advanced', icon: 'bi-sliders', fields: [
+ { key: 'path.hash.mode', label: 'rptmgmt.set.path_hash_mode', type: 'number', min: 0, max: 2, step: 1 },
+ { key: 'txdelay', label: 'rptmgmt.set.txdelay', type: 'number', min: 0, max: 2, step: 'any' },
+ { key: 'direct.txdelay', label: 'rptmgmt.set.direct_txdelay', type: 'number', min: 0, max: 2, step: 'any' },
+ { key: 'int.thresh', label: 'rptmgmt.set.int_thresh', type: 'number', step: 1 },
+ { key: 'agc.reset.interval', label: 'rptmgmt.set.agc_reset', type: 'number', min: 0, step: 4,
+ help: 'rptmgmt.set.agc_reset_help' },
]},
];
@@ -1104,10 +1107,10 @@ function settingsControlHtml(f) {
function settingsFieldHtml(f) {
return `
-
${esc(f.label)}
+
${esc(t(f.label))}
${settingsControlHtml(f)}
- ${f.help ? `
${esc(f.help)}
` : ''}
+ ${f.help ? `
${esc(t(f.help))}
` : ''}
`;
}
@@ -1120,22 +1123,22 @@ function renderSettingsPane(body) {
- ${sec.note ? `
${esc(sec.note)}
` : ''}
+ ${sec.note ? `
${esc(t(sec.note))}
` : ''}
- Some changes take effect after a reboot — use Actions → Reboot.
+ ${tHtml('rptmgmt.set.reboot_note')}
${sec.fields.map(settingsFieldHtml).join('')}
${sec.key === 'location' ? `
- Pick from map
+ title="${tHtml('rptmgmt.set.map_pick_title')}">
+ ${tHtml('rptmgmt.set.map_pick')}
` : ''}
@@ -1241,7 +1244,7 @@ function setFieldBadge(item, fieldKey, kind, text) {
} else if (kind === 'ok') {
badge.innerHTML = ' ';
} else if (kind === 'reboot') {
- badge.innerHTML = 'reboot required ';
+ badge.innerHTML = `${tHtml('rptmgmt.reboot_required')} `;
} else if (kind === 'error') {
badge.innerHTML = ' ';
if (text) {
@@ -1310,7 +1313,7 @@ async function loadSettingsSection(secKey) {
const resp = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}/settings?section=${encodeURIComponent(secKey)}`);
data = await resp.json();
} catch (e) {
- data = { success: false, error: 'Request failed' };
+ data = { success: false, error: t('rptmgmt.request_failed') };
}
st.loading = false;
@@ -1319,7 +1322,7 @@ async function loadSettingsSection(secKey) {
if (!data || !data.success) {
statusEl.classList.remove('text-muted');
statusEl.classList.add('text-danger');
- statusEl.textContent = (data && data.error) || 'Failed to read settings';
+ statusEl.textContent = (data && data.error) || t('rptmgmt.settings_read_failed');
updateSectionDirty(secKey);
return;
}
@@ -1340,7 +1343,7 @@ async function loadSettingsSection(secKey) {
} else {
errCount++;
disableSettingsField(item, f, true);
- setFieldBadge(item, f.key, 'error', errors[f.key] || 'Failed to read');
+ setFieldBadge(item, f.key, 'error', errors[f.key] || t('rptmgmt.read_failed'));
}
});
if (mapPickBtn) mapPickBtn.disabled = !('lat' in st.loaded && 'lon' in st.loaded);
@@ -1373,7 +1376,7 @@ async function applySettingsSection(secKey) {
const emptyKey = keys.find(k => dirty[k] === '' && k !== 'owner.info');
if (emptyKey) {
- showNotification(`"${emptyKey}" cannot be empty`, 'warning');
+ showNotification(t('rptmgmt.toast.field_empty', { field: emptyKey }), 'warning');
return;
}
@@ -1398,7 +1401,7 @@ async function applySettingsSection(secKey) {
});
data = await resp.json();
} catch (e) {
- data = { success: false, error: 'Request failed' };
+ data = { success: false, error: t('rptmgmt.request_failed') };
}
st.applying = false;
@@ -1406,7 +1409,7 @@ async function applySettingsSection(secKey) {
if (!data || !data.success) {
keys.forEach(k => setFieldBadge(item, k, 'error'));
- showNotification((data && data.error) || 'Failed to apply settings', 'danger');
+ showNotification((data && data.error) || t('rptmgmt.settings_apply_failed'), 'danger');
updateSectionDirty(secKey);
return;
}
@@ -1415,7 +1418,7 @@ async function applySettingsSection(secKey) {
let okCount = 0, failCount = 0, rebootCount = 0;
for (const f of sec.fields) {
if (!(f.key in dirty)) continue;
- const res = results[f.key] || { status: 'failed', error: 'No result' };
+ const res = results[f.key] || { status: 'failed', error: t('rptmgmt.no_result') };
if (res.status === 'ok' || res.status === 'reboot_required') {
if (res.status === 'reboot_required') {
rebootCount++;
@@ -1436,7 +1439,7 @@ async function applySettingsSection(secKey) {
}
} else {
failCount++;
- setFieldBadge(item, f.key, 'error', res.error || res.reply || 'Failed');
+ setFieldBadge(item, f.key, 'error', res.error || res.reply || t('rptmgmt.failed'));
}
}
@@ -1444,9 +1447,9 @@ async function applySettingsSection(secKey) {
updateSectionDirty(secKey);
if (failCount === 0) {
- showNotification(rebootCount ? 'Applied — reboot required for some changes' : 'Settings applied', 'success');
+ showNotification(rebootCount ? t('rptmgmt.toast.applied_reboot') : t('rptmgmt.toast.applied'), 'success');
} else {
- showNotification(`${failCount} setting${failCount > 1 ? 's' : ''} failed to apply`, 'danger');
+ showNotification(tn('rptmgmt.toast.apply_failed_count', failCount), 'danger');
}
if (okCount) {
setTimeout(() => {
@@ -1472,7 +1475,7 @@ async function syncSavedPassword(newPassword) {
});
const data = await resp.json();
if (data && data.success) {
- showNotification('Saved password updated to the new one', 'success');
+ showNotification(t('rptmgmt.toast.password_synced'), 'success');
}
} catch (e) {
console.error('Failed to update saved password:', e);
@@ -1570,16 +1573,17 @@ function applyLocationFromMap() {
// ================================================================
// Keys mirror _REPEATER_ACTIONS in api.py.
+// title/desc/btn hold catalog KEYS, resolved with t() at render time.
const REPEATER_ACTIONS = [
{ key: 'zerohop_advert', icon: 'bi-megaphone', iconClass: 'text-primary',
- title: 'Send zero-hop advert', btn: 'Send', btnClass: 'btn-outline-primary',
- desc: 'Announce this repeater to its direct neighbours only.' },
+ title: 'rptmgmt.act.zerohop', btn: 'rptmgmt.act.send', btnClass: 'btn-outline-primary',
+ desc: 'rptmgmt.act.zerohop_desc' },
{ key: 'flood_advert', icon: 'bi-broadcast-pin', iconClass: 'text-warning',
- title: 'Send flood advert', btn: 'Send', btnClass: 'btn-outline-warning',
- desc: 'Not recommended — the advert is flooded across the whole mesh (high network load).' },
+ title: 'rptmgmt.act.flood', btn: 'rptmgmt.act.send', btnClass: 'btn-outline-warning',
+ desc: 'rptmgmt.act.flood_desc' },
{ key: 'clock_sync', icon: 'bi-clock-history', iconClass: 'text-primary',
- title: 'Sync clock', btn: 'Sync', btnClass: 'btn-outline-primary',
- desc: "Set the repeater's clock from this device's current time. The firmware refuses to move the clock backwards." },
+ title: 'rptmgmt.act.clock_sync', btn: 'rptmgmt.act.sync', btnClass: 'btn-outline-primary',
+ desc: 'rptmgmt.act.clock_sync_desc' },
];
let _actionPending = false;
@@ -1589,12 +1593,12 @@ function actionRowHtml(a) {
-
${esc(a.title)}
-
${esc(a.desc)}
+
${esc(t(a.title))}
+
${esc(t(a.desc))}
- ${esc(a.btn)}
+ ${esc(t(a.btn))}
`;
}
@@ -1609,17 +1613,16 @@ function renderActionsPane(body) {
${actionRowHtml({
key: 'reboot', icon: 'bi-arrow-clockwise', iconClass: 'text-danger',
- title: 'Reboot repeater', btn: 'Reboot', btnClass: 'btn-danger',
- desc: 'The repeater drops off the mesh for a few seconds while it restarts.',
+ title: 'rptmgmt.act.reboot', btn: 'rptmgmt.act.reboot_btn', btnClass: 'btn-danger',
+ desc: 'rptmgmt.act.reboot_desc',
})}
- Erase file system is not available over the mesh —
- the firmware only accepts it on the USB serial console (use the MeshCore flasher instead).
+ ${tHtml('rptmgmt.erase_fs_note')}
@@ -1657,7 +1660,7 @@ async function runRepeaterAction(action) {
});
data = await resp.json();
} catch (e) {
- data = { success: false, error: 'Request failed' };
+ data = { success: false, error: t('rptmgmt.request_failed') };
}
_actionPending = false;
@@ -1669,14 +1672,14 @@ async function runRepeaterAction(action) {
if (data && data.success) {
resultEl.classList.add(data.ok ? 'text-success' : 'text-danger');
const elapsed = data.elapsed_ms != null ? ` (${(data.elapsed_ms / 1000).toFixed(1)} s)` : '';
- resultEl.textContent = (data.reply || 'Done') + elapsed;
+ resultEl.textContent = (data.reply || t('rptmgmt.done')) + elapsed;
} else {
resultEl.classList.add('text-danger');
- resultEl.textContent = (data && data.error) || 'Action failed';
+ resultEl.textContent = (data && data.error) || t('rptmgmt.action_failed');
}
}
if (!data || !data.success) {
- showNotification((data && data.error) || 'Action failed', 'danger');
+ showNotification((data && data.error) || t('rptmgmt.action_failed'), 'danger');
}
}
@@ -1688,7 +1691,7 @@ async function fetchRepeater() {
const response = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}`);
const data = await response.json();
if (!data.success) {
- throw new Error(data.error || 'Failed to load repeater');
+ throw new Error(data.error || t('rptmgmt.load_failed'));
}
_repeater = data.repeater;
_session = data.session;
@@ -1713,7 +1716,7 @@ async function doLogin(password, save) {
data = await response.json();
} catch (e) {
console.error('Login request failed:', e);
- data = { success: false, error: 'Login request failed' };
+ data = { success: false, error: t('rptmgmt.login_request_failed') };
}
if (data && data.success) {
diff --git a/app/templates/repeater-manage.html b/app/templates/repeater-manage.html
index be4417d..c3fa9ea 100644
--- a/app/templates/repeater-manage.html
+++ b/app/templates/repeater-manage.html
@@ -3,7 +3,7 @@
-
Repeater Management - mc-webui
+
{{ t('rptmgmt.title') }} - mc-webui