diff --git a/AppImage/components/lxc-app-panel.tsx b/AppImage/components/lxc-app-panel.tsx index a5720d73..fd369eb8 100644 --- a/AppImage/components/lxc-app-panel.tsx +++ b/AppImage/components/lxc-app-panel.tsx @@ -33,6 +33,7 @@ import { Label } from "./ui/label" import { Badge } from "./ui/badge" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { fetchApi } from "../lib/api-config" +import { useT } from "@/lib/i18n/provider" // installed_via is optional now — an empty value means "register only, // no version tracking, no warnings, just a clickable link". Docker @@ -216,6 +217,7 @@ function suggestPackageName(name: string) { } export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { + const t = useT() const [loading, setLoading] = useState(true) const [sidecar, setSidecar] = useState(null) const [suggestions, setSuggestions] = useState(null) @@ -474,7 +476,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { setEditing(null) onChange?.() } catch (e: any) { - setError(e?.message || "Save failed") + setError(e?.message || t("vmLxc.appEditor.saveFailed")) } finally { setSaving(false) } @@ -517,7 +519,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { // waiting for the round-trip, then persist to the server. If the // POST fails, reload from server to resync. const dismissDetection = async (slug: string, name: string) => { - if (!confirm(`Hide the "${name}" detection permanently on this container?\n\nTo bring it back later, click "Register a different app" — the hidden list appears there with a Restore option.`)) return + if (!confirm(t("vmLxc.appEditor.confirmHide", { name }))) return setSidecar((prev) => prev ? { ...prev, dismissed_slugs: [...(prev.dismissed_slugs || []), slug] } : prev, @@ -679,11 +681,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
{managed.installed_version && (
-
Installed
+
{t("vmLxc.appEditor.installedStatus")}
{managed.installed_version} {upToDate && ( - + )}
@@ -694,9 +696,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { Latest upstream
- {managed.latest_version || checking…} + {managed.latest_version || {t("vmLxc.appEditor.checkingStatus")}} {hasUpdate && managed.latest_version && ( - + )}
@@ -715,7 +717,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { CT's lifecycle is owned by ProxMenux (not user CRUD). */}
- Installed and managed by ProxMenux + {t("vmLxc.appEditor.installedManaged")}
@@ -789,7 +791,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
- +
- {matches.length === 20 ? "Top 20 matches — narrow the query for more" : `${matches.length} match${matches.length === 1 ? "" : "es"}`} + {matches.length === 20 ? t("vmLxc.appEditor.top20Matches") : `${matches.length} match${matches.length === 1 ? "" : "es"}`}
{matches.map((c) => ( @@ -996,7 +998,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { setPort(i, { logo_url: e.target.value })} - placeholder="Logo URL for this link (optional)" + placeholder={t("vmLxc.appEditor.portLogoLabel")} maxLength={512} className="col-start-1 col-end-4 text-xs font-mono h-8 opacity-70 focus:opacity-100" type="url" @@ -1048,7 +1050,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
- +
{isPackaged && (
- + - + setField({ binary_path: e.target.value })} - placeholder="e.g., /opt//binary" + placeholder={t("vmLxc.appEditor.binaryPathPlaceholder")} className="font-mono text-xs" />
@@ -1112,7 +1114,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {method === "file" && (
- +
- + setField({ file_regex: e.target.value })} - placeholder="version:\\s*(\\d+\\.\\d+\\.\\d+)" + placeholder={t("vmLxc.appEditor.regexPlaceholderVersion")} className="font-mono text-xs" />
@@ -1137,22 +1139,22 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {method === "python_dist" && (
- + setField({ python_path: e.target.value })} - placeholder="e.g., /opt//venv/bin/python" + placeholder={t("vmLxc.appEditor.pythonInterpreterPlaceholder")} className="font-mono text-xs" />
- + setField({ distribution: e.target.value })} - placeholder="e.g., " + placeholder={t("vmLxc.appEditor.pipDistPlaceholder")} className="font-mono text-xs" />
@@ -1162,22 +1164,22 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {method === "docker_label" && (
- + setField({ container_name: e.target.value })} - placeholder="e.g., " + placeholder={t("vmLxc.appEditor.containerNamePlaceholder")} className="font-mono text-xs" />
- + setField({ label: e.target.value })} - placeholder="org.opencontainers.image.version" + placeholder={t("vmLxc.appEditor.ociLabelPlaceholder")} className="font-mono text-xs" />
@@ -1187,38 +1189,38 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {method === "docker_exec" && (
- + setField({ container_name: e.target.value })} - placeholder="e.g., " + placeholder={t("vmLxc.appEditor.containerNamePlaceholder")} className="font-mono text-xs" />
- + setField({ binary_path: e.target.value })} - placeholder="e.g., myapp (or absolute path)" + placeholder={t("vmLxc.appEditor.binaryPathBarePlaceholder")} className="font-mono text-xs" />
- + setField({ binary_args: e.target.value.split(",").map(s => s.trim()).filter(Boolean), })} - placeholder="e.g., --version (comma-separated)" + placeholder={t("vmLxc.appEditor.binaryArgsPlaceholder")} className="font-mono text-xs" />
- Left empty defaults to --version. Grafana needs server, -v. + {t("vmLxc.appEditor.binaryArgsHintPrefix")} --version. {t("vmLxc.appEditor.binaryArgsHintGrafana")} server, -v.
@@ -1226,26 +1228,26 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {method === "command" && (
- + setField({ command_argv: e.target.value.split(",").map(s => s.trim()).filter(Boolean), })} - placeholder="myapp, version, --json" + placeholder={t("vmLxc.appEditor.commandPlaceholder")} className="font-mono text-xs" />
Runs argv-style inside the CT as root — never through a shell. You are responsible for what the command does. Use installed_regex below to extract the version from the output.
- + setField({ installed_regex: e.target.value })} - placeholder="v?(\\d+\\.\\d+\\.\\d+)" + placeholder={t("vmLxc.appEditor.tagRegexBare")} className="font-mono text-xs mt-1" />
@@ -1254,7 +1256,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {method === "manual" && (
- +
- +
@@ -1319,27 +1321,27 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {upstreamType === "github" && (
- + setField({ repo: e.target.value })} - placeholder="e.g., owner/name" + placeholder={t("vmLxc.appEditor.githubRepoPlaceholder")} />
Public GitHub repository where releases or tags are published.
- +
@@ -1349,13 +1351,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {upstreamType === "http_json" && (
- + setField({ upstream_url: e.target.value })} - placeholder="e.g., https://vendor.example.com/api/latest.json" + placeholder={t("vmLxc.appEditor.endpointUrlPlaceholder")} className="font-mono text-xs" maxLength={512} /> @@ -1364,12 +1366,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
- + setField({ upstream_json_path: e.target.value })} - placeholder="e.g., data.version or releases[0].tag_name" + placeholder={t("vmLxc.appEditor.jsonPathPlaceholder")} className="font-mono text-xs" maxLength={128} /> @@ -1382,12 +1384,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {upstreamType === "docker_hub" && (
- + setField({ docker_image: e.target.value })} - placeholder="e.g., linuxserver/plex or nginx" + placeholder={t("vmLxc.appEditor.dockerImagePlaceholder")} className="font-mono text-xs" maxLength={255} /> @@ -1402,13 +1404,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { {upstreamType && (
setField({ tag_regex: e.target.value })} - placeholder="e.g., v?(\\d+\\.\\d+\\.\\d+)" + placeholder={t("vmLxc.appEditor.tagRegexPlaceholder")} className="font-mono text-xs" />
@@ -1433,14 +1435,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { )}
- +
@@ -1466,7 +1468,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { )}
{d.name}
-
Currently hidden — will re-appear in the detection list
+
{t("vmLxc.appEditor.hiddenBadge")}
@@ -1523,11 +1525,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { variant="ghost" onClick={() => dismissDetection(d.slug, d.name)} aria-label={`Hide ${d.name} detection`} - title="Hide this detection permanently" + title={t("vmLxc.appEditor.hidePermanentlyTooltip")} className="flex-1 sm:flex-none bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 text-red-400 hover:text-red-300" > - Hide + {t("vmLxc.appEditor.hideButton")}
@@ -1546,19 +1548,19 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
-

Register a different app

+

{t("vmLxc.appEditor.registerDifferent")}

- You have {hiddenDetections.length} hidden detection{hiddenDetections.length === 1 ? "" : "s"}. Restore one to bring it back, or register a custom app manually. + {t(hiddenDetections.length === 1 ? "vmLxc.appEditor.hiddenDetectionsHelpSingular" : "vmLxc.appEditor.hiddenDetectionsHelpPlural", { count: hiddenDetections.length })}

{hiddenDetections.length > 0 && ( @@ -1572,7 +1574,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { className="bg-blue-500 hover:bg-blue-600 text-white" > - Register a custom app + {t("vmLxc.appEditor.registerCustom")}
@@ -1590,12 +1592,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {

- No applications registered + {t("vmLxc.appEditor.noAppsTitle")}

- Register each application running in this container. Get - clickable web links, and — optionally — upstream version - tracking + notifications for new releases. + {t("vmLxc.appEditor.noAppsBody")}

{visibleDetected.length > 0 && (
@@ -1606,10 +1606,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { @@ -1705,11 +1705,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
{st?.installed_version && (
-
Installed
+
{t("vmLxc.appEditor.installedStatus")}
{st.installed_version} {upToDate && ( - + )}
@@ -1720,9 +1720,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) { Latest upstream
- {st?.latest_version || checking…} + {st?.latest_version || {t("vmLxc.appEditor.checkingStatus")}} {hasUpdate && st?.latest_version && ( - + )}
diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index 2c121040..2d16f0d9 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -1229,12 +1229,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { // 5-field cron expression the backend parser accepts. Order + slugs // stable so the Select value round-trips a saved schedule. const CRON_PRESETS: { value: string; label: string; cron: string }[] = [ - { value: "hourly", label: "Hourly", cron: "0 * * * *" }, - { value: "daily-3am", label: "Daily at 3:00", cron: "0 3 * * *" }, - { value: "daily-noon", label: "Daily at 12:00", cron: "0 12 * * *" }, - { value: "weekly-sun-3am", label: "Weekly (Sunday 3:00)", cron: "0 3 * * 0" }, - { value: "monthly-1st-3am", label: "Monthly (1st at 3:00)", cron: "0 3 1 * *" }, - { value: "custom", label: "Custom cron…", cron: "" }, + { value: "hourly", label: t("vmLxc.cronPresets.hourly"), cron: "0 * * * *" }, + { value: "daily-3am", label: t("vmLxc.cronPresets.dailyAt3"), cron: "0 3 * * *" }, + { value: "daily-noon", label: t("vmLxc.cronPresets.dailyAtNoon"), cron: "0 12 * * *" }, + { value: "weekly-sun-3am", label: t("vmLxc.cronPresets.weeklySun3"), cron: "0 3 * * 0" }, + { value: "monthly-1st-3am", label: t("vmLxc.cronPresets.monthly1st3"), cron: "0 3 1 * *" }, + { value: "custom", label: t("vmLxc.cronPresets.custom"), cron: "" }, ] const loadSchedule = async (vmid: number) => { @@ -1329,7 +1329,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { } const deleteScheduleFromOptions = async () => { if (!selectedVM) return - if (!confirm("Remove the scheduled updates for this container? Apply defaults (backup + restart) are kept.")) return + if (!confirm(t("vmLxc.scheduled.deleteConfirm"))) return setScheduleSaving(true) try { await fetchApi(`/api/vms/${selectedVM.vmid}/schedule`, { method: "DELETE" }) @@ -3530,7 +3530,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
Last checked: {lastChecked} {aw?.installed_version && <> · Tailscale v{aw.installed_version}} - {" · "}No updates available + {" · "}{t("vmLxc.updates.noUpdatesAvailableChip")}
) : (
@@ -3640,13 +3640,13 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
-

OS packages

+

{t("vmLxc.updates.osPackagesTitle")}

- Last checked:{" "} + {t("vmLxc.updates.lastCheckedPrefix")}{" "} {uc?.last_check ? new Date(uc.last_check).toLocaleString() : "—"} {uc?.os_family && ( - <> · Family: {uc.os_family} + <> · {t("vmLxc.updates.familyLabel")} {uc.os_family} )}
{hasOsUpdates ? (() => { @@ -3661,7 +3661,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
{p.security && ( - + )} {p.name} @@ -3679,12 +3679,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
- {total} package{total === 1 ? "" : "s"} pending + {total} {t(total === 1 ? "vmLxc.updates.packagePendingLabel" : "vmLxc.updates.packagesPendingLabel")}
{sec > 0 && (
- {sec} security update{sec === 1 ? "" : "s"} + {sec} {t(sec === 1 ? "vmLxc.updates.securityUpdateLabel" : "vmLxc.updates.securityUpdatesLabel")}
)}
@@ -3692,7 +3692,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { })() : (
- No OS updates pending + {t("vmLxc.updates.noOsUpdatesPending")}
)}
@@ -3702,7 +3702,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { className={hasOsUpdates ? pendingBtnCls : upToDateBtnCls} > {hasOsUpdates && } - {hasOsUpdates ? "Apply OS update" : "OS up to date"} + {hasOsUpdates ? t("vmLxc.updates.applyOsUpdate") : t("vmLxc.updates.osUpToDate")}
@@ -3736,12 +3736,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {

- {helperName || "Application"} + {helperName || t("vmLxc.updates.applicationDefaultName")}

{matchApp?.checked_at && (
- Last checked: {new Date(matchApp.checked_at).toLocaleString()} + {t("vmLxc.updates.lastCheckedPrefix")} {new Date(matchApp.checked_at).toLocaleString()}
)} {/* Educational note — apps installed via @@ -3764,8 +3764,8 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }} /> - Installed by Proxmox Helper-Scripts - {" "}— updates run the community-scripts helper. + {t("vmLxc.updates.installedByHelperPrefix")} {t("vmLxc.updates.helperScriptsName")} + {" "}{t("vmLxc.updates.helperUpdatesRun")}
)} @@ -3773,7 +3773,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
- installed {matchApp!.installed_version} + {t("vmLxc.updates.installedLabel")} {matchApp!.installed_version}
@@ -3783,12 +3783,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { ) : upToD ? (
- Up to date at {matchApp!.installed_version} + {t("vmLxc.updates.upToDateAtLabel")} {matchApp!.installed_version}
) : helperExists ? (

- Version tracking pending — the next scheduled check will - populate installed and upstream numbers. + {t("vmLxc.updates.versionTrackingPending")}

) : null} {helperExists && (() => { @@ -3806,7 +3805,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { const noState = !hasUpd && !upToD const neutralBtnCls = "bg-muted/40 hover:bg-muted/60 border border-border text-foreground/80 hover:text-foreground" const cls = hasUpd ? pendingBtnCls : upToD ? upToDateBtnCls : neutralBtnCls - const label = hasUpd ? "Apply update" : upToD ? "Up to date" : "Run updater" + const label = hasUpd ? t("vmLxc.updates.applyUpdate") : upToD ? t("vmLxc.updates.upToDate") : t("vmLxc.updates.runUpdater") return (
{aw.checked_at && !editing && (
- Last checked: {new Date(aw.checked_at).toLocaleString()} + {t("vmLxc.updates.lastCheckedPrefix")} {new Date(aw.checked_at).toLocaleString()}
)} {editing ? (