mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
i18n: localize host backup application errors
This commit is contained in:
@@ -31,6 +31,7 @@ PYTHON_FILES = (
|
||||
"AppImage/scripts/tests/test_health_persistence_recurrence.py",
|
||||
)
|
||||
NODE_FILES = (
|
||||
"tests/test_host_backup_messages.cjs",
|
||||
"tests/test_pbs_i18n.cjs",
|
||||
"tests/test_monitor_links_i18n.cjs",
|
||||
"tests/lxc_updates/test_local_app_icons_ui.cjs",
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { ScriptTerminalModal } from "./script-terminal-modal"
|
||||
import { RestoreProgressCard } from "./restore-progress-card"
|
||||
import { fetchApi, getApiUrl } from "../lib/api-config"
|
||||
import { hostBackupErrorText } from "../lib/host-backup-messages"
|
||||
import { fetchTerminalTicket } from "../lib/terminal-ws"
|
||||
import { formatStorage, formatBytes, cancelButtonClass } from "../lib/utils"
|
||||
import { getStorageUsageColor } from "../lib/storage-usage-color"
|
||||
@@ -525,7 +526,7 @@ function PbsKeyfileActions({ pbsRepository }: { pbsRepository?: string }) {
|
||||
await mutateInfo()
|
||||
closeUpload()
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e))
|
||||
setErr(hostBackupErrorText(e, t))
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
@@ -542,7 +543,7 @@ function PbsKeyfileActions({ pbsRepository }: { pbsRepository?: string }) {
|
||||
await mutateInfo()
|
||||
closeDelete()
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e))
|
||||
setErr(hostBackupErrorText(e, t))
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
@@ -765,7 +766,7 @@ function KeyfileActionsBar({
|
||||
setPass1("")
|
||||
setPass2("")
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e))
|
||||
setErr(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
@@ -956,7 +957,7 @@ export function HostBackup() {
|
||||
mutateJobs()
|
||||
setJobToDelete(null)
|
||||
} catch (e) {
|
||||
setActionError(t("backup.errors.deleteFailed", { error: `"${id}": ${e instanceof Error ? e.message : String(e)}` }))
|
||||
setActionError(t("backup.errors.deleteFailed", { error: `"${id}": ${hostBackupErrorText(e, t)}` }))
|
||||
} finally {
|
||||
setBusyJobId(null)
|
||||
}
|
||||
@@ -1334,7 +1335,7 @@ export function HostBackup() {
|
||||
<div className="font-medium">{t("backup.archives.remoteQueryWarning")}</div>
|
||||
{remoteArchivesResp.errors.map((e, i) => (
|
||||
<div key={i} className="font-mono break-all">
|
||||
{e.backend}/{e.repo_name}: {e.error}
|
||||
{e.backend}/{e.repo_name}: {hostBackupErrorText(e.error, t)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1761,7 +1762,7 @@ function InspectModal({
|
||||
setImportFile(null)
|
||||
setImportPath("")
|
||||
} catch (e) {
|
||||
setImportError(e instanceof Error ? e.message : String(e))
|
||||
setImportError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setImporting(false)
|
||||
}
|
||||
@@ -1812,7 +1813,7 @@ function InspectModal({
|
||||
: undefined,
|
||||
})
|
||||
} catch (e) {
|
||||
setRestoreError(e instanceof Error ? e.message : String(e))
|
||||
setRestoreError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setRestorePreparing(false)
|
||||
}
|
||||
@@ -1841,7 +1842,7 @@ function InspectModal({
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
} catch (e) {
|
||||
setError(t("backup.errors.downloadFailed", { error: e instanceof Error ? e.message : String(e) }))
|
||||
setError(t("backup.errors.downloadFailed", { error: hostBackupErrorText(e, t) }))
|
||||
} finally {
|
||||
// The download itself runs in the browser's network stack;
|
||||
// we just initiated it. Clear the spinner immediately.
|
||||
@@ -1914,7 +1915,7 @@ function InspectModal({
|
||||
document.body.removeChild(a)
|
||||
setExportTask(null)
|
||||
} catch (e) {
|
||||
setError(t("backup.errors.downloadFailed", { error: e instanceof Error ? e.message : String(e) }))
|
||||
setError(t("backup.errors.downloadFailed", { error: hostBackupErrorText(e, t) }))
|
||||
} finally {
|
||||
setDownloading(false)
|
||||
}
|
||||
@@ -1962,7 +1963,7 @@ function InspectModal({
|
||||
setShowDeleteArchiveConfirm(false)
|
||||
if (onDeleted) onDeleted(); else onClose()
|
||||
} catch (e) {
|
||||
setError(t("backup.errors.deleteFailed", { error: e instanceof Error ? e.message : String(e) }))
|
||||
setError(t("backup.errors.deleteFailed", { error: hostBackupErrorText(e, t) }))
|
||||
} finally {
|
||||
setDeletingArchive(false)
|
||||
}
|
||||
@@ -1989,7 +1990,7 @@ function InspectModal({
|
||||
) as NonNullable<typeof instructions>
|
||||
setInstructions(res)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setFetchingInstructions(false)
|
||||
}
|
||||
@@ -2021,7 +2022,7 @@ function InspectModal({
|
||||
)
|
||||
setReport(res)
|
||||
} catch (e: any) {
|
||||
setError(e?.message || t("backup.errors.preflightFailed"))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
@@ -2164,7 +2165,7 @@ function InspectModal({
|
||||
<span className="text-muted-foreground">— {exportTask.message}</span>
|
||||
</div>
|
||||
{exportTask.state === "failed" && exportTask.error && (
|
||||
<div className="text-red-500 mt-1">{exportTask.error}</div>
|
||||
<div className="text-red-500 mt-1">{hostBackupErrorText(exportTask.error, t)}</div>
|
||||
)}
|
||||
{exportTask.state === "completed" && exportTask.size_bytes > 0 && (
|
||||
<div className="text-emerald-400">{t("backup.archives.packedSizeLabel")} {formatBytes(exportTask.size_bytes)}</div>
|
||||
@@ -2706,7 +2707,7 @@ function PreflightReportView({ report }: { report: PreflightReport }) {
|
||||
<Icon className={`h-3.5 w-3.5 ${color} flex-shrink-0 mt-0.5`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className={`font-mono ${color}`}>{c.id}</span>
|
||||
<span className="text-muted-foreground ml-2">{c.message}</span>
|
||||
<span className="text-muted-foreground ml-2">{hostBackupErrorText(c.message, t)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -3326,9 +3327,9 @@ function CreateJobDialog({
|
||||
} catch (e) {
|
||||
const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
|
||||
const detail = err.body?.tool_output
|
||||
? `${err.message}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
|
||||
: err.message
|
||||
setError(t("backup.errors.encryptionSetupFailed", { detail: detail || String(e) }))
|
||||
? `${hostBackupErrorText(err, t)}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
|
||||
: hostBackupErrorText(err, t)
|
||||
setError(t("backup.errors.encryptionSetupFailed", { error: detail || hostBackupErrorText(e, t) }))
|
||||
setPbsImportBusy(false)
|
||||
setSubmitting(false)
|
||||
return
|
||||
@@ -3391,7 +3392,7 @@ function CreateJobDialog({
|
||||
})
|
||||
onCreated()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -3764,7 +3765,7 @@ function CreateJobDialog({
|
||||
</>
|
||||
) : (
|
||||
<div className="text-red-500">
|
||||
<span className="font-medium">{t("backup.validation.invalidLabel")}</span> {calendarPreview.error}
|
||||
<span className="font-medium">{t("backup.validation.invalidLabel")}</span> {hostBackupErrorText(calendarPreview.error, t)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
@@ -4696,9 +4697,9 @@ function ManualBackupDialog({
|
||||
} catch (e) {
|
||||
const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
|
||||
const detail = err.body?.tool_output
|
||||
? `${err.message}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
|
||||
: err.message
|
||||
setError(t("backup.errors.encryptionSetupFailed", { detail: detail || String(e) }))
|
||||
? `${hostBackupErrorText(err, t)}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
|
||||
: hostBackupErrorText(err, t)
|
||||
setError(t("backup.errors.encryptionSetupFailed", { error: detail || hostBackupErrorText(e, t) }))
|
||||
setPbsImportBusy(false)
|
||||
setSubmitting(false)
|
||||
return
|
||||
@@ -4738,7 +4739,7 @@ function ManualBackupDialog({
|
||||
)
|
||||
onLaunched(resp.job_id)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -5542,7 +5543,7 @@ function DestinationsSection({
|
||||
await fetchApi(`/api/host-backups/destinations/pbs/${encodeURIComponent(name)}${force ? "?force=true" : ""}`, { method: "DELETE" })
|
||||
onChanged()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -5554,7 +5555,7 @@ function DestinationsSection({
|
||||
await fetchApi(`/api/host-backups/destinations/borg/${encodeURIComponent(name)}${force ? "?force=true" : ""}`, { method: "DELETE" })
|
||||
onChanged()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -5571,7 +5572,7 @@ function DestinationsSection({
|
||||
)
|
||||
onChanged()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -5599,7 +5600,7 @@ function DestinationsSection({
|
||||
})
|
||||
onChanged()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -6042,7 +6043,7 @@ function DestinationRow({
|
||||
|
||||
{capacity?.error && (
|
||||
<p className="mt-3 text-[11px] text-muted-foreground italic">
|
||||
{t("backup.destinations.capacityUnavailable", { error: capacity.error })}
|
||||
{t("backup.destinations.capacityUnavailable", { error: hostBackupErrorText(capacity.error, t) })}
|
||||
</p>
|
||||
)}
|
||||
{!capacity && (
|
||||
@@ -6341,7 +6342,7 @@ function AddDestinationDialog({
|
||||
)
|
||||
setGeneratedKey(resp)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setGeneratingKey(false)
|
||||
}
|
||||
@@ -6405,7 +6406,7 @@ function AddDestinationDialog({
|
||||
}
|
||||
onSaved(savedRepo)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -6756,7 +6757,7 @@ function UsbPicker({
|
||||
mutate()
|
||||
if (resp?.mountpoint) onPick(resp.mountpoint)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -6784,7 +6785,7 @@ function UsbPicker({
|
||||
setFormatTyped("")
|
||||
if (resp?.mountpoint) onPick(resp.mountpoint)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -7001,7 +7002,7 @@ function ExtraPathsSection() {
|
||||
setNewPath("")
|
||||
mutate()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -7014,7 +7015,7 @@ function ExtraPathsSection() {
|
||||
await fetchApi(`/api/host-backups/extra-paths?path=${encodeURIComponent(p)}`, { method: "DELETE" })
|
||||
mutate()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyPath(null)
|
||||
}
|
||||
@@ -7180,7 +7181,7 @@ function UsbDrivesSection() {
|
||||
})
|
||||
mutate()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -7198,7 +7199,7 @@ function UsbDrivesSection() {
|
||||
})
|
||||
mutate()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -7222,7 +7223,7 @@ function UsbDrivesSection() {
|
||||
setFormatTarget(null)
|
||||
setFormatTyped("")
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
@@ -7508,7 +7509,7 @@ function JobDetailModal({
|
||||
setRunning(false)
|
||||
setRunBaseline(null)
|
||||
setRunBaselineLogPath(null)
|
||||
setActionError(t("backup.errors.runFailed", { error: e instanceof Error ? e.message : String(e) }))
|
||||
setActionError(t("backup.errors.runFailed", { error: hostBackupErrorText(e, t) }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7522,7 +7523,7 @@ function JobDetailModal({
|
||||
await refetch()
|
||||
onChanged()
|
||||
} catch (e) {
|
||||
setActionError(t("backup.errors.toggleFailed", { error: e instanceof Error ? e.message : String(e) }))
|
||||
setActionError(t("backup.errors.toggleFailed", { error: hostBackupErrorText(e, t) }))
|
||||
} finally {
|
||||
setBusy("")
|
||||
}
|
||||
@@ -8155,7 +8156,7 @@ function PbsKeyfileRecoveryDialog({
|
||||
})
|
||||
onRecovered()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setError(hostBackupErrorText(e, t))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
@@ -8342,7 +8343,7 @@ function ArchiveContentsModal({
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((r: any) => setData(r))
|
||||
.catch((e) => setError(e instanceof Error ? e.message : String(e)))
|
||||
.catch((e) => setError(hostBackupErrorText(e, t)))
|
||||
.finally(() => setLoading(false))
|
||||
}, [open, source, repo_name, snapshot, path])
|
||||
|
||||
@@ -8571,7 +8572,7 @@ function PlanSummary({ plan }: { plan: any }) {
|
||||
<div className="text-[10px] uppercase tracking-wider text-red-400 mb-1">{t("backup.plan.blockers")}</div>
|
||||
<ul className="space-y-0.5 ml-4 list-disc">
|
||||
{blockers.map((b, i) => (
|
||||
<li key={i} className="text-red-400">{typeof b === "string" ? b : (b.message || JSON.stringify(b))}</li>
|
||||
<li key={i} className="text-red-400">{hostBackupErrorText(typeof b === "string" ? b : (b.message || JSON.stringify(b)), t)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -8581,7 +8582,7 @@ function PlanSummary({ plan }: { plan: any }) {
|
||||
<div className="text-[10px] uppercase tracking-wider text-amber-400 mb-1">{t("backup.plan.warnings")}</div>
|
||||
<ul className="space-y-0.5 ml-4 list-disc">
|
||||
{warnings.map((w, i) => (
|
||||
<li key={i} className="text-amber-400">{typeof w === "string" ? w : (w.message || JSON.stringify(w))}</li>
|
||||
<li key={i} className="text-amber-400">{hostBackupErrorText(typeof w === "string" ? w : (w.message || JSON.stringify(w)), t)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -8611,7 +8612,7 @@ function StorageSection({ storage }: { storage: any }) {
|
||||
<div className="min-w-0 flex-1">
|
||||
<code className="font-mono">{e.name || e.storage || `entry ${i}`}</code>
|
||||
{e.type && <span className="text-muted-foreground ml-2">{e.type}</span>}
|
||||
{e.message && <div className="text-muted-foreground text-[11px]">{e.message}</div>}
|
||||
{e.message && <div className="text-muted-foreground text-[11px]">{hostBackupErrorText(e.message, t)}</div>}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Legacy HostBackup API messages are an English wire contract shared with CLI callers.
|
||||
// Match complete, source-verified messages only; never derive a translation key from input.
|
||||
// This presentation-only adapter keeps unknown tool diagnostics and parameter text intact.
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string
|
||||
|
||||
const fixed = new Map<string, string>([
|
||||
["path must be absolute", "backup.apiErrors.absolutePath"],
|
||||
["this path is already configured", "backup.apiErrors.pathConfigured"],
|
||||
["path is required", "backup.apiErrors.pathRequired"],
|
||||
["path query parameter is required", "backup.apiErrors.pathQuery"],
|
||||
["path not found", "backup.apiErrors.pathNotFound"],
|
||||
["invalid job id", "backup.apiErrors.invalidJobId"],
|
||||
["job not found", "backup.apiErrors.jobNotFound"],
|
||||
["runner script not installed", "backup.apiErrors.runnerMissing"],
|
||||
["no restore state to dismiss", "backup.apiErrors.noRestore"],
|
||||
["invalid file name", "backup.apiErrors.invalidFile"],
|
||||
["not found", "backup.apiErrors.notFound"],
|
||||
["log path outside allowed directory", "backup.apiErrors.logOutside"],
|
||||
["targets must be a list", "backup.apiErrors.targetsList"],
|
||||
["empty expression", "backup.apiErrors.emptyExpression"],
|
||||
["invalid id (use letters, digits, _ or -)", "backup.apiErrors.invalidId"],
|
||||
["backend must be pbs, local or borg", "backup.apiErrors.backend"],
|
||||
["backend must be pbs or borg", "backup.apiErrors.remoteBackend"],
|
||||
["backend must be 'pbs' or 'borg'", "backup.apiErrors.remoteBackendQuoted"],
|
||||
["profile_mode must be default or custom", "backup.apiErrors.profileMode"],
|
||||
["borg cannot be attached (PVE vzdump only writes to pbs/local storages)", "backup.apiErrors.borgAttached"],
|
||||
["borg is not a valid backend for attached jobs (PVE vzdump only writes to pbs/local storages)", "backup.apiErrors.borgAttachedLong"],
|
||||
["pve_parent_job_id and pve_storage are required for attached", "backup.apiErrors.parentRequired"],
|
||||
["paths must be a list of absolute paths", "backup.apiErrors.absolutePaths"],
|
||||
["custom profile needs at least one path", "backup.apiErrors.customPath"],
|
||||
["could not resolve default profile paths", "backup.apiErrors.defaultPaths"],
|
||||
["retention must be an object", "backup.apiErrors.retentionObject"],
|
||||
["pbs_repository is required", "backup.apiErrors.pbsRepository"],
|
||||
["pbs_encrypt_mode must be 'none', 'new' or 'existing'", "backup.apiErrors.pbsEncryption"],
|
||||
["failed to create PBS encryption keyfile", "backup.apiErrors.pbsKeyFailed"],
|
||||
["failed to create PBS encryption keyfile (is proxmox-backup-client installed?)", "backup.apiErrors.pbsClientMissing"],
|
||||
["borg_repo is required", "backup.apiErrors.borgRepository"],
|
||||
["borg_encrypt_mode must be one of: none, repokey, keyfile, authenticated", "backup.apiErrors.borgEncryption"],
|
||||
["encrypt_mode must be one of: none, repokey, keyfile, authenticated", "backup.apiErrors.encryption"],
|
||||
["borg_passphrase is required when encryption is enabled (save it on the destination or pass one in the request)", "backup.apiErrors.borgPassphrase"],
|
||||
["passphrase is required when encryption is enabled", "backup.apiErrors.passphrase"],
|
||||
["password is required", "backup.apiErrors.password"],
|
||||
["confirm_device must match device exactly (typo / safety check)", "backup.apiErrors.deviceConfirmation"],
|
||||
["invalid device path", "backup.apiErrors.invalidDevice"],
|
||||
["absolute mountpoint required", "backup.apiErrors.absoluteMountpoint"],
|
||||
["archive layout not recognized — no rootfs/ found", "backup.apiErrors.archiveLayout"],
|
||||
["archive not found", "backup.apiErrors.archiveMissing"],
|
||||
["archive path is not a regular file", "backup.apiErrors.archiveNotFile"],
|
||||
["invalid name", "backup.apiErrors.invalidName"],
|
||||
["invalid staging_path", "backup.apiErrors.invalidStaging"],
|
||||
["job name cannot be changed (delete + recreate to rename)", "backup.apiErrors.jobRename"],
|
||||
["key_path must be absolute", "backup.apiErrors.absoluteKey"],
|
||||
["mode must be \"local\" or \"ssh\"", "backup.apiErrors.mode"],
|
||||
["name is required (letters, digits, _ or -)", "backup.apiErrors.nameRequired"],
|
||||
["no log available for this job yet", "backup.apiErrors.noLog"],
|
||||
["no manual PBS configs file", "backup.apiErrors.noPbsConfig"],
|
||||
["output file missing", "backup.apiErrors.outputMissing"],
|
||||
["parser output was not valid JSON", "backup.apiErrors.parserJson"],
|
||||
["run_restore output was not valid JSON", "backup.apiErrors.restoreJson"],
|
||||
["repo is required", "backup.apiErrors.repoRequired"],
|
||||
["repo_name and snapshot are required", "backup.apiErrors.repoSnapshot"],
|
||||
["repo_name and snapshot are required for borg", "backup.apiErrors.borgRepoSnapshot"],
|
||||
["repo_name and snapshot are required for pbs", "backup.apiErrors.pbsRepoSnapshot"],
|
||||
["repo_name is required", "backup.apiErrors.repoName"],
|
||||
["restore tooling not installed on this host", "backup.apiErrors.restoreMissing"],
|
||||
["server and datastore are required", "backup.apiErrors.serverDatastore"],
|
||||
["snapshot is required", "backup.apiErrors.snapshotRequired"],
|
||||
["source must be pbs, borg, or local", "backup.apiErrors.source"],
|
||||
["ssh_port must be an integer between 1 and 65535", "backup.apiErrors.sshPort"],
|
||||
["ssh_user, ssh_host and ssh_remote_path are required for ssh mode", "backup.apiErrors.sshRequired"],
|
||||
["target has no repository path", "backup.apiErrors.repositoryPath"],
|
||||
["task not found or expired", "backup.apiErrors.taskExpired"],
|
||||
["invalid expression", "backup.apiErrors.invalidExpression"],
|
||||
["no report emitted", "backup.apiErrors.noReport"],
|
||||
["parse_manifest exited non-zero", "backup.apiErrors.parseManifestFailed"],
|
||||
])
|
||||
|
||||
const parameterized: ReadonlyArray<readonly [RegExp, string]> = [
|
||||
[new RegExp("^job already exists: ([\\s\\S]+)$"), "backup.apiErrors.jobExists"],
|
||||
[new RegExp("^path does not exist on this host: ([\\s\\S]+)$"), "backup.apiErrors.pathMissing"],
|
||||
[new RegExp("^device does not exist: ([\\s\\S]+)$"), "backup.apiErrors.deviceMissing"],
|
||||
[new RegExp("^not a mountpoint: ([\\s\\S]+)$"), "backup.apiErrors.mountpoint"],
|
||||
[new RegExp("^local archive not found: ([\\s\\S]+)$"), "backup.apiErrors.localArchiveMissing"],
|
||||
[new RegExp("^Borg destination '([\\s\\S]+)' not found$"), "backup.apiErrors.borgDestination"],
|
||||
[new RegExp("^PBS destination '([\\s\\S]+)' not found$"), "backup.apiErrors.pbsDestination"],
|
||||
[new RegExp("^no password available for PBS repo '([\\s\\S]+)'$"), "backup.apiErrors.pbsPassword"],
|
||||
[new RegExp("^Borg repo \"([\\s\\S]+)\" not configured$"), "backup.apiErrors.borgRepoMissing"],
|
||||
[new RegExp("^PBS repo \"([\\s\\S]+)\" not configured$"), "backup.apiErrors.pbsRepoMissing"],
|
||||
[new RegExp("^Borg target not found: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetMissing"],
|
||||
[new RegExp("^PBS repo not found: ([\\s\\S]+)$"), "backup.apiErrors.pbsTargetMissing"],
|
||||
[new RegExp("^PVE vzdump job not found: ([\\s\\S]+)$"), "backup.apiErrors.pveJobMissing"],
|
||||
[new RegExp("^cannot persist state: ([\\s\\S]+)$"), "backup.apiErrors.statePersist"],
|
||||
[new RegExp("^cannot prepare ssh dir: ([\\s\\S]+)$"), "backup.apiErrors.sshDirectory"],
|
||||
[new RegExp("^cannot read log: ([\\s\\S]+)$"), "backup.apiErrors.logRead"],
|
||||
[new RegExp("^could not update \\.env: ([\\s\\S]+)$"), "backup.apiErrors.envUpdate"],
|
||||
[new RegExp("^failed to persist PBS config: ([\\s\\S]+)$"), "backup.apiErrors.pbsConfigWrite"],
|
||||
[new RegExp("^failed to persist: ([\\s\\S]+)$"), "backup.apiErrors.persist"],
|
||||
[new RegExp("^failed to remove archive: ([\\s\\S]+)$"), "backup.apiErrors.archiveRemove"],
|
||||
[new RegExp("^failed to start: ([\\s\\S]+)$"), "backup.apiErrors.start"],
|
||||
[new RegExp("^failed to update borg-targets\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetsUpdate"],
|
||||
[new RegExp("^failed to update pbs-manual-configs\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.pbsTargetsUpdate"],
|
||||
[new RegExp("^failed to update systemd units: ([\\s\\S]+)$"), "backup.apiErrors.unitsUpdate"],
|
||||
[new RegExp("^failed to write borg-targets\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetsWrite"],
|
||||
[new RegExp("^failed to write job files: ([\\s\\S]+)$"), "backup.apiErrors.jobWrite"],
|
||||
[new RegExp("^failed to write local-target\\.conf: ([\\s\\S]+)$"), "backup.apiErrors.localTargetWrite"],
|
||||
[new RegExp("^failed to write systemd units: ([\\s\\S]+)$"), "backup.apiErrors.unitsWrite"],
|
||||
[new RegExp("^format failed: ([\\s\\S]+)$"), "backup.apiErrors.format"],
|
||||
[new RegExp("^invalid on_calendar: ([\\s\\S]+)$"), "backup.apiErrors.calendar"],
|
||||
[new RegExp("^mount failed: ([\\s\\S]+)$"), "backup.apiErrors.mount"],
|
||||
[new RegExp("^parser invocation failed: ([\\s\\S]+)$"), "backup.apiErrors.parserInvocation"],
|
||||
[new RegExp("^preflight invocation failed: ([\\s\\S]+)$"), "backup.apiErrors.preflightInvocation"],
|
||||
[new RegExp("^public key not found after generation: ([\\s\\S]+)$"), "backup.apiErrors.publicKeyMissing"],
|
||||
[new RegExp("^retention\\.([\\s\\S]+) must be an integer$"), "backup.apiErrors.retentionInteger"],
|
||||
[new RegExp("^retention\\.([\\s\\S]+) must be non-negative$"), "backup.apiErrors.retentionNegative"],
|
||||
[new RegExp("^ssh-keygen failed: ([\\s\\S]+)$"), "backup.apiErrors.keygen"],
|
||||
[new RegExp("^systemctl failed: ([\\s\\S]+)$"), "backup.apiErrors.systemctl"],
|
||||
[new RegExp("^umount failed: ([\\s\\S]+)$"), "backup.apiErrors.unmount"],
|
||||
[new RegExp("^task is in state \"([\\s\\S]+)\", not completed$"), "backup.apiErrors.taskIncomplete"],
|
||||
[new RegExp("^unknown kind: ([\\s\\S]+)$"), "backup.apiErrors.unknownKind"],
|
||||
[new RegExp("^unknown mode \"([\\s\\S]+)\"$"), "backup.apiErrors.unknownMode"],
|
||||
[new RegExp("^([\\s\\S]+) is the built-in default and cannot be removed$"), "backup.apiErrors.defaultRemove"],
|
||||
[new RegExp("^([\\s\\S]+) is the built-in default; no need to add it$"), "backup.apiErrors.defaultAdd"],
|
||||
[new RegExp("^pbs_encrypt_mode='existing' but no keyfile is installed at ([\\s\\S]+) — import one first with POST /api/host-backups/pbs-encryption/import$"), "backup.apiErrors.keyImport"],
|
||||
[new RegExp("^PVE job storage mismatch \\(job uses ([\\s\\S]+), payload says ([\\s\\S]+)\\)$"), "backup.apiErrors.storageMismatch"],
|
||||
]
|
||||
|
||||
export function hostBackupErrorText(problem: unknown, t: Translate): string {
|
||||
const message = typeof problem === "string" ? problem
|
||||
: problem && typeof problem === "object" && "message" in problem && typeof problem.message === "string"
|
||||
? problem.message : ""
|
||||
if (!message) return t("backup.apiErrors.requestFailed")
|
||||
const key = fixed.get(message)
|
||||
if (key) return t(key)
|
||||
// Keep unusually large tool output verbatim, without running pattern matching over it.
|
||||
if (message.length > 16384) return message
|
||||
for (const [pattern, patternKey] of parameterized) {
|
||||
const match = pattern.exec(message)
|
||||
if (match) return t(patternKey, { value: match[1], other: match[2] ?? "" })
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -4368,6 +4368,124 @@
|
||||
"unrecoverable": "Without the key, encrypted backups cannot be restored.",
|
||||
"usesSharedKeyfileAt": "Uses shared keyfile at"
|
||||
},
|
||||
"apiErrors": {
|
||||
"requestFailed": "The request failed.",
|
||||
"absolutePath": "path must be absolute",
|
||||
"pathConfigured": "this path is already configured",
|
||||
"pathRequired": "path is required",
|
||||
"pathQuery": "path query parameter is required",
|
||||
"pathNotFound": "path not found",
|
||||
"invalidJobId": "invalid job id",
|
||||
"jobNotFound": "job not found",
|
||||
"runnerMissing": "runner script not installed",
|
||||
"noRestore": "no restore state to dismiss",
|
||||
"invalidFile": "invalid file name",
|
||||
"notFound": "not found",
|
||||
"logOutside": "log path outside allowed directory",
|
||||
"targetsList": "targets must be a list",
|
||||
"emptyExpression": "empty expression",
|
||||
"invalidId": "invalid id (use letters, digits, _ or -)",
|
||||
"backend": "backend must be pbs, local or borg",
|
||||
"remoteBackend": "backend must be pbs or borg",
|
||||
"remoteBackendQuoted": "backend must be 'pbs' or 'borg'",
|
||||
"profileMode": "profile_mode must be default or custom",
|
||||
"borgAttached": "borg cannot be attached (PVE vzdump only writes to pbs/local storages)",
|
||||
"borgAttachedLong": "borg is not a valid backend for attached jobs (PVE vzdump only writes to pbs/local storages)",
|
||||
"parentRequired": "pve_parent_job_id and pve_storage are required for attached",
|
||||
"absolutePaths": "paths must be a list of absolute paths",
|
||||
"customPath": "custom profile needs at least one path",
|
||||
"defaultPaths": "could not resolve default profile paths",
|
||||
"retentionObject": "retention must be an object",
|
||||
"pbsRepository": "pbs_repository is required",
|
||||
"pbsEncryption": "pbs_encrypt_mode must be 'none', 'new' or 'existing'",
|
||||
"pbsKeyFailed": "failed to create PBS encryption keyfile",
|
||||
"pbsClientMissing": "failed to create PBS encryption keyfile (is proxmox-backup-client installed?)",
|
||||
"borgRepository": "borg_repo is required",
|
||||
"borgEncryption": "borg_encrypt_mode must be one of: none, repokey, keyfile, authenticated",
|
||||
"encryption": "encrypt_mode must be one of: none, repokey, keyfile, authenticated",
|
||||
"borgPassphrase": "borg_passphrase is required when encryption is enabled (save it on the destination or pass one in the request)",
|
||||
"passphrase": "passphrase is required when encryption is enabled",
|
||||
"password": "password is required",
|
||||
"deviceConfirmation": "confirm_device must match device exactly (typo / safety check)",
|
||||
"invalidDevice": "invalid device path",
|
||||
"absoluteMountpoint": "absolute mountpoint required",
|
||||
"archiveLayout": "archive layout not recognized — no rootfs/ found",
|
||||
"archiveMissing": "archive not found",
|
||||
"archiveNotFile": "archive path is not a regular file",
|
||||
"invalidName": "invalid name",
|
||||
"invalidStaging": "invalid staging_path",
|
||||
"jobRename": "job name cannot be changed (delete + recreate to rename)",
|
||||
"absoluteKey": "key_path must be absolute",
|
||||
"mode": "mode must be \"local\" or \"ssh\"",
|
||||
"nameRequired": "name is required (letters, digits, _ or -)",
|
||||
"noLog": "no log available for this job yet",
|
||||
"noPbsConfig": "no manual PBS configs file",
|
||||
"outputMissing": "output file missing",
|
||||
"parserJson": "parser output was not valid JSON",
|
||||
"restoreJson": "run_restore output was not valid JSON",
|
||||
"repoRequired": "repo is required",
|
||||
"repoSnapshot": "repo_name and snapshot are required",
|
||||
"borgRepoSnapshot": "repo_name and snapshot are required for borg",
|
||||
"pbsRepoSnapshot": "repo_name and snapshot are required for pbs",
|
||||
"repoName": "repo_name is required",
|
||||
"restoreMissing": "restore tooling not installed on this host",
|
||||
"serverDatastore": "server and datastore are required",
|
||||
"snapshotRequired": "snapshot is required",
|
||||
"source": "source must be pbs, borg, or local",
|
||||
"sshPort": "ssh_port must be an integer between 1 and 65535",
|
||||
"sshRequired": "ssh_user, ssh_host and ssh_remote_path are required for ssh mode",
|
||||
"repositoryPath": "target has no repository path",
|
||||
"taskExpired": "task not found or expired",
|
||||
"invalidExpression": "invalid expression",
|
||||
"noReport": "no report emitted",
|
||||
"parseManifestFailed": "parse_manifest exited non-zero",
|
||||
"jobExists": "job already exists: {value}",
|
||||
"pathMissing": "path does not exist on this host: {value}",
|
||||
"deviceMissing": "device does not exist: {value}",
|
||||
"mountpoint": "not a mountpoint: {value}",
|
||||
"localArchiveMissing": "local archive not found: {value}",
|
||||
"borgDestination": "Borg destination '{value}' not found",
|
||||
"pbsDestination": "PBS destination '{value}' not found",
|
||||
"pbsPassword": "no password available for PBS repo '{value}'",
|
||||
"borgRepoMissing": "Borg repo \"{value}\" not configured",
|
||||
"pbsRepoMissing": "PBS repo \"{value}\" not configured",
|
||||
"borgTargetMissing": "Borg target not found: {value}",
|
||||
"pbsTargetMissing": "PBS repo not found: {value}",
|
||||
"pveJobMissing": "PVE vzdump job not found: {value}",
|
||||
"statePersist": "cannot persist state: {value}",
|
||||
"sshDirectory": "cannot prepare ssh dir: {value}",
|
||||
"logRead": "cannot read log: {value}",
|
||||
"envUpdate": "could not update .env: {value}",
|
||||
"pbsConfigWrite": "failed to persist PBS config: {value}",
|
||||
"persist": "failed to persist: {value}",
|
||||
"archiveRemove": "failed to remove archive: {value}",
|
||||
"start": "failed to start: {value}",
|
||||
"borgTargetsUpdate": "failed to update borg-targets.txt: {value}",
|
||||
"pbsTargetsUpdate": "failed to update pbs-manual-configs.txt: {value}",
|
||||
"unitsUpdate": "failed to update systemd units: {value}",
|
||||
"borgTargetsWrite": "failed to write borg-targets.txt: {value}",
|
||||
"jobWrite": "failed to write job files: {value}",
|
||||
"localTargetWrite": "failed to write local-target.conf: {value}",
|
||||
"unitsWrite": "failed to write systemd units: {value}",
|
||||
"format": "format failed: {value}",
|
||||
"calendar": "invalid on_calendar: {value}",
|
||||
"mount": "mount failed: {value}",
|
||||
"parserInvocation": "parser invocation failed: {value}",
|
||||
"preflightInvocation": "preflight invocation failed: {value}",
|
||||
"publicKeyMissing": "public key not found after generation: {value}",
|
||||
"retentionInteger": "retention.{value} must be an integer",
|
||||
"retentionNegative": "retention.{value} must be non-negative",
|
||||
"keygen": "ssh-keygen failed: {value}",
|
||||
"systemctl": "systemctl failed: {value}",
|
||||
"unmount": "umount failed: {value}",
|
||||
"taskIncomplete": "task is in state \"{value}\", not completed",
|
||||
"unknownKind": "unknown kind: {value}",
|
||||
"unknownMode": "unknown mode \"{value}\"",
|
||||
"defaultRemove": "{value} is the built-in default and cannot be removed",
|
||||
"defaultAdd": "{value} is the built-in default; no need to add it",
|
||||
"keyImport": "No PBS keyfile is installed at {value}. Import the keyfile first.",
|
||||
"storageMismatch": "PVE job storage mismatch (job uses {value}, payload says {other})"
|
||||
},
|
||||
"errors": {
|
||||
"backendNoStagingPath": "Backend did not return a staging path.",
|
||||
"deleteFailed": "Delete failed: {error}",
|
||||
|
||||
@@ -4178,7 +4178,7 @@
|
||||
"unmountTitle": "Odpojiť tento cieľ",
|
||||
"whereIsBorgRepo": "Kde je Borg repozitár?",
|
||||
"backupsKeptMessage": "Existujúce zálohy sa neodstránia.",
|
||||
"localAttachHelpMessage": "Najprv pripojte jednotky USB.Ak chcete použiť cestu k úložisku PVE + /dump, nechajte pole prázdne (záložný: /var/lib/vz/dump).",
|
||||
"localAttachHelpMessage": "Najprv pripoj USB disk. Ak pole necháš prázdne, použije sa cesta úložiska PVE doplnená o /dump; náhradná cesta je /var/lib/vz/dump.",
|
||||
"sshAuthorizedKeyHelp": "Pred pridaním do ~/.ssh/authorized_keys pre {user} na vzdialenom hostiteľovi skontrolujte riadok nižšie.",
|
||||
"sshKeyHelpMessage": "Cesta súkromného kľúča na tomto hostiteľovi.Použite {action}, potom skontrolujte a pridajte zobrazený riadok verejného kľúča do ~/.ssh/authorized_keys vzdialeného používateľa.",
|
||||
"sshKeySetupHelp": "Vytvorí kľúč na zadanej lokálnej ceste, ak žiadny neexistuje;inak prečíta svoj verejný kľúč.Na vzdialenom hostiteľovi nie je nainštalovaný žiadny kľúč.",
|
||||
@@ -4222,6 +4222,124 @@
|
||||
"usesSharedKeyfileAt": "Používa spoločný súbor s kľúčom v",
|
||||
"pbsHelp": "Monitor používa {keyfile} s {option} na šifrované zálohy a obnovy PBS.Súbor kľúča je uložený v režime {mode}.Uschovajte si bezpečnú kópiu mimo tohto hostiteľa."
|
||||
},
|
||||
"apiErrors": {
|
||||
"requestFailed": "Požiadavka zlyhala.",
|
||||
"absolutePath": "Cesta musí začínať znakom /.",
|
||||
"pathConfigured": "Táto cesta už je nastavená.",
|
||||
"pathRequired": "Zadaj cestu.",
|
||||
"pathQuery": "V požiadavke chýba cesta.",
|
||||
"pathNotFound": "Cesta sa nenašla.",
|
||||
"invalidJobId": "Neplatný identifikátor úlohy.",
|
||||
"jobNotFound": "Úloha sa nenašla.",
|
||||
"runnerMissing": "Na serveri chýba skript na spustenie zálohy.",
|
||||
"noRestore": "Nie je tu žiadny záznam obnovy na zatvorenie.",
|
||||
"invalidFile": "Neplatný názov súboru.",
|
||||
"notFound": "Požadovaný záznam sa nenašiel.",
|
||||
"logOutside": "Cesta k záznamu je mimo povoleného priečinka.",
|
||||
"targetsList": "Ciele musia byť zadané ako zoznam.",
|
||||
"emptyExpression": "Zadaj časový plán.",
|
||||
"invalidId": "V identifikátore použi iba písmená, číslice, _ alebo -.",
|
||||
"backend": "Vyber cieľ PBS, miestny archív alebo Borg.",
|
||||
"remoteBackend": "Vyber cieľ PBS alebo Borg.",
|
||||
"remoteBackendQuoted": "Vyber cieľ PBS alebo Borg.",
|
||||
"profileMode": "Vyber predvolený alebo vlastný profil.",
|
||||
"borgAttached": "Borg sa nedá pripojiť k úlohe PVE; vzdump zapisuje iba na PBS alebo miestne úložisko.",
|
||||
"borgAttachedLong": "Borg sa nedá pripojiť k úlohe PVE; vzdump zapisuje iba na PBS alebo miestne úložisko.",
|
||||
"parentRequired": "Vyber nadradenú úlohu PVE aj jej úložisko.",
|
||||
"absolutePaths": "Zadaj zoznam ciest, ktoré začínajú znakom /.",
|
||||
"customPath": "Do vlastného profilu pridaj aspoň jednu cestu.",
|
||||
"defaultPaths": "Cesty predvoleného profilu sa nepodarilo zistiť.",
|
||||
"retentionObject": "Pravidlá uchovávania záloh majú neplatný formát.",
|
||||
"pbsRepository": "Zadaj repozitár PBS.",
|
||||
"pbsEncryption": "Vyber režim šifrovania PBS: bez šifrovania, nový kľúč alebo existujúci kľúč.",
|
||||
"pbsKeyFailed": "Súbor so šifrovacím kľúčom PBS sa nepodarilo vytvoriť.",
|
||||
"pbsClientMissing": "Súbor so šifrovacím kľúčom PBS sa nepodarilo vytvoriť. Skontroluj, či je nainštalovaný proxmox-backup-client.",
|
||||
"borgRepository": "Zadaj repozitár Borg.",
|
||||
"borgEncryption": "Vyber podporovaný režim šifrovania Borg: none, repokey, keyfile alebo authenticated.",
|
||||
"encryption": "Vyber podporovaný režim šifrovania: none, repokey, keyfile alebo authenticated.",
|
||||
"borgPassphrase": "Šifrovaný repozitár Borg potrebuje heslo. Ulož ho pri cieli alebo ho zadaj v požiadavke.",
|
||||
"passphrase": "Pri zapnutom šifrovaní zadaj heslo.",
|
||||
"password": "Zadaj heslo.",
|
||||
"deviceConfirmation": "Na potvrdenie zadaj presný názov zariadenia.",
|
||||
"invalidDevice": "Neplatná cesta k zariadeniu.",
|
||||
"absoluteMountpoint": "Prípojný bod musí byť cesta začínajúca znakom /.",
|
||||
"archiveLayout": "Archív nemá očakávanú štruktúru: chýba priečinok rootfs/.",
|
||||
"archiveMissing": "Archív sa nenašiel.",
|
||||
"archiveNotFile": "Cesta k archívu neoznačuje bežný súbor.",
|
||||
"invalidName": "Neplatný názov.",
|
||||
"invalidStaging": "Neplatná cesta k dočasným súborom.",
|
||||
"jobRename": "Názov existujúcej úlohy sa nedá zmeniť. Ak ju chceš premenovať, odstráň ju a vytvor novú.",
|
||||
"absoluteKey": "Cesta k súboru s kľúčom musí začínať znakom /.",
|
||||
"mode": "Vyber miestny cieľ alebo pripojenie cez SSH.",
|
||||
"nameRequired": "Zadaj názov; použi iba písmená, číslice, _ alebo -.",
|
||||
"noLog": "Pre túto úlohu zatiaľ nie je dostupný záznam.",
|
||||
"noPbsConfig": "Súbor s ručne nastavenými cieľmi PBS sa nenašiel.",
|
||||
"outputMissing": "Chýba výstupný súbor.",
|
||||
"parserJson": "Výstup spracovania archívu nemá platný formát JSON.",
|
||||
"restoreJson": "Výstup prípravy obnovy nemá platný formát JSON.",
|
||||
"repoRequired": "Zadaj repozitár.",
|
||||
"repoSnapshot": "Vyber repozitár aj zálohu.",
|
||||
"borgRepoSnapshot": "Pre Borg vyber repozitár aj zálohu.",
|
||||
"pbsRepoSnapshot": "Pre PBS vyber repozitár aj zálohu.",
|
||||
"repoName": "Zadaj názov repozitára.",
|
||||
"restoreMissing": "Na tomto serveri chýbajú nástroje na obnovu.",
|
||||
"serverDatastore": "Zadaj server aj dátové úložisko.",
|
||||
"snapshotRequired": "Vyber zálohu.",
|
||||
"source": "Vyber zdroj PBS, Borg alebo miestny archív.",
|
||||
"sshPort": "Port SSH musí byť celé číslo od 1 do 65535.",
|
||||
"sshRequired": "Pre SSH zadaj používateľa, hostiteľa a vzdialenú cestu.",
|
||||
"repositoryPath": "Cieľ nemá nastavenú cestu k repozitáru.",
|
||||
"taskExpired": "Úloha sa nenašla alebo už vypršala.",
|
||||
"invalidExpression": "Neplatný časový plán.",
|
||||
"noReport": "Kontrola nevrátila žiadnu správu.",
|
||||
"parseManifestFailed": "Spracovanie manifestu skončilo chybou.",
|
||||
"jobExists": "Úloha s názvom {value} už existuje.",
|
||||
"pathMissing": "Na tomto serveri sa nenašla cesta: {value}",
|
||||
"deviceMissing": "Zariadenie neexistuje: {value}",
|
||||
"mountpoint": "Táto cesta nie je prípojný bod: {value}",
|
||||
"localArchiveMissing": "Miestny archív sa nenašiel: {value}",
|
||||
"borgDestination": "Cieľ Borg „{value}“ sa nenašiel.",
|
||||
"pbsDestination": "Cieľ PBS „{value}“ sa nenašiel.",
|
||||
"pbsPassword": "Pre repozitár PBS „{value}“ nie je dostupné heslo.",
|
||||
"borgRepoMissing": "Repozitár Borg „{value}“ nie je nastavený.",
|
||||
"pbsRepoMissing": "Repozitár PBS „{value}“ nie je nastavený.",
|
||||
"borgTargetMissing": "Cieľ Borg sa nenašiel: {value}",
|
||||
"pbsTargetMissing": "Repozitár PBS sa nenašiel: {value}",
|
||||
"pveJobMissing": "Úloha zálohovania PVE sa nenašla: {value}",
|
||||
"statePersist": "Stav sa nepodarilo uložiť: {value}",
|
||||
"sshDirectory": "Priečinok SSH sa nepodarilo pripraviť: {value}",
|
||||
"logRead": "Záznam sa nepodarilo prečítať: {value}",
|
||||
"envUpdate": "Súbor .env sa nepodarilo aktualizovať: {value}",
|
||||
"pbsConfigWrite": "Nastavenia PBS sa nepodarilo uložiť: {value}",
|
||||
"persist": "Údaje sa nepodarilo uložiť: {value}",
|
||||
"archiveRemove": "Archív sa nepodarilo odstrániť: {value}",
|
||||
"start": "Úlohu sa nepodarilo spustiť: {value}",
|
||||
"borgTargetsUpdate": "Súbor borg-targets.txt sa nepodarilo aktualizovať: {value}",
|
||||
"pbsTargetsUpdate": "Súbor pbs-manual-configs.txt sa nepodarilo aktualizovať: {value}",
|
||||
"unitsUpdate": "Jednotky systemd sa nepodarilo aktualizovať: {value}",
|
||||
"borgTargetsWrite": "Súbor borg-targets.txt sa nepodarilo uložiť: {value}",
|
||||
"jobWrite": "Súbory úlohy sa nepodarilo uložiť: {value}",
|
||||
"localTargetWrite": "Súbor local-target.conf sa nepodarilo uložiť: {value}",
|
||||
"unitsWrite": "Jednotky systemd sa nepodarilo uložiť: {value}",
|
||||
"format": "Formátovanie zlyhalo: {value}",
|
||||
"calendar": "Neplatný časový plán: {value}",
|
||||
"mount": "Disk sa nepodarilo pripojiť: {value}",
|
||||
"parserInvocation": "Spracovanie archívu sa nepodarilo spustiť: {value}",
|
||||
"preflightInvocation": "Kontrolu pred obnovou sa nepodarilo spustiť: {value}",
|
||||
"publicKeyMissing": "Po vytvorení kľúča sa nenašiel verejný kľúč: {value}",
|
||||
"retentionInteger": "Pravidlo uchovávania „{value}“ musí byť celé číslo.",
|
||||
"retentionNegative": "Pravidlo uchovávania „{value}“ nesmie byť záporné.",
|
||||
"keygen": "Vytvorenie kľúča SSH zlyhalo: {value}",
|
||||
"systemctl": "Správa služby cez systemctl zlyhala: {value}",
|
||||
"unmount": "Disk sa nepodarilo odpojiť: {value}",
|
||||
"taskIncomplete": "Úloha ešte nie je dokončená. Jej stav: {value}",
|
||||
"unknownKind": "Neznámy typ: {value}",
|
||||
"unknownMode": "Neznámy režim: {value}",
|
||||
"defaultRemove": "Cesta {value} je predvolená a nedá sa odstrániť.",
|
||||
"defaultAdd": "Cesta {value} je predvolená; nemusíš ju pridávať.",
|
||||
"keyImport": "V {value} nie je uložený súbor s kľúčom PBS. Najprv ho importuj.",
|
||||
"storageMismatch": "Úložisko sa nezhoduje: úloha PVE používa {value}, požiadavka uvádza {other}."
|
||||
},
|
||||
"errors": {
|
||||
"backendNoStagingPath": "Cieľ nevrátil cestu k dočasným súborom.",
|
||||
"deleteFailed": "Mazanie zlyhalo: {error}",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Exercise the shipped error adapter and actual HostBackup handlers with a fake transport.
|
||||
const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
|
||||
const { ts, read, compile, translator } = require('./backup_i18n_seam.cjs');
|
||||
const library = { exports: {} };
|
||||
vm.runInNewContext(compile(read('lib/host-backup-messages.ts')), library);
|
||||
const { hostBackupErrorText } = library.exports;
|
||||
// Independently read the real API's literal/f-string contracts without importing
|
||||
// Flask or running any route, host probe, script or persistence operation.
|
||||
const contracts = JSON.parse(require('node:child_process').execFileSync('python3', ['-I', '-B', '-c', `
|
||||
import ast,json,sys
|
||||
tree=ast.parse(open(sys.argv[1]).read())
|
||||
def messages(node):
|
||||
if isinstance(node,ast.Constant) and isinstance(node.value,str): return [node.value]
|
||||
if isinstance(node,ast.JoinedStr): return [''.join(part.value if isinstance(part,ast.Constant) else 'PARAMETER' for part in node.values)]
|
||||
if isinstance(node,ast.BoolOp): return [text for child in node.values for text in messages(child)]
|
||||
return []
|
||||
result=set()
|
||||
for function in tree.body:
|
||||
if isinstance(function,ast.FunctionDef) and function.name.startswith(('api_host_backups_','api_host_restore')):
|
||||
for node in ast.walk(function):
|
||||
if isinstance(node,ast.Dict):
|
||||
for key,value in zip(node.keys,node.values):
|
||||
if isinstance(key,ast.Constant) and key.value=='error': result.update(messages(value))
|
||||
print(json.dumps(sorted(result)))
|
||||
`, 'AppImage/scripts/flask_server.py'], { encoding: 'utf8' }));
|
||||
const flatten = n => !n || typeof n === 'boolean' ? [] : Array.isArray(n) ? n.flatMap(flatten)
|
||||
: typeof n === 'object' ? [n, ...flatten(n.props?.children)] : [n];
|
||||
const text = n => flatten(n).filter(n => typeof n === 'string' || typeof n === 'number').join('');
|
||||
|
||||
function fixture(locale) {
|
||||
let cursor = 0, slots = [], tree, failure = 'path must be absolute', mutations = 0;
|
||||
const calls = [], t = translator(locale);
|
||||
const jsx = (type, props) => typeof type === 'function' ? type(props) : { type, props };
|
||||
const hooks = { useState(initial) { const i = cursor++; if (!(i in slots)) slots[i] = initial; return [slots[i], value => { slots[i] = value; }]; } };
|
||||
const requireModule = name => {
|
||||
if (name === 'react') return hooks;
|
||||
if (name === 'react/jsx-runtime') return { jsx, jsxs: jsx };
|
||||
if (name === 'swr') return { default: () => ({ data: { paths: [{ path: '/kept' }] }, mutate: () => { mutations++; } }) };
|
||||
if (name.includes('i18n/provider')) return { useT: () => t };
|
||||
if (name.endsWith('host-backup-messages')) return library.exports;
|
||||
if (name.endsWith('api-config')) return { fetchApi: async (url, options) => { calls.push({ url, ...options }); if (failure) throw new Error(failure); return { status: 'ok' }; } };
|
||||
return new Proxy({}, { get: (_, key) => key });
|
||||
};
|
||||
const source = read('components/host-backup.tsx') + '\nexports.ExtraPathsSection = ExtraPathsSection;';
|
||||
const module = { exports: {} };
|
||||
vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, jsx: ts.JsxEmit.ReactJSX } }).outputText,
|
||||
{ exports: module.exports, require: requireModule, Error, console });
|
||||
return { calls, t, render() { cursor = 0; tree = module.exports.ExtraPathsSection(); return tree; }, text: () => text(tree),
|
||||
input: () => flatten(tree).find(n => n.type === 'Input'),
|
||||
add: () => flatten(tree).find(n => n.type === 'Button' && text(n).includes(t('backup.actions.addPath'))),
|
||||
fail: message => { failure = message; }, mutations: () => mutations };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
assert.ok(contracts.length > 100, 'real HostBackup error contracts discovered');
|
||||
for (const message of contracts) {
|
||||
const translated = hostBackupErrorText(message, translator('sk'));
|
||||
assert.notEqual(translated, message, 'application-owned message must be localized: ' + message);
|
||||
assert.ok(!translated.includes('backup.apiErrors.'), 'every mapped key has a real Slovak value');
|
||||
assert.equal(hostBackupErrorText(message, translator('missing')), hostBackupErrorText(message, translator('en')), 'missing locale uses English');
|
||||
}
|
||||
for (const locale of ['en', 'sk', 'missing']) {
|
||||
const t = translator(locale);
|
||||
assert.equal(hostBackupErrorText(new Error('path must be absolute'), t), t('backup.apiErrors.absolutePath'));
|
||||
assert.equal(hostBackupErrorText({ message: 'this path is already configured' }, t), t('backup.apiErrors.pathConfigured'));
|
||||
assert.equal(hostBackupErrorText('job already exists: test_1', t), t('backup.apiErrors.jobExists', { value: 'test_1' }));
|
||||
const raw = 'tool failed: <img src=x onerror=unsafe()> path must be absolute';
|
||||
assert.equal(hostBackupErrorText(raw, t), raw, 'external diagnostics must not be partially matched or discarded');
|
||||
const long = 'path does not exist on this host: ' + 'x'.repeat(20000);
|
||||
assert.equal(hostBackupErrorText(long, t), long, 'large diagnostics preserved without pattern matching');
|
||||
for (const unknown of ['constructor', '__proto__', 'toString', 'future backend message']) assert.equal(hostBackupErrorText(unknown, t), unknown);
|
||||
for (const invalid of [null, undefined, {}, 23]) assert.equal(hostBackupErrorText(invalid, t), t('backup.apiErrors.requestFailed'));
|
||||
const unsafe = '<img src=x onerror=unsafe()>{backup.actions.run}';
|
||||
assert.equal(hostBackupErrorText('path does not exist on this host: ' + unsafe, t), t('backup.apiErrors.pathMissing', { value: unsafe }), 'captured paths remain literal data');
|
||||
const f = fixture(locale); f.render();
|
||||
assert.equal(f.add().props.disabled, true);
|
||||
await f.add().props.onClick(); assert.equal(f.calls.length, 0, 'blank path causes no request');
|
||||
f.input().props.onChange({ target: { value: 'relative-path' } }); f.render(); await f.add().props.onClick(); f.render();
|
||||
assert.equal(f.calls.length, 1); assert.equal(f.calls[0].url, '/api/host-backups/extra-paths');
|
||||
assert.equal(f.calls[0].method, 'POST'); assert.deepEqual(JSON.parse(f.calls[0].body), { path: 'relative-path' });
|
||||
assert.equal(f.input().props.value, 'relative-path'); assert.equal(f.mutations(), 0, 'rejected input preserves state and does not refresh as success');
|
||||
assert.ok(f.text().includes(t('backup.apiErrors.absolutePath'))); assert.ok(f.text().includes('/kept'));
|
||||
f.fail('future backend message'); await f.add().props.onClick(); f.render(); assert.ok(f.text().includes('future backend message'));
|
||||
f.fail(null); await f.add().props.onClick(); f.render(); assert.equal(f.input().props.value, ''); assert.equal(f.mutations(), 1);
|
||||
}
|
||||
// Adapter has no effect on the server's CLI/API/persistence contracts.
|
||||
const host = read('components/host-backup.tsx');
|
||||
const ast = ts.createSourceFile('host-backup.tsx', host, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX), encryptionCatches = [];
|
||||
function visit(node) { if (ts.isCatchClause(node) && node.block.getText(ast).includes('err.body?.tool_output')) encryptionCatches.push(node.block); ts.forEachChild(node, visit); }
|
||||
visit(ast); assert.equal(encryptionCatches.length, 2, 'scheduled and manual backup encryption paths both checked');
|
||||
for (const locale of ['en', 'sk', 'missing']) for (const block of encryptionCatches) {
|
||||
let displayed, effects = 0;
|
||||
const code = ts.transpileModule('function run(e) '+block.getText(ast)+'; run(error);', { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
|
||||
const t = translator(locale), error = Object.assign(new Error('failed to create PBS encryption keyfile'), { body: { tool_output: 'EXTERNAL TOOL DIAGNOSTIC' } });
|
||||
vm.runInNewContext(code, { error, t, hostBackupErrorText, setError: value => { displayed = value; }, setPbsImportBusy: () => {}, setSubmitting: () => {}, onCreated: () => effects++ });
|
||||
assert.ok(displayed.includes(hostBackupErrorText(error, t)) && displayed.includes('EXTERNAL TOOL DIAGNOSTIC'));
|
||||
assert.ok(!displayed.includes('{error}') && !displayed.includes('{detail}'), 'error interpolation must not leave an unresolved placeholder');
|
||||
assert.equal(effects, 0);
|
||||
}
|
||||
assert.ok(!host.includes('e instanceof Error ? e.message : String(e)'), 'all HostBackup catch consumers use the adapter');
|
||||
assert.ok(!host.includes('{e.error}') && !host.includes('{calendarPreview.error}'), 'structured error consumers use the adapter too');
|
||||
console.log('PASS: '+contracts.length+' real API contracts, HostBackup actual extra-path handlers, EN/SK/fallback, 0 calls for blank, rejected input/state, success, literal parameters and unknown diagnostics');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user