Merge pull request #330 from Vaso73/i18n/sk-vm-lxc-complete

i18n: translate Slovak VM and LXC status and update scheduling
This commit is contained in:
MacRimi
2026-09-03 22:20:27 +02:00
committed by GitHub
5 changed files with 103 additions and 40 deletions
+3 -2
View File
@@ -13,7 +13,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
import { fetchApi } from "../lib/api-config" import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network" import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
import { formatStorage } from "../lib/utils" import { formatStorage } from "../lib/utils"
import { useT } from "../lib/i18n/provider" import { getCountFormKey, useI18n, useT } from "../lib/i18n/provider"
import { Area, AreaChart, ResponsiveContainer } from "recharts" import { Area, AreaChart, ResponsiveContainer } from "recharts"
interface TempDataPoint { interface TempDataPoint {
@@ -173,6 +173,7 @@ const getUnitsSettings = (): "Bytes" | "Bits" => {
export function SystemOverview() { export function SystemOverview() {
const t = useT() const t = useT()
const { language } = useI18n()
const [systemData, setSystemData] = useState<SystemData | null>(null) const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([]) const [vmData, setVmData] = useState<VMData[]>([])
const [storageData, setStorageData] = useState<StorageData | null>(null) const [storageData, setStorageData] = useState<StorageData | null>(null)
@@ -511,7 +512,7 @@ export function SystemOverview() {
<span className="text-lg font-medium ml-1 text-muted-foreground">/ {vmStats.vms + vmStats.lxc}</span> <span className="text-lg font-medium ml-1 text-muted-foreground">/ {vmStats.vms + vmStats.lxc}</span>
</div> </div>
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20"> <Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{t("overview.runningCount", { count: vmStats.running })} {t(getCountFormKey(language, "overview.runningCount", vmStats.running), { count: vmStats.running })}
</Badge> </Badge>
</div> </div>
<div className="mt-3 flex gap-1 flex-wrap"> <div className="mt-3 flex gap-1 flex-wrap">
+24 -24
View File
@@ -27,7 +27,7 @@ import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
import { fetchApi } from "../lib/api-config" import { fetchApi } from "../lib/api-config"
import DOMPurify from "dompurify" import DOMPurify from "dompurify"
import { marked } from "marked" import { marked } from "marked"
import { useT } from "@/lib/i18n/provider" import { getCountFormKey, useI18n, useT } from "@/lib/i18n/provider"
// Sent by /api/vms only for LXC rows, only when the user has enabled // Sent by /api/vms only for LXC rows, only when the user has enabled
// `lxc_updates_available` notifications. The Monitor populates this // `lxc_updates_available` notifications. The Monitor populates this
@@ -753,6 +753,7 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) {
export function VirtualMachines() { export function VirtualMachines() {
const t = useT() const t = useT()
const { language } = useI18n()
const { const {
data: vmData, data: vmData,
error, error,
@@ -1929,7 +1930,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
applySchedulePayload(s) applySchedulePayload(s)
} }
} catch (e: any) { } catch (e: any) {
setScheduleError(e?.message || "Could not load schedule") setScheduleError(e?.message || t("vmLxc.scheduled.loadFailed"))
} finally { } finally {
setScheduleLoaded(vmid) setScheduleLoaded(vmid)
} }
@@ -1953,7 +1954,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
setBulkConfigured(false) setBulkConfigured(false)
setBulkTargets(["os"]) setBulkTargets(["os"])
setBulkPersistedTargets(["os"]) setBulkPersistedTargets(["os"])
setBulkError(e?.message || "Could not load bulk update") setBulkError(e?.message || t("vmLxc.bulkUpdate.loadFailed"))
} finally { } finally {
setBulkLoaded(vmid) setBulkLoaded(vmid)
} }
@@ -2073,7 +2074,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
}) })
if (cronToSave.trim()) setScheduleConfigured(true) if (cronToSave.trim()) setScheduleConfigured(true)
} catch (e: any) { } catch (e: any) {
setScheduleError(e?.message || "Save failed") setScheduleError(e?.message || t("vmLxc.scheduled.saveFailed"))
} finally { } finally {
setScheduleSaving(false) setScheduleSaving(false)
} }
@@ -2132,13 +2133,13 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
setScheduleLastRunRebootPackages([]) setScheduleLastRunRebootPackages([])
setScheduleReleaseDelayDays(0) setScheduleReleaseDelayDays(0)
} catch (e: any) { } catch (e: any) {
setScheduleError(e?.message || "Delete failed") setScheduleError(e?.message || t("vmLxc.scheduled.deleteFailed"))
} finally { } finally {
setScheduleSaving(false) setScheduleSaving(false)
} }
} }
// Turn a 5-field cron into a plain-English label — mirrors the // Turn a 5-field cron into a localized label — mirrors the
// backend's _humanise_cron so view mode matches the picker's // backend's _humanise_cron so view mode matches the picker's
// preset labels. // preset labels.
const humanCron = (expr: string): string => { const humanCron = (expr: string): string => {
@@ -2151,15 +2152,14 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
if (isNaN(hn) || isNaN(mn)) return `${h}:${m}` if (isNaN(hn) || isNaN(mn)) return `${h}:${m}`
return `${String(hn).padStart(2, "0")}:${String(mn).padStart(2, "0")}` return `${String(hn).padStart(2, "0")}:${String(mn).padStart(2, "0")}`
} }
if (d === "*" && mo === "*" && w === "*" && /^\d+$/.test(m) && /^\d+$/.test(h)) return `Daily at ${hhmm()}` if (d === "*" && mo === "*" && w === "*" && /^\d+$/.test(m) && /^\d+$/.test(h)) return t("vmLxc.scheduled.humanDaily", { time: hhmm() })
if (d === "*" && mo === "*" && /^\d+$/.test(w) && /^\d+$/.test(m) && /^\d+$/.test(h)) { if (d === "*" && mo === "*" && /^\d+$/.test(w) && /^\d+$/.test(m) && /^\d+$/.test(h)) {
const wdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
const wn = parseInt(w, 10) const wn = parseInt(w, 10)
const wname = (wn >= 0 && wn <= 6) ? wdays[wn] : w const wname = wn >= 0 && wn <= 6 ? t(`vmLxc.scheduled.weekdays.${wn}`) : w
return `Weekly (${wname} ${hhmm()})` return t("vmLxc.scheduled.humanWeekly", { day: wname, time: hhmm() })
} }
if (mo === "*" && w === "*" && /^\d+$/.test(d) && /^\d+$/.test(m) && /^\d+$/.test(h)) return `Monthly (day ${parseInt(d, 10)} at ${hhmm()})` if (mo === "*" && w === "*" && /^\d+$/.test(d) && /^\d+$/.test(m) && /^\d+$/.test(h)) return t("vmLxc.scheduled.humanMonthly", { day: parseInt(d, 10), time: hhmm() })
if (h === "*" && d === "*" && mo === "*" && w === "*" && m === "0") return "Hourly" if (h === "*" && d === "*" && mo === "*" && w === "*" && m === "0") return t("vmLxc.scheduled.humanHourly")
return expr return expr
} }
@@ -2296,7 +2296,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
// record and would drop any field we omitted. // record and would drop any field we omitted.
const full: any = await fetchApi(`/api/vms/${vmid}/apps`) const full: any = await fetchApi(`/api/vms/${vmid}/apps`)
const current = (full?.apps || []).find((a: any) => a.id === app.id) const current = (full?.apps || []).find((a: any) => a.id === app.id)
if (!current) throw new Error("app not found in sidecar") if (!current) throw new Error(t("vmLxc.errors.appNotFound"))
const { id: _id, state: _state, created_at: _created, ...rest } = current const { id: _id, state: _state, created_at: _created, ...rest } = current
const payload = { ...rest, ...patch } const payload = { ...rest, ...patch }
const updated: any = await fetchApi(`/api/vms/${vmid}/apps/${app.id}`, { const updated: any = await fetchApi(`/api/vms/${vmid}/apps/${app.id}`, {
@@ -2321,19 +2321,19 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
}) })
closeCustomCmdEditor() closeCustomCmdEditor()
} catch (e) { } catch (e) {
alert(`Could not save custom command: ${(e as any)?.message || e}`) alert(t("vmLxc.errors.saveCustomCommandFailed", { message: (e as any)?.message || String(e) }))
} finally { } finally {
setCustomCmdSaving(false) setCustomCmdSaving(false)
} }
} }
const removeCustomCommand = async (vmid: number, app: LxcAppWatch) => { const removeCustomCommand = async (vmid: number, app: LxcAppWatch) => {
if (!confirm(`Remove the custom update command for "${app.name}"?`)) return if (!confirm(t("vmLxc.errors.removeCustomCommandConfirm", { name: app.name }))) return
setCustomCmdSaving(true) setCustomCmdSaving(true)
try { try {
await patchAppWatch(vmid, app, { update_command: "" }) await patchAppWatch(vmid, app, { update_command: "" })
closeCustomCmdEditor() closeCustomCmdEditor()
} catch (e) { } catch (e) {
alert(`Could not remove custom command: ${(e as any)?.message || e}`) alert(t("vmLxc.errors.removeCustomCommandFailed", { message: (e as any)?.message || String(e) }))
} finally { } finally {
setCustomCmdSaving(false) setCustomCmdSaving(false)
} }
@@ -2342,7 +2342,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
try { try {
await patchAppWatch(vmid, app, { hide_no_updater_notice: true }) await patchAppWatch(vmid, app, { hide_no_updater_notice: true })
} catch (e) { } catch (e) {
alert(`Could not hide notice: ${(e as any)?.message || e}`) alert(t("vmLxc.errors.hideNoticeFailed", { message: (e as any)?.message || String(e) }))
} }
} }
@@ -2363,10 +2363,10 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
? ["docker-images"] ? ["docker-images"]
: ["apps"] : ["apps"]
const fallbackLabels = target === "os" const fallbackLabels = target === "os"
? ["OS"] ? [t("vmLxc.bulkUpdate.osTarget")]
: target === "both" : target === "both"
? ["OS", "Applications"] ? [t("vmLxc.bulkUpdate.osTarget"), t("vmLxc.scheduled.targetApp")]
: [opts?.appName || (opts?.dockerEngine ? "Docker Engine" : "Application")] : [opts?.appName || (opts?.dockerEngine ? "Docker Engine" : t("vmLxc.scheduled.targetApp"))]
const runId = typeof window !== "undefined" && typeof window.crypto?.randomUUID === "function" const runId = typeof window !== "undefined" && typeof window.crypto?.randomUUID === "function"
? window.crypto.randomUUID() ? window.crypto.randomUUID()
: `manual-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` : `manual-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
@@ -2817,7 +2817,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
<span className="text-lg font-medium ml-1 text-muted-foreground">/ {total}</span> <span className="text-lg font-medium ml-1 text-muted-foreground">/ {total}</span>
</div> </div>
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20"> <Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{t("overview.runningCount", { count: running })} {t(getCountFormKey(language, "overview.runningCount", running), { count: running })}
</Badge> </Badge>
</div> </div>
<div className="mt-3 flex gap-1 flex-wrap"> <div className="mt-3 flex gap-1 flex-wrap">
@@ -2987,12 +2987,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
</CardTitle> </CardTitle>
<div <div
role="tablist" role="tablist"
aria-label="Filter by status" aria-label={t("vmLxc.statusFilter.ariaLabel")}
className="inline-flex w-full sm:w-auto rounded-lg border border-border bg-muted/40 p-1 gap-1" className="inline-flex w-full sm:w-auto rounded-lg border border-border bg-muted/40 p-1 gap-1"
> >
{(["all", "running", "stopped"] as const).map((key) => { {(["all", "running", "stopped"] as const).map((key) => {
const active = statusFilter === key const active = statusFilter === key
const label = key === "all" ? "All" : key === "running" ? "Running" : "Stopped" const label = t(`vmLxc.statusFilter.${key}`)
// Icon color: white when the tab is active (over the blue fill); // Icon color: white when the tab is active (over the blue fill);
// green / red on inactive tabs so the state mapping stays legible // green / red on inactive tabs so the state mapping stays legible
// before selection. The black-text variant was tested and dropped // before selection. The black-text variant was tested and dropped
@@ -3031,7 +3031,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
<div className="text-center py-8 text-muted-foreground">{t("vmLxc.empty")}</div> <div className="text-center py-8 text-muted-foreground">{t("vmLxc.empty")}</div>
) : filteredVMs.length === 0 ? ( ) : filteredVMs.length === 0 ? (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
No {statusFilter === "running" ? "running" : "stopped"} virtual machines {t("vmLxc.statusFilter.empty", { status: t(`vmLxc.statusFilter.${statusFilter}`) })}
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
+14
View File
@@ -132,4 +132,18 @@ export function useT() {
return useI18n().t return useI18n().t
} }
export function getCountFormKey(language: LanguageCode, key: string, count: number): string {
if (language !== "sk") return key
const absoluteCount = Math.abs(count)
const lastDigit = absoluteCount % 10
const lastTwoDigits = absoluteCount % 100
if (lastDigit === 1 && lastTwoDigits !== 11) return `${key}One`
if (lastDigit >= 2 && lastDigit <= 4 && !(lastTwoDigits >= 12 && lastTwoDigits <= 14)) {
return `${key}Few`
}
return `${key}Many`
}
export { SUPPORTED_LANGUAGES } export { SUPPORTED_LANGUAGES }
+27 -3
View File
@@ -97,6 +97,9 @@
"free": "Free", "free": "Free",
"activeVmLxc": "Active VM & LXC", "activeVmLxc": "Active VM & LXC",
"runningCount": "{count} running", "runningCount": "{count} running",
"runningCountOne": "{count} running",
"runningCountFew": "{count} running",
"runningCountMany": "{count} running",
"vmsCount": "{count} VMs", "vmsCount": "{count} VMs",
"stoppedCount": "{count} stopped", "stoppedCount": "{count} stopped",
"temperature": "Temperature", "temperature": "Temperature",
@@ -913,6 +916,13 @@
"idle": "Idle", "idle": "Idle",
"listTitle": "Virtual Machines & Containers", "listTitle": "Virtual Machines & Containers",
"empty": "No virtual machines found", "empty": "No virtual machines found",
"statusFilter": {
"ariaLabel": "Filter virtual machines and containers by status",
"all": "All",
"running": "Running",
"stopped": "Stopped",
"empty": "No virtual machines or containers with status \"{status}\""
},
"uptime": "Uptime: {uptime}", "uptime": "Uptime: {uptime}",
"cpuUsage": "CPU Usage", "cpuUsage": "CPU Usage",
"memory": "Memory", "memory": "Memory",
@@ -1092,7 +1102,12 @@
"unknown": "Unknown error", "unknown": "Unknown error",
"backupStartFailed": "Failed to start backup: {message}", "backupStartFailed": "Failed to start backup: {message}",
"controlFailed": "Failed to {action} VM {vmid}: {message}", "controlFailed": "Failed to {action} VM {vmid}: {message}",
"saveNotesFailed": "Error saving notes. Please try again." "saveNotesFailed": "Error saving notes. Please try again.",
"appNotFound": "This application is no longer available. Refresh the page and try again.",
"saveCustomCommandFailed": "Could not save the custom update command: {message}",
"removeCustomCommandConfirm": "Remove the custom update command for \"{name}\"?",
"removeCustomCommandFailed": "Could not remove the custom update command: {message}",
"hideNoticeFailed": "Could not hide this notice: {message}"
}, },
"backupModal": { "backupModal": {
"title": "Backup {type} {vmid} ({name})", "title": "Backup {type} {vmid} ({name})",
@@ -1197,7 +1212,15 @@
"noTargets": "No targets selected", "noTargets": "No targets selected",
"selectAtLeastOne": "Select at least one update target.", "selectAtLeastOne": "Select at least one update target.",
"deleteButton": "Delete schedule", "deleteButton": "Delete schedule",
"deleteConfirm": "Remove the scheduled updates for this container? Apply defaults (backup + restart) are kept." "deleteConfirm": "Remove the scheduled updates for this container? Apply defaults (backup + restart) are kept.",
"loadFailed": "Could not load the scheduled updates.",
"saveFailed": "Could not save the scheduled updates.",
"deleteFailed": "Could not remove the scheduled updates.",
"humanDaily": "Daily at {time}",
"humanWeekly": "Weekly ({day} {time})",
"humanMonthly": "Monthly (day {day} at {time})",
"humanHourly": "Hourly",
"weekdays": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
}, },
"cronChip": { "cronChip": {
"detected": "host cron detected", "detected": "host cron detected",
@@ -1343,7 +1366,8 @@
"deleteConfirm": "Remove this bulk update configuration?", "deleteConfirm": "Remove this bulk update configuration?",
"saveFailed": "Could not save the bulk update configuration.", "saveFailed": "Could not save the bulk update configuration.",
"deleteFailed": "Could not remove the bulk update configuration.", "deleteFailed": "Could not remove the bulk update configuration.",
"planFailed": "Could not prepare the bulk update. Edit the configuration and review unavailable methods." "planFailed": "Could not prepare the bulk update. Edit the configuration and review unavailable methods.",
"loadFailed": "Could not load the bulk update configuration."
}, },
"appEditor": { "appEditor": {
"closePanel": "Close panel", "closePanel": "Close panel",
+35 -11
View File
@@ -96,7 +96,10 @@
"total": "Spolu", "total": "Spolu",
"free": "Voľné", "free": "Voľné",
"activeVmLxc": "Aktívne VM a LXC", "activeVmLxc": "Aktívne VM a LXC",
"runningCount": "{count} beží", "runningCount": "{count} spustené",
"runningCountOne": "{count} spustený",
"runningCountFew": "{count} spustené",
"runningCountMany": "{count} spustených",
"vmsCount": "{count} VM", "vmsCount": "{count} VM",
"stoppedCount": "{count} vypnuté", "stoppedCount": "{count} vypnuté",
"temperature": "Teplota", "temperature": "Teplota",
@@ -901,7 +904,7 @@
"totalCpuAllocated": "Pridelené CPU spolu", "totalCpuAllocated": "Pridelené CPU spolu",
"totalMemory": "Pamäť spolu", "totalMemory": "Pamäť spolu",
"totalDisk": "Disk spolu", "totalDisk": "Disk spolu",
"running": "beží", "running": "spustené",
"stopped": "vypnuté", "stopped": "vypnuté",
"vms": "VM", "vms": "VM",
"used": "Použité", "used": "Použité",
@@ -913,6 +916,13 @@
"idle": "Voľné", "idle": "Voľné",
"listTitle": "Virtuálne stroje a kontajnery", "listTitle": "Virtuálne stroje a kontajnery",
"empty": "Nenašli sa žiadne virtuálne stroje", "empty": "Nenašli sa žiadne virtuálne stroje",
"statusFilter": {
"ariaLabel": "Filtrovať virtuálne stroje a kontajnery podľa stavu",
"all": "Všetky",
"running": "Spustené",
"stopped": "Vypnuté",
"empty": "Žiadne virtuálne stroje ani kontajnery so stavom „{status}“"
},
"uptime": "Beží: {uptime}", "uptime": "Beží: {uptime}",
"cpuUsage": "Využitie CPU", "cpuUsage": "Využitie CPU",
"memory": "Pamäť", "memory": "Pamäť",
@@ -1092,7 +1102,12 @@
"unknown": "Neznáma chyba", "unknown": "Neznáma chyba",
"backupStartFailed": "Zálohu sa nepodarilo spustiť: {message}", "backupStartFailed": "Zálohu sa nepodarilo spustiť: {message}",
"controlFailed": "Akcia {action} pre VM {vmid} zlyhala: {message}", "controlFailed": "Akcia {action} pre VM {vmid} zlyhala: {message}",
"saveNotesFailed": "Poznámky sa nepodarilo uložiť. Skúste to znova." "saveNotesFailed": "Poznámky sa nepodarilo uložiť. Skúste to znova.",
"appNotFound": "Táto aplikácia už nie je dostupná. Obnovte stránku a skúste to znova.",
"saveCustomCommandFailed": "Vlastný príkaz na aktualizáciu sa nepodarilo uložiť: {message}",
"removeCustomCommandConfirm": "Odstrániť vlastný príkaz na aktualizáciu pre „{name}“?",
"removeCustomCommandFailed": "Vlastný príkaz na aktualizáciu sa nepodarilo odstrániť: {message}",
"hideNoticeFailed": "Toto upozornenie sa nepodarilo skryť: {message}"
}, },
"backupModal": { "backupModal": {
"title": "Záloha {type} {vmid} ({name})", "title": "Záloha {type} {vmid} ({name})",
@@ -1197,7 +1212,15 @@
"noTargets": "Nie sú vybrané žiadne ciele", "noTargets": "Nie sú vybrané žiadne ciele",
"selectAtLeastOne": "Vyberte aspoň jeden cieľ aktualizácie.", "selectAtLeastOne": "Vyberte aspoň jeden cieľ aktualizácie.",
"deleteButton": "Odstrániť plán", "deleteButton": "Odstrániť plán",
"deleteConfirm": "Odstrániť plánované aktualizácie tohto kontajnera? Predvolené nastavenia (záloha a reštart) zostanú zachované." "deleteConfirm": "Odstrániť plánované aktualizácie tohto kontajnera? Predvolené nastavenia (záloha a reštart) zostanú zachované.",
"loadFailed": "Plánované aktualizácie sa nepodarilo načítať.",
"saveFailed": "Plánované aktualizácie sa nepodarilo uložiť.",
"deleteFailed": "Plánované aktualizácie sa nepodarilo odstrániť.",
"humanDaily": "Denne o {time}",
"humanWeekly": "Týždenne ({day} o {time})",
"humanMonthly": "Mesačne ({day}. deň o {time})",
"humanHourly": "Každú hodinu",
"weekdays": ["nedeľa", "pondelok", "utorok", "streda", "štvrtok", "piatok", "sobota"]
}, },
"cronChip": { "cronChip": {
"detected": "zistený cron na serveri", "detected": "zistený cron na serveri",
@@ -1343,7 +1366,8 @@
"deleteConfirm": "Odstrániť túto konfiguráciu hromadnej aktualizácie?", "deleteConfirm": "Odstrániť túto konfiguráciu hromadnej aktualizácie?",
"saveFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo uložiť.", "saveFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo uložiť.",
"deleteFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo odstrániť.", "deleteFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo odstrániť.",
"planFailed": "Hromadnú aktualizáciu sa nepodarilo pripraviť. Skontrolujte nedostupné metódy." "planFailed": "Hromadnú aktualizáciu sa nepodarilo pripraviť. Skontrolujte nedostupné metódy.",
"loadFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo načítať."
}, },
"appEditor": { "appEditor": {
"closePanel": "Zavrieť panel", "closePanel": "Zavrieť panel",
@@ -1441,16 +1465,16 @@
"upToDateBadge": "Aktuálne", "upToDateBadge": "Aktuálne",
"updateAvailableBadge": "Dostupná aktualizácia", "updateAvailableBadge": "Dostupná aktualizácia",
"portDescriptionPlaceholder": "Popis (napr. Web UI, go2rtc, admin)", "portDescriptionPlaceholder": "Popis (napr. Web UI, go2rtc, admin)",
"portPortPlaceholder": "prístav", "portPortPlaceholder": "port",
"portHttp": "http", "portHttp": "http",
"portHttps": "https", "portHttps": "https",
"portLogoLabel": "URL loga pre tento odkaz (voliteľné)", "portLogoLabel": "URL loga pre tento odkaz (voliteľné)",
"portLogoPlaceholder": "napr. https://example.com/logo.webp", "portLogoPlaceholder": "napr. https://example.com/logo.webp",
"portCategoryPlaceholder": "Category for the Apps dashboard (optional)", "portCategoryPlaceholder": "Kategória pre prehľad aplikácií (voliteľné)",
"portCategoryNone": "No category", "portCategoryNone": "Bez kategórie",
"portCategoryAddNew": "+ Add new category…", "portCategoryAddNew": "+ Pridať novú kategóriu…",
"portCategoryCustomPlaceholder": "Type a category and press Enter (Esc to cancel)", "portCategoryCustomPlaceholder": "Napíšte názov kategórie a stlačte Enter (Esc zruší)",
"portCustomUrlPlaceholder": "Custom URL (e.g. https://vault.example.com) — overrides IP:port", "portCustomUrlPlaceholder": "Vlastná URL (napr. https://vault.example.com) — nahradí IP:port",
"removePortTooltip": "Odstrániť port", "removePortTooltip": "Odstrániť port",
"detectMethodDpkg": "dpkg ·", "detectMethodDpkg": "dpkg ·",
"detectMethodApk": "apk ·", "detectMethodApk": "apk ·",