mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-06 15:56:23 +00:00
fix(monitor): localize dynamic labels and generated status text
Localize runtime labels for SMART/NVMe details, backup target badges, firewall interface types, settings units, and generated health status copy while keeping the English and Slovak catalogs in key parity.
This commit is contained in:
@@ -334,6 +334,8 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
||||
if (match) return t("healthStatus.details.gatewayLatency", { latency: match[1] })
|
||||
match = value.match(/^(\d+) failed login attempts in 24h$/)
|
||||
if (match) return t("healthStatus.details.failedLogins", { count: match[1] })
|
||||
match = value.match(/^(\d+) IP\(s\) currently banned by Fail2Ban \(jails: (.+)\)$/)
|
||||
if (match) return t("healthStatus.details.fail2banBannedIps", { count: match[1], jails: match[2] })
|
||||
match = value.match(/^Uptime (\d+) days?$/)
|
||||
if (match) return t("healthStatus.details.uptimeDays", { count: match[1] })
|
||||
match = value.match(/^(\d+) package\(s\) pending$/)
|
||||
|
||||
@@ -51,7 +51,7 @@ import { fetchApi, getApiUrl } from "../lib/api-config"
|
||||
import { fetchTerminalTicket } from "../lib/terminal-ws"
|
||||
import { formatStorage, formatBytes } from "../lib/utils"
|
||||
import { getStorageUsageColor } from "../lib/storage-usage-color"
|
||||
import { useT } from "../lib/i18n/provider"
|
||||
import { useI18n, useT } from "../lib/i18n/provider"
|
||||
|
||||
type TFunction = (key: string, params?: Record<string, string | number>) => string
|
||||
|
||||
@@ -380,6 +380,23 @@ const methodBadgeCls = (m: string | undefined): string => {
|
||||
}
|
||||
}
|
||||
|
||||
const localizedBackendLabel = (source: string, t: (key: string) => string): string =>
|
||||
source === "pbs" ? "PBS" : source === "borg" ? "Borg" : t("backup.backends.local")
|
||||
|
||||
const formatCalendarPreview = (value: string, language: string, t: (key: string) => string): string => {
|
||||
if (language !== "sk") return value
|
||||
const match = value.match(/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(\d{4})-(\d{2})-(\d{2})\s+(.+)$/i)
|
||||
if (!match) return value
|
||||
const [, weekday, year, month, day, rest] = match
|
||||
return `${t(`backup.weekdays.short.${weekday.toLowerCase()}`)} ${Number(day)}. ${Number(month)}. ${year} ${rest}`
|
||||
}
|
||||
|
||||
const formatCalendarDistance = (value: string, language: string): string => {
|
||||
if (language !== "sk") return value
|
||||
const distance = value.replace(/\s+left$/i, "").replace(/\bdays?\b/gi, "d")
|
||||
return `zostáva ${distance}`
|
||||
}
|
||||
|
||||
const formatRunAt = (iso: string | null) => {
|
||||
if (!iso) return null
|
||||
try {
|
||||
@@ -1314,7 +1331,7 @@ export function HostBackup() {
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5 flex items-center gap-3 flex-wrap">
|
||||
<span className={`uppercase tracking-wide text-[10px] px-1.5 py-0.5 rounded border ${sourceBadgeCls}`}>
|
||||
{u.source}
|
||||
{localizedBackendLabel(u.source, t)}
|
||||
</span>
|
||||
{u.remote?.encrypted && (
|
||||
<span
|
||||
@@ -1981,7 +1998,7 @@ function InspectModal({
|
||||
? "text-fuchsia-400 border-fuchsia-500/40 bg-fuchsia-500/10"
|
||||
: "text-blue-400 border-blue-500/40 bg-blue-500/10"
|
||||
}`}>
|
||||
{archive.source}
|
||||
{localizedBackendLabel(archive.source, t)}
|
||||
</Badge>
|
||||
)}
|
||||
{remoteArc?.encrypted && (
|
||||
@@ -2016,9 +2033,9 @@ function InspectModal({
|
||||
<div><span className="text-muted-foreground">{t("backup.fields.sizeLabel")}</span> {formatBytes(localArc.size_bytes)}</div>
|
||||
<div className="sm:col-span-2"><span className="text-muted-foreground">{t("backup.fields.pathLabel")}</span> <code className="font-mono break-all">{localArc.path}</code></div>
|
||||
{localArc.job_id && <div><span className="text-muted-foreground">{t("backup.fields.jobIdLabel")}</span> <code className="font-mono">{localArc.job_id}</code></div>}
|
||||
{localArc.profile && <div><span className="text-muted-foreground">{t("backup.fields.profileLabel")}</span> <code className="font-mono">{localArc.profile}</code></div>}
|
||||
{localArc.profile && <div><span className="text-muted-foreground">{t("backup.fields.profileLabel")}</span> <code className="font-mono">{localArc.profile === "default" ? t("backup.profile.default") : localArc.profile === "custom" ? t("backup.profile.custom") : localArc.profile}</code></div>}
|
||||
{localArc.source_hostname && <div><span className="text-muted-foreground">{t("backup.fields.sourceHostLabel")}</span> <code className="font-mono">{localArc.source_hostname}</code></div>}
|
||||
<div><span className="text-muted-foreground">{t("backup.fields.detectedViaLabel")}</span> <code className="font-mono text-[10px]">{localArc.detected_via}</code></div>
|
||||
<div><span className="text-muted-foreground">{t("backup.fields.detectedViaLabel")}</span> <code className="font-mono text-[10px]">{localArc.detected_via === "sidecar" ? t("backup.archives.companionFile") : localArc.detected_via}</code></div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -2779,7 +2796,7 @@ function CreateJobDialog({
|
||||
onCreated: () => void
|
||||
editingJobId?: string | null
|
||||
}) {
|
||||
const t = useT()
|
||||
const { t, language } = useI18n()
|
||||
const isEdit = !!editingJobId
|
||||
const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1)
|
||||
const [jobId, setJobId] = useState("")
|
||||
@@ -3343,7 +3360,7 @@ function CreateJobDialog({
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{isEdit
|
||||
? t("backup.jobs.jobNameLocked")
|
||||
: <>{t("backup.jobs.jobNameHelpBefore")} <code className="font-mono">_</code> {t("backup.jobs.jobNameHelpAnd")} <code className="font-mono">-</code> {t("backup.jobs.jobNameHelpAfter")}</>}
|
||||
: <>{t("backup.jobs.jobNameHelpBefore")} <code className="font-mono">_</code> {t("backup.jobs.jobNameHelpAnd")} <code className="font-mono">-</code>. {t("backup.jobs.jobNameHelpAfter")}</>}
|
||||
</p>
|
||||
{!idValid && jobId.length > 0 && !isEdit && (
|
||||
<p className="text-xs text-red-500 mt-1">{t("backup.jobs.invalidJobName")}</p>
|
||||
@@ -3654,9 +3671,9 @@ function CreateJobDialog({
|
||||
{calendarPreview.next_elapse && (
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-muted-foreground">{t("backup.jobs.nextRunLabel")}</span>
|
||||
<span className="text-emerald-400">{calendarPreview.next_elapse}</span>
|
||||
<span className="text-emerald-400">{formatCalendarPreview(calendarPreview.next_elapse, language, t)}</span>
|
||||
{calendarPreview.from_now && (
|
||||
<span className="text-muted-foreground">({calendarPreview.from_now})</span>
|
||||
<span className="text-muted-foreground">({formatCalendarDistance(calendarPreview.from_now, language)})</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -5774,7 +5791,7 @@ function DestinationRow({
|
||||
<Icon className={`h-5 w-5 flex-shrink-0 ${iconColor} mt-0.5`} />
|
||||
<h3 className="font-mono font-semibold text-sm break-all">{headline}</h3>
|
||||
<Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${accent}`}>
|
||||
{item.kind}
|
||||
{localizedBackendLabel(item.kind, t)}
|
||||
</Badge>
|
||||
{item.kind === "local" && item.source === "default" && (
|
||||
<Badge variant="outline" className="text-[10px] uppercase tracking-wide border-border text-muted-foreground">
|
||||
@@ -5802,7 +5819,7 @@ function DestinationRow({
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px] uppercase tracking-wide border-amber-500/40 text-amber-400 bg-amber-500/5">
|
||||
local
|
||||
{t("backup.backends.local")}
|
||||
</Badge>
|
||||
)}
|
||||
{/* Encryption indicator — icon-only chip for encrypted
|
||||
|
||||
@@ -68,6 +68,10 @@ function validatePasswordStrength(pw: string, t: (key: string) => string): strin
|
||||
export function Security() {
|
||||
const { language, t } = useI18n()
|
||||
const st = (key: string, params?: Record<string, string | number>) => t(`securityPage.${key}`, params)
|
||||
const interfaceTypeLabel = (type: string) =>
|
||||
["physical", "bridge", "bond", "vlan", "virtual"].includes(type)
|
||||
? t(`network.interfaceTypes.${type}`)
|
||||
: type
|
||||
const authErrorText = (message: unknown, fallbackKey: string) => {
|
||||
const raw = typeof message === "string" ? message : ""
|
||||
const normalized = raw.toLowerCase()
|
||||
@@ -2927,7 +2931,7 @@ ${(report.sections && report.sections.length > 0) ? `
|
||||
<option value="">{st("firewall.anyInterface")}</option>
|
||||
{networkInterfaces.map((iface) => (
|
||||
<option key={iface.name} value={iface.name}>
|
||||
{iface.name} ({iface.type}{iface.status === "up" ? `, ${st("values.up")}` : `, ${st("values.down")}`})
|
||||
{iface.name} ({interfaceTypeLabel(iface.type)}{iface.status === "up" ? `, ${st("values.up")}` : `, ${st("values.down")}`})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -3113,7 +3117,7 @@ ${(report.sections && report.sections.length > 0) ? `
|
||||
<option value="">{st("firewall.any")}</option>
|
||||
{networkInterfaces.map((iface) => (
|
||||
<option key={iface.name} value={iface.name}>
|
||||
{iface.name} ({iface.type})
|
||||
{iface.name} ({interfaceTypeLabel(iface.type)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -313,6 +313,8 @@ export function Settings() {
|
||||
const translated = t(key)
|
||||
return translated === key ? fallback : translated
|
||||
}
|
||||
const interfaceTypeLabel = (type: string) =>
|
||||
tFallback(`network.interfaceTypes.${type.toLowerCase()}`, type)
|
||||
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
||||
const [updatesAvailableCount, setUpdatesAvailableCount] = useState(0)
|
||||
const [loadingTools, setLoadingTools] = useState(true)
|
||||
@@ -1005,8 +1007,8 @@ export function Settings() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Bytes">Bytes</SelectItem>
|
||||
<SelectItem value="Bits">Bits</SelectItem>
|
||||
<SelectItem value="Bytes">{t("settings.networkUnits.bytes")}</SelectItem>
|
||||
<SelectItem value="Bits">{t("settings.networkUnits.bits")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -1263,7 +1265,7 @@ export function Settings() {
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5">
|
||||
<span>{t("settings.healthMonitor.labels.category")}: <span className="font-medium text-foreground/80">{s.category ? tFallback(`settings.healthMonitor.categories.${s.category}`, s.category) : "—"}</span></span>
|
||||
{s.severity && <span>{t("settings.healthMonitor.labels.severity")}: <span className="font-medium text-foreground/80">{tFallback(`status.${s.severity}`, s.severity)}</span></span>}
|
||||
{s.severity && <span>{t("settings.healthMonitor.labels.severity")}: <span className="font-medium text-foreground/80">{tFallback(`status.${s.severity.toLowerCase()}`, s.severity)}</span></span>}
|
||||
{dismissedAtLabel && <span>{t("settings.healthMonitor.labels.dismissed")}: {dismissedAtLabel}</span>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1467,7 +1469,7 @@ export function Settings() {
|
||||
{iface.name}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||
{iface.type}
|
||||
{interfaceTypeLabel(iface.type)}
|
||||
</Badge>
|
||||
{isDown && !isExcluded && (
|
||||
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
|
||||
@@ -1569,9 +1571,9 @@ export function Settings() {
|
||||
<CardTitle>{t("settings.snippets.title")}</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
{t("settings.snippets.description")}
|
||||
{t("settings.snippets.description")}{" "}
|
||||
<code className="mx-1">local</code>
|
||||
{t("settings.snippets.localNote")}
|
||||
{" "}{t("settings.snippets.localNote")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -20,6 +20,45 @@ import {
|
||||
type DiskTempMap,
|
||||
} from "../lib/health-thresholds"
|
||||
|
||||
// Raw smartctl names are shared by the compact SMART tab and the full
|
||||
// report. Keep one canonical mapping so both views use the same labels
|
||||
// and explanations instead of drifting into separate translations.
|
||||
const NVME_SMART_ATTRIBUTE_KEYS: Record<string, string> = {
|
||||
"Critical Warning": "criticalWarning",
|
||||
"Temperature": "nvmeTemperature",
|
||||
"Temperature Sensor 1": "temperatureSensor1",
|
||||
"Temperature Sensor 2": "temperatureSensor2",
|
||||
"Temperature Sensor 3": "temperatureSensor3",
|
||||
"Available Spare": "availableSpare",
|
||||
"Available Spare Threshold": "availableSpareThreshold",
|
||||
"Percentage Used": "percentageUsed",
|
||||
"Percent Used": "percentageUsed",
|
||||
"Endurance Group Warning": "enduranceGroupWarning",
|
||||
"Media Errors": "mediaErrors",
|
||||
"Media and Data Integrity Errors": "mediaIntegrityErrors",
|
||||
"Unsafe Shutdowns": "unsafeShutdowns",
|
||||
"Power Cycles": "nvmePowerCycles",
|
||||
"Power On Hours": "nvmePowerOnHours",
|
||||
"Data Units Read": "dataUnitsRead",
|
||||
"Data Units Written": "dataUnitsWritten",
|
||||
"Host Read Commands": "hostReadCommands",
|
||||
"Host Write Commands": "hostWriteCommands",
|
||||
"Controller Busy Time": "controllerBusyTime",
|
||||
"Error Log Entries": "errorLogEntries",
|
||||
"Error Information Log Entries": "errorLogEntries",
|
||||
"Warning Temp Time": "warningTempTime",
|
||||
"Critical Temp Time": "criticalTempTime",
|
||||
"Warning Composite Temperature Time": "warningCompositeTemperatureTime",
|
||||
"Critical Composite Temperature Time": "criticalCompositeTemperatureTime",
|
||||
"Thermal Management T1 Trans Count": "thermalManagementT1TransCount",
|
||||
"Thermal Management T2 Trans Count": "thermalManagementT2TransCount",
|
||||
"Thermal Management T1 Total Time": "thermalManagementT1TotalTime",
|
||||
"Thermal Management T2 Total Time": "thermalManagementT2TotalTime",
|
||||
}
|
||||
|
||||
const getNvmeSmartAttributeKey = (name: string): string | undefined =>
|
||||
NVME_SMART_ATTRIBUTE_KEYS[name.replace(/_/g, " ")] || NVME_SMART_ATTRIBUTE_KEYS[name]
|
||||
|
||||
interface DiskInfo {
|
||||
name: string
|
||||
size?: number // Changed from string to number (KB) for formatMemory()
|
||||
@@ -2359,38 +2398,6 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
||||
// Build attributes table - format differs for NVMe vs SATA
|
||||
const isNvmeForTable = diskType === 'NVMe'
|
||||
|
||||
const nvmeExplanationKeys: Record<string, string> = {
|
||||
'Critical Warning': 'criticalWarning',
|
||||
'Temperature': 'nvmeTemperature',
|
||||
'Temperature Sensor 1': 'temperatureSensor1',
|
||||
'Temperature Sensor 2': 'temperatureSensor2',
|
||||
'Temperature Sensor 3': 'temperatureSensor3',
|
||||
'Available Spare': 'availableSpare',
|
||||
'Available Spare Threshold': 'availableSpareThreshold',
|
||||
'Percentage Used': 'percentageUsed',
|
||||
'Percent Used': 'percentageUsed',
|
||||
'Media Errors': 'mediaErrors',
|
||||
'Media and Data Integrity Errors': 'mediaIntegrityErrors',
|
||||
'Unsafe Shutdowns': 'unsafeShutdowns',
|
||||
'Power Cycles': 'nvmePowerCycles',
|
||||
'Power On Hours': 'nvmePowerOnHours',
|
||||
'Data Units Read': 'dataUnitsRead',
|
||||
'Data Units Written': 'dataUnitsWritten',
|
||||
'Host Read Commands': 'hostReadCommands',
|
||||
'Host Write Commands': 'hostWriteCommands',
|
||||
'Controller Busy Time': 'controllerBusyTime',
|
||||
'Error Log Entries': 'errorLogEntries',
|
||||
'Error Information Log Entries': 'errorLogEntries',
|
||||
'Warning Temp Time': 'warningTempTime',
|
||||
'Critical Temp Time': 'criticalTempTime',
|
||||
'Warning Composite Temperature Time': 'warningCompositeTemperatureTime',
|
||||
'Critical Composite Temperature Time': 'criticalCompositeTemperatureTime',
|
||||
'Thermal Management T1 Trans Count': 'thermalManagementT1TransCount',
|
||||
'Thermal Management T2 Trans Count': 'thermalManagementT2TransCount',
|
||||
'Thermal Management T1 Total Time': 'thermalManagementT1TotalTime',
|
||||
'Thermal Management T2 Total Time': 'thermalManagementT2TotalTime',
|
||||
}
|
||||
|
||||
const sataExplanationKeys: Record<string, string> = {
|
||||
'Raw Read Error Rate': 'rawReadErrorRate',
|
||||
'Write Error Rate': 'writeErrorRate',
|
||||
@@ -2518,7 +2525,7 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
||||
const cleanName = name.replace(/_/g, ' ')
|
||||
const keyPrefix = 'storage.smartReport.attributeExplanations.'
|
||||
if (diskKind === 'NVMe') {
|
||||
const key = nvmeExplanationKeys[cleanName] || nvmeExplanationKeys[name]
|
||||
const key = getNvmeSmartAttributeKey(cleanName)
|
||||
return key ? t(`${keyPrefix}${key}`) : ''
|
||||
}
|
||||
if (diskKind === 'SAS') {
|
||||
@@ -2529,6 +2536,12 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
||||
return key ? t(`${keyPrefix}${key}`) : ''
|
||||
}
|
||||
|
||||
const getAttrLabel = (name: string, diskKind: string): string => {
|
||||
if (diskKind !== 'NVMe') return name.replace(/_/g, ' ')
|
||||
const key = getNvmeSmartAttributeKey(name)
|
||||
return key ? t(`storage.smartReport.attributeLabels.${key}`) : name.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
const attrStatusText = (status?: string) => {
|
||||
const s = (status || '').toLowerCase()
|
||||
if (s === 'ok') return tSmart("statusValues.ok")
|
||||
@@ -2615,7 +2628,7 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
||||
const displayValue = isSasDisk ? attr.raw_value : attr.value
|
||||
return `
|
||||
<tr>
|
||||
<td class="col-name" style="font-weight:500;${explanation ? 'border-bottom:none;padding-bottom:2px;' : ''}">${attr.name}</td>
|
||||
<td class="col-name" style="font-weight:500;${explanation ? 'border-bottom:none;padding-bottom:2px;' : ''}">${getAttrLabel(attr.name, diskType)}</td>
|
||||
<td style="text-align:center;font-family:monospace;${explanation ? 'border-bottom:none;' : ''}">${displayValue}</td>
|
||||
<td style="${explanation ? 'border-bottom:none;' : ''}"><span class="f-tag" style="background:${statusBg};color:${statusColor}">${attrStatusText(attr.status)}</span></td>
|
||||
</tr>
|
||||
@@ -3813,6 +3826,11 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
|
||||
|
||||
// Extract SMART attributes from testStatus for the report
|
||||
const smartAttributes = testStatus.smart_data?.attributes || []
|
||||
const smartAttributeLabel = (name: string): string => {
|
||||
if (!disk.name.startsWith("nvme")) return name.replace(/_/g, " ")
|
||||
const key = getNvmeSmartAttributeKey(name)
|
||||
return key ? t(`storage.smartReport.attributeLabels.${key}`) : name.replace(/_/g, " ")
|
||||
}
|
||||
|
||||
const fetchSmartStatus = async () => {
|
||||
try {
|
||||
@@ -4138,7 +4156,7 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
|
||||
{testStatus.smart_data.attributes.slice(0, 15).map((attr) => (
|
||||
<div key={attr.id} className={`grid ${(isNvme || testStatus.smart_data?.is_sas) ? 'grid-cols-10' : 'grid-cols-12'} gap-2 p-3 text-sm items-center`}>
|
||||
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-1 text-muted-foreground">{attr.id}</div>}
|
||||
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-5' : 'col-span-5'} truncate`} title={attr.name}>{attr.name}</div>
|
||||
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-5' : 'col-span-5'} truncate`} title={smartAttributeLabel(attr.name)}>{smartAttributeLabel(attr.name)}</div>
|
||||
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-3' : 'col-span-2'} text-center font-mono`}>{testStatus.smart_data?.is_sas ? attr.raw_value : attr.value}</div>
|
||||
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-2 text-center font-mono text-muted-foreground">{attr.worst}</div>}
|
||||
<div className="col-span-2 text-center">
|
||||
|
||||
@@ -541,6 +541,36 @@
|
||||
"footer": {
|
||||
"generatedBy": "Report generated by ProxMenux Monitor"
|
||||
},
|
||||
"attributeLabels": {
|
||||
"criticalWarning": "Critical warning",
|
||||
"nvmeTemperature": "Temperature",
|
||||
"temperatureSensor1": "Temperature sensor 1",
|
||||
"temperatureSensor2": "Temperature sensor 2",
|
||||
"temperatureSensor3": "Temperature sensor 3",
|
||||
"availableSpare": "Available spare",
|
||||
"availableSpareThreshold": "Available spare threshold",
|
||||
"percentageUsed": "Percentage used",
|
||||
"enduranceGroupWarning": "Endurance group warning",
|
||||
"mediaErrors": "Media errors",
|
||||
"mediaIntegrityErrors": "Media and data integrity errors",
|
||||
"unsafeShutdowns": "Unsafe shutdowns",
|
||||
"nvmePowerCycles": "Power cycles",
|
||||
"nvmePowerOnHours": "Power-on hours",
|
||||
"dataUnitsRead": "Data units read",
|
||||
"dataUnitsWritten": "Data units written",
|
||||
"hostReadCommands": "Host read commands",
|
||||
"hostWriteCommands": "Host write commands",
|
||||
"controllerBusyTime": "Controller busy time",
|
||||
"errorLogEntries": "Error log entries",
|
||||
"warningTempTime": "Warning temperature time",
|
||||
"criticalTempTime": "Critical temperature time",
|
||||
"warningCompositeTemperatureTime": "Warning composite temperature time",
|
||||
"criticalCompositeTemperatureTime": "Critical composite temperature time",
|
||||
"thermalManagementT1TransCount": "Thermal management T1 transitions",
|
||||
"thermalManagementT2TransCount": "Thermal management T2 transitions",
|
||||
"thermalManagementT1TotalTime": "Thermal management T1 total time",
|
||||
"thermalManagementT2TotalTime": "Thermal management T2 total time"
|
||||
},
|
||||
"attributeExplanations": {
|
||||
"criticalWarning": "Active alert flags from the NVMe controller. Any non-zero value requires immediate investigation.",
|
||||
"nvmeTemperature": "Composite temperature reported by the controller. Sustained high temperatures cause thermal throttling and reduce NAND lifespan.",
|
||||
@@ -1119,7 +1149,9 @@
|
||||
"networkUnits": {
|
||||
"title": "Network Units",
|
||||
"description": "Change how network traffic is displayed",
|
||||
"label": "Network Unit Display"
|
||||
"label": "Network Unit Display",
|
||||
"bytes": "Bytes",
|
||||
"bits": "Bits"
|
||||
},
|
||||
"healthMonitor": {
|
||||
"title": "Health Monitor",
|
||||
@@ -1856,26 +1888,32 @@
|
||||
"counts": {
|
||||
"tests": {
|
||||
"one": "{count} test",
|
||||
"few": "{count} tests",
|
||||
"many": "{count} tests"
|
||||
},
|
||||
"warnings": {
|
||||
"one": "{count} warning",
|
||||
"few": "{count} warnings",
|
||||
"many": "{count} warnings"
|
||||
},
|
||||
"suggestions": {
|
||||
"one": "{count} suggestion",
|
||||
"few": "{count} suggestions",
|
||||
"many": "{count} suggestions"
|
||||
},
|
||||
"testsExecuted": {
|
||||
"one": "{count} test was executed.",
|
||||
"few": "{count} tests were executed.",
|
||||
"many": "{count} tests were executed."
|
||||
},
|
||||
"actionableWarnings": {
|
||||
"one": "{count} actionable warning",
|
||||
"few": "{count} actionable warnings",
|
||||
"many": "{count} actionable warnings"
|
||||
},
|
||||
"actionableSuggestions": {
|
||||
"one": "{count} actionable suggestion",
|
||||
"few": "{count} actionable suggestions",
|
||||
"many": "{count} actionable suggestions"
|
||||
}
|
||||
},
|
||||
@@ -2319,7 +2357,8 @@
|
||||
"bridges": "Bridges",
|
||||
"bond": "Bond",
|
||||
"vlan": "VLAN",
|
||||
"virtual": "Virtual"
|
||||
"virtual": "Virtual",
|
||||
"other": "Other"
|
||||
},
|
||||
"labels": {
|
||||
"activeSlave": "Active slave",
|
||||
@@ -2805,8 +2844,9 @@
|
||||
"deleteLocalTitle": "Delete local archive",
|
||||
"deletePbsDescription": "This removes the PBS snapshot from the datastore.",
|
||||
"deletePbsTitle": "Delete PBS snapshot",
|
||||
"descriptionAfter": "known backups",
|
||||
"descriptionBefore": "Browse and restore",
|
||||
"companionFile": "companion file",
|
||||
"descriptionAfter": "",
|
||||
"descriptionBefore": "Browse and restore backups found in",
|
||||
"downloadTitle": "Download this backup",
|
||||
"emptyAfter": "backups yet.",
|
||||
"emptyBefore": "No",
|
||||
@@ -3113,7 +3153,7 @@
|
||||
"invalidJobName": "Use only letters, numbers, dashes and underscores.",
|
||||
"jobNameHelpAfter": "Keep it short and readable.",
|
||||
"jobNameHelpAnd": "and",
|
||||
"jobNameHelpBefore": "Used in timer names, logs",
|
||||
"jobNameHelpBefore": "Used in timer names and logs. Allowed characters also include",
|
||||
"jobNameLocked": "Job name is locked after creation.",
|
||||
"lastRun": "Last run",
|
||||
"lastRunLabel": "Last run",
|
||||
@@ -3517,7 +3557,7 @@
|
||||
"active": "Active", "up": "UP", "kernelUpToDate": "Kernel/PVE is up to date", "proxmoxUpToDate": "Proxmox VE is up to date",
|
||||
"noSecurityUpdates": "No security updates pending", "noContainerErrors": "No container startup errors", "noOomEvents": "No OOM events detected",
|
||||
"noQmpTimeouts": "No QMP timeouts detected", "noVmFailures": "No VM startup failures", "dismissedByUser": "Dismissed by user",
|
||||
"gatewayLatency": "Latency to gateway: {latency} ms", "failedLogins": "{count} failed login attempts in 24h", "uptimeDays": "Uptime: {count} days",
|
||||
"gatewayLatency": "Latency to gateway: {latency} ms", "failedLogins": "{count} failed login attempts in 24h", "fail2banBannedIps": "Fail2Ban is currently blocking {count} IP addresses (jails: {jails})", "uptimeDays": "Uptime: {count} days",
|
||||
"pendingPackages": "{count} packages pending", "updatedDaysAgo": "Last updated {count} days ago", "storageAvailable": "{type} storage is available",
|
||||
"mountReachable": "{type} mount is reachable", "rootfsUsed": "rootfs usage: {percent}% ({size})", "runningCtsSafe": "{count} running CTs have safe rootfs usage",
|
||||
"pveStorageSafe": "{count} PVE block storage targets have safe usage", "remoteMountsHealthy": "{count} remote mounts are healthy"
|
||||
|
||||
Reference in New Issue
Block a user