mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
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:
@@ -13,7 +13,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
||||
import { fetchApi } from "../lib/api-config"
|
||||
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
|
||||
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"
|
||||
|
||||
interface TempDataPoint {
|
||||
@@ -173,6 +173,7 @@ const getUnitsSettings = (): "Bytes" | "Bits" => {
|
||||
|
||||
export function SystemOverview() {
|
||||
const t = useT()
|
||||
const { language } = useI18n()
|
||||
const [systemData, setSystemData] = useState<SystemData | null>(null)
|
||||
const [vmData, setVmData] = useState<VMData[]>([])
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="mt-3 flex gap-1 flex-wrap">
|
||||
|
||||
@@ -27,7 +27,7 @@ import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
|
||||
import { fetchApi } from "../lib/api-config"
|
||||
import DOMPurify from "dompurify"
|
||||
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
|
||||
// `lxc_updates_available` notifications. The Monitor populates this
|
||||
@@ -753,6 +753,7 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) {
|
||||
|
||||
export function VirtualMachines() {
|
||||
const t = useT()
|
||||
const { language } = useI18n()
|
||||
const {
|
||||
data: vmData,
|
||||
error,
|
||||
@@ -1929,7 +1930,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
applySchedulePayload(s)
|
||||
}
|
||||
} catch (e: any) {
|
||||
setScheduleError(e?.message || "Could not load schedule")
|
||||
setScheduleError(e?.message || t("vmLxc.scheduled.loadFailed"))
|
||||
} finally {
|
||||
setScheduleLoaded(vmid)
|
||||
}
|
||||
@@ -1953,7 +1954,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
setBulkConfigured(false)
|
||||
setBulkTargets(["os"])
|
||||
setBulkPersistedTargets(["os"])
|
||||
setBulkError(e?.message || "Could not load bulk update")
|
||||
setBulkError(e?.message || t("vmLxc.bulkUpdate.loadFailed"))
|
||||
} finally {
|
||||
setBulkLoaded(vmid)
|
||||
}
|
||||
@@ -2073,7 +2074,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
})
|
||||
if (cronToSave.trim()) setScheduleConfigured(true)
|
||||
} catch (e: any) {
|
||||
setScheduleError(e?.message || "Save failed")
|
||||
setScheduleError(e?.message || t("vmLxc.scheduled.saveFailed"))
|
||||
} finally {
|
||||
setScheduleSaving(false)
|
||||
}
|
||||
@@ -2132,13 +2133,13 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
setScheduleLastRunRebootPackages([])
|
||||
setScheduleReleaseDelayDays(0)
|
||||
} catch (e: any) {
|
||||
setScheduleError(e?.message || "Delete failed")
|
||||
setScheduleError(e?.message || t("vmLxc.scheduled.deleteFailed"))
|
||||
} finally {
|
||||
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
|
||||
// preset labels.
|
||||
const humanCron = (expr: string): string => {
|
||||
@@ -2151,15 +2152,14 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
if (isNaN(hn) || isNaN(mn)) return `${h}:${m}`
|
||||
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)) {
|
||||
const wdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
||||
const wn = parseInt(w, 10)
|
||||
const wname = (wn >= 0 && wn <= 6) ? wdays[wn] : w
|
||||
return `Weekly (${wname} ${hhmm()})`
|
||||
const wname = wn >= 0 && wn <= 6 ? t(`vmLxc.scheduled.weekdays.${wn}`) : w
|
||||
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 (h === "*" && d === "*" && mo === "*" && w === "*" && m === "0") return "Hourly"
|
||||
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 t("vmLxc.scheduled.humanHourly")
|
||||
return expr
|
||||
}
|
||||
|
||||
@@ -2296,7 +2296,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
// record and would drop any field we omitted.
|
||||
const full: any = await fetchApi(`/api/vms/${vmid}/apps`)
|
||||
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 payload = { ...rest, ...patch }
|
||||
const updated: any = await fetchApi(`/api/vms/${vmid}/apps/${app.id}`, {
|
||||
@@ -2321,19 +2321,19 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
})
|
||||
closeCustomCmdEditor()
|
||||
} 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 {
|
||||
setCustomCmdSaving(false)
|
||||
}
|
||||
}
|
||||
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)
|
||||
try {
|
||||
await patchAppWatch(vmid, app, { update_command: "" })
|
||||
closeCustomCmdEditor()
|
||||
} 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 {
|
||||
setCustomCmdSaving(false)
|
||||
}
|
||||
@@ -2342,7 +2342,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
try {
|
||||
await patchAppWatch(vmid, app, { hide_no_updater_notice: true })
|
||||
} 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"]
|
||||
: ["apps"]
|
||||
const fallbackLabels = target === "os"
|
||||
? ["OS"]
|
||||
? [t("vmLxc.bulkUpdate.osTarget")]
|
||||
: target === "both"
|
||||
? ["OS", "Applications"]
|
||||
: [opts?.appName || (opts?.dockerEngine ? "Docker Engine" : "Application")]
|
||||
? [t("vmLxc.bulkUpdate.osTarget"), t("vmLxc.scheduled.targetApp")]
|
||||
: [opts?.appName || (opts?.dockerEngine ? "Docker Engine" : t("vmLxc.scheduled.targetApp"))]
|
||||
const runId = typeof window !== "undefined" && typeof window.crypto?.randomUUID === "function"
|
||||
? window.crypto.randomUUID()
|
||||
: `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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="mt-3 flex gap-1 flex-wrap">
|
||||
@@ -2987,12 +2987,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
</CardTitle>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
{(["all", "running", "stopped"] as const).map((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);
|
||||
// green / red on inactive tabs so the state mapping stays legible
|
||||
// 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>
|
||||
) : filteredVMs.length === 0 ? (
|
||||
<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 className="space-y-3">
|
||||
|
||||
@@ -132,4 +132,18 @@ export function useT() {
|
||||
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 }
|
||||
|
||||
@@ -97,6 +97,9 @@
|
||||
"free": "Free",
|
||||
"activeVmLxc": "Active VM & LXC",
|
||||
"runningCount": "{count} running",
|
||||
"runningCountOne": "{count} running",
|
||||
"runningCountFew": "{count} running",
|
||||
"runningCountMany": "{count} running",
|
||||
"vmsCount": "{count} VMs",
|
||||
"stoppedCount": "{count} stopped",
|
||||
"temperature": "Temperature",
|
||||
@@ -913,6 +916,13 @@
|
||||
"idle": "Idle",
|
||||
"listTitle": "Virtual Machines & Containers",
|
||||
"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}",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memory": "Memory",
|
||||
@@ -1092,7 +1102,12 @@
|
||||
"unknown": "Unknown error",
|
||||
"backupStartFailed": "Failed to start backup: {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": {
|
||||
"title": "Backup {type} {vmid} ({name})",
|
||||
@@ -1197,7 +1212,15 @@
|
||||
"noTargets": "No targets selected",
|
||||
"selectAtLeastOne": "Select at least one update target.",
|
||||
"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": {
|
||||
"detected": "host cron detected",
|
||||
@@ -1343,7 +1366,8 @@
|
||||
"deleteConfirm": "Remove this bulk update configuration?",
|
||||
"saveFailed": "Could not save 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": {
|
||||
"closePanel": "Close panel",
|
||||
|
||||
@@ -96,7 +96,10 @@
|
||||
"total": "Spolu",
|
||||
"free": "Voľné",
|
||||
"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",
|
||||
"stoppedCount": "{count} vypnuté",
|
||||
"temperature": "Teplota",
|
||||
@@ -901,7 +904,7 @@
|
||||
"totalCpuAllocated": "Pridelené CPU spolu",
|
||||
"totalMemory": "Pamäť spolu",
|
||||
"totalDisk": "Disk spolu",
|
||||
"running": "beží",
|
||||
"running": "spustené",
|
||||
"stopped": "vypnuté",
|
||||
"vms": "VM",
|
||||
"used": "Použité",
|
||||
@@ -913,6 +916,13 @@
|
||||
"idle": "Voľné",
|
||||
"listTitle": "Virtuálne stroje a kontajnery",
|
||||
"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}",
|
||||
"cpuUsage": "Využitie CPU",
|
||||
"memory": "Pamäť",
|
||||
@@ -1092,7 +1102,12 @@
|
||||
"unknown": "Neznáma chyba",
|
||||
"backupStartFailed": "Zálohu sa nepodarilo spustiť: {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": {
|
||||
"title": "Záloha {type} {vmid} ({name})",
|
||||
@@ -1197,7 +1212,15 @@
|
||||
"noTargets": "Nie sú vybrané žiadne ciele",
|
||||
"selectAtLeastOne": "Vyberte aspoň jeden cieľ aktualizácie.",
|
||||
"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": {
|
||||
"detected": "zistený cron na serveri",
|
||||
@@ -1343,7 +1366,8 @@
|
||||
"deleteConfirm": "Odstrániť túto konfiguráciu hromadnej aktualizácie?",
|
||||
"saveFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo uložiť.",
|
||||
"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": {
|
||||
"closePanel": "Zavrieť panel",
|
||||
@@ -1441,16 +1465,16 @@
|
||||
"upToDateBadge": "Aktuálne",
|
||||
"updateAvailableBadge": "Dostupná aktualizácia",
|
||||
"portDescriptionPlaceholder": "Popis (napr. Web UI, go2rtc, admin)",
|
||||
"portPortPlaceholder": "prístav",
|
||||
"portPortPlaceholder": "port",
|
||||
"portHttp": "http",
|
||||
"portHttps": "https",
|
||||
"portLogoLabel": "URL loga pre tento odkaz (voliteľné)",
|
||||
"portLogoPlaceholder": "napr. https://example.com/logo.webp",
|
||||
"portCategoryPlaceholder": "Category for the Apps dashboard (optional)",
|
||||
"portCategoryNone": "No category",
|
||||
"portCategoryAddNew": "+ Add new category…",
|
||||
"portCategoryCustomPlaceholder": "Type a category and press Enter (Esc to cancel)",
|
||||
"portCustomUrlPlaceholder": "Custom URL (e.g. https://vault.example.com) — overrides IP:port",
|
||||
"portCategoryPlaceholder": "Kategória pre prehľad aplikácií (voliteľné)",
|
||||
"portCategoryNone": "Bez kategórie",
|
||||
"portCategoryAddNew": "+ Pridať novú kategóriu…",
|
||||
"portCategoryCustomPlaceholder": "Napíšte názov kategórie a stlačte Enter (Esc zruší)",
|
||||
"portCustomUrlPlaceholder": "Vlastná URL (napr. https://vault.example.com) — nahradí IP:port",
|
||||
"removePortTooltip": "Odstrániť port",
|
||||
"detectMethodDpkg": "dpkg ·",
|
||||
"detectMethodApk": "apk ·",
|
||||
|
||||
Reference in New Issue
Block a user