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
+211
View File
@@ -0,0 +1,211 @@
// Package i18n is the controller's message bundle: one flat key → text map per language, embedded in
// the binary, Hungarian first and authoritative.
//
// Design: felhom.eu/documentation/architecture/10-localisation.md. The three rules that matter here:
//
// 1. HUNGARIAN IS THE SOURCE. hu.json holds every key. A key missing from hu is a build error (the
// template set refuses to load, TestBundleKeysUsedExistInHungarian fails) — never a runtime blank.
// 2. FALLBACK. A key missing from another language shows the Hungarian text, and is COUNTED
// (MissingKeys). A missing line never shows a key or an empty box.
// 3. TEMPLATES ARE EXPANDED, NOT CALLED. A `{{T "key"}}` marker in a dashboard template is replaced by
// the bundle text BEFORE html/template parses the file (Expand). The Hungarian template set is
// therefore parsed from exactly the bytes the template carried before it was converted, in the same
// escaping contexts — which is what makes byte-identical Hungarian output achievable at all.
// A runtime T func would route every string through the contextual escaper (`+`, `'`, `"` change
// bytes) and would force per-context handling inside <script>. Pinned by the parity test
// (internal/web/i18n_parity_test.go), not by this comment.
//
// A consequence of rule 3, stated so nobody is surprised by it: a bundle value MAY contain template
// actions ({{.Domain}}) and markup (<strong>). They are the message's parameters and emphasis, kept
// inside one message so a translator can move them. TestBundleActionsMatchAcrossLanguages pins that a
// translation carries the same actions as the Hungarian.
package i18n
import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"regexp"
"sort"
"strings"
"sync"
)
//go:embed locales/*.json
var localeFS embed.FS
// Default is the language of a box that never chose one — and the only language every key has.
const Default = "hu"
// Supported lists the languages a household may pick, Default first.
var Supported = []string{"hu", "en"}
// markerRe matches a template marker. Deliberately strict: a malformed marker is NOT expanded, so it
// reaches html/template as a call to an undefined function "T" and the template set fails to load —
// loud at startup and in every render test, never a silently shown marker.
var markerRe = regexp.MustCompile(`\{\{\s*T\s+"([A-Za-z0-9_.\-]+)"\s*\}\}`)
// Bundle holds every language's messages.
type Bundle struct {
msgs map[string]map[string]string
}
// Normalize maps any input to a supported language; everything unknown is Default.
func Normalize(lang string) string {
l := strings.ToLower(strings.TrimSpace(lang))
for _, s := range Supported {
if l == s {
return s
}
}
return Default
}
// IsSupported reports whether lang is exactly one of Supported.
func IsSupported(lang string) bool {
for _, s := range Supported {
if lang == s {
return true
}
}
return false
}
// Load reads the embedded bundles.
func Load() (*Bundle, error) {
return loadFrom(localeFS)
}
func loadFrom(fsys fs.FS) (*Bundle, error) {
b := &Bundle{msgs: map[string]map[string]string{}}
for _, lang := range Supported {
data, err := fs.ReadFile(fsys, "locales/"+lang+".json")
if err != nil {
return nil, fmt.Errorf("i18n: read %s bundle: %w", lang, err)
}
m := map[string]string{}
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("i18n: parse %s bundle: %w", lang, err)
}
b.msgs[lang] = m
}
return b, nil
}
var (
shared *Bundle
sharedErr error
sharedOnce sync.Once
)
// Shared returns the process-wide bundle, loaded once. A bundle that fails to load is a broken build
// (the files are embedded), so callers treat the error as fatal.
func Shared() (*Bundle, error) {
sharedOnce.Do(func() { shared, sharedErr = Load() })
return shared, sharedErr
}
// Text returns key in lang. fellBack is true when the Hungarian text was used instead (lang lacks the
// key); ok is false only when even Hungarian lacks it.
func (b *Bundle) Text(lang, key string) (text string, fellBack, ok bool) {
lang = Normalize(lang)
if v, found := b.msgs[lang][key]; found {
return v, false, true
}
if v, found := b.msgs[Default][key]; found {
return v, lang != Default, true
}
return "", false, false
}
// Msg is Text for Go-side copy. A key absent from Hungarian too is a programming error that the key
// test refuses to ship; at runtime it returns the Hungarian-less key rather than an empty string so the
// defect is visible, never a blank box.
func (b *Bundle) Msg(lang, key string) string {
if v, _, ok := b.Text(lang, key); ok {
return v
}
return key
}
// Plural picks a count-dependent form and formats n into it (%d). Hungarian does not inflect a noun
// after a numeral ("3 perce", "1 perce"), so hu carries ONE form under the key itself. English carries
// key+".one" and key+".other". A language without the split forms falls back to the plain key.
func (b *Bundle) Plural(lang, key string, n int) string {
lang = Normalize(lang)
form := ".other"
if n == 1 {
form = ".one"
}
if v, ok := b.msgs[lang][key+form]; ok {
return fmt.Sprintf(v, n)
}
return fmt.Sprintf(b.Msg(lang, key), n)
}
// Keys returns lang's keys, sorted.
func (b *Bundle) Keys(lang string) []string {
out := make([]string, 0, len(b.msgs[lang]))
for k := range b.msgs[lang] {
out = append(out, k)
}
sort.Strings(out)
return out
}
// Has reports whether lang itself carries key (a plural key counts when its ".other" form exists).
func (b *Bundle) Has(lang, key string) bool {
if _, ok := b.msgs[lang][key]; ok {
return true
}
_, ok := b.msgs[lang][key+".other"]
return ok
}
// MissingKeys lists the Hungarian keys lang does not carry — the lines that show Hungarian.
func (b *Bundle) MissingKeys(lang string) []string {
var out []string
for _, k := range b.Keys(Default) {
if !b.Has(lang, k) {
out = append(out, k)
}
}
return out
}
// ExpandStats counts what one Expand did.
type ExpandStats struct {
Markers int // markers replaced
FellBack []string // keys that used the Hungarian text
Undefined []string // keys absent from every language — left unexpanded, so the parse fails
}
// Expand replaces every `{{T "key"}}` marker in a template source with lang's text. Undefined keys are
// left in place on purpose (see markerRe): the caller's template parse then fails naming "T".
func (b *Bundle) Expand(lang, src string) (string, ExpandStats) {
var st ExpandStats
out := markerRe.ReplaceAllStringFunc(src, func(m string) string {
key := markerRe.FindStringSubmatch(m)[1]
text, fellBack, ok := b.Text(lang, key)
if !ok {
st.Undefined = append(st.Undefined, key)
return m
}
st.Markers++
if fellBack {
st.FellBack = append(st.FellBack, key)
}
return text
})
return out, st
}
// MarkerKeys returns the keys referenced by markers in src, in order of appearance.
func MarkerKeys(src string) []string {
var out []string
for _, m := range markerRe.FindAllStringSubmatch(src, -1) {
out = append(out, m[1])
}
return out
}
+162
View File
@@ -0,0 +1,162 @@
package i18n
import (
"regexp"
"sort"
"strings"
"testing"
"testing/fstest"
)
func testBundle(t *testing.T, hu, en string) *Bundle {
t.Helper()
b, err := loadFrom(fstest.MapFS{
"locales/hu.json": {Data: []byte(hu)},
"locales/en.json": {Data: []byte(en)},
})
if err != nil {
t.Fatal(err)
}
return b
}
func TestNormalize(t *testing.T) {
for in, want := range map[string]string{"en": "en", " EN ": "en", "hu": "hu", "": "hu", "de": "hu", "en-GB": "hu"} {
if got := Normalize(in); got != want {
t.Errorf("Normalize(%q) = %q, want %q", in, got, want)
}
}
if IsSupported("EN") || IsSupported("") || !IsSupported("en") {
t.Error("IsSupported must be exact")
}
}
// Rule 2 of the package: a missing English line shows the Hungarian one, and says it fell back; a key
// absent everywhere is reported as not ok — it never becomes an empty string silently.
func TestTextFallback(t *testing.T) {
b := testBundle(t, `{"a":"alma","b":"banán"}`, `{"a":"apple"}`)
if v, fb, ok := b.Text("en", "a"); v != "apple" || fb || !ok {
t.Errorf("translated key: %q fellBack=%v ok=%v", v, fb, ok)
}
if v, fb, ok := b.Text("en", "b"); v != "banán" || !fb || !ok {
t.Errorf("missing English must show Hungarian and flag the fallback: %q fellBack=%v ok=%v", v, fb, ok)
}
if v, fb, ok := b.Text("hu", "b"); v != "banán" || fb || !ok {
t.Errorf("Hungarian is never a fallback of itself: %q fellBack=%v", v, fb)
}
if _, _, ok := b.Text("en", "zzz"); ok {
t.Error("a key absent from Hungarian must not be ok")
}
if got := b.Msg("en", "zzz"); got != "zzz" {
t.Errorf("Msg of an undefined key must be visible, never blank: %q", got)
}
if got := b.MissingKeys("en"); len(got) != 1 || got[0] != "b" {
t.Errorf("MissingKeys(en) = %v, want [b]", got)
}
}
func TestPlural(t *testing.T) {
b := testBundle(t, `{"m":"%d perce"}`, `{"m.one":"%d minute ago","m.other":"%d minutes ago"}`)
for n, want := range map[int]string{0: "0 minutes ago", 1: "1 minute ago", 5: "5 minutes ago"} {
if got := b.Plural("en", "m", n); got != want {
t.Errorf("en Plural(%d) = %q, want %q", n, got, want)
}
}
if got := b.Plural("hu", "m", 1); got != "1 perce" {
t.Errorf("hu has one form: %q", got)
}
if !b.Has("en", "m") || len(b.MissingKeys("en")) != 0 {
t.Error("a key carried as .one/.other is present, not missing")
}
}
// A marker whose key exists nowhere is LEFT IN PLACE, so html/template refuses the file ("function T
// not defined") — loud at startup, never a silently shown marker or an empty string.
func TestExpandLeavesUndefinedMarker(t *testing.T) {
b := testBundle(t, `{"k":"Szia {{.Name}}"}`, `{}`)
out, st := b.Expand("en", `<p>{{T "k"}}</p><p>{{ T "nope" }}</p>`)
if out != `<p>Szia {{.Name}}</p><p>{{ T "nope" }}</p>` {
t.Errorf("Expand = %q", out)
}
if st.Markers != 1 || len(st.FellBack) != 1 || len(st.Undefined) != 1 || st.Undefined[0] != "nope" {
t.Errorf("stats = %+v", st)
}
}
// The embedded bundles themselves.
func TestEmbeddedBundles(t *testing.T) {
b, err := Load()
if err != nil {
t.Fatal(err)
}
if len(b.Keys(Default)) < 200 {
t.Fatalf("hu bundle has %d keys — the embedded file is not the converted one", len(b.Keys(Default)))
}
// No orphan: every English key translates a Hungarian key (plural forms count for their base).
for _, k := range b.Keys("en") {
base := strings.TrimSuffix(strings.TrimSuffix(k, ".one"), ".other")
if _, ok := b.msgs[Default][k]; !ok {
if _, ok := b.msgs[Default][base]; !ok {
t.Errorf("en key %q has no Hungarian source", k)
}
}
}
for lang, m := range b.msgs {
for k, v := range m {
if strings.TrimSpace(v) == "" {
t.Errorf("%s %q is empty — a blank box", lang, k)
}
}
}
}
var fieldRefRe = regexp.MustCompile(`\.[A-Z][A-Za-z0-9_]*(\.[A-Z][A-Za-z0-9_]*)*`)
var verbRe = regexp.MustCompile(`%[-+# 0-9.]*[sdvq]`)
func refs(s string) []string {
var out []string
for _, a := range regexp.MustCompile(`\{\{.*?\}\}`).FindAllString(s, -1) {
out = append(out, fieldRefRe.FindAllString(a, -1)...)
}
seen := map[string]bool{}
var uniq []string
for _, r := range out {
if !seen[r] {
seen[r] = true
uniq = append(uniq, r)
}
}
sort.Strings(uniq)
return uniq
}
// A translation carries the same parameters as the Hungarian: the same page fields inside its template
// actions, and the same printf verbs. A dropped {{.RecoveryAbandonDate}} would render an English banner
// with no date; a dropped %d would print "%!(EXTRA int=5)".
func TestBundleParametersMatchAcrossLanguages(t *testing.T) {
b, err := Load()
if err != nil {
t.Fatal(err)
}
for _, lang := range Supported[1:] {
for _, k := range b.Keys(Default) {
hu := b.msgs[Default][k]
forms := []string{k}
if _, ok := b.msgs[lang][k]; !ok {
forms = []string{k + ".one", k + ".other"}
}
for _, f := range forms {
v, ok := b.msgs[lang][f]
if !ok {
continue
}
if a, c := strings.Join(refs(hu), ","), strings.Join(refs(v), ","); a != c {
t.Errorf("%s %q: template fields %q, Hungarian has %q", lang, f, c, a)
}
if a, c := len(verbRe.FindAllString(hu, -1)), len(verbRe.FindAllString(v, -1)); a != c {
t.Errorf("%s %q: %d printf verbs, Hungarian has %d", lang, f, c, a)
}
}
}
}
}
+240
View File
@@ -0,0 +1,240 @@
{
"layout.html_lang": "en",
"launcher.inditopult": "Launcher",
"launcher.inditopult_megosztasa": "Share the launcher",
"launcher.meg_nincs_telepitett_alkalmazas": "No app is installed yet.",
"launcher.alkalmazasok_telepitese": "Install apps",
"launcher.egy_megoszthato_link_amely_csak": "A link you can share. It only shows your apps and opens them in a new tab. No account and no admin access: every app stays behind its own sign-in.",
"launcher.megosztasi_link": "Share link",
"launcher.link_masolasa": "Copy link",
"launcher.megosztas": "Share…",
"launcher.qr_kod_a_megosztasi_linkhez": "QR code for the share link",
"launcher.olvassa_be_telefonnal_a_gyors": "Scan it with your phone to open it quickly.",
"launcher.jelszo": "Password",
"launcher.a_megosztas_jelszoval_vedett": "The share is protected by a password.",
"launcher.jelszo_torlese": "Remove password",
"launcher.jelszo_nem_kotelezo": "Password (optional)",
"launcher.legalabb_8_karakter": "At least 8 characters",
"launcher.jelszo_beallitasa": "Set password",
"launcher.uj_link_keszitese_a_regi": "Make a new link? The old link and every earlier sign-in stop working.",
"launcher.uj_link_keszitese": "Make a new link",
"launcher.biztosan_kikapcsolja_a_megosztast_a": "Turn off sharing? The link stops working immediately.",
"launcher.megosztas_kikapcsolasa": "Turn off sharing",
"launcher.a_megosztas_jelenleg_ki_van": "Sharing is off.",
"launcher.bezaras": "Close",
"launcher.megosztas_bekapcsolasa": "Turn on sharing",
"launcher.masolva": "Copied",
"launcher.inditopult_2": "Launcher — {{.Domain}}",
"launcher.az_otthoni_alkalmazasok_egy_helyen": "Your home apps in one place.",
"backups.biztonsagi_mentes": "Backup",
"backups.kijelolod_ezt_a_meghajtot_a": "Use this drive for the system backup?",
"backups.kijelolom": "Use this drive",
"backups.tarhely_attekintes": "Storage overview",
"backups.rendszer": "System (/)",
"backups.levalasztva": "Disconnected",
"backups.db_mentesek": "Database backups",
"backups.fajl": "{{$n := len .Backup.DumpFiles}}{{$n}} {{if eq $n 1}}file{{else}}files{{end}}",
"backups.rendszermentes_teljes_mentes": "System backup (full backup)",
"backups.a_teljes_szerver_alkalmazasok_beallitaso": "A regular backup of the whole server — apps, settings and databases together — that can bring back the entire device. The host agent makes and manages it.",
"backups.utolso_teljes_mentes": "Last full backup",
"backups.meret_cel": "Size / target",
"backups.esedekes": "Due",
"backups.naprakesz": "Up to date",
"backups.kovetkezo_mentes": "Next backup",
"backups.oraja_a_mentesi_ablakon_belul": "{{.AgeHours}} {{if eq .AgeHours 1}}hour{{else}}hours{{end}} ago — within the backup window",
"backups.visszaallitas_ellenorizve": "Restore checked",
"backups.meg_nem_futott": "Not run yet",
"backups.nincs_beallitva": "not set up",
"backups.utolso_sikeres_mentes": "&#10003; Last successful backup: {{fmtTime .SuccessAt}} ({{timeAgo .SuccessAt}})",
"backups.meg_nincs_sikeres_mentes": "– No successful backup yet",
"backups.sikertelen": "&#10007; {{fmtTime .FailedAt}} — failed",
"backups.mentes_folyamatban_fazis": "Backup in progress… (phase:",
"backups.mentes_most": "Back up now",
"backups.a_mentes_alatt_az_alkalmazasok": "Your apps stop during the backup — usually for a few minutes, longer with more data.",
"backups.mentesi_idoablak": "Backup window",
"backups.a_mentesek_egymas_utan_futnak": "The backups run one after the other: database backup, local copy, remote backup, then the full system backup.",
"backups.mentesi_idoablak_kezdete": "Backup window start",
"backups.mentes": "Save",
"backups.adatbazis_mentes": "Database backup: <strong>{{.BackupLegDB}}</strong>",
"backups.helyi_masolat": "Local copy: <strong>{{.BackupLegTier2}}</strong>",
"backups.tavoli_mentes": "Remote backup: <strong>{{.BackupLegOffbox}}</strong>",
"backups.teljes_rendszermentes_kb_kozott": "Full system backup: about <strong>{{.BackupGateFrom}}–{{.BackupGateTo}}</strong>",
"backups.adatmentes_aktiv": "Data backup active",
"backups.adatmentes_sikertelen": "Data backup failed",
"backups.adatmentes": "Data backup",
"backups.tavoli_rendszermentes": "Remote system backup",
"backups.kulon_hardveren_pbs": "on separate hardware (PBS)",
"backups.adatbazis_mentve": "Databases backed up",
"backups.beagyazott_db_k_a_kotetmentesben": "embedded databases, inside the volume backup",
"backups.elinditod_a_teljes_rendszermentest_most": "Start the full system backup now? Your apps stop during the backup — usually for a few minutes, longer with more data.",
"backups.mentes_inditasa": "Starting the backup…",
"backups.hiba": "Error",
"backups.mentes_folyamatban": "Backup in progress…",
"backups.hiba_2": "Error:",
"backups.allapot": "Status:",
"backups.a_rendszermentes_elkeszult": "The system backup is done.",
"backups.a_mentes_sikertelen": "The backup failed.",
"backups.kijeloles_folyamatban": "Assigning the drive…",
"backups.a_kijeloles_nem_sikerult": "The drive could not be assigned:",
"backups.a_meghajto_kijelolve_a_beallitas": "The drive is assigned. The setting takes effect after the host agent next restarts.",
"backups.a_meghajto_kijelolve": "The drive is assigned.",
"app_info.alkalmazasok": "← Apps",
"app_info.elavult": "Outdated",
"app_info.megnyitas": "Open ↗",
"app_info.naplo": "Log",
"app_info.torles": "Delete",
"app_info.exportalas": "Export",
"app_info.beallitasok": "Settings",
"app_info.telepites": "Install",
"app_info.mentesek": "Backups",
"app_info.hianyzo_tarhely": "Missing storage:",
"app_info.ennek_az_alkalmazasnak_az_adattaroloja": "This app’s data storage is not available right now, so the app is stopped. Connect the drive again, or move the data to another storage.",
"app_info.hdd_szukseges": "Needs a hard drive",
"app_info.pi_kompatibilis": "Runs on Pi",
"app_info.csak_x86": "x86 only",
"app_info.magyar_felulet": "Hungarian interface",
"app_info.az_alkalmazas_fejlesztoje_felhagyott_a": "The developer of this app has stopped working on it. The installed version still works,\n but it gets no more updates or security fixes.",
"app_info.hova_tegyem_a_fajlokat": "Where do I put my files?",
"app_info.ezeket_a_mappakat_a_fajlkezeloben": "You reach these folders in the File manager. You may need to sign in to the File manager first.",
"app_info.athelyezes_masik_tarhelyre": "Move to another storage",
"app_info.ennek_az_alkalmazasnak_az_adatait": "You can move this app’s data to another connected storage. The app stops briefly during the move, and the data is deleted from the old place only after the check and a successful restart.",
"app_info.valassz_celtarhelyet": "Choose a target storage…",
"app_info.athelyezes": "Move",
"app_info.leallitas": "Stopping",
"app_info.adatok_masolasa": "Copying data",
"app_info.ellenorzes": "Checking",
"app_info.ujratelepites": "Reinstalling",
"app_info.regi_adatok_torlese": "Deleting old data",
"app_info.nincs_folyamatban_athelyezes": "No move in progress.",
"app_info.kesz": "Done",
"app_info.megszakadt": "Stopped:",
"app_info.a_regi_adatok_erintetlenek": "The old data is untouched.",
"app_info.valassz_celtarhelyet_2": "Choose a target storage.",
"app_info.athelyezed_a_z": "Move the",
"app_info.adatait_ide": "data to:",
"app_info.az_alkalmazas_rovid_idore_leall": "? The app stops briefly. The old data is deleted only after a successful move.",
"app_info.hiba": "Error:",
"app_info.ismeretlen": "unknown",
"app_info.mire_hasznalhato": "What is it for?",
"app_info.elso_lepesek": "First steps",
"app_info.elofeltetelek": "Before you start",
"app_info.kezdeti_belepesi_adatok": "First sign-in details",
"app_info.felhasznalonev": "Username",
"app_info.jelszo": "Password",
"app_info.az_uzemelteto_allitotta_be": "set by the operator",
"app_info.megjelenites": "Show",
"app_info.masolas": "Copy",
"app_info.az_elso_bejelentkezes_utan_azonnal": "Change it right after your first sign-in! This is the first, automatically generated password — if you already changed it, ignore this.",
"app_info.alapertelmezett_belepes": "Default sign-in",
"app_info.az_elso_bejelentkezes_utan_azonnal_2": "Change it right after your first sign-in!",
"app_info.dokumentacio": "Documentation",
"app_info.hivatalos_dokumentacio": "Official documentation ↗",
"app_info.a_kezdeti_jelszo_beolvasasa_nem": "The first password could not be read.",
"app_info.elrejtes": "Hide",
"app_info.masolva": "Copied",
"layout.igen": "Yes",
"layout.megse": "Cancel",
"layout.uzenet": "Message",
"layout.menu": "Menu",
"layout.inditopult": "Launcher",
"layout.vezerlopult": "Dashboard",
"layout.alkalmazasok": "Apps",
"layout.tarhely": "Storage",
"layout.meghajtok": "Drives",
"layout.halozati_tarhely": "Network storage",
"layout.biztonsagi_mentes": "Backup",
"layout.attekintes": "Overview",
"layout.tavoli_mentes": "Remote backup",
"layout.visszaallitas": "Restore",
"layout.megosztas": "Sharing",
"layout.halozati_megosztas": "Network sharing",
"layout.rendszermonitor": "System monitor",
"layout.beallitasok": "Settings",
"layout.rendszer": "System",
"layout.ertesitesek": "Notifications",
"layout.biztonsag_es_hozzaferes": "Security and access",
"layout.kijelentkezes": "Sign out ↗",
"layout.a_vezerlopult_meg_nincs_jelszoval": "The dashboard is not protected by a password yet — we will send you the setup code by e-mail soon.",
"layout.a_korabbi_tavoli_menteseidet_nap": "We will permanently delete your earlier remote backups in <strong>{{.RecoveryAbandonDays}} {{if eq .RecoveryAbandonDays 1}}day{{else}}days{{end}}</strong> ({{.RecoveryAbandonDate}}), as you asked.",
"layout.addig_meg_visszaszerezheted_oket_a": "Until then you can still get them back with your recovery code.",
"layout.hogy_ezek_meg_visszaszerezhetok_e": "We cannot tell from here whether your recovery code can still get them back — if you want them, <strong>write to us before the deletion</strong>.",
"layout.ket_hete_varnak_rad_a": "Your earlier remote backups have been waiting for you for <strong>two weeks</strong>, and you have not entered your recovery code yet. Until you do, you cannot reach these backups.",
"layout.mar_egy_hete_megvannak_a": "Your earlier remote backups have been here for <strong>a week</strong>, but we cannot open them without your recovery code.",
"layout.a_korabbi_tavoli_menteseid_megvannak": "Your earlier remote backups are safe — you need your recovery code to open them.",
"layout.a_korabbi_tavoli_menteseid_megvannak_2": "Your earlier remote backups are safe, but this device needs your recovery code for them.",
"layout.megnezem": "Show me",
"layout.most_nem": "Not now",
"layout.ne_emlekeztessen_ujra": "Do not remind me again",
"layout.a_tavoli_mentes_szunetel_amig": "Remote backup is paused until you create your recovery code.\n <a href=\"/backup/escrow\"><strong>Create recovery code &rarr;</strong></a>",
"layout.ez_az_alkalmazas_mar_telepitve": "This app is already installed.",
"layout.frissites": "↻ Refreshing...",
"layout.sablonok_frissitve": "Templates refreshed",
"layout.hiba": "Error:",
"layout.ismeretlen_hiba": "Unknown error",
"layout.halozati_hiba": "Network error:",
"layout.folyamatban": "In progress...",
"layout.betoltes": "Loading...",
"layout.felhasznaloi_adatok_a_merevlemezen": "Your data on the hard drive:",
"layout.nem_letezik": "does not exist",
"layout.felhasznaloi_adatok_torlese_a_merevlemez": "Delete your data from the hard drive",
"layout.alkalmazas_torlese": "Delete app:",
"layout.ez_a_muvelet_eltavolitja_a": "This removes the containers, the volumes and the configuration files.",
"layout.ez_a_muvelet_nem_visszavonhato": "You cannot undo this!",
"layout.megsem": "Cancel",
"layout.torles": "Delete",
"layout.hiba_2": "Error",
"layout.nem_sikerult_lekerni_az_adatokat": "Could not load the details:",
"layout.bezaras": "Close",
"layout.torles_folyamatban": "Deleting...",
"layout.torolt_adatok": "Deleted data:",
"layout.megorzott_adatok": "Kept data:",
"layout.sikeresen_torolve": "Deleted!",
"layout.az_alkalmazas": "The app (",
"layout.torolve_lett": ") was deleted.",
"layout.mindig_torlodik": "Always deleted:",
"layout.docker_kotetek_adatbazis_alkalmazas_konf": "Docker volumes (database, app configuration)",
"layout.telepitesi_konfiguracio_app_yaml": "Install configuration (app.yaml)",
"layout.masodlagos_mentes_utemezese": "Secondary backup schedule",
"layout.felhasznaloi_adatok_torlese": "Delete your data",
"layout.ha_ujratelepiti_az_alkalmazast_az": "If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.",
"layout.mentesi_adatok": "Backup data:",
"layout.mentesi_adatok_torlese": "Delete backup data",
"layout.az_ejszakai_restic_pillanatkepek_nem": "The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.",
"layout.alkalmazas_eltavolitasa": "Remove app:",
"layout.az_alkalmazas_visszaall_nincs_telepitve": "The app goes back to \"Not installed\". The template stays, and you can install it again.",
"layout.eltavolitas": "Remove",
"layout.eltavolitas_folyamatban": "Removing...",
"layout.torolt_mentesek": "Deleted backups:",
"layout.nem_torolt_mentesek": "Backups not deleted:",
"layout.sikeresen_eltavolitva": "Removed!",
"layout.eltavolitva_ujratelepitheto_a_telepites": ") was removed. You can install it again with the Install button.",
"backups_shared.empty_title": "Backup is not set up",
"backups_shared.empty_body": "The backup feature is not active.<br>\n Contact the Felhom team to set it up.",
"page.title.launcher": "Launcher",
"page.title.backups": "Backup",
"func.state.running": "Running",
"func.state.starting": "Starting...",
"func.state.deploying": "Installing...",
"func.state.unhealthy": "Not healthy",
"func.state.degraded": "Partly stopped",
"func.state.stopped": "Stopped",
"func.state.restarting": "Restarting...",
"func.state.not_deployed": "Not installed",
"func.state.paused": "Paused",
"func.state.unknown": "Unknown",
"func.time.now": "just now",
"func.time.minutes_ago.one": "%d minute ago",
"func.time.minutes_ago.other": "%d minutes ago",
"func.time.hours_ago.one": "%d hour ago",
"func.time.hours_ago.other": "%d hours ago",
"func.time.yesterday": "yesterday",
"func.time.days_ago.one": "%d day ago",
"func.time.days_ago.other": "%d days ago",
"func.time.today_at": "today %s",
"func.time.tomorrow_at": "tomorrow %s",
"func.restore_status.pending": "Waiting",
"func.restore_status.restoring": "Restoring...",
"func.restore_status.done": "Done",
"func.restore_status.failed": "Failed",
"func.restore_status.skipped": "Skipped"
}
+237
View File
@@ -0,0 +1,237 @@
{
"layout.html_lang": "hu",
"launcher.inditopult": "Indítópult",
"launcher.inditopult_megosztasa": "Indítópult megosztása",
"launcher.meg_nincs_telepitett_alkalmazas": "Még nincs telepített alkalmazás.",
"launcher.alkalmazasok_telepitese": "Alkalmazások telepítése",
"launcher.egy_megoszthato_link_amely_csak": "Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.",
"launcher.megosztasi_link": "Megosztási link",
"launcher.link_masolasa": "Link másolása",
"launcher.megosztas": "Megosztás…",
"launcher.qr_kod_a_megosztasi_linkhez": "QR-kód a megosztási linkhez",
"launcher.olvassa_be_telefonnal_a_gyors": "Olvassa be telefonnal a gyors megnyitáshoz.",
"launcher.jelszo": "Jelszó",
"launcher.a_megosztas_jelszoval_vedett": "A megosztás jelszóval védett.",
"launcher.jelszo_torlese": "Jelszó törlése",
"launcher.jelszo_nem_kotelezo": "Jelszó (nem kötelező)",
"launcher.legalabb_8_karakter": "Legalább 8 karakter",
"launcher.jelszo_beallitasa": "Jelszó beállítása",
"launcher.uj_link_keszitese_a_regi": "Új link készítése? A régi link és minden korábbi belépés érvénytelenné válik.",
"launcher.uj_link_keszitese": "Új link készítése",
"launcher.biztosan_kikapcsolja_a_megosztast_a": "Biztosan kikapcsolja a megosztást? A link azonnal érvénytelenné válik.",
"launcher.megosztas_kikapcsolasa": "Megosztás kikapcsolása",
"launcher.a_megosztas_jelenleg_ki_van": "A megosztás jelenleg ki van kapcsolva.",
"launcher.bezaras": "Bezárás",
"launcher.megosztas_bekapcsolasa": "Megosztás bekapcsolása",
"launcher.masolva": "Másolva",
"launcher.inditopult_2": "Indítópult — {{.Domain}}",
"launcher.az_otthoni_alkalmazasok_egy_helyen": "Az otthoni alkalmazások egy helyen.",
"backups.biztonsagi_mentes": "Biztonsági mentés",
"backups.kijelolod_ezt_a_meghajtot_a": "Kijelölöd ezt a meghajtót a rendszermentés helyéül?",
"backups.kijelolom": "Kijelölöm",
"backups.tarhely_attekintes": "Tárhely áttekintés",
"backups.rendszer": "Rendszer (/)",
"backups.levalasztva": "Leválasztva",
"backups.db_mentesek": "DB mentések",
"backups.fajl": "{{len .Backup.DumpFiles}} fájl",
"backups.rendszermentes_teljes_mentes": "Rendszermentés (teljes mentés)",
"backups.a_teljes_szerver_alkalmazasok_beallitaso": "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.",
"backups.utolso_teljes_mentes": "Utolsó teljes mentés",
"backups.meret_cel": "Méret / cél",
"backups.esedekes": "Esedékes",
"backups.naprakesz": "Naprakész",
"backups.kovetkezo_mentes": "Következő mentés",
"backups.oraja_a_mentesi_ablakon_belul": "{{.AgeHours}} órája — a mentési ablakon belül",
"backups.visszaallitas_ellenorizve": "Visszaállítás ellenőrizve",
"backups.meg_nem_futott": "Még nem futott",
"backups.nincs_beallitva": "nincs beállítva",
"backups.utolso_sikeres_mentes": "&#10003; Utolsó sikeres mentés: {{fmtTime .SuccessAt}} ({{timeAgo .SuccessAt}})",
"backups.meg_nincs_sikeres_mentes": "– Még nincs sikeres mentés",
"backups.sikertelen": "&#10007; {{fmtTime .FailedAt}} — sikertelen",
"backups.mentes_folyamatban_fazis": "Mentés folyamatban… (fázis:",
"backups.mentes_most": "Mentés most",
"backups.a_mentes_alatt_az_alkalmazasok": "A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.",
"backups.mentesi_idoablak": "Mentési időablak",
"backups.a_mentesek_egymas_utan_futnak": "A mentések egymás után futnak: adatbázis-mentés, helyi másolat, távoli mentés, majd a teljes rendszermentés.",
"backups.mentesi_idoablak_kezdete": "Mentési időablak kezdete",
"backups.mentes": "Mentés",
"backups.adatbazis_mentes": "Adatbázis-mentés: <strong>{{.BackupLegDB}}</strong>",
"backups.helyi_masolat": "Helyi másolat: <strong>{{.BackupLegTier2}}</strong>",
"backups.tavoli_mentes": "Távoli mentés: <strong>{{.BackupLegOffbox}}</strong>",
"backups.teljes_rendszermentes_kb_kozott": "Teljes rendszermentés: kb. <strong>{{.BackupGateFrom}}–{{.BackupGateTo}}</strong> között",
"backups.adatmentes_aktiv": "Adatmentés aktív",
"backups.adatmentes_sikertelen": "Adatmentés sikertelen",
"backups.adatmentes": "Adatmentés",
"backups.tavoli_rendszermentes": "Távoli rendszermentés",
"backups.kulon_hardveren_pbs": "külön hardveren (PBS)",
"backups.adatbazis_mentve": "Adatbázis mentve",
"backups.beagyazott_db_k_a_kotetmentesben": "beágyazott DB-k a kötetmentésben",
"backups.elinditod_a_teljes_rendszermentest_most": "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.",
"backups.mentes_inditasa": "Mentés indítása…",
"backups.hiba": "Hiba",
"backups.mentes_folyamatban": "Mentés folyamatban…",
"backups.hiba_2": "Hiba:",
"backups.allapot": "Állapot:",
"backups.a_rendszermentes_elkeszult": "A rendszermentés elkészült.",
"backups.a_mentes_sikertelen": "A mentés sikertelen.",
"backups.kijeloles_folyamatban": "Kijelölés folyamatban…",
"backups.a_kijeloles_nem_sikerult": "A kijelölés nem sikerült:",
"backups.a_meghajto_kijelolve_a_beallitas": "A meghajtó kijelölve. A beállítás a host-ügynök következő újraindulása után lép életbe.",
"backups.a_meghajto_kijelolve": "A meghajtó kijelölve.",
"app_info.alkalmazasok": "← Alkalmazások",
"app_info.elavult": "Elavult",
"app_info.megnyitas": "Megnyitás ↗",
"app_info.naplo": "Napló",
"app_info.torles": "Törlés",
"app_info.exportalas": "Exportálás",
"app_info.beallitasok": "Beállítások",
"app_info.telepites": "Telepítés",
"app_info.mentesek": "Mentések",
"app_info.hianyzo_tarhely": "Hiányzó tárhely:",
"app_info.ennek_az_alkalmazasnak_az_adattaroloja": "Ennek az alkalmazásnak az adattárolója jelenleg nem elérhető, ezért le van állítva. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre.",
"app_info.hdd_szukseges": "HDD szükséges",
"app_info.pi_kompatibilis": "Pi kompatibilis",
"app_info.csak_x86": "Csak x86",
"app_info.magyar_felulet": "Magyar felület",
"app_info.az_alkalmazas_fejlesztoje_felhagyott_a": "Az alkalmazás fejlesztője felhagyott a fejlesztéssel. A telepített verzió továbbra is használható,\n de frissítések és biztonsági javítások már nem érkeznek hozzá.",
"app_info.hova_tegyem_a_fajlokat": "Hova tegyem a fájlokat?",
"app_info.ezeket_a_mappakat_a_fajlkezeloben": "Ezeket a mappákat a Fájlkezelőben éred el. Előfordulhat, hogy először be kell jelentkezned a Fájlkezelőbe.",
"app_info.athelyezes_masik_tarhelyre": "Áthelyezés másik tárhelyre",
"app_info.ennek_az_alkalmazasnak_az_adatait": "Ennek az alkalmazásnak az adatait másik csatlakoztatott tárhelyre helyezheted át. Az alkalmazás az áthelyezés alatt rövid időre leáll, az adatok pedig csak az ellenőrzés és a sikeres újraindítás után törlődnek a régi helyről.",
"app_info.valassz_celtarhelyet": "Válassz céltárhelyet…",
"app_info.athelyezes": "Áthelyezés",
"app_info.leallitas": "Leállítás",
"app_info.adatok_masolasa": "Adatok másolása",
"app_info.ellenorzes": "Ellenőrzés",
"app_info.ujratelepites": "Újratelepítés",
"app_info.regi_adatok_torlese": "Régi adatok törlése",
"app_info.nincs_folyamatban_athelyezes": "Nincs folyamatban áthelyezés.",
"app_info.kesz": "Kész",
"app_info.megszakadt": "Megszakadt:",
"app_info.a_regi_adatok_erintetlenek": "A régi adatok érintetlenek.",
"app_info.valassz_celtarhelyet_2": "Válassz céltárhelyet.",
"app_info.athelyezed_a_z": "Áthelyezed a(z)",
"app_info.adatait_ide": "adatait ide:",
"app_info.az_alkalmazas_rovid_idore_leall": "? Az alkalmazás rövid időre leáll. A régi adatok csak sikeres áthelyezés után törlődnek.",
"app_info.hiba": "Hiba:",
"app_info.ismeretlen": "ismeretlen",
"app_info.mire_hasznalhato": "Mire használható?",
"app_info.elso_lepesek": "Első lépések",
"app_info.elofeltetelek": "Előfeltételek",
"app_info.kezdeti_belepesi_adatok": "Kezdeti belépési adatok",
"app_info.felhasznalonev": "Felhasználónév",
"app_info.jelszo": "Jelszó",
"app_info.az_uzemelteto_allitotta_be": "az üzemeltető állította be",
"app_info.megjelenites": "Megjelenítés",
"app_info.masolas": "Másolás",
"app_info.az_elso_bejelentkezes_utan_azonnal": "Az első bejelentkezés után azonnal változtasd meg! Ez a kezdeti, automatikusan generált jelszó — ha már megváltoztattad, hagyd figyelmen kívül.",
"app_info.alapertelmezett_belepes": "Alapértelmezett belépés",
"app_info.az_elso_bejelentkezes_utan_azonnal_2": "Az első bejelentkezés után azonnal változtasd meg!",
"app_info.dokumentacio": "Dokumentáció",
"app_info.hivatalos_dokumentacio": "Hivatalos dokumentáció ↗",
"app_info.a_kezdeti_jelszo_beolvasasa_nem": "A kezdeti jelszó beolvasása nem sikerült.",
"app_info.elrejtes": "Elrejtés",
"app_info.masolva": "Másolva",
"layout.igen": "Igen",
"layout.megse": "Mégse",
"layout.uzenet": "Üzenet",
"layout.menu": "Menü",
"layout.inditopult": "Indítópult",
"layout.vezerlopult": "Vezérlőpult",
"layout.alkalmazasok": "Alkalmazások",
"layout.tarhely": "Tárhely",
"layout.meghajtok": "Meghajtók",
"layout.halozati_tarhely": "Hálózati tárhely",
"layout.biztonsagi_mentes": "Biztonsági mentés",
"layout.attekintes": "Áttekintés",
"layout.tavoli_mentes": "Távoli mentés",
"layout.visszaallitas": "Visszaállítás",
"layout.megosztas": "Megosztás",
"layout.halozati_megosztas": "Hálózati megosztás",
"layout.rendszermonitor": "Rendszermonitor",
"layout.beallitasok": "Beállítások",
"layout.rendszer": "Rendszer",
"layout.ertesitesek": "Értesítések",
"layout.biztonsag_es_hozzaferes": "Biztonság és hozzáférés",
"layout.kijelentkezes": "Kijelentkezés ↗",
"layout.a_vezerlopult_meg_nincs_jelszoval": "A vezérlőpult még nincs jelszóval védve — a beállító kódot hamarosan e-mailben küldjük.",
"layout.a_korabbi_tavoli_menteseidet_nap": "A korábbi távoli mentéseidet <strong>{{.RecoveryAbandonDays}} nap múlva</strong> ({{.RecoveryAbandonDate}}) véglegesen töröljük, a kérésed szerint.",
"layout.addig_meg_visszaszerezheted_oket_a": "Addig még visszaszerezheted őket a helyreállítási kóddal.",
"layout.hogy_ezek_meg_visszaszerezhetok_e": "Hogy ezek még visszaszerezhetők-e a helyreállítási kóddal, azt innen nem tudjuk megállapítani — ha vissza szeretnéd kapni őket, <strong>írj nekünk a törlés előtt</strong>.",
"layout.ket_hete_varnak_rad_a": "<strong>Két hete</strong> várnak rád a korábbi távoli mentéseid, és még nem adtad meg a helyreállítási kódodat. Amíg nem teszed, ezekhez a mentésekhez nem férsz hozzá.",
"layout.mar_egy_hete_megvannak_a": "Már <strong>egy hete</strong> megvannak a korábbi távoli mentéseid, de a helyreállítási kódod nélkül nem tudjuk megnyitni őket.",
"layout.a_korabbi_tavoli_menteseid_megvannak": "A korábbi távoli mentéseid megvannak — a megnyitásukhoz a helyreállítási kódod szükséges.",
"layout.a_korabbi_tavoli_menteseid_megvannak_2": "A korábbi távoli mentéseid megvannak, de ehhez a géphez a helyreállítási kódod szükséges.",
"layout.megnezem": "Megnézem",
"layout.most_nem": "Most nem",
"layout.ne_emlekeztessen_ujra": "Ne emlékeztessen újra",
"layout.a_tavoli_mentes_szunetel_amig": "A távoli mentés szünetel, amíg nem hozod létre a helyreállítási kódot.\n <a href=\"/backup/escrow\"><strong>Helyreállítási kód létrehozása &rarr;</strong></a>",
"layout.ez_az_alkalmazas_mar_telepitve": "Ez az alkalmazás már telepítve van.",
"layout.frissites": "↻ Frissítés...",
"layout.sablonok_frissitve": "Sablonok frissítve",
"layout.hiba": "Hiba:",
"layout.ismeretlen_hiba": "Ismeretlen hiba",
"layout.halozati_hiba": "Hálózati hiba:",
"layout.folyamatban": "Folyamatban...",
"layout.betoltes": "Betöltés...",
"layout.felhasznaloi_adatok_a_merevlemezen": "Felhasználói adatok a merevlemezen:",
"layout.nem_letezik": "nem létezik",
"layout.felhasznaloi_adatok_torlese_a_merevlemez": "Felhasználói adatok törlése a merevlemezről",
"layout.alkalmazas_torlese": "Alkalmazás törlése:",
"layout.ez_a_muvelet_eltavolitja_a": "Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.",
"layout.ez_a_muvelet_nem_visszavonhato": "Ez a művelet nem visszavonható!",
"layout.megsem": "Mégsem",
"layout.torles": "Törlés",
"layout.hiba_2": "Hiba",
"layout.nem_sikerult_lekerni_az_adatokat": "Nem sikerült lekérni az adatokat:",
"layout.bezaras": "Bezárás",
"layout.torles_folyamatban": "Törlés folyamatban...",
"layout.torolt_adatok": "Törölt adatok:",
"layout.megorzott_adatok": "Megőrzött adatok:",
"layout.sikeresen_torolve": "Sikeresen törölve!",
"layout.az_alkalmazas": "Az alkalmazás (",
"layout.torolve_lett": ") törölve lett.",
"layout.mindig_torlodik": "Mindig törlődik:",
"layout.docker_kotetek_adatbazis_alkalmazas_konf": "Docker kötetek (adatbázis, alkalmazás konfiguráció)",
"layout.telepitesi_konfiguracio_app_yaml": "Telepítési konfiguráció (app.yaml)",
"layout.masodlagos_mentes_utemezese": "Másodlagos mentés ütemezése",
"layout.felhasznaloi_adatok_torlese": "Felhasználói adatok törlése",
"layout.ha_ujratelepiti_az_alkalmazast_az": "Ha újratelepíti az alkalmazást, az adatokat újra importálnia kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.",
"layout.mentesi_adatok": "Mentési adatok:",
"layout.mentesi_adatok_torlese": "Mentési adatok törlése",
"layout.az_ejszakai_restic_pillanatkepek_nem": "Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.",
"layout.alkalmazas_eltavolitasa": "Alkalmazás eltávolítása:",
"layout.az_alkalmazas_visszaall_nincs_telepitve": "Az alkalmazás visszaáll \"Nincs telepítve\" állapotba. A sablon megmarad, újratelepíthető.",
"layout.eltavolitas": "Eltávolítás",
"layout.eltavolitas_folyamatban": "Eltávolítás folyamatban...",
"layout.torolt_mentesek": "Törölt mentések:",
"layout.nem_torolt_mentesek": "Nem törölt mentések:",
"layout.sikeresen_eltavolitva": "Sikeresen eltávolítva!",
"layout.eltavolitva_ujratelepitheto_a_telepites": ") eltávolítva. Újratelepíthető a Telepítés gombbal.",
"backups_shared.empty_title": "Biztonsági mentés nincs beállítva",
"backups_shared.empty_body": "A biztonsági mentés funkció nem aktív.<br>\n Kérjük, vegye fel a kapcsolatot a Felhom csapattal a beállításhoz.",
"page.title.launcher": "Indítópult",
"page.title.backups": "Biztonsági mentés",
"func.state.running": "Fut",
"func.state.starting": "Indulás...",
"func.state.deploying": "Telepítés...",
"func.state.unhealthy": "Nem egészséges",
"func.state.degraded": "Részlegesen leállt",
"func.state.stopped": "Leállítva",
"func.state.restarting": "Újraindítás...",
"func.state.not_deployed": "Nincs telepítve",
"func.state.paused": "Szüneteltetve",
"func.state.unknown": "Ismeretlen",
"func.time.now": "most",
"func.time.minutes_ago": "%d perce",
"func.time.hours_ago": "%d órája",
"func.time.yesterday": "tegnap",
"func.time.days_ago": "%d napja",
"func.time.today_at": "ma %s",
"func.time.tomorrow_at": "holnap %s",
"func.restore_status.pending": "Várakozik",
"func.restore_status.restoring": "Visszaállítás...",
"func.restore_status.done": "Kész",
"func.restore_status.failed": "Sikertelen",
"func.restore_status.skipped": "Kihagyva"
}