From e9b310743adca91bd99eb2b88a2d0a583be35d35 Mon Sep 17 00:00:00 2001
From: VAIO73 <50487331+Vaso73@users.noreply.github.com>
Date: Tue, 20 Oct 2026 20:11:39 +0000
Subject: [PATCH] i18n: complete Slovak application editor and validation
messages
---
.github/scripts/run_offline_qualified.py | 1 +
AppImage/components/lxc-app-panel.tsx | 23 +--
AppImage/lib/lxc-app-editor-errors.ts | 130 ++++++++++++++++
AppImage/messages/en/common.json | 121 +++++++++++++++
AppImage/messages/sk/common.json | 149 +++++++++++++++++--
tests/lxc_updates/test_app_editor_errors.cjs | 104 +++++++++++++
6 files changed, 503 insertions(+), 25 deletions(-)
create mode 100644 AppImage/lib/lxc-app-editor-errors.ts
create mode 100644 tests/lxc_updates/test_app_editor_errors.cjs
diff --git a/.github/scripts/run_offline_qualified.py b/.github/scripts/run_offline_qualified.py
index 8a9548cd..ea3ba14a 100644
--- a/.github/scripts/run_offline_qualified.py
+++ b/.github/scripts/run_offline_qualified.py
@@ -28,6 +28,7 @@ PYTHON_FILES = (
"AppImage/scripts/tests/test_health_persistence_recurrence.py",
)
NODE_FILES = (
+ "tests/lxc_updates/test_app_editor_errors.cjs",
"tests/lxc_updates/test_local_app_icons_ui.cjs",
"tests/lxc_updates/test_docker_app_discovery_ui.cjs",
"tests/lxc_updates/test_native_app_discovery_ui.cjs",
diff --git a/AppImage/components/lxc-app-panel.tsx b/AppImage/components/lxc-app-panel.tsx
index ffd06ea5..0c7cd6e4 100644
--- a/AppImage/components/lxc-app-panel.tsx
+++ b/AppImage/components/lxc-app-panel.tsx
@@ -37,6 +37,7 @@ import { fetchLxcApps, getLxcAppsCached, setLxcAppsCached, subscribeLxcApps } fr
import { categoryChipStyle, useIsLightTheme } from "../lib/category-color"
import { getCategoryLabel } from "../lib/category-label"
import { useT } from "@/lib/i18n/provider"
+import { localizeAppEditorError } from "../lib/lxc-app-editor-errors"
// installed_via is optional now — an empty value means "register only,
// no version tracking, no warnings, just a clickable link". Docker
@@ -728,7 +729,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
if (lower.startsWith("registry unreachable")) {
return t("vmLxc.appEditor.upstreamErrorRegistry")
}
- return msg
+ return localizeAppEditorError(msg, t)
}
const editorOpen = !!editing && editing.targetVmid === vmid
@@ -1212,7 +1213,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
}
const removeOne = async (appId: string) => {
- if (!confirm("Remove this application from the CT's App tab?")) return
+ if (!confirm(t("vmLxc.appEditor.confirmRemove"))) return
setBusyAppId(appId)
setError(null)
try {
@@ -2069,14 +2070,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
id="app-package"
value={draft.package || ""}
onChange={(e) => setField({ package: e.target.value })}
- placeholder={method === "dpkg" ? "e.g. jellyfin-server" : "e.g. tailscale"}
+ placeholder={t("vmLxc.appEditor.packageExample", { package: method === "dpkg" ? "jellyfin-server" : "tailscale" })}
/>
- Auto-filled from Name. Verify with{" "}
+ {t("vmLxc.appEditor.packageHelpBefore")}{" "}
{method === "dpkg" ? "dpkg -l | grep " : "apk info | grep "}
{" "}
- inside the CT.
+ {t("vmLxc.appEditor.packageHelpAfter")}
)}
@@ -2092,8 +2093,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
className="font-mono text-xs"
/>
- Absolute path. Find it with{" "}
- which <app> or{" "}
+ {t("vmLxc.appEditor.binaryHelpBefore")}{" "}
+ which <app>{" "}{t("vmLxc.appEditor.binaryHelpOr")}{" "}
systemctl show <service> -p ExecStart.
@@ -2558,7 +2559,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
{detectorTest.installed.version ? (
{detectorTest.installed.version}
) : (
- {detectorTest.installed.error || t("vmLxc.appEditor.detectorTestNoVersion")}
+ {localizeUpstreamError(detectorTest.installed.error) || t("vmLxc.appEditor.detectorTestNoVersion")}
)}
{detectorTest.installed.method || "—"}
@@ -2574,7 +2575,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
) : detectorTest.upstream.version ? (
{detectorTest.upstream.version}
) : (
-
{detectorTest.upstream.error || t("vmLxc.appEditor.detectorTestNoVersion")}
+
{localizeUpstreamError(detectorTest.upstream.error) || t("vmLxc.appEditor.detectorTestNoVersion")}
)}
{detectorTest.upstream.type && (
{detectorTest.upstream.type}
@@ -2587,7 +2588,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
{error && (
-
{error}
+
{localizeAppEditorError(error, t)}
)}
@@ -3343,7 +3344,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
{error && (
-
{error}
+
{localizeAppEditorError(error, t)}
)}
diff --git a/AppImage/lib/lxc-app-editor-errors.ts b/AppImage/lib/lxc-app-editor-errors.ts
new file mode 100644
index 00000000..506091e7
--- /dev/null
+++ b/AppImage/lib/lxc-app-editor-errors.ts
@@ -0,0 +1,130 @@
+type Translate = (key: string, params?: Record) => string
+
+// Exact lxc_apps validation/persistence messages; never change their API contract.
+// Anchored patterns leave unknown diagnostics intact, including paths and commands.
+const RULES: [RegExp, string, string[]][] = [
+ [/^an OCI operation on this container is running$/, "ociBusy", []],
+ [/^([\s\S]+) is not installed via dpkg$/, "dpkgMissing", ["package"]],
+ [/^dpkg\-query failed$/, "dpkgFailed", []],
+ [/^no version matched in dpkg output$/, "dpkgNoVersion", []],
+ [/^apk info failed$/, "apkFailed", []],
+ [/^no version matched in apk output$/, "apkNoVersion", []],
+ [/^could not read ([\s\S]+)$/, "fileRead", ["path"]],
+ [/^file_regex did not match ([\s\S]+)$/, "fileNoMatch", ["path"]],
+ [/^binary invocation failed$/, "binaryFailed", []],
+ [/^no version matched in binary output$/, "binaryNoVersion", []],
+ [/^python \-c importlib\.metadata failed$/, "pythonFailed", []],
+ [/^no version matched in Python distribution output$/, "pythonNoVersion", []],
+ [/^could not resolve the container's installed image$/, "dockerImageResolve", []],
+ [/^docker inspect failed$/, "dockerInspectFailed", []],
+ [/^container has no ([\s\S]+) label$/, "dockerLabelMissing", ["label"]],
+ [/^docker label reports ([\s\S]+) \(mutable tag, not a version\)$/, "dockerLabelMutable", ["tag"]],
+ [/^no version matched in docker label output$/, "dockerLabelNoVersion", []],
+ [/^docker exec failed$/, "dockerExecFailed", []],
+ [/^no version matched in docker exec output$/, "dockerExecNoVersion", []],
+ [/^command_argv is empty$/, "commandEmpty", []],
+ [/^command failed$/, "commandFailed", []],
+ [/^no version matched in command output$/, "commandNoVersion", []],
+ [/^unsupported method: ([\s\S]+)$/, "unsupportedMethod", ["method"]],
+ [/^unknown upstream_type: ([\s\S]+)$/, "unknownUpstream", ["type"]],
+ [/^([\s\S]+): not found$/, "githubNotFound", ["repo"]],
+ [/^github rejected the request \(403\)$/, "githubRejected", []],
+ [/^github error (\d+)$/, "githubError", ["code"]],
+ [/^no tag \/ release name in response$/, "noRelease", []],
+ [/^tag_regex did not match '([\s\S]+)'$/, "tagNoMatch", ["tag"]],
+ [/^http error (\d+)$/, "httpError", ["code"]],
+ [/^invalid JSON response: ([\s\S]+)$/, "invalidJson", ["detail"]],
+ [/^json_path '([\s\S]+)' did not resolve$/, "jsonPathUnresolved", ["path"]],
+ [/^empty value at json_path$/, "jsonEmpty", []],
+ [/^no tags matched \(docker_hub\)$/, "dockerNoTags", []],
+ [/^tag_regex matched only moving tags; use Docker image digest tracking$/, "dockerMovingOnly", []],
+ [/^docker hub error (\d+)$/, "dockerHubError", ["code"]],
+ [/^invalid docker hub JSON: ([\s\S]+)$/, "dockerHubJson", ["detail"]],
+ [/^no tags returned by docker hub$/, "dockerHubEmpty", []],
+ [/^payload must be a JSON object$/, "payload", []],
+ [/^name is required and must be 1\-64 chars of letters\/digits\/spaces\/\._\+\-\(\)\/$/, "name", []],
+ [/^installed_via must be one of: ([\s\S]+) or empty$/, "method", ["values"]],
+ [/^package is required \(letters\/digits\/\._\+@:\/ up to 127 chars\)$/, "package", []],
+ [/^file_path is required and must be an absolute path$/, "filePath", []],
+ [/^file_regex is required$/, "fileRegex", []],
+ [/^file_regex is not a valid regex: ([\s\S]+)$/, "fileRegexInvalid", ["detail"]],
+ [/^binary_path is required and must be an absolute path$/, "binaryPath", []],
+ [/^python_path is required and must be an absolute path$/, "pythonPath", []],
+ [/^distribution is required \(PEP 503 name\)$/, "distribution", []],
+ [/^container_name is required \(docker naming rules\)$/, "container", []],
+ [/^label is required \(OCI label naming rules\)$/, "label", []],
+ [/^binary_path is required \(absolute path or bare command\)$/, "dockerBinary", []],
+ [/^installed_version is required for the manual method$/, "manualVersion", []],
+ [/^installed_version exceeds (\d+) chars$/, "versionLength", ["max"]],
+ [/^installed_version contains a forbidden control character$/, "versionControl", []],
+ [/^installed_regex is not a valid regex: ([\s\S]+)$/, "installedRegexInvalid", ["detail"]],
+ [/^upstream_type must be one of: ([\s\S]+)$/, "upstreamType", ["values"]],
+ [/^tag_regex is not a valid regex: ([\s\S]+)$/, "tagRegexInvalid", ["detail"]],
+ [/^repo is required for upstream_type=github$/, "repoRequired", []],
+ [/^repo must be 'owner\/name'$/, "repoFormat", []],
+ [/^github_source must be one of: ([\s\S]+)$/, "githubSource", ["values"]],
+ [/^upstream_url is required for upstream_type=http_json$/, "upstreamUrlRequired", []],
+ [/^upstream_url must be an http\(s\) URL$/, "upstreamUrl", []],
+ [/^upstream_url exceeds (\d+) chars$/, "upstreamUrlLength", ["max"]],
+ [/^upstream_json_path is required for upstream_type=http_json$/, "jsonPathRequired", []],
+ [/^upstream_json_path exceeds (\d+) chars$/, "jsonPathLength", ["max"]],
+ [/^upstream_json_path uses forbidden characters$/, "jsonPathCharacters", []],
+ [/^docker_image is required for upstream_type=docker_hub$/, "dockerImageRequired", []],
+ [/^docker_image exceeds (\d+) chars$/, "dockerImageLength", ["max"]],
+ [/^docker_image must match Docker Hub naming \(owner\/name or name\)$/, "dockerImageFormat", []],
+ [/^health_path must be a valid URL path \(max 255 chars\)$/, "healthPath", []],
+ [/^logo_url must be an http\(s\) URL or this VM's cached local icon$/, "logoUrl", []],
+ [/^invalid discovery_id$/, "discoveryId", []],
+ [/^helper_slug must be a lowercase slug \(letters\/digits\/\._\-\)$/, "helperSlug", []],
+ [/^update_via only accepts 'docker'$/, "updateVia", []],
+ [/^update_via=docker requires a docker_label or docker_exec detector$/, "dockerDetector", []],
+ [/^update_via=docker cannot be combined with ([\s\S]+)$/, "dockerConflict", ["field"]],
+ [/^update_command must be a string$/, "updateCommandType", []],
+ [/^update_command exceeds (\d+) chars$/, "updateCommandLength", ["max"]],
+ [/^update_command contains a null byte$/, "updateCommandNull", []],
+ [/^update_method must be none, helper or custom$/, "updateMethod", []],
+ [/^update_command is required for update_method=custom$/, "updateCommandRequired", []],
+ [/^update_command is only allowed for update_method=custom$/, "updateCommandOnlyCustom", []],
+ [/^a supported helper_slug is required for update_method=helper$/, "helperRequired", []],
+ [/^Docker\-delegated apps update through their image, not a separate app updater$/, "dockerUpdater", []],
+ [/^command_argv is required \(non\-empty list\)$/, "commandRequired", []],
+ [/^command_argv must be a non\-empty list of strings$/, "commandList", []],
+ [/^command_argv accepts at most (\d+) entries$/, "commandCount", ["max"]],
+ [/^command_argv\[(\d+)\] must be a non\-empty string$/, "commandEntry", ["index"]],
+ [/^command_argv\[(\d+)\] exceeds (\d+) chars$/, "commandEntryLength", ["index", "max"]],
+ [/^command_argv\[(\d+)\] contains a forbidden control character$/, "commandEntryControl", ["index"]],
+ [/^binary_args must be a list of strings$/, "binaryArgsList", []],
+ [/^binary_args accepts at most (\d+) entries$/, "binaryArgsCount", ["max"]],
+ [/^binary_args\[(\d+)\] must be a non\-empty string$/, "binaryArgEntry", ["index"]],
+ [/^binary_args\[(\d+)\] exceeds (\d+) chars$/, "binaryArgLength", ["index", "max"]],
+ [/^binary_args\[(\d+)\] contains a forbidden control character$/, "binaryArgControl", ["index"]],
+ [/^ports must be a list of \{port, description, scheme\}$/, "portsList", []],
+ [/^ports\[(\d+)\] must be an object$/, "portObject", ["index"]],
+ [/^ports\[(\d+)\]\.port is required$/, "portRequired", ["index"]],
+ [/^ports\[(\d+)\]\.port must be an integer$/, "portInteger", ["index"]],
+ [/^ports\[(\d+)\]\.port must be 1\-65535$/, "portRange", ["index"]],
+ [/^port (\d+) appears more than once for this app$/, "portDuplicate", ["port"]],
+ [/^ports\[(\d+)\]\.description has invalid characters$/, "portDescription", ["index"]],
+ [/^ports\[(\d+)\]\.scheme must be 'http' or 'https'$/, "portScheme", ["index"]],
+ [/^ports\[(\d+)\]\.web_path must be a valid URL path \(max 255 chars\)$/, "portPath", ["index"]],
+ [/^ports\[(\d+)\]\.logo_url must be an http\(s\) URL \(max 512 chars\)$/, "portLogo", ["index"]],
+ [/^ports\[(\d+)\]\.category has invalid characters or is too long$/, "portCategory", ["index"]],
+ [/^ports\[(\d+)\]\.custom_url must be an http\(s\) URL \(max 512 chars\)$/, "portCustomUrl", ["index"]],
+ [/^tag_regex exceeds 512 chars$/, "tagRegexLength", []],
+ [/^could not persist sidecar \(permission\?\)$/, "persistPermission", []],
+ [/^could not persist sidecar$/, "persist", []],
+ [/^no apps registered for this vmid$/, "noApps", []],
+ [/^app_id '([\s\S]+)' not found$/, "appMissing", ["appId"]],
+ [/^app not found$/, "appNotFound", []],
+]
+
+export function localizeAppEditorError(message: string | null | undefined, t: Translate): string {
+ if (!message) return ""
+ for (const [pattern, key, names] of RULES) {
+ const match = pattern.exec(message)
+ if (!match) continue
+ const params = Object.fromEntries(names.map((name, index) => [name, match[index + 1]]))
+ return t(`vmLxc.appEditor.errors.${key}`, params)
+ }
+ return message
+}
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json
index cb93dc90..0453a804 100644
--- a/AppImage/messages/en/common.json
+++ b/AppImage/messages/en/common.json
@@ -1620,6 +1620,127 @@
"loadFailed": "Could not load the bulk update configuration."
},
"appEditor": {
+ "confirmRemove": "Remove this application's record from the App tab? This does not uninstall the application.",
+ "packageExample": "e.g. {package}",
+ "packageHelpBefore": "Auto-filled from Name. Verify with",
+ "packageHelpAfter": "inside the CT.",
+ "binaryHelpBefore": "Absolute path. Find it with",
+ "binaryHelpOr": "or",
+ "errors": {
+ "ociBusy": "an OCI operation on this container is running",
+ "dpkgMissing": "{package} is not installed via dpkg",
+ "dpkgFailed": "dpkg-query failed",
+ "dpkgNoVersion": "no version matched in dpkg output",
+ "apkFailed": "apk info failed",
+ "apkNoVersion": "no version matched in apk output",
+ "fileRead": "could not read {path}",
+ "fileNoMatch": "file_regex did not match {path}",
+ "binaryFailed": "binary invocation failed",
+ "binaryNoVersion": "no version matched in binary output",
+ "pythonFailed": "python -c importlib.metadata failed",
+ "pythonNoVersion": "no version matched in Python distribution output",
+ "dockerImageResolve": "could not resolve the container's installed image",
+ "dockerInspectFailed": "docker inspect failed",
+ "dockerLabelMissing": "container has no {label} label",
+ "dockerLabelMutable": "docker label reports {tag} (mutable tag, not a version)",
+ "dockerLabelNoVersion": "no version matched in docker label output",
+ "dockerExecFailed": "docker exec failed",
+ "dockerExecNoVersion": "no version matched in docker exec output",
+ "commandEmpty": "command_argv is empty",
+ "commandFailed": "command failed",
+ "commandNoVersion": "no version matched in command output",
+ "unsupportedMethod": "unsupported method: {method}",
+ "unknownUpstream": "unknown upstream_type: {type}",
+ "githubNotFound": "{repo}: not found",
+ "githubRejected": "github rejected the request (403)",
+ "githubError": "github error {code}",
+ "noRelease": "no tag / release name in response",
+ "tagNoMatch": "tag_regex did not match '{tag}'",
+ "httpError": "http error {code}",
+ "invalidJson": "invalid JSON response: {detail}",
+ "jsonPathUnresolved": "json_path '{path}' did not resolve",
+ "jsonEmpty": "empty value at json_path",
+ "dockerNoTags": "no tags matched (docker_hub)",
+ "dockerMovingOnly": "tag_regex matched only moving tags; use Docker image digest tracking",
+ "dockerHubError": "docker hub error {code}",
+ "dockerHubJson": "invalid docker hub JSON: {detail}",
+ "dockerHubEmpty": "no tags returned by docker hub",
+ "payload": "payload must be a JSON object",
+ "name": "name is required and must be 1-64 chars of letters/digits/spaces/._+-()/",
+ "method": "installed_via must be one of: {values} or empty",
+ "package": "package is required (letters/digits/._+@:/ up to 127 chars)",
+ "filePath": "file_path is required and must be an absolute path",
+ "fileRegex": "file_regex is required",
+ "fileRegexInvalid": "file_regex is not a valid regex: {detail}",
+ "binaryPath": "binary_path is required and must be an absolute path",
+ "pythonPath": "python_path is required and must be an absolute path",
+ "distribution": "distribution is required (PEP 503 name)",
+ "container": "container_name is required (docker naming rules)",
+ "label": "label is required (OCI label naming rules)",
+ "dockerBinary": "binary_path is required (absolute path or bare command)",
+ "manualVersion": "installed_version is required for the manual method",
+ "versionLength": "installed_version exceeds {max} chars",
+ "versionControl": "installed_version contains a forbidden control character",
+ "installedRegexInvalid": "installed_regex is not a valid regex: {detail}",
+ "upstreamType": "upstream_type must be one of: {values}",
+ "tagRegexInvalid": "tag_regex is not a valid regex: {detail}",
+ "repoRequired": "repo is required for upstream_type=github",
+ "repoFormat": "repo must be 'owner/name'",
+ "githubSource": "github_source must be one of: {values}",
+ "upstreamUrlRequired": "upstream_url is required for upstream_type=http_json",
+ "upstreamUrl": "upstream_url must be an http(s) URL",
+ "upstreamUrlLength": "upstream_url exceeds {max} chars",
+ "jsonPathRequired": "upstream_json_path is required for upstream_type=http_json",
+ "jsonPathLength": "upstream_json_path exceeds {max} chars",
+ "jsonPathCharacters": "upstream_json_path uses forbidden characters",
+ "dockerImageRequired": "docker_image is required for upstream_type=docker_hub",
+ "dockerImageLength": "docker_image exceeds {max} chars",
+ "dockerImageFormat": "docker_image must match Docker Hub naming (owner/name or name)",
+ "healthPath": "health_path must be a valid URL path (max 255 chars)",
+ "logoUrl": "logo_url must be an http(s) URL or this VM's cached local icon",
+ "discoveryId": "invalid discovery_id",
+ "helperSlug": "helper_slug must be a lowercase slug (letters/digits/._-)",
+ "updateVia": "update_via only accepts 'docker'",
+ "dockerDetector": "update_via=docker requires a docker_label or docker_exec detector",
+ "dockerConflict": "update_via=docker cannot be combined with {field}",
+ "updateCommandType": "update_command must be a string",
+ "updateCommandLength": "update_command exceeds {max} chars",
+ "updateCommandNull": "update_command contains a null byte",
+ "updateMethod": "update_method must be none, helper or custom",
+ "updateCommandRequired": "update_command is required for update_method=custom",
+ "updateCommandOnlyCustom": "update_command is only allowed for update_method=custom",
+ "helperRequired": "a supported helper_slug is required for update_method=helper",
+ "dockerUpdater": "Docker-delegated apps update through their image, not a separate app updater",
+ "commandRequired": "command_argv is required (non-empty list)",
+ "commandList": "command_argv must be a non-empty list of strings",
+ "commandCount": "command_argv accepts at most {max} entries",
+ "commandEntry": "command_argv[{index}] must be a non-empty string",
+ "commandEntryLength": "command_argv[{index}] exceeds {max} chars",
+ "commandEntryControl": "command_argv[{index}] contains a forbidden control character",
+ "binaryArgsList": "binary_args must be a list of strings",
+ "binaryArgsCount": "binary_args accepts at most {max} entries",
+ "binaryArgEntry": "binary_args[{index}] must be a non-empty string",
+ "binaryArgLength": "binary_args[{index}] exceeds {max} chars",
+ "binaryArgControl": "binary_args[{index}] contains a forbidden control character",
+ "portsList": "ports must be a list of {port, description, scheme}",
+ "portObject": "ports[{index}] must be an object",
+ "portRequired": "ports[{index}].port is required",
+ "portInteger": "ports[{index}].port must be an integer",
+ "portRange": "ports[{index}].port must be 1-65535",
+ "portDuplicate": "port {port} appears more than once for this app",
+ "portDescription": "ports[{index}].description has invalid characters",
+ "portScheme": "ports[{index}].scheme must be 'http' or 'https'",
+ "portPath": "ports[{index}].web_path must be a valid URL path (max 255 chars)",
+ "portLogo": "ports[{index}].logo_url must be an http(s) URL (max 512 chars)",
+ "portCategory": "ports[{index}].category has invalid characters or is too long",
+ "portCustomUrl": "ports[{index}].custom_url must be an http(s) URL (max 512 chars)",
+ "tagRegexLength": "tag_regex exceeds 512 chars",
+ "persistPermission": "could not persist sidecar (permission?)",
+ "persist": "could not persist sidecar",
+ "noApps": "no apps registered for this vmid",
+ "appMissing": "app_id '{appId}' not found",
+ "appNotFound": "app not found"
+ },
"closePanel": "Close panel",
"cancelButton": "Cancel",
"saveButton": "Save",
diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json
index 18c9805b..0e7203ab 100644
--- a/AppImage/messages/sk/common.json
+++ b/AppImage/messages/sk/common.json
@@ -1545,7 +1545,128 @@
"loadFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo načítať."
},
"appEditor": {
- "methodOciImage": "OCI obraz (nainštalovaný ProxMenux)",
+ "confirmRemove": "Odobrať záznam aplikácie zo záložky Aplikácie? Samotná aplikácia zostane nainštalovaná.",
+ "packageExample": "napr. {package}",
+ "packageHelpBefore": "Doplní sa podľa názvu. Over ho príkazom",
+ "packageHelpAfter": "v kontajneri.",
+ "binaryHelpBefore": "Úplná cesta k programu. Nájdeš ju príkazom",
+ "binaryHelpOr": "alebo",
+ "errors": {
+ "ociBusy": "V tomto kontajneri práve prebieha operácia OCI.",
+ "dpkgMissing": "Balík {package} nie je nainštalovaný cez dpkg.",
+ "dpkgFailed": "Príkaz dpkg-query zlyhal.",
+ "dpkgNoVersion": "Vo výstupe dpkg sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
+ "apkFailed": "Príkaz apk info zlyhal.",
+ "apkNoVersion": "Vo výstupe apk sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
+ "fileRead": "Súbor {path} sa nepodarilo prečítať.",
+ "fileNoMatch": "Regulárny výraz file_regex nenašiel zhodu v súbore {path}.",
+ "binaryFailed": "Program sa nepodarilo spustiť.",
+ "binaryNoVersion": "Vo výstupe programu sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
+ "pythonFailed": "Zisťovanie verzie cez python -c importlib.metadata zlyhalo.",
+ "pythonNoVersion": "Vo výstupe balíka Pythonu sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
+ "dockerImageResolve": "Nainštalovaný obraz kontajnera sa nepodarilo určiť.",
+ "dockerInspectFailed": "Príkaz docker inspect zlyhal.",
+ "dockerLabelMissing": "Kontajner nemá štítok {label}.",
+ "dockerLabelMutable": "Štítok Dockeru uvádza {tag}. Je to premenlivý tag, ktorý neurčuje konkrétnu verziu.",
+ "dockerLabelNoVersion": "V štítku Dockeru sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
+ "dockerExecFailed": "Príkaz docker exec zlyhal.",
+ "dockerExecNoVersion": "Vo výstupe docker exec sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
+ "commandEmpty": "Príkaz a argumenty (command_argv) nie sú vyplnené.",
+ "commandFailed": "Príkaz zlyhal.",
+ "commandNoVersion": "Vo výstupe príkazu sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
+ "unsupportedMethod": "Nepodporovaný spôsob zisťovania verzie: {method}",
+ "unknownUpstream": "Neznámy zdroj dostupnej verzie (upstream_type): {type}",
+ "githubNotFound": "Repozitár {repo} sa nenašiel.",
+ "githubRejected": "GitHub odmietol požiadavku (403).",
+ "githubError": "Chyba GitHubu {code}.",
+ "noRelease": "Odpoveď neobsahuje tag ani názov vydania.",
+ "tagNoMatch": "Regulárny výraz tag_regex nenašiel zhodu s hodnotou „{tag}“.",
+ "httpError": "Chyba HTTP {code}.",
+ "invalidJson": "Odpoveď neobsahuje platný JSON: {detail}",
+ "jsonPathUnresolved": "Cesta v JSON „{path}“ sa nenašla.",
+ "jsonEmpty": "Na zadanej ceste v JSON je prázdna hodnota.",
+ "dockerNoTags": "Na Docker Hube sa nenašli tagy zodpovedajúce filtru.",
+ "dockerMovingOnly": "Filtru tag_regex zodpovedajú iba premenlivé tagy. Použi sledovanie obrazov Docker podľa digestu.",
+ "dockerHubError": "Chyba Docker Hubu {code}.",
+ "dockerHubJson": "Docker Hub nevrátil platný JSON: {detail}",
+ "dockerHubEmpty": "Docker Hub nevrátil žiadne tagy.",
+ "payload": "Údaje musia byť objekt JSON.",
+ "name": "Zadaj názov s dĺžkou 1 až 64 znakov. Povolené sú písmená, číslice, medzery a ._+-()/.",
+ "method": "Spôsob zisťovania verzie (installed_via) musí byť jedna z hodnôt: {values}, alebo môže zostať prázdny.",
+ "package": "Zadaj názov balíka (package), najviac 127 znakov. Povolené sú písmená, číslice a ._+@:/.",
+ "filePath": "Zadaj úplnú cestu k súboru (file_path), začínajúcu lomkou /.",
+ "fileRegex": "Zadaj regulárny výraz na získanie verzie zo súboru (file_regex).",
+ "fileRegexInvalid": "Regulárny výraz pre súbor (file_regex) je neplatný: {detail}",
+ "binaryPath": "Zadaj úplnú cestu k programu (binary_path), začínajúcu lomkou /.",
+ "pythonPath": "Zadaj úplnú cestu k interpretu Pythonu (python_path), začínajúcu lomkou /.",
+ "distribution": "Zadaj názov balíka Pythonu (distribution) podľa PEP 503.",
+ "container": "Zadaj názov kontajnera (container_name) podľa pravidiel Dockeru.",
+ "label": "Zadaj názov štítka (label) podľa pravidiel OCI.",
+ "dockerBinary": "Zadaj cestu k programu alebo samotný názov príkazu (binary_path).",
+ "manualVersion": "Pri ručnom zadávaní vyplň nainštalovanú verziu (installed_version).",
+ "versionLength": "Nainštalovaná verzia (installed_version) môže mať najviac {max} znakov.",
+ "versionControl": "Nainštalovaná verzia (installed_version) obsahuje nepovolený riadiaci znak.",
+ "installedRegexInvalid": "Regulárny výraz pre nainštalovanú verziu (installed_regex) je neplatný: {detail}",
+ "upstreamType": "Zdroj dostupnej verzie (upstream_type) musí byť jedna z hodnôt: {values}.",
+ "tagRegexInvalid": "Regulárny výraz pre tagy (tag_regex) je neplatný: {detail}",
+ "repoRequired": "Pre zdroj GitHub zadaj repozitár (repo).",
+ "repoFormat": "Repozitár (repo) musí mať tvar „vlastník/názov“ (owner/name).",
+ "githubSource": "Zdroj na GitHube (github_source) musí byť jedna z hodnôt: {values}.",
+ "upstreamUrlRequired": "Pre zdroj HTTP JSON zadaj adresu (upstream_url).",
+ "upstreamUrl": "Adresa zdroja (upstream_url) musí začínať na http:// alebo https://.",
+ "upstreamUrlLength": "Adresa zdroja (upstream_url) môže mať najviac {max} znakov.",
+ "jsonPathRequired": "Pre zdroj HTTP JSON zadaj cestu k verzii v JSON (upstream_json_path).",
+ "jsonPathLength": "Cesta k verzii v JSON (upstream_json_path) môže mať najviac {max} znakov.",
+ "jsonPathCharacters": "Cesta k verzii v JSON (upstream_json_path) obsahuje nepovolené znaky.",
+ "dockerImageRequired": "Pre zdroj Docker Hub zadaj názov obrazu (docker_image).",
+ "dockerImageLength": "Názov obrazu (docker_image) môže mať najviac {max} znakov.",
+ "dockerImageFormat": "Názov obrazu (docker_image) musí mať tvar owner/name alebo name podľa pravidiel Docker Hubu.",
+ "healthPath": "Cesta na kontrolu stavu (health_path) musí byť platná cesta URL s najviac 255 znakmi.",
+ "logoUrl": "Logo (logo_url) musí byť adresa HTTP/HTTPS alebo lokálna ikona uložená pre tento stroj.",
+ "discoveryId": "Identifikátor nájdenej aplikácie (discovery_id) je neplatný.",
+ "helperSlug": "Identifikátor inštalačného skriptu (helper_slug) môže obsahovať malé písmená, číslice a ._-.",
+ "updateVia": "Pole update_via podporuje iba hodnotu docker.",
+ "dockerDetector": "Pri update_via=docker vyber detektor docker_label alebo docker_exec.",
+ "dockerConflict": "Pole {field} nemožno použiť spolu s update_via=docker.",
+ "updateCommandType": "Aktualizačný príkaz (update_command) musí byť text.",
+ "updateCommandLength": "Aktualizačný príkaz (update_command) môže mať najviac {max} znakov.",
+ "updateCommandNull": "Aktualizačný príkaz (update_command) obsahuje nepovolený nulový znak.",
+ "updateMethod": "Spôsob aktualizácie (update_method) musí byť none, helper alebo custom.",
+ "updateCommandRequired": "Pre vlastnú aktualizáciu zadaj príkaz (update_command).",
+ "updateCommandOnlyCustom": "Aktualizačný príkaz (update_command) je povolený iba pri update_method=custom.",
+ "helperRequired": "Pre update_method=helper vyber podporovaný inštalačný skript (helper_slug).",
+ "dockerUpdater": "Aplikácia v Dockeri sa aktualizuje spolu s obrazom. Samostatný aktualizátor aplikácie tu nemožno nastaviť.",
+ "commandRequired": "Zadaj príkaz a jeho argumenty (command_argv) ako neprázdny zoznam.",
+ "commandList": "Príkaz a argumenty (command_argv) musia byť neprázdny zoznam textových hodnôt.",
+ "commandCount": "Príkaz a argumenty (command_argv) môžu mať najviac {max} položiek.",
+ "commandEntry": "Položka command_argv[{index}] musí obsahovať text.",
+ "commandEntryLength": "Položka command_argv[{index}] môže mať najviac {max} znakov.",
+ "commandEntryControl": "Položka command_argv[{index}] obsahuje nepovolený riadiaci znak.",
+ "binaryArgsList": "Argumenty programu (binary_args) musia byť zoznam textových hodnôt.",
+ "binaryArgsCount": "Argumenty programu (binary_args) môžu mať najviac {max} položiek.",
+ "binaryArgEntry": "Argument binary_args[{index}] musí obsahovať text.",
+ "binaryArgLength": "Argument binary_args[{index}] môže mať najviac {max} znakov.",
+ "binaryArgControl": "Argument binary_args[{index}] obsahuje nepovolený riadiaci znak.",
+ "portsList": "Webové odkazy (ports) musia byť zoznam položiek s poľami port, description a scheme.",
+ "portObject": "Webový odkaz ports[{index}] musí byť objekt.",
+ "portRequired": "Vo webovom odkaze ports[{index}] zadaj port.",
+ "portInteger": "Port vo webovom odkaze ports[{index}] musí byť celé číslo.",
+ "portRange": "Port vo webovom odkaze ports[{index}] musí byť od 1 do 65535.",
+ "portDuplicate": "Port {port} je pri tejto aplikácii uvedený viackrát.",
+ "portDescription": "Popis webového odkazu ports[{index}] obsahuje nepovolené znaky.",
+ "portScheme": "Protokol webového odkazu ports[{index}] musí byť http alebo https.",
+ "portPath": "Cesta webového odkazu ports[{index}].web_path musí byť platná cesta URL s najviac 255 znakmi.",
+ "portLogo": "Logo webového odkazu ports[{index}].logo_url musí byť adresa HTTP/HTTPS s najviac 512 znakmi.",
+ "portCategory": "Kategória webového odkazu ports[{index}].category obsahuje nepovolené znaky alebo je príliš dlhá.",
+ "portCustomUrl": "Vlastná adresa webového odkazu ports[{index}].custom_url musí byť adresa HTTP/HTTPS s najviac 512 znakmi.",
+ "tagRegexLength": "Regulárny výraz pre tagy (tag_regex) môže mať najviac 512 znakov.",
+ "persistPermission": "Evidenciu aplikácie sa nepodarilo uložiť. Skontroluj oprávnenia na zápis.",
+ "persist": "Evidenciu aplikácie sa nepodarilo uložiť.",
+ "noApps": "Pre tento stroj nie sú evidované žiadne aplikácie.",
+ "appMissing": "Záznam aplikácie „{appId}“ sa nenašiel.",
+ "appNotFound": "Záznam aplikácie sa nenašiel."
+ },
+ "methodOciImage": "OCI obraz (nainštaloval ProxMenux)",
"ociImageHelp": "Verzia aj dostupné aktualizácie sa určujú podľa obrazu, z ktorého bol tento kontajner vytvorený. Keďže ho nainštaloval ProxMenux, netreba nič nastavovať: nainštalovaný obraz sa porovnáva s obrazom, ktorý je dnes dostupný v jeho registri.",
"ociAppVersion": "Aplikácia",
"ociImage": "Obraz",
@@ -1585,7 +1706,7 @@
"installedRegexPlaceholder": "napr. v?(\\d+\\.\\d+\\.\\d+)",
"installedRegexAlt": "install_regex",
"regexCaptureGroupLabel": "Regex (so zachytávacou skupinou)",
- "regexPlaceholderVersion": "verzia:\\s*(\\d+\\.\\d+\\.\\d+)",
+ "regexPlaceholderVersion": "version:\\s*(\\d+\\.\\d+\\.\\d+)",
"optionalSuffix": "(voliteľné)",
"methodNone": "Žiadny (iba odkaz)",
"methodDpkg": "balík dpkg (Debian/Ubuntu)",
@@ -1606,12 +1727,12 @@
"binaryArgsPlaceholder": "napr. --version (oddelené čiarkou)",
"filePathLabel": "Cesta k súboru",
"commandLabel": "Príkaz (argv oddelené čiarkou, bez shellu)",
- "commandPlaceholder": "myapp, verzia, --json",
+ "commandPlaceholder": "myapp, version, --json",
"commandServerExample": "server, -v",
"containerNameLabel": "Názov kontajnera",
"containerNamePlaceholder": "napr. ",
- "ociLabelKeyLabel": "Kľúč OCI labelu",
- "pythonInterpreterLabel": "Cesta k Python interpreteru",
+ "ociLabelKeyLabel": "Kľúč štítka OCI",
+ "pythonInterpreterLabel": "Cesta k interpretu Pythonu",
"pythonInterpreterPlaceholder": "napr. /opt//venv/bin/python",
"pipDistLabel": "Názov distribúcie (pip balík)",
"pipDistPlaceholder": "napr. ",
@@ -1651,7 +1772,7 @@
"latestFromLabel": "Najnovšia verzia z",
"upToDateBadge": "Aktuálne",
"updateAvailableBadge": "Dostupná aktualizácia",
- "portDescriptionPlaceholder": "Popis (napr. Web UI, go2rtc, admin)",
+ "portDescriptionPlaceholder": "Popis (napr. webové rozhranie, go2rtc, správa)",
"portPortPlaceholder": "port",
"portHttp": "http",
"portHttps": "https",
@@ -1670,7 +1791,7 @@
"detectMethodDockerLabel": "štítok docker",
"detectMethodDockerExec": "docker exec",
"detectMethodCommand": "príkaz",
- "binaryVersionHint": "binárna verzia",
+ "binaryVersionHint": "program (výstup --version)",
"systemctlHint": "systemctl show -p ExecStart",
"whichHint": "ktorá ",
"apkInfoHint": "\" : \"informácie o apk | grep",
@@ -1721,7 +1842,7 @@
"detectorTestTitle": "Výsledok testu detektora",
"detectorTestNotSaved": "Iba koncept · neuložené",
"detectorTestNoVersion": "Nebola zistená žiadna verzia",
- "detectorTestNoUpstream": "Nie je nastavený zdroj upstream",
+ "detectorTestNoUpstream": "Nie je nastavený zdroj dostupnej verzie",
"detectorTestFailed": "Test detektora zlyhal",
"deleteFailed": "Odstránenie zlyhalo",
"dismissFailed": "Nájdenú aplikáciu sa nepodarilo skryť",
@@ -1768,17 +1889,17 @@
"addAnotherApplication": "Registrovať ďalšiu aplikáciu",
"doneButton": "Hotovo",
"editButton": "Upraviť",
- "upstreamErrorTimeout": "Časový limit siete pri kontaktovaní upstream",
+ "upstreamErrorTimeout": "Zdroj dostupnej verzie neodpovedal včas.",
"upstreamErrorNetwork": "Chyba siete: {detail}",
"upstreamErrorGithubRateLimit": "Bol dosiahnutý limit požiadaviek GitHubu. V časti Nastavenia → GitHub API nakonfigurujte voliteľný token alebo to skúste znova neskôr.",
"upstreamErrorRegistry": "Register obrazu sa nepodarilo kontaktovať. O niekoľko minút sa skontroluje znova.",
"upstreamErrorGeneric": "Kontrola proti prúdu zlyhala: {detail}",
- "notificationsEnabled": "Upozornenia na upstream aktualizácie sú ZAPNUTÉ – kliknutím ich stlmíte",
- "notificationsMuted": "Upstream upozornenia na aktualizácie MUTED – kliknutím aktivujete",
- "notifyUpstreamLabel": "Upozorniť ma, keď bude k dispozícii nová upstream verzia",
- "notifyUpstreamHelp": "Odošle `app_update_available` do kanálov povolených v Nastaveniach → Upozornenia. Vypnite, ak túto aplikáciu nie je možné aktualizovať na vašom boxe.",
+ "notificationsEnabled": "Upozornenia na nové verzie sú zapnuté — kliknutím ich stlmíš",
+ "notificationsMuted": "Upozornenia na nové verzie sú stlmené — kliknutím ich zapneš",
+ "notifyUpstreamLabel": "Upozorniť ma na novú dostupnú verziu",
+ "notifyUpstreamHelp": "Odošle upozornenie app_update_available do kanálov zapnutých v Nastaveniach → Upozornenia. Vypni, ak túto aplikáciu na svojom serveri nemôžeš aktualizovať.",
"excludeFromBadgeLabel": "Vylúčiť z počítadla aktualizácií LXC",
- "excludeFromBadgeHelp": "Nezapočítajte túto aplikáciu do odznaku súhrnných aktualizácií na karte zoznamu LXC.Užitočné, keď ste úmyselne pripnutý ku konkrétnej verzii (požiadavka na sledovanie, zmrazenie kompatibility).Nemá vplyv na vlastný stav karty Aplikácia ani na odchádzajúce upozornenie.",
+ "excludeFromBadgeHelp": "Nezapočíta túto aplikáciu do počtu aktualizácií v zozname LXC. Hodí sa, ak chceš zámerne zostať pri konkrétnej verzii, napríklad kvôli kompatibilite. Stav na záložke Aplikácie aj upozornenia zostanú nezmenené.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"stackMemberTitle": "Časť aplikácie s viacerými kontajnermi",
"stackMemberBody": "Tento kontajner podporuje aplikáciu, ktorej hlavný kontajner je {primary}. Zobrazuje sa tam aplikácia, jej webové odkazy a jej verzia."
diff --git a/tests/lxc_updates/test_app_editor_errors.cjs b/tests/lxc_updates/test_app_editor_errors.cjs
new file mode 100644
index 00000000..dd1f5e52
--- /dev/null
+++ b/tests/lxc_updates/test_app_editor_errors.cjs
@@ -0,0 +1,104 @@
+// Real transport templates and actual fetchApi, with no network/guest access.
+const assert = require('node:assert/strict')
+const fs = require('node:fs')
+const path = require('node:path')
+const vm = require('node:vm')
+const {execFileSync} = require('node:child_process')
+const root = path.resolve(__dirname, '../..')
+const app = path.join(root, 'AppImage')
+const ts = require(path.join(app, 'node_modules/typescript'))
+function load(relative, extra = {}) {
+ const compiled = ts.transpileModule(fs.readFileSync(path.join(app, relative), 'utf8'), {
+ compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020}, reportDiagnostics: true,
+ })
+ assert.deepEqual(compiled.diagnostics, [])
+ const context = {exports: {}, process: {env: {}}, console: {error() {}}, ...extra}
+ vm.runInNewContext(compiled.outputText, context)
+ return context.exports
+}
+const {localizeAppEditorError: localize} = load('lib/lxc-app-editor-errors.ts')
+const catalogs = Object.fromEntries(['en', 'sk', 'de'].map(locale => [locale,
+ JSON.parse(fs.readFileSync(path.join(app, `messages/${locale}/common.json`)))]))
+const translate = locale => (key, params = {}) => {
+ const lookup = data => key.split('.').reduce((item, segment) => item?.[segment], data)
+ const template = lookup(catalogs[locale]) ?? lookup(catalogs.en)
+ assert.equal(typeof template, 'string', 'Missing translation ' + key)
+ return template.replace(/\{(\w+)\}/g, (all, key) => params[key] ?? all)
+}
+const en = translate('en'), sk = translate('sk')
+// Enumerate only pure validator ASTs; importing lxc_apps would access host state.
+const contract = JSON.parse(execFileSync('python3', ['-c', String.raw`
+import ast,json,re,typing,sys
+tree=ast.parse(open(sys.argv[1]).read())
+names={'_err','_validate_command_argv','_validate_binary_args','_validate_ports','validate_config'}
+ns={'re':re,'Any':typing.Any,'Optional':typing.Optional}
+for node in tree.body:
+ if isinstance(node,ast.Assign) and len(node.targets)==1 and isinstance(node.targets[0],ast.Name) and node.targets[0].id.startswith('_'):
+ try: value=ast.literal_eval(node.value)
+ except (ValueError,TypeError):
+ if not (isinstance(node.value,ast.Call) and isinstance(node.value.func,ast.Attribute) and isinstance(node.value.func.value,ast.Name) and node.value.func.value.id=='re' and node.value.func.attr=='compile'): continue
+ args=[getattr(re,a.attr) if isinstance(a,ast.Attribute) and isinstance(a.value,ast.Name) and a.value.id=='re' else ast.literal_eval(a) for a in node.value.args]
+ value=re.compile(*args)
+ ns[node.targets[0].id]=value
+functions=[n for n in tree.body if isinstance(n,ast.FunctionDef) and n.name in names]
+exec(compile(ast.Module(body=functions,type_ignores=[]),'','exec'),ns)
+messages=set()
+context={**ns,'i':3,'p':8080,'field':'repo','e':'missing ], position 4\npattern: v?(test'}
+for function in functions:
+ for n in ast.walk(function):
+ if isinstance(n,ast.Call) and isinstance(n.func,ast.Name) and n.func.id=='_err' and n.args and isinstance(n.args[0],(ast.Constant,ast.JoinedStr)):
+ messages.add(eval(compile(ast.Expression(n.args[0]),'','eval'),context))
+cases=[[],{}, {'name':'Test','installed_via':'file','file_path':'relative'},
+ {'name':'Test','installed_via':'file','file_path':'/fiction/version','file_regex':'['},
+ {'name':'Test','installed_via':'command','command_argv':['test','bad\narg']},
+ {'name':'Test','ports':[{'port':8080},{'port':8080}]},
+ {'name':'Test','installed_via':'manual'}]
+errors=[]
+for case in cases:
+ ok,error=ns['validate_config'](case)
+ assert not ok
+ errors.append(error)
+print(json.dumps({'templates':sorted(messages),'actual':errors}))
+`, path.join(app, 'scripts/lxc_apps.py')], {encoding: 'utf8'}))
+assert.equal(contract.templates.length, 69, 'Update the contract inventory when validation changes')
+for (const message of [...contract.templates, ...contract.actual]) {
+ assert.equal(localize(message, en), message, 'EN must preserve the exact backend message')
+ assert.notEqual(localize(message, sk), message, 'Untranslated backend validation: ' + message)
+ assert.equal(localize(message, translate('de')), message, 'Missing locale falls back to EN')
+}
+// Every supported message, including detector errors, preserves interpolated data.
+const enErrors = catalogs.en.vmLxc.appEditor.errors, skErrors = catalogs.sk.vmLxc.appEditor.errors
+assert.deepEqual(Object.keys(enErrors).sort(), Object.keys(skErrors).sort())
+for (const [key, template] of Object.entries(enErrors)) {
+ const params = Object.fromEntries([...template.matchAll(/\{(\w+)\}/g)].map(([, name]) => [name,
+ ['index','port','max','code'].includes(name) ? '17' : "detail /opt/app --version {literal} \nnext line"]))
+ const message = en(`vmLxc.appEditor.errors.${key}`, params)
+ assert.equal(localize(message, en), message)
+ assert.equal(localize(message, sk), sk(`vmLxc.appEditor.errors.${key}`, params), key)
+}
+assert.equal(localize('binary_args[4] exceeds 256 chars', sk), 'Argument binary_args[4] môže mať najviac 256 znakov.')
+assert.equal(localize('tag_regex is not a valid regex: [bad]\nposition 4', sk), 'Regulárny výraz pre tagy (tag_regex) je neplatný: [bad]\nposition 4')
+for (const message of ['unknown /opt/app --version', '', 'file_regex is required extra', 'prefix file_regex is required', 'binary_args[-1] exceeds 4 chars']) {
+ assert.equal(localize(message, sk), message, 'Unknown detail must remain intact')
+}
+for (const empty of [null, undefined, '']) assert.equal(localize(empty, sk), '')
+assert.equal(catalogs.sk.vmLxc.appEditor.commandPlaceholder, catalogs.en.vmLxc.appEditor.commandPlaceholder)
+assert.equal(catalogs.sk.vmLxc.appEditor.regexPlaceholderVersion, catalogs.en.vmLxc.appEditor.regexPlaceholderVersion)
+async function main() {
+ let requests = 0
+ const backend = 'file_path is required and must be an absolute path'
+ const {fetchApi} = load('lib/api-config.ts', {fetch: async () => {
+ requests++
+ return {ok: false, status: 400, statusText: 'BAD REQUEST', headers: {get: () => 'application/json'}, json: async () => ({error: backend})}
+ }})
+ for (const [method, route] of [['POST','/api/vms/999999/apps'], ['PUT','/api/vms/999999/apps/fake'], ['POST','/api/vms/999999/apps/test']]) {
+ try {await fetchApi(route, {method, body: '{}'}); assert.fail('Expected validation failure')}
+ catch (error) {
+ assert.equal(error.message, backend)
+ assert.equal(localize(error.message, sk), 'Zadaj úplnú cestu k súboru (file_path), začínajúcu lomkou /.')
+ }
+ }
+ assert.equal(requests, 3)
+ console.log(`PASS app editor: ${contract.templates.length} backend validation templates, ${Object.keys(enErrors).length} EN/SK rules, real fetchApi POST/PUT/test fixtures; external calls 0`)
+}
+main().catch(error => {console.error(error); process.exitCode = 1})