v0.247.0: i18n spike — the dashboard can speak English, Hungarian byte-identical
gates / gates (push) Successful in 19s

Message bundles (internal/i18n) expanded into templates before parsing, one
template set per language. Launcher, /backups, /apps/<slug> and the layout
converted; household language setting, POST /settings/language, ?lang= override,
report field. Parity test against fixtures captured from unconverted templates;
copy gates read templates expanded; new i18n_missing_gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-17 14:50:57 +02:00
parent 89dd3e94b1
commit 612c417024
49 changed files with 11734 additions and 248 deletions
+52 -52
View File
@@ -2,7 +2,7 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Biztonsági mentés</h2>
<h2>{{T "backups.biztonsagi_mentes"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
@@ -20,7 +20,7 @@
<p class="form-hint">{{.BackupTarget.OfferLabel}}</p>
<button type="button" class="btn btn-sm" id="backup-target-assign"
data-path="{{.BackupTarget.OfferPath}}"
data-confirm="Kijelölöd ezt a meghajtót a rendszermentés helyéül?">Kijelölöm</button>
data-confirm="{{T "backups.kijelolod_ezt_a_meghajtot_a"}}">{{T "backups.kijelolom"}}</button>
<div id="backup-target-assign-result"></div>
</div>
{{end}}
@@ -32,13 +32,13 @@
<!-- Section 0: Storage overview -->
<div class="backup-section-card">
<h3>Tárhely áttekintés</h3>
<h3>{{T "backups.tarhely_attekintes"}}</h3>
<div class="storage-overview-grid">
<div class="storage-bars">
{{with $.SystemInfo}}
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">Rendszer (/)</span>
<span class="storage-label">{{T "backups.rendszer"}}</span>
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
</div>
<div class="meter-track">
@@ -50,7 +50,7 @@
<div class="storage-item meter meter-empty storage-disconnected">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value">— <span class="state-text-neutral">Leválasztva</span></span>
<span class="storage-value">— <span class="state-text-neutral">{{T "backups.levalasztva"}}</span></span>
</div>
<div class="meter-track"></div>
</div>
@@ -70,8 +70,8 @@
</div>
<div class="storage-stats">
<div class="storage-stat-row">
<span class="storage-stat-label">DB mentések</span>
<span class="storage-stat-value mono">{{if .Backup.DumpFiles}}{{len .Backup.DumpFiles}} fájl{{else}}–{{end}}</span>
<span class="storage-stat-label">{{T "backups.db_mentesek"}}</span>
<span class="storage-stat-value mono">{{if .Backup.DumpFiles}}{{T "backups.fajl"}}{{else}}–{{end}}</span>
</div>
</div>
</div>
@@ -80,32 +80,32 @@
<!-- Section: Whole-guest (appliance) backup — agent-sourced, read-only + manual trigger -->
{{with .GuestBackup}}
<div class="backup-section-card">
<h3>Rendszermentés (teljes mentés)</h3>
<p class="form-hint" style="margin-bottom:1rem">A teljes szerver — alkalmazások, beállítások és adatbázisok együtt — időszakos mentése, amelyből az egész készülék visszaállítható. Ezt a host-ügynök készíti és kezeli.</p>
<h3>{{T "backups.rendszermentes_teljes_mentes"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">{{T "backups.a_teljes_szerver_alkalmazasok_beallitaso"}}</p>
{{if not .Available}}
<div class="alert alert-info">{{.Note}}</div>
{{else}}
<div class="stats-grid backup-page-cards">
<div class="stat-card {{if .HasBackup}}{{if .Success}}stat-running{{else}}stat-stopped{{end}}{{end}}">
<div class="stat-value">{{if .HasBackup}}{{if .Success}}&#10003;{{else}}&#10007;{{end}}{{else}}–{{end}}</div>
<div class="stat-label">Utolsó teljes mentés
<div class="stat-label">{{T "backups.utolso_teljes_mentes"}}
{{if .HasBackup}}<br><span class="relative-time">{{fmtTime .StartedAt}} ({{timeAgo .StartedAt}})</span>{{end}}
</div>
</div>
<div class="stat-card stat-total">
<div class="stat-value">{{if and .HasBackup (not .SizeUnknown)}}{{fmtBytes .SizeBytes}}{{else}}–{{end}}</div>
<div class="stat-label">{{if .HasBackup}}{{.Target}}{{else}}Méret / cél{{end}}</div>
<div class="stat-label">{{if .HasBackup}}{{.Target}}{{else}}{{T "backups.meret_cel"}}{{end}}</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.15rem">{{if .Due}}Esedékes{{else}}Naprakész{{end}}</div>
<div class="stat-label">Következő mentés
{{if not .Due}}<br><span class="relative-time">{{.AgeHours}} órája — a mentési ablakon belül</span>{{end}}
<div class="stat-value" style="font-size:1.15rem">{{if .Due}}{{T "backups.esedekes"}}{{else}}{{T "backups.naprakesz"}}{{end}}</div>
<div class="stat-label">{{T "backups.kovetkezo_mentes"}}
{{if not .Due}}<br><span class="relative-time">{{T "backups.oraja_a_mentesi_ablakon_belul"}}</span>{{end}}
</div>
</div>
<div class="stat-card">
<div class="stat-value">{{if .HasRestoreTest}}{{if .RestorePass}}&#10003;{{else}}&#10007;{{end}}{{else}}–{{end}}</div>
<div class="stat-label">Visszaállítás ellenőrizve
{{if .HasRestoreTest}}<br><span class="relative-time">{{fmtTime .RestoreTestedAt}}</span>{{else}}<br><span class="relative-time">Még nem futott</span>{{end}}
<div class="stat-label">{{T "backups.visszaallitas_ellenorizve"}}
{{if .HasRestoreTest}}<br><span class="relative-time">{{fmtTime .RestoreTestedAt}}</span>{{else}}<br><span class="relative-time">{{T "backups.meg_nem_futott"}}</span>{{end}}
</div>
</div>
</div>
@@ -116,16 +116,16 @@
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>{{.Label}}</strong></td>
<td style="padding:.5rem 0;vertical-align:top">
{{if .NotSetUp}}
<span class="tag"><span class="dot"></span>nincs beállítva</span>
<span class="tag"><span class="dot"></span>{{T "backups.nincs_beallitva"}}</span>
{{else}}
{{if .HasSuccess}}
<div>&#10003; Utolsó sikeres mentés: {{fmtTime .SuccessAt}} ({{timeAgo .SuccessAt}}){{if .SizeKnown}} · {{fmtBytes .SizeBytes}}{{end}}
{{if .Current}}<span class="tag tag-run" style="margin-left:.4rem">Naprakész</span>{{else}}<span class="tag tag-warn" style="margin-left:.4rem">Esedékes</span>{{end}}</div>
<div>{{T "backups.utolso_sikeres_mentes"}}{{if .SizeKnown}} · {{fmtBytes .SizeBytes}}{{end}}
{{if .Current}}<span class="tag tag-run" style="margin-left:.4rem">{{T "backups.naprakesz"}}</span>{{else}}<span class="tag tag-warn" style="margin-left:.4rem">{{T "backups.esedekes"}}</span>{{end}}</div>
{{else}}
<div>– Még nincs sikeres mentés <span class="tag tag-warn" style="margin-left:.4rem">Esedékes</span></div>
<div>{{T "backups.meg_nincs_sikeres_mentes"}} <span class="tag tag-warn" style="margin-left:.4rem">{{T "backups.esedekes"}}</span></div>
{{end}}
{{if .FailedAfter}}
<div style="color:var(--red);margin-top:.25rem">&#10007; {{fmtTime .FailedAt}} — sikertelen</div>
<div style="color:var(--red);margin-top:.25rem">{{T "backups.sikertelen"}}</div>
{{end}}
{{end}}
</td>
@@ -134,12 +134,12 @@
</table>
{{end}}
{{if .Running}}
<div class="alert alert-info" id="wg-running">Mentés folyamatban… (fázis: <span id="wg-phase">{{.Phase}}</span>)</div>
<div class="alert alert-info" id="wg-running">{{T "backups.mentes_folyamatban_fazis"}} <span id="wg-phase">{{.Phase}}</span>)</div>
{{end}}
{{if .CanTrigger}}
<div class="schedule-actions" style="margin-top:1rem">
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" {{if .Running}}disabled{{end}}>Mentés most</button>
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.</span>
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" {{if .Running}}disabled{{end}}>{{T "backups.mentes_most"}}</button>
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "backups.a_mentes_alatt_az_alkalmazasok"}}</span>
<div id="wg-backup-result" style="margin-top:.6rem"></div>
</div>
{{end}}
@@ -149,21 +149,21 @@
<!-- Section: customer-configurable backup window (v0.168.0) — one setting drives every nightly leg -->
<div class="backup-section-card">
<h3>Mentési időablak</h3>
<p class="form-hint" style="margin-bottom:1rem">A mentések egymás után futnak: adatbázis-mentés, helyi másolat, távoli mentés, majd a teljes rendszermentés.</p>
<h3>{{T "backups.mentesi_idoablak"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">{{T "backups.a_mentesek_egymas_utan_futnak"}}</p>
<form method="POST" action="/backups/window" class="schedule-actions" style="display:flex;align-items:flex-end;gap:.75rem;flex-wrap:wrap">
{{.CSRFField}}
<div class="form-group" style="margin:0">
<label for="window_start">Mentési időablak kezdete</label>
<label for="window_start">{{T "backups.mentesi_idoablak_kezdete"}}</label>
<input type="time" id="window_start" name="window_start" value="{{.BackupWindow}}" class="form-control" style="max-width:9rem" required>
</div>
<button type="submit" class="btn btn-sm btn-primary">Mentés</button>
<button type="submit" class="btn btn-sm btn-primary">{{T "backups.mentes"}}</button>
</form>
<ul class="form-hint" style="margin-top:1rem;margin-bottom:0;list-style:none;padding:0;line-height:1.9">
<li>Adatbázis-mentés: <strong>{{.BackupLegDB}}</strong></li>
<li>Helyi másolat: <strong>{{.BackupLegTier2}}</strong></li>
<li>Távoli mentés: <strong>{{.BackupLegOffbox}}</strong></li>
<li>Teljes rendszermentés: kb. <strong>{{.BackupGateFrom}}–{{.BackupGateTo}}</strong> között</li>
<li>{{T "backups.adatbazis_mentes"}}</li>
<li>{{T "backups.helyi_masolat"}}</li>
<li>{{T "backups.tavoli_mentes"}}</li>
<li>{{T "backups.teljes_rendszermentes_kb_kozott"}}</li>
</ul>
</div>
@@ -173,42 +173,42 @@
{{if .Backup.LastDBDump.Success}}
<div class="stat-card stat-running">
<div class="stat-value">&#10003;</div>
<div class="stat-label">Adatmentés aktív</div>
<div class="stat-label">{{T "backups.adatmentes_aktiv"}}</div>
</div>
{{else}}
<div class="stat-card stat-stopped">
<div class="stat-value">&#10007;</div>
<div class="stat-label">Adatmentés sikertelen</div>
<div class="stat-label">{{T "backups.adatmentes_sikertelen"}}</div>
</div>
{{end}}
{{else}}
<div class="stat-card">
<div class="stat-value">–</div>
<div class="stat-label">Adatmentés</div>
<div class="stat-label">{{T "backups.adatmentes"}}</div>
</div>
{{end}}
{{if and .GuestBackup .GuestBackup.Offsite}}
<div class="stat-card stat-running">
<div class="stat-value">&#10003;</div>
<div class="stat-label">Távoli rendszermentés<br><span class="relative-time">külön hardveren (PBS)</span></div>
<div class="stat-label">{{T "backups.tavoli_rendszermentes"}}<br><span class="relative-time">{{T "backups.kulon_hardveren_pbs"}}</span></div>
</div>
{{else}}
<div class="stat-card" style="border-left-color: var(--text-3);">
<div class="stat-value" style="background:var(--text-3);-webkit-background-clip:text;background-clip:text;">–</div>
<div class="stat-label">Távoli rendszermentés<br><span class="relative-time">nincs beállítva</span></div>
<div class="stat-label">{{T "backups.tavoli_rendszermentes"}}<br><span class="relative-time">{{T "backups.nincs_beallitva"}}</span></div>
</div>
{{end}}
<div class="stat-card stat-total">
{{if eq .DBSectionState "embedded"}}
<div class="stat-value">–</div>
<div class="stat-label">Adatbázis mentve<br><span class="relative-time">beágyazott DB-k a kötetmentésben</span></div>
<div class="stat-label">{{T "backups.adatbazis_mentve"}}<br><span class="relative-time">{{T "backups.beagyazott_db_k_a_kotetmentesben"}}</span></div>
{{else}}
<div class="stat-value">
{{if .Backup.LastDBDump}}{{len .Backup.LastDBDump.Results}}{{else}}{{len .Backup.DumpFiles}}{{end}}
</div>
<div class="stat-label">Adatbázis mentve</div>
<div class="stat-label">{{T "backups.adatbazis_mentve"}}</div>
{{end}}
</div>
</div>
@@ -220,18 +220,18 @@
// stop stacks → agent vzdump → resume). Returns immediately; we poll /api/guest-backup/status.
function triggerGuestBackup() {
var btn = document.getElementById('wg-backup-btn');
felhomConfirm(btn, 'Elindítod a teljes rendszermentést most? A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.', function () {
felhomConfirm(btn, '{{T "backups.elinditod_a_teljes_rendszermentest_most"}}', function () {
var out = document.getElementById('wg-backup-result');
btn.disabled = true;
out.innerHTML = '<span class="form-hint">Mentés indítása…</span>';
out.innerHTML = '<span class="form-hint">{{T "backups.mentes_inditasa"}}</span>';
fetch('/api/guest-backup/trigger', { method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()) })
.then(r => r.json())
.then(data => {
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || 'Hiba') + '</div>'; btn.disabled = false; return; }
out.innerHTML = '<span class="form-hint">Mentés folyamatban…</span>';
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || '{{T "backups.hiba"}}') + '</div>'; btn.disabled = false; return; }
out.innerHTML = '<span class="form-hint">{{T "backups.mentes_folyamatban"}}</span>';
pollGuestBackup(out, btn);
})
.catch(e => { out.innerHTML = '<div class="alert alert-error">Hiba: ' + e + '</div>'; btn.disabled = false; });
.catch(e => { out.innerHTML = '<div class="alert alert-error">{{T "backups.hiba_2"}} ' + e + '</div>'; btn.disabled = false; });
});
}
@@ -244,9 +244,9 @@ function pollGuestBackup(out, btn) {
.then(data => {
if (data.ok && data.data) {
var ph = data.data.phase;
out.innerHTML = '<span class="form-hint">Állapot: ' + ph + '</span>';
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">A rendszermentés elkészült.</div>'; setTimeout(() => window.location.reload(), 1500); }
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">A mentés sikertelen.</div>'; btn.disabled = false; }
out.innerHTML = '<span class="form-hint">{{T "backups.allapot"}} ' + ph + '</span>';
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">{{T "backups.a_rendszermentes_elkeszult"}}</div>'; setTimeout(() => window.location.reload(), 1500); }
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">{{T "backups.a_mentes_sikertelen"}}</div>'; btn.disabled = false; }
}
})
.catch(() => {});
@@ -263,7 +263,7 @@ function pollGuestBackup(out, btn) {
btn.addEventListener('click', function () {
var out = document.getElementById('backup-target-assign-result');
btn.disabled = true;
out.innerHTML = '<p class="form-hint">Kijelölés folyamatban…</p>';
out.innerHTML = '<p class="form-hint">{{T "backups.kijeloles_folyamatban"}}</p>';
fetch('/api/storage/backup-target/assign', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()),
@@ -272,20 +272,20 @@ function pollGuestBackup(out, btn) {
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.ok) {
out.innerHTML = '<div class="alert alert-error">A kijelölés nem sikerült: ' + (j.error || '') + '</div>';
out.innerHTML = '<div class="alert alert-error">{{T "backups.a_kijeloles_nem_sikerult"}} ' + (j.error || '') + '</div>';
btn.disabled = false;
return;
}
// The agent deliberately does NOT restart itself — restarting with a backup in flight
// records a spurious tier failure. So say so plainly instead of hiding it or faking it.
if (j.data && j.data.restart_required) {
out.innerHTML = '<div class="flash flash-success">A meghajtó kijelölve. A beállítás a host-ügynök következő újraindulása után lép életbe.</div>';
out.innerHTML = '<div class="flash flash-success">{{T "backups.a_meghajto_kijelolve_a_beallitas"}}</div>';
} else {
out.innerHTML = '<div class="flash flash-success">A meghajtó kijelölve.</div>';
out.innerHTML = '<div class="flash flash-success">{{T "backups.a_meghajto_kijelolve"}}</div>';
}
})
.catch(function (e) {
out.innerHTML = '<div class="alert alert-error">Hiba: ' + e.message + '</div>';
out.innerHTML = '<div class="alert alert-error">{{T "backups.hiba_2"}} ' + e.message + '</div>';
btn.disabled = false;
});
});