From 6b41ff4247fbe692e6169cc56646992de8e532d2 Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:43:40 +0000 Subject: [PATCH 1/3] i18n: complete Slovak Monitor and CLI messages --- .github/scripts/run_offline_qualified.py | 5 + .../scripts/tests/test_terminal_catalogs.py | 34 ++- AppImage/components/notification-settings.tsx | 3 +- AppImage/components/settings.tsx | 11 +- AppImage/lib/api-config.ts | 16 +- AppImage/lib/api-errors.ts | 17 ++ AppImage/lib/optimization-descriptions.ts | 18 ++ AppImage/messages/en/common.json | 79 +++++- AppImage/messages/sk/common.json | 91 +++++- lang/sk.json | 120 +++++++- .../update_post_install_function.sh | 37 +-- scripts/utilities/uup_dump_iso_creator.sh | 28 +- scripts/vm/synology.sh | 262 +++++++++--------- .../cli-before-i18n/synology-display.sh | 141 ++++++++++ .../update_post_install_function.sh | 200 +++++++++++++ .../uup_dump_iso_creator-display.sh | 23 ++ tests/optimization-settings-fixture.cjs | 61 ++++ tests/test_api_errors.cjs | 66 +++++ tests/test_api_import_boundaries.cjs | 18 ++ tests/test_optimization_labels.cjs | 46 +++ tests/test_post_install_i18n.py | 46 +++ tests/test_remaining_cli_i18n.py | 75 +++++ tests/ts-fixture.cjs | 19 ++ 23 files changed, 1228 insertions(+), 188 deletions(-) create mode 100644 AppImage/lib/api-errors.ts create mode 100644 AppImage/lib/optimization-descriptions.ts create mode 100644 tests/fixtures/cli-before-i18n/synology-display.sh create mode 100644 tests/fixtures/cli-before-i18n/update_post_install_function.sh create mode 100644 tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh create mode 100644 tests/optimization-settings-fixture.cjs create mode 100644 tests/test_api_errors.cjs create mode 100644 tests/test_api_import_boundaries.cjs create mode 100644 tests/test_optimization_labels.cjs create mode 100644 tests/test_post_install_i18n.py create mode 100644 tests/test_remaining_cli_i18n.py create mode 100644 tests/ts-fixture.cjs diff --git a/.github/scripts/run_offline_qualified.py b/.github/scripts/run_offline_qualified.py index 8a9548cd..5e291922 100644 --- a/.github/scripts/run_offline_qualified.py +++ b/.github/scripts/run_offline_qualified.py @@ -13,6 +13,8 @@ import sys import tempfile PYTHON_FILES = ( + "tests/test_post_install_i18n.py", + "tests/test_remaining_cli_i18n.py", "AppImage/scripts/tests/test_local_app_icon_isolation.py", "AppImage/scripts/tests/test_local_app_icons.py", "AppImage/scripts/tests/test_docker_app_discovery.py", @@ -28,6 +30,9 @@ PYTHON_FILES = ( "AppImage/scripts/tests/test_health_persistence_recurrence.py", ) NODE_FILES = ( + "tests/test_api_import_boundaries.cjs", + "tests/test_api_errors.cjs", + "tests/test_optimization_labels.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/.github/scripts/tests/test_terminal_catalogs.py b/.github/scripts/tests/test_terminal_catalogs.py index f74966cb..a69aedd8 100644 --- a/.github/scripts/tests/test_terminal_catalogs.py +++ b/.github/scripts/tests/test_terminal_catalogs.py @@ -69,13 +69,43 @@ class TerminalCatalogsTests(unittest.TestCase): def test_shipped_active_catalogs_keep_literal_contracts(self): result = subprocess.run( [sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'), - '--root', str(ROOT), '--require-complete'], + '--root', str(ROOT)], capture_output=True, text=True, timeout=60, ) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) for locale in MODULE.DEFAULT_LANGUAGES: self.assertIn(f'[{locale}] active=', result.stdout) - self.assertNotIn('missing=1', result.stdout) + # New hand-reviewed Slovak keys ship with their source changes; + # other locales may use the runtime English fallback until translated. + result = subprocess.run( + [sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'), + '--root', str(ROOT), '--languages', 'sk', '--require-complete'], + capture_output=True, text=True, timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('missing=0', result.stdout) + + def test_optional_completeness_still_rejects_invalid_present_translations(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / 'scripts').mkdir() + (root / 'lang').mkdir() + (root / 'scripts/check.sh').write_text('translate "Check /etc/pve/storage.cfg"') + (root / 'lang/sk.json').write_text(json.dumps({'Check /etc/pve/storage.cfg': 'Skontrolujte /etc/pve/storage.cfg'})) + (root / 'lang/es.json').write_text('{}') + + def check(languages, *flags): + return subprocess.run( + [sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'), + '--root', str(root), '--languages', languages, *flags], + capture_output=True, text=True, timeout=60, + ) + + self.assertEqual(check('es,sk').returncode, 0) + self.assertEqual(check('sk', '--require-complete').returncode, 0) + self.assertNotEqual(check('es', '--require-complete').returncode, 0) + (root / 'lang/es.json').write_text(json.dumps({'Check /etc/pve/storage.cfg': 'Compruebe /etc/PVE/storage.cfg'})) + self.assertNotEqual(check('es,sk').returncode, 0) if __name__ == '__main__': diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index 194d5574..962ac8d3 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -1530,7 +1530,7 @@ export function NotificationSettings() { updateChannel("telegram", "bot_token", e.target.value)} disabled={!editMode} @@ -1542,6 +1542,7 @@ export function NotificationSettings() { {showSecrets["tg_token"] ? : } +

{t("settings.notifications.ui.botTokenHelp")}

diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 913e4c86..8aa9ffa8 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -9,6 +9,7 @@ import { NotificationSettings } from "./notification-settings" import { HealthThresholds } from "./health-thresholds" import { LxcUpdateDetection } from "./lxc-update-detection" import { DiskExclusions } from "./disk-exclusions" +import { optimizationDescription, optimizationToolName } from "../lib/optimization-descriptions" import { ScriptTerminalModal } from "./script-terminal-modal" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Switch } from "./ui/switch" @@ -580,7 +581,7 @@ export function Settings() { if (entries.length === 0) return const batch = entries.map(e => `${e.source}:${e.function}:${e.key}`).join("\n") const title = entries.length === 1 - ? t("settings.optimizations.updateOneTitle", { name: entries[0].name }) + ? t("settings.optimizations.updateOneTitle", { name: optimizationToolName(entries[0].key, entries[0].name, t) }) : t("settings.optimizations.updateManyTitle", { count: entries.length }) const description = entries.length === 1 ? t("settings.optimizations.updateOneDescription", { functionName: entries[0].function, source: entries[0].source }) @@ -2117,7 +2118,7 @@ export function Settings() {
- {tool.name} + {optimizationToolName(tool.key, tool.name, t)} {isDeprecated && ( {t("settings.optimizations.legacy")} @@ -2133,7 +2134,7 @@ export function Settings() { @@ -2289,13 +2290,13 @@ export function Settings() { />
- {tool.name} + {optimizationToolName(tool.key, tool.name, t)} v{tool.version || '1.0'} → v{tool.available_version || '?'}
{tool.description && ( -

{tool.description}

+

{optimizationDescription(tool.description, t)}

)}
diff --git a/AppImage/lib/api-config.ts b/AppImage/lib/api-config.ts index 04af2cbf..543418d7 100644 --- a/AppImage/lib/api-config.ts +++ b/AppImage/lib/api-config.ts @@ -2,6 +2,12 @@ * API Configuration for ProxMenux Monitor * Handles API URL generation with automatic proxy detection */ +// Resolve wording only for a generic transport failure. Normal responses and +// backend error payloads keep the transport free of locale-loading work. +async function apiFailure(...args: Parameters): Promise { + const messages = await import("./api-errors") + return messages.apiFailure(...args) +} /** * API Server Port Configuration @@ -114,7 +120,7 @@ export async function fetchApi(endpoint: string, options?: RequestInit): Prom hadToken = true } if (!hadToken) { - throw new Error(`Unauthorized: ${endpoint}`) + throw new Error(await apiFailure("unauthorized", { endpoint })) } try { localStorage.removeItem("proxmenux-auth-token") @@ -131,7 +137,7 @@ export async function fetchApi(endpoint: string, options?: RequestInit): Prom window.location.reload() } } - throw new Error(`Unauthorized: ${endpoint}`) + throw new Error(await apiFailure("unauthorized", { endpoint })) } // Try to surface the backend's JSON error payload instead of a // bare `500 INTERNAL SERVER ERROR`. The Flask routes consistently @@ -168,7 +174,7 @@ export async function fetchApi(endpoint: string, options?: RequestInit): Prom throw parseErr } } - throw new Error(`API request failed: ${response.status} ${response.statusText}`) + throw new Error(await apiFailure("http", { status: response.status, statusText: response.statusText })) } // Check content type to ensure we're getting JSON @@ -176,13 +182,13 @@ export async function fetchApi(endpoint: string, options?: RequestInit): Prom if (!contentType || !contentType.includes("application/json")) { const text = await response.text() console.error("fetchApi: Expected JSON but got:", contentType, "- Body preview:", text.substring(0, 200)) - throw new Error(`Expected JSON response but got ${contentType || "unknown content type"}`) + throw new Error(await apiFailure("unexpectedContentType", { contentType: contentType || await apiFailure("unknownContentType") })) } try { return await response.json() } catch (jsonError) { console.error("fetchApi: JSON parse error for", endpoint, "-", jsonError) - throw new Error(`Invalid JSON response from ${endpoint}`) + throw new Error(await apiFailure("invalidJson", { endpoint })) } } diff --git a/AppImage/lib/api-errors.ts b/AppImage/lib/api-errors.ts new file mode 100644 index 00000000..7e995dee --- /dev/null +++ b/AppImage/lib/api-errors.ts @@ -0,0 +1,17 @@ +import enMessages from "../messages/en/common.json" +import skMessages from "../messages/sk/common.json" +import { detectBrowserLanguage, LANGUAGE_STORAGE_KEY } from "./i18n/languages" + +type Failure = keyof typeof enMessages.apiErrors + +// fetchApi also runs outside React hooks. Read the provider's current document +// language, with its stored/browser preference as a fallback. +export function apiFailure(key: Failure, params: Record = {}): string { + let language = typeof document === "undefined" ? "" : document.documentElement?.lang + if (!language && typeof window !== "undefined") { + try { language = window.localStorage.getItem(LANGUAGE_STORAGE_KEY) || "" } catch { /* private browsing */ } + } + language ||= detectBrowserLanguage() + const messages = language === "sk" ? skMessages.apiErrors : enMessages.apiErrors + return messages[key].replace(/\{(\w+)\}/g, (match, name) => params[name] === undefined ? match : String(params[name])) +} diff --git a/AppImage/lib/optimization-descriptions.ts b/AppImage/lib/optimization-descriptions.ts new file mode 100644 index 00000000..a01fc3aa --- /dev/null +++ b/AppImage/lib/optimization-descriptions.ts @@ -0,0 +1,18 @@ +import enMessages from "../messages/en/common.json" + +type Translate = (key: string, params?: Record) => string + +// Descriptions are live server metadata. Translate only exact known wording; +// a newer script description must remain visible rather than become stale text. +export function optimizationDescription(description: string, t: Translate): string { + const entries = Object.entries(enMessages.settings.optimizations.toolDescriptions) + const match = entries.find(([, english]) => english === description) + return match ? t(`settings.optimizations.toolDescriptions.${match[0]}`) : description +} + +// Unknown tools retain the server's name until their key is translated. +export function optimizationToolName(tool: string, fallback: string, t: Translate): string { + return Object.prototype.hasOwnProperty.call(enMessages.settings.optimizations.toolNames, tool) + ? t(`settings.optimizations.toolNames.${tool}`) + : fallback +} diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index cb93dc90..08fe0147 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -2231,7 +2231,9 @@ "inactiveFooter": "Enable notifications to receive system, health and security alerts through the configured channels.", "notificationLanguage": "Notification language", "selectNotificationLanguage": "Select notification language", - "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled." + "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled.", + "botTokenPlaceholder": "7595377878:AAGE6Fb2cy...", + "botTokenHelp": "The token can include the 'bot' prefix or be entered without it." }, "ai": { "title": "AI enhancement", @@ -2373,7 +2375,73 @@ "availableUpdates": "Available updates", "availableUpdatesDescription": "{count} optimizations can be updated to a newer version.", "selectedCount": "{selected} of {total} selected", - "updateSelected": "Update selected" + "updateSelected": "Update selected", + "toolDescriptions": { + "subscriptionAuto": "Patch the Proxmox web UI to suppress the subscription dialog and register a successful patch.", + "timeSync": "Detect timezone from public IP and enable systemd time sync (NTP).", + "aptLanguages": "Stop APT from downloading translation files to speed up updates.", + "journald": "Cap journald size, raise rate limit and force info-level logging so the log viewer and Fail2Ban work.", + "logrotate": "Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate).", + "systemLimits": "Raise inotify watches, file descriptors, process keys and PID limits to enterprise levels.", + "memory": "Tune swappiness, dirty page ratios and compaction proactiveness for VM hosts without overriding the kernel's memory-overcommit policy.", + "kernelPanic": "Auto-reboot on kernel panic / oops / hardlockup; write crash dumps to /var/crash.", + "aptIpv4": "Force APT to use IPv4 to avoid stalls on hosts with broken IPv6 connectivity.", + "network": "Tune TCP buffers, somaxconn, IPv4 hardening and disable rp_filter on fw bridges (PVE 9 compatible).", + "bashrc": "Install and safely migrate the managed ProxMenux Bash prompt and aliases while preserving or selecting the short/full working-directory style.", + "log2ramAuto": "Install Log2RAM with size auto-tuned to host RAM (128M/256M/512M); SSD/M.2 detection skips on rotational disks.", + "zfsAutotrim": "Enable ZFS autotrim on detected pools and record only pools changed by ProxMenux.", + "persistentNetwork": "Pin NIC names to MAC addresses via systemd .link files so kernel updates don't shuffle interface names.", + "kexec": "Install kexec-tools and add a Ctrl+Alt+K hotkey + systemd unit for fast reboots that skip BIOS/POST.", + "amd": "Detect AMD EPYC/Ryzen CPUs and apply microcode + IOMMU + KVM-specific kernel boot params.", + "openVSwitch": "Install OpenVSwitch for software-defined networking inside VMs and containers.", + "tcpBbr": "Enable TCP Fast Open (clients + server) and BBR congestion control for better latency under load.", + "ceph": "Install Ceph (client + server packages) for distributed RBD/CephFS storage; PVE 8/9 aware repo selection.", + "zfsArc": "Cap ZFS ARC max using Proxmox VE's 10%-of-RAM policy (16 GiB ceiling), safely reconcile conflicting module settings and report the pool-size guideline before applying it.", + "zfsSnapshots": "Install zfs-auto-snapshot with cron schedules for hourly/daily/weekly/monthly snapshots.", + "rpc": "Disable rpcbind service/socket while preserving their exact previous systemd state for rollback.", + "pigz": "Replace gzip with pigz (parallel implementation) for faster vzdump backup compression.", + "guestAgent": "Detect the host's hypervisor (qemu/vmware/hyperv/virtualbox) and install the matching guest agent.", + "vfio": "Enable IOMMU and load VFIO modules to allow GPU/PCI passthrough into VMs.", + "motd": "Add the ProxMenux MOTD banner while preserving the original file contents or absence for rollback.", + "subscriptionCustom": "Patch the Proxmox web UI to suppress the \"no valid subscription\" dialog (PVE 8 + 9 variants supported).", + "zram": "Enable an opt-in ZRAM swap profile sized to one tenth of RAM.", + "backupLimits": "Lift vzdump bandwidth/IO limits so backups run at the storage's real throughput.", + "ovh": "Detect OVH-rented hardware via whois lookup and install OVH Real-Time Monitoring (no-op on non-OVH).", + "ha": "Enable the Proxmox HA stack (pve-ha-lrm, pve-ha-crm, corosync) for cluster failover.", + "fastfetch": "Install Fastfetch system summary tool with the ProxMenux logo + status block as the SSH login banner.", + "figurine": "Install Figurine (ASCII-art hostname banner) and wire it into the SSH login flow.", + "log2ramCustom": "Install Log2RAM with user-chosen RAM size; prompts for size and SSD/M.2 awareness before applying.", + "systemUtilities": "Install selected system utilities and track only packages that were newly added by ProxMenux." + }, + "toolNames": { + "subscription_banner": "Subscription Banner Removal", + "time_sync": "Time Synchronization", + "apt_languages": "APT Language Skip", + "journald": "Journald Optimization", + "logrotate": "Logrotate Optimization", + "system_limits": "System Limits Increase", + "memory_settings": "Memory Settings Optimization", + "zram_swap": "ZRAM Swap Profile", + "kernel_panic": "Kernel Panic Configuration", + "apt_ipv4": "APT IPv4 Force", + "kexec": "kexec for quick reboots", + "rpc": "RPC / rpcbind Disable", + "motd": "Custom MOTD Banner", + "system_utils": "System Utilities", + "network_optimization": "Network Optimizations", + "bashrc_custom": "Bashrc Customization", + "figurine": "Figurine", + "fastfetch": "Fastfetch", + "log2ram": "Log2ram (SSD Protection)", + "zfs_autotrim": "ZFS Autotrim", + "amd_fixes": "AMD CPU (Ryzen/EPYC) fixes", + "persistent_network": "Setting persistent network interfaces", + "vfio_iommu": "VFIO/IOMMU Passthrough", + "lvm_repair": "LVM PV Headers Repair", + "repo_cleanup": "Repository Cleanup", + "proxmox_repos": "Proxmox APT Repositories", + "entropy": "Entropy Generation (haveged)" + } }, "healthThresholds": { "title": "Health Monitor Thresholds", @@ -6617,5 +6685,12 @@ "pbsWebInterface": "PBS web interface", "changeAddress": "Change address", "cancel": "Cancel" + }, + "apiErrors": { + "http": "API request failed: {status} {statusText}", + "unexpectedContentType": "Expected JSON response but got {contentType}", + "unknownContentType": "unknown content type", + "invalidJson": "Invalid JSON response from {endpoint}", + "unauthorized": "Unauthorized: {endpoint}" } } diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json index 18c9805b..f1479b4b 100644 --- a/AppImage/messages/sk/common.json +++ b/AppImage/messages/sk/common.json @@ -820,7 +820,7 @@ "idleTitle": "Nečítaj, kým ho nič nepoužíva, aby sa to mohlo roztočiť. Jeho teplota sa znova zobrazí, akonáhle sa disk používa.", "diskExcluded": "Vylúčené", "diskExcludedTitle": "Vylúčené z periodických čítania v Nastaveniach.", - "temperatureNoDisks": "No disks", + "temperatureNoDisks": "Žiadne disky", "temperatureNormal": "Teploty normálne", "temperatureHighCount": "Vysoké teploty: {count}", "temperatureCriticalCount": "Kritické teploty: {count}", @@ -2156,7 +2156,9 @@ "appriseSlackDocs": "Formáty URL a nastavenie Slack", "appriseMatrixSteps": "Použite účet pre oznámenia, ktorý môže odosielať správy do miestnosti. Nahraďte vzorového používateľa, heslo, domovský server a úplné ID miestnosti; špeciálne znaky v prihlasovacích údajoch zakódujte pre URL. Schéma matrixs používa HTTPS. Podporu tokenov, SSO a šifrovaných miestností v nainštalovanej verzii Apprise nájdete v dokumentácii.", "appriseMatrixDocs": "Formáty URL a nastavenie Matrix", - "appriseServiceFinish": "Povoľte Apprise, zadajte svoju URL, vyberte kategórie oznámení alebo jednotlivé udalosti, uložte a potom použite Odoslať test. Tieto príklady nemenia uloženú URL. Tento kanál Apprise má jeden cieľ; pre viac služieb naraz použite API Apprise." + "appriseServiceFinish": "Povoľte Apprise, zadajte svoju URL, vyberte kategórie oznámení alebo jednotlivé udalosti, uložte a potom použite Odoslať test. Tieto príklady nemenia uloženú URL. Tento kanál Apprise má jeden cieľ; pre viac služieb naraz použite API Apprise.", + "botTokenPlaceholder": "7595377878:AAGE6Fb2cy...", + "botTokenHelp": "Token môže byť s prefixom „bot“ alebo bez neho." }, "ai": { "title": "AI vylepšenie", @@ -2284,21 +2286,87 @@ "emptyTitle": "Zatiaľ nie sú nainštalované žiadne systémové optimalizácie", "emptyDescription": "Spustite ProxMenux a nastavte systémové optimalizácie", "installedTools": "Nainštalované nástroje", - "activeCount": "{count} aktívne", + "activeCount": "Aktívne: {count}", "viewUpdates": "Zobraziť dostupné aktualizácie", - "updateCount": "{count} aktualizácií", + "updateCount": "Aktualizácie: {count}", "updateOneTitle": "Aktualizovať: {name}", - "updateManyTitle": "Aktualizovať {count} optimalizácií", + "updateManyTitle": "Aktualizácia optimalizácií ({count})", "updateOneDescription": "Znova sa spustí {functionName} z toku {source}.", - "updateManyDescription": "Postupne sa spustí {count} post-install funkcií.", + "updateManyDescription": "Počet funkcií, ktoré sa postupne spustia: {count}.", "legacySourceTitle": "Staršia optimalizácia - kliknutím zobrazíte zdroj", "sourceTitle": "Kliknutím zobrazíte zdrojový kód", "legacy": "staršie", "updateToolTitle": "Aktualizovať {name} na v{version}", "availableUpdates": "Dostupné aktualizácie", - "availableUpdatesDescription": "{count} optimalizácií sa dá aktualizovať na novšiu verziu.", + "availableUpdatesDescription": "Nástroje s dostupnou novšou verziou: {count}.", "selectedCount": "Vybrané {selected} z {total}", - "updateSelected": "Aktualizovať vybrané" + "updateSelected": "Aktualizovať vybrané", + "toolDescriptions": { + "subscriptionAuto": "Upraví web Proxmoxu tak, aby nezobrazoval upozornenie na predplatné, a zaznamená úspešnú úpravu.", + "timeSync": "Zistí časové pásmo podľa verejnej IP adresy a zapne synchronizáciu času cez systemd (NTP).", + "aptLanguages": "Vypne sťahovanie prekladových súborov APT a zrýchli aktualizácie.", + "journald": "Obmedzí veľkosť journald, zvýši limit správ a zapne úroveň info, aby fungoval prehliadač záznamov aj Fail2Ban.", + "logrotate": "Nahradí logrotate.conf profilom vhodným pre Log2RAM (denná rotácia, copytruncate).", + "systemLimits": "Zvýši limity sledovaní inotify, otvorených súborov, kľúčov procesov a PID pre náročnú prevádzku.", + "memory": "Upraví swappiness, podiel neuložených stránok a aktívne zhusťovanie pamäte pre VM servery bez zmeny pravidiel jadra pre nadmerné prideľovanie pamäte.", + "kernelPanic": "Zapne automatický reštart pri kernel panic, oops alebo hardlockup a uloží výpisy pádu do /var/crash.", + "aptIpv4": "Vynúti IPv4 pre APT, aby sa predišlo čakaniu pri nefunkčnom pripojení IPv6.", + "network": "Upraví TCP vyrovnávacie pamäte, somaxconn a zabezpečenie IPv4; vypne rp_filter na firewallových mostoch (kompatibilné s PVE 9).", + "bashrc": "Nainštaluje a bezpečne prenesie spravovaný Bash prompt a aliasy ProxMenuxu so zachovaním alebo výberom krátkej či úplnej cesty pracovného priečinka.", + "log2ramAuto": "Nainštaluje Log2RAM s veľkosťou podľa RAM servera (128M/256M/512M); pri rotačných diskoch vynechá detekciu SSD/M.2.", + "zfsAutotrim": "Zapne ZFS autotrim na nájdených pooloch a zaznamená iba pooly zmenené ProxMenuxom.", + "persistentNetwork": "Priradí názvy sieťových rozhraní k MAC adresám cez súbory systemd .link, aby ich aktualizácie jadra nemenili.", + "kexec": "Nainštaluje kexec-tools a pridá skratku Ctrl+Alt+K aj službu systemd na rýchly reštart s vynechaním BIOS/POST.", + "amd": "Rozpozná procesory AMD EPYC/Ryzen a nastaví mikrokód aj parametre jadra pre IOMMU a KVM.", + "openVSwitch": "Nainštaluje OpenVSwitch na softvérovo riadené siete vo VM a kontajneroch.", + "tcpBbr": "Zapne TCP Fast Open pre klienta aj server a riadenie zahltenia BBR na zníženie oneskorenia pri záťaži.", + "ceph": "Nainštaluje klientské aj serverové balíky Ceph pre úložisko RBD/CephFS; repozitár vyberie podľa PVE 8/9.", + "zfsArc": "Obmedzí ZFS ARC podľa pravidla Proxmox VE na 10 % RAM (najviac 16 GiB), bezpečne zosúladí konfliktné nastavenia modulu a pred použitím zobrazí odporúčanie podľa veľkosti poolu.", + "zfsSnapshots": "Nainštaluje zfs-auto-snapshot s hodinovými, dennými, týždennými a mesačnými plánmi v crone.", + "rpc": "Vypne službu aj socket rpcbind a zachová ich presný pôvodný stav systemd na prípadnú obnovu.", + "pigz": "Nahradí gzip paralelným pigz na rýchlejšiu kompresiu záloh vzdump.", + "guestAgent": "Rozpozná hypervízor servera (qemu/vmware/hyperv/virtualbox) a nainštaluje príslušného hosťovského agenta.", + "vfio": "Zapne IOMMU a načíta moduly VFIO na priame priradenie GPU/PCI do VM.", + "motd": "Pridá uvítaciu správu MOTD ProxMenuxu a na obnovu zachová pôvodný obsah súboru alebo jeho neprítomnosť.", + "subscriptionCustom": "Upraví web Proxmoxu tak, aby nezobrazoval upozornenie na neplatné predplatné (podporuje PVE 8 aj 9).", + "zram": "Zapne voliteľný profil swapu ZRAM s veľkosťou jednej desatiny RAM.", + "backupLimits": "Odstráni limity prenosu a I/O vzdump, aby zálohy využili skutočnú priepustnosť úložiska.", + "ovh": "Zistí hardvér prenajatý od OVH cez whois a nainštaluje OVH Real-Time Monitoring (mimo OVH nič nemení).", + "ha": "Zapne služby Proxmox HA (pve-ha-lrm, pve-ha-crm, corosync) na prevzatie prevádzky v klastri pri výpadku.", + "fastfetch": "Nainštaluje systémový prehľad Fastfetch s logom a stavom ProxMenuxu ako uvítanie pri prihlásení cez SSH.", + "figurine": "Nainštaluje Figurine na zobrazenie názvu servera pomocou ASCII grafiky pri prihlásení cez SSH.", + "log2ramCustom": "Nainštaluje Log2RAM s veľkosťou zvolenou používateľom; pred použitím sa opýta na veľkosť a zohľadní SSD/M.2.", + "systemUtilities": "Nainštaluje vybrané systémové nástroje a zaznamená iba balíky novo pridané ProxMenuxom." + }, + "toolNames": { + "subscription_banner": "Odstránenie upozornenia na predplatné", + "time_sync": "Synchronizácia času", + "apt_languages": "Vynechanie jazykových balíkov APT", + "journald": "Optimalizácia journald", + "logrotate": "Optimalizácia logrotate", + "system_limits": "Zvýšenie systémových limitov", + "memory_settings": "Optimalizácia nastavení pamäte", + "zram_swap": "Profil swapu ZRAM", + "kernel_panic": "Nastavenie správania pri páde jadra", + "apt_ipv4": "Vynútenie IPv4 pre APT", + "kexec": "Rýchly reštart cez kexec", + "rpc": "Vypnutie RPC / rpcbind", + "motd": "Vlastná uvítacia správa MOTD", + "system_utils": "Systémové nástroje", + "network_optimization": "Optimalizácia siete", + "bashrc_custom": "Prispôsobenie bashrc", + "figurine": "Figurine", + "fastfetch": "Fastfetch", + "log2ram": "Log2ram (ochrana SSD)", + "zfs_autotrim": "ZFS Autotrim", + "amd_fixes": "Opravy pre procesory AMD (Ryzen/EPYC)", + "persistent_network": "Trvalé názvy sieťových rozhraní", + "vfio_iommu": "Priame priradenie zariadení (VFIO/IOMMU)", + "lvm_repair": "Oprava hlavičiek fyzických zväzkov LVM", + "repo_cleanup": "Vyčistenie repozitárov", + "proxmox_repos": "Repozitáre APT pre Proxmox", + "entropy": "Generovanie entropie (haveged)" + } }, "healthThresholds": { "title": "Hranice kontroly stavu", @@ -7408,5 +7476,12 @@ "pbsWebInterface": "Webové rozhranie PBS", "changeAddress": "Zmeniť adresu", "cancel": "Zrušiť" + }, + "apiErrors": { + "http": "Požiadavka na API zlyhala: {status} {statusText}", + "unexpectedContentType": "Očakávala sa odpoveď JSON, ale prišiel typ {contentType}", + "unknownContentType": "neznámy typ obsahu", + "invalidJson": "Odpoveď z {endpoint} obsahuje neplatný JSON", + "unauthorized": "Chýba platné prihlásenie: {endpoint}" } } diff --git a/lang/sk.json b/lang/sk.json index fe1f96ce..58b879dd 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -661,7 +661,7 @@ "Bookstack is a free and open source Wiki designed for creating beautiful documentation. Featuring a simple, but powerful WYSIWYG editor it allows for teams to create detailed and useful documentation with ease.": "Bookstack je bezplatný a open source Wiki navrhnutý pre vytvorenie krásnej dokumentácie. Vďaka jednoduchému, ale výkonnému WYSIWYG editoru umožňuje tímom jednoducho vytvárať podrobnú a užitočnú dokumentáciu.", "Boot Disk": "Bootovací disk", "Boot artifacts regenerated — reboot the host to activate the merged config.": "Boot súbory boli znovu vytvorené - reštartujte host, aby sa zlúčené nastavenie aktivovalo.", - "Boot disk:": "Boot disk:", + "Boot disk:": "Zavádzací disk:", "Boot order set to": "Poradie bootovania nastavené na", "Boot partition UUIDs from backup not found on this host:": "UUID boot oddielov zo zálohy sa na tomto hostovi nenašli:", "Boot type (grub/zfs):": "Typ bootovania (grub/zfs):", @@ -3774,7 +3774,7 @@ "Mount Point": "Bod pripojenia", "Mount Point ID": "ID mount pointu", "Mount Point Removal Summary:": "Zhrnutie odstránenia mount pointu:", - "Mount Point:": "Mount point:", + "Mount Point:": "Bod pripojenia:", "Mount Samba Share": "Pripojiť Samba zdieľanie", "Mount Samba Share on Host": "Pripojiť Samba zdieľanie na hostovi", "Mount USB disk?": "Pripojiť USB disk?", @@ -3800,7 +3800,7 @@ "Mount point not visible inside the container yet": "Mount point zatiaľ nie je vo vnútri kontajnera viditeľný", "Mount point ready:": "Mount point je pripravený:", "Mount point removed successfully": "Mount point bol úspešne odstránený", - "Mount point:": "Mount point:", + "Mount point:": "Bod pripojenia:", "Mount points added:": "Pridanie bodov pripojenia:", "Mount read-only": "Pripojiť iba na čítanie", "Mount shares on HOST first": "Najprv pripojiť zdieľania na HOSTE", @@ -8243,5 +8243,117 @@ "⚠ Disk data will NOT be erased.": "⚠ Dáta na disku sa NEVYMAŽÚ.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Disk sa odpojí a odstráni z /etc/fstab.", "⚠ The /etc/fstab entry will be removed.": "⚠ Záznam v /etc/fstab bude odstránený.", - "⚠ The disk will be unmounted.": "⚠ Disk bude odpojený." + "⚠ The disk will be unmounted.": "⚠ Disk bude odpojený.", + "Allocated Cores:": "Pridelené jadrá:", + "Allocated RAM:": "Pridelená RAM:", + "Allocating virtual disk for %s...": "Prideľuje sa virtuálny disk pre %s...", + "BRIDGE": "SIEŤOVÝ MOST (BRIDGE)", + "CORE COUNT": "POČET JADIER", + "Choose": "Vyber", + "Choose BIOS Type": "Vyber typ BIOS", + "Choose Type": "Vyber typ", + "Code": "Kód", + "Configured VM description": "Popis VM je nastavený", + "Configured controller/NVMe as hostpci%s: %s": "Radič/NVMe je nastavený ako hostpci%s: %s", + "Configured import disk %s as %s": "Importovaný disk %s je nastavený ako %s", + "Configured loader disk as ide0": "Disk zavádzača je nastavený ako ide0", + "Configured virtual disk as %s, %sGB on %s": "Virtuálny disk je nastavený ako %s, %sGB na %s", + "Configuring Serial Port:": "Nastavenie sériového portu:", + "Controller/NVMe already present in VM config (%s)": "Radič/NVMe už je v nastaveniach VM (%s)", + "Create a %s": "Vytvorený %s", + "Created with ProxMenuX": "Vytvorené pomocou ProxMenuX", + "Creating a %s using the above advanced settings": "Vytvára sa %s s uvedenými pokročilými nastaveniami", + "Creating a %s using the above default settings": "Vytvára sa %s s uvedenými predvolenými nastaveniami", + "Creating images directory": "Vytvára sa priečinok s obrazmi", + "Creating virtual disk (format=raw) for %s...": "Vytvára sa virtuálny disk (formát raw) pre %s...", + "Custom Loader Image (from %s)": "Vlastný obraz zavádzača (z %s)", + "Default": "Predvolené", + "Do-Over": "Začať znova", + "Docs": "Dokumentácia", + "Downloaded %s to %s": "Stiahnuté %s do %s", + "EFI Disk Storage": "Úložisko EFI disku", + "EFI disk created (raw) and configured on %s": "EFI disk (raw) bol vytvorený a nastavený na %s", + "EFI disk created and configured on %s": "EFI disk bol vytvorený a nastavený na %s", + "EFI storage selection failed or was cancelled.": "Výber úložiska pre EFI disk zlyhal alebo bol zrušený.", + "Edition:": "Edícia:", + "Enter temporary folder path (default: %s):": "Zadaj cestu dočasného priečinka (predvolené: %s):", + "Exit-Script": "Ukončiť skript", + "Failed to allocate virtual disk %s": "Pridelenie virtuálneho disku %s zlyhalo", + "Failed to assign disk %s (%s) on %s": "Priradenie disku %s (%s) na %s zlyhalo", + "Failed to assign loader disk": "Priradenie disku zavádzača zlyhalo", + "Failed to assign loader disk to ide0": "Priradenie disku zavádzača k ide0 zlyhalo", + "Failed to configure controller/NVMe: %s": "Nastavenie radiča/NVMe zlyhalo: %s", + "Failed to configure import disk %s as %s": "Nastavenie importovaného disku %s ako %s zlyhalo", + "Failed to configure virtual disk as %s": "Nastavenie virtuálneho disku ako %s zlyhalo", + "Failed to install unzip": "Inštalácia unzip zlyhala", + "Failed to install:": "Inštalácia zlyhala:", + "HOSTNAME": "NÁZOV SERVERA", + "ID %s is already in use": "ID %s sa už používa", + "Images directory created: %s": "Priečinok s obrazmi bol vytvorený: %s", + "SeaBIOS (Legacy)": "SeaBIOS (starší režim BIOS)", + "Installed unzip successfully.": "unzip bol úspešne nainštalovaný.", + "Installing dependencies:": "Inštalujú sa závislosti:", + "Installing unzip...": "Inštaluje sa unzip...", + "Language:": "Jazyk:", + "Loader": "Zavádzač", + "Loader configured as boot device.": "Zavádzač je nastavený ako zariadenie na zavedenie systému.", + "Loader import failed. No disk detected in config.": "Import zavádzača zlyhal. V nastaveniach sa nenašiel disk.", + "Loader import failed. No disk detected.": "Import zavádzača zlyhal. Nenašiel sa disk.", + "Loader imported successfully to %s": "Zavádzač bol úspešne importovaný do %s", + "Loader storage selection failed or was cancelled.": "Výber úložiska pre zavádzač zlyhal alebo bol zrušený.", + "Loader:": "Zavádzač:", + "MAC ADDRESS": "MAC ADRESA", + "MTU SIZE": "VEĽKOSŤ MTU", + "Machine i440fx": "Stroj i440fx", + "Machine q35": "Stroj q35", + "No compatible images found in %s\n\nSupported formats: .img, .iso, .qcow2, .vmdk\n\nPlease add some images and try again.": "V %s sa nenašli podporované obrazy.\n\nPodporované formáty: .img, .iso, .qcow2, .vmdk\n\nPridaj obrazy a skús to znova.", + "No custom image selected": "Nebol vybraný vlastný obraz", + "No disks/controllers configured.": "Nie sú nastavené žiadne disky ani radiče.", + "Ready to create a %s?": "Chceš vytvoriť %s?", + "Retrieving the URL for the %s loader": "Zisťuje sa URL zavádzača %s", + "SELECT CUSTOM IMAGE": "VÝBER VLASTNÉHO OBRAZU", + "SELECT LOADER": "VÝBER ZAVÁDZAČA", + "SETTINGS": "NASTAVENIA", + "STORAGE PLAN": "PLÁN ÚLOŽISKA", + "Selected Loader:": "Vybraný zavádzač:", + "Start VM when completed:": "Spustiť VM po dokončení:", + "Starting ISO conversion...": "Spúšťa sa vytváranie ISO...", + "Synology VM Creator": "Vytvorenie VM Synology", + "This will create a New %s. Proceed?": "Vytvorí sa nový %s. Pokračovať?", + "Unable to detect a valid storage location for EFI disk.": "Nenašlo sa použiteľné úložisko pre EFI disk.", + "Unable to detect a valid storage location.": "Nenašlo sa použiteľné úložisko.", + "Using Advanced Settings": "Použijú sa pokročilé nastavenia", + "Using BIOS Type:": "Typ BIOS:", + "Using Bridge:": "Sieťový most (bridge):", + "Using CPU Model:": "Model CPU:", + "Using Custom Image:": "Vlastný obraz:", + "Using Default Settings": "Použijú sa predvolené nastavenia", + "Using Hostname:": "Názov servera:", + "Using Interface MTU Size:": "Veľkosť MTU rozhrania:", + "Using MAC Address:": "MAC adresa:", + "Using Machine Type:": "Typ stroja:", + "Using VLAN:": "VLAN:", + "Using Virtual Machine ID:": "ID virtuálneho stroja:", + "Using Vlan:": "VLAN:", + "VIRTUAL DISK": "VIRTUÁLNY DISK", + "VIRTUAL MACHINE ID": "ID VIRTUÁLNEHO STROJA", + "VM created with errors. Check configuration.": "VM bola vytvorená s chybami. Skontroluj jej nastavenia.", + "Virtual Machine ID:": "ID virtuálneho stroja:", + "Command not found.": "Príkaz sa nenašiel.", + "Custom Image": "Vlastný obraz", + "Error generating UUP download list.": "Vytvorenie zoznamu súborov UUP na stiahnutie zlyhalo.", + "Importing loader disk:": "Importuje sa disk zavádzača:", + "genisoimage/mkisofs not found.": "Nenašiel sa genisoimage ani mkisofs.", + "%s of %s updates failed — see the messages above.": "Zlyhalo %s z %s aktualizácií — pozri hlášky vyššie.", + "%s of %s updates failed.": "Zlyhalo %s z %s aktualizácií.", + "All %s update(s) completed successfully.": "Všetky aktualizácie (%s) sa úspešne dokončili.", + "Function '%s' is not defined in the %s flow.": "Funkcia '%s' nie je definovaná v postupe %s.", + "Press Enter to close this terminal...": "Stlač Enter na zatvorenie terminálu...", + "Update for %s completed.": "Aktualizácia %s je dokončená.", + "Update for %s exited with status %s.": "Aktualizácia %s skončila s návratovým kódom %s.", + "Updating %s (running %s from %s flow)...": "Aktualizuje sa %s (spúšťa sa %s z postupu %s)...", + "function '%s' is not defined in the %s flow.": "funkcia '%s' nie je definovaná v postupe %s.", + "invalid source '%s' (must be 'auto', 'custom', or 'detected')": "neplatný zdroj '%s' (musí byť 'auto', 'custom' alebo 'detected')", + "provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode).": "zadaj SOURCE_TYPE+FUNCTION_NAME pre jednu funkciu alebo FUNCTIONS_BATCH pre dávku.", + "update for %s exited with status %s.": "aktualizácia %s skončila s návratovým kódom %s." } diff --git a/scripts/post_install/update_post_install_function.sh b/scripts/post_install/update_post_install_function.sh index 17690202..1119efe9 100644 --- a/scripts/post_install/update_post_install_function.sh +++ b/scripts/post_install/update_post_install_function.sh @@ -40,6 +40,11 @@ if command -v initialize_cache >/dev/null 2>&1; then initialize_cache fi +# Keep standalone invocations usable when the shared utilities are unavailable. +if ! command -v translate >/dev/null 2>&1; then + translate() { printf '%s\n' "$1"; } +fi + if command -v show_proxmenux_logo >/dev/null 2>&1; then show_proxmenux_logo fi @@ -60,7 +65,7 @@ if [[ -n "$FUNCTIONS_BATCH" ]]; then elif [[ -n "$SOURCE_TYPE" && -n "$FUNCTION_NAME" ]]; then BATCH+=("${SOURCE_TYPE}:${FUNCTION_NAME}:${TOOL_KEY}") else - echo "ERROR: provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode)." + echo "ERROR: $(translate "provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode).")" exit 2 fi @@ -99,7 +104,7 @@ ensure_flow_loaded() { fi ;; *) - echo "ERROR: invalid source '$source_type' (must be 'auto', 'custom', or 'detected')" + echo "ERROR: $(printf "$(translate "invalid source '%s' (must be 'auto', 'custom', or 'detected')")" "$source_type")" return 2 ;; esac @@ -135,9 +140,9 @@ for entry in "${BATCH[@]}"; do [[ -z "$tkey" ]] && tkey="$fn" if command -v msg_info2 >/dev/null 2>&1; then - msg_info2 "[$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..." + msg_info2 "[$INDEX/$TOTAL] $(printf "$(translate "Updating %s (running %s from %s flow)...")" "$tkey" "$fn" "$src")" else - echo "[ProxMenux] [$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..." + echo "[ProxMenux] [$INDEX/$TOTAL] $(printf "$(translate "Updating %s (running %s from %s flow)...")" "$tkey" "$fn" "$src")" fi if ! ensure_flow_loaded "$src"; then @@ -147,9 +152,9 @@ for entry in "${BATCH[@]}"; do if ! declare -F "$fn" >/dev/null 2>&1; then if command -v msg_error >/dev/null 2>&1; then - msg_error "Function '$fn' is not defined in the ${src} flow." + msg_error "$(printf "$(translate "Function '%s' is not defined in the %s flow.")" "$fn" "$src")" else - echo "ERROR: function '$fn' is not defined in the ${src} flow." + echo "ERROR: $(printf "$(translate "function '%s' is not defined in the %s flow.")" "$fn" "$src")" fi FAILED=$((FAILED + 1)) continue @@ -162,15 +167,15 @@ for entry in "${BATCH[@]}"; do if [[ $RC -eq 0 ]]; then if command -v msg_ok >/dev/null 2>&1; then - msg_ok "Update for ${tkey} completed." + msg_ok "$(printf "$(translate "Update for %s completed.")" "$tkey")" else - echo "[ProxMenux] Update for ${tkey} completed." + echo "[ProxMenux] $(printf "$(translate "Update for %s completed.")" "$tkey")" fi else if command -v msg_error >/dev/null 2>&1; then - msg_error "Update for ${tkey} exited with status $RC." + msg_error "$(printf "$(translate "Update for %s exited with status %s.")" "$tkey" "$RC")" else - echo "ERROR: update for ${tkey} exited with status $RC." + echo "ERROR: $(printf "$(translate "update for %s exited with status %s.")" "$tkey" "$RC")" fi FAILED=$((FAILED + 1)) fi @@ -178,22 +183,22 @@ done if [[ $FAILED -eq 0 ]]; then if command -v msg_success >/dev/null 2>&1; then - msg_success "All $TOTAL update(s) completed successfully." + msg_success "$(printf "$(translate "All %s update(s) completed successfully.")" "$TOTAL")" else - echo "[ProxMenux] All $TOTAL update(s) completed successfully." + echo "[ProxMenux] $(printf "$(translate "All %s update(s) completed successfully.")" "$TOTAL")" fi else if command -v msg_warn >/dev/null 2>&1; then - msg_warn "$FAILED of $TOTAL updates failed — see the messages above." + msg_warn "$(printf "$(translate "%s of %s updates failed — see the messages above.")" "$FAILED" "$TOTAL")" else - echo "WARNING: $FAILED of $TOTAL updates failed." + echo "WARNING: $(printf "$(translate "%s of %s updates failed.")" "$FAILED" "$TOTAL")" fi fi if command -v msg_success >/dev/null 2>&1; then - msg_success "Press Enter to close this terminal..." + msg_success "$(translate "Press Enter to close this terminal...")" else - echo "Press Enter to close this terminal..." + echo "$(translate "Press Enter to close this terminal...")" fi read -r 2>/dev/null || true diff --git a/scripts/utilities/uup_dump_iso_creator.sh b/scripts/utilities/uup_dump_iso_creator.sh index abcfc6c6..64e8fc2d 100644 --- a/scripts/utilities/uup_dump_iso_creator.sh +++ b/scripts/utilities/uup_dump_iso_creator.sh @@ -125,18 +125,18 @@ function run_uupdump_creator() { clear show_proxmenux_logo echo -e - msg_info "$(translate "Installing dependencies: ${MISSING[*]}")" + msg_info "$(translate "Installing dependencies:") ${MISSING[*]}" apt-get update -qq >/dev/null 2>&1 msg_ok "$(translate "All dependencies installed and verified.")" # Build dependencies land on the host, so they are recorded; # building the ISO is an operation and is not. if declare -F pmx_install_pkg >/dev/null 2>&1; then if ! PMX_JOURNAL_SOURCE="uup_dump_iso_creator.sh" pmx_install_pkg "${MISSING[@]}"; then - msg_error "$(translate "Failed to install: ${MISSING[*]}")" + msg_error "$(translate "Failed to install:") ${MISSING[*]}" exit 1 fi elif ! apt-get install -y "${MISSING[@]}" >/dev/null 2>&1; then - msg_error "$(translate "Failed to install: ${MISSING[*]}")" + msg_error "$(translate "Failed to install:") ${MISSING[*]}" exit 1 fi fi @@ -151,7 +151,7 @@ function run_uupdump_creator() { #if [[ ${#FAILED[@]} -eq 0 ]]; then # msg_ok "$(translate "All dependencies installed and verified.")" #else - # msg_error "$(translate "Missing commands after installation: ${FAILED[*]}")" + # msg_error "$(translate "Missing commands after installation:") ${FAILED[*]}" # exit 1 #fi @@ -166,7 +166,7 @@ function run_uupdump_creator() { ###################################### DEFAULT_TMP="/root/uup-temp" - USER_INPUT=$(dialog --backtitle "ProxMenux" --inputbox "Enter temporary folder path (default: $DEFAULT_TMP):" 10 60 "$DEFAULT_TMP" 3>&1 1>&2 2>&3) + USER_INPUT=$(dialog --backtitle "ProxMenux" --inputbox "$(printf "$(translate "Enter temporary folder path (default: %s):")" "$DEFAULT_TMP")" 10 60 "$DEFAULT_TMP" 3>&1 1>&2 2>&3) ###################################### @@ -237,10 +237,10 @@ function run_uupdump_creator() { echo -e echo -e "\n${BGN}=============== UUP Dump Creator ===============${CL}" echo -e " ${BGN}🆔 ID:${CL} ${DGN}$BUILD_ID${CL}" - echo -e " ${BGN}🌐 Language:${CL} ${DGN}$LANG${CL}" - echo -e " ${BGN}💿 Edition:${CL} ${DGN}$EDITION${CL}" - echo -e " ${BGN}🖥️ Architecture:${CL} ${DGN}$ARCH${CL}" - echo -e " ${BGN}📁 Destination:${CL} ${DGN}$OUT_DIR${CL}" + echo -e " ${BGN}🌐 $(translate "Language:")${CL} ${DGN}$LANG${CL}" + echo -e " ${BGN}💿 $(translate "Edition:")${CL} ${DGN}$EDITION${CL}" + echo -e " ${BGN}🖥️ $(translate "Architecture:")${CL} ${DGN}$ARCH${CL}" + echo -e " ${BGN}📁 $(translate "Destination:")${CL} ${DGN}$OUT_DIR${CL}" echo -e "${BGN}===============================================${CL}\n" @@ -262,10 +262,10 @@ mkdir -p files echo "https://git.uupdump.net/uup-dump/converter/archive/refs/heads/master.tar.gz" > files/converter_multi for prog in aria2c cabextract wimlib-imagex chntpw; do - which \$prog &>/dev/null || { echo "\$prog not found."; exit 1; } + which \$prog &>/dev/null || { printf '%s\n' "\$prog: $(translate "Command not found.")"; exit 1; } done -which genisoimage &>/dev/null || which mkisofs &>/dev/null || { echo "genisoimage/mkisofs not found."; exit 1; } +which genisoimage &>/dev/null || which mkisofs &>/dev/null || { echo "$(translate "genisoimage/mkisofs not found.")"; exit 1; } destDir="UUPs" tempScript="aria2_script.\$RANDOM.txt" @@ -277,7 +277,7 @@ aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download -o"\$tempScript" --allow-overwrite=true --auto-file-renaming=false \\ "https://uupdump.net/get.php?id=$BUILD_ID&pack=$LANG&edition=$EDITION&aria2=2" || exit 1 -grep '#UUPDUMP_ERROR:' "\$tempScript" && { echo "❌ Error generating UUP download list."; exit 1; } +grep '#UUPDUMP_ERROR:' "\$tempScript" && { echo "❌ $(translate "Error generating UUP download list.")"; exit 1; } aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download.log" \\ -x16 -s16 -j5 -c -R -d"\$destDir" -i"\$tempScript" || exit 1 @@ -293,7 +293,7 @@ EOF [[ -z "$UUP_FOLDER" ]] && msg_error "$(translate "No UUP folder found.")" && exit 1 echo -e "\n${GN}=======================================${CL}" - echo -e " 💿 ${GN}Starting ISO conversion...${CL}" + echo -e " 💿 ${GN}$(translate "Starting ISO conversion...")${CL}" echo -e "${GN}=======================================${CL}\n" @@ -336,4 +336,4 @@ EOF fi } -run_uupdump_creator \ No newline at end of file +run_uupdump_creator diff --git a/scripts/vm/synology.sh b/scripts/vm/synology.sh index f8478cb7..66668b95 100644 --- a/scripts/vm/synology.sh +++ b/scripts/vm/synology.sh @@ -90,7 +90,7 @@ VM_STORAGE_IOMMU_PENDING_REBOOT=0 function exit_script() { clear - if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then + if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(printf "$(translate "This will create a New %s. Proceed?")" "$NAME")" 10 58; then start_script else clear @@ -104,7 +104,7 @@ function exit_script() { function header_info() { clear show_proxmenux_logo - msg_title "Synology VM Creator" + msg_title "$(translate "Synology VM Creator")" } # ========================================================== @@ -211,13 +211,13 @@ function run_gpu_passthrough_wizard() { # start Script # ========================================================== function start_script() { - if (whiptail --backtitle "ProxMenuX" --title "SETTINGS" --yesno "$(translate "Use Default Settings?")" --no-button Advanced 10 58); then + if (whiptail --backtitle "ProxMenuX" --title "$(translate "SETTINGS")" --yesno "$(translate "Use Default Settings?")" --no-button "$(translate "Advanced")" 10 58); then header_info - echo -e "${DEF}Using Default Settings${CL}" + echo -e "${DEF}$(translate "Using Default Settings")${CL}" default_settings || return 1 else header_info - echo -e "${CUS}Using Advanced Settings${CL}" + echo -e "${CUS}$(translate "Using Advanced Settings")${CL}" advanced_settings || return 1 fi return 0 @@ -247,21 +247,21 @@ function default_settings() { SERIAL_PORT="socket" START_VM="no" - echo -e " ${TAB}${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}" - echo -e " ${TAB}${DGN}Using Machine Type: ${BGN}q35${CL}" - echo -e " ${TAB}${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" - echo -e " ${TAB}${DGN}Using Hostname: ${BGN}${HN}${CL}" - echo -e " ${TAB}${DGN}Using CPU Model: ${BGN}Host${CL}" - echo -e " ${TAB}${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e " ${TAB}${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}" - echo -e " ${TAB}${DGN}Using Bridge: ${BGN}${BRG}${CL}" - echo -e " ${TAB}${DGN}Using MAC Address: ${BGN}${MAC}${CL}" - echo -e " ${TAB}${DGN}Using VLAN: ${BGN}Default${CL}" - echo -e " ${TAB}${DGN}Using Interface MTU Size: ${BGN}Default${CL}" - echo -e " ${TAB}${DGN}Configuring Serial Port: ${BGN}${SERIAL_PORT}${CL}" - echo -e " ${TAB}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}" + echo -e " ${TAB}${DGN}$(translate "Using Virtual Machine ID:") ${BGN}${VMID}${CL}" + echo -e " ${TAB}${DGN}$(translate "Using Machine Type:") ${BGN}q35${CL}" + echo -e " ${TAB}${DGN}$(translate "Using BIOS Type:") ${BGN}OVMF (UEFI)${CL}" + echo -e " ${TAB}${DGN}$(translate "Using Hostname:") ${BGN}${HN}${CL}" + echo -e " ${TAB}${DGN}$(translate "Using CPU Model:") ${BGN}Host${CL}" + echo -e " ${TAB}${DGN}$(translate "Allocated Cores:") ${BGN}${CORE_COUNT}${CL}" + echo -e " ${TAB}${DGN}$(translate "Allocated RAM:") ${BGN}${RAM_SIZE}${CL}" + echo -e " ${TAB}${DGN}$(translate "Using Bridge:") ${BGN}${BRG}${CL}" + echo -e " ${TAB}${DGN}$(translate "Using MAC Address:") ${BGN}${MAC}${CL}" + echo -e " ${TAB}${DGN}$(translate "Using VLAN:") ${BGN}$(translate "Default")${CL}" + echo -e " ${TAB}${DGN}$(translate "Using Interface MTU Size:") ${BGN}$(translate "Default")${CL}" + echo -e " ${TAB}${DGN}$(translate "Configuring Serial Port:") ${BGN}${SERIAL_PORT}${CL}" + echo -e " ${TAB}${DGN}$(translate "Start VM when completed:") ${BGN}${START_VM}${CL}" echo -e - echo -e "${DEF}Creating a $NAME using the above default settings${CL}" + echo -e "${DEF}$(printf "$(translate "Creating a %s using the above default settings")" "$NAME")${CL}" sleep 1 select_disk_type || return 1 @@ -278,16 +278,16 @@ function default_settings() { function advanced_settings() { # VM ID Selection while true; do - if VMID=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Virtual Machine ID")" 8 58 $NEXTID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if VMID=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Virtual Machine ID")" 8 58 $NEXTID --title "$(translate "VIRTUAL MACHINE ID")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z "$VMID" ]; then VMID="$NEXTID" fi if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then - echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" + echo -e "${CROSS}${RD} $(printf "$(translate "ID %s is already in use")" "$VMID")${CL}" sleep 1 continue fi - echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" + echo -e "${DGN}$(translate "Virtual Machine ID:") ${BGN}$VMID${CL}" break else exit_script @@ -295,16 +295,16 @@ function advanced_settings() { done # Machine Type Selection - if MACH=$(whiptail --backtitle "ProxMenuX" --title "$(translate "MACHINE TYPE")" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \ - "q35" "Machine q35" ON \ - "i440fx" "Machine i440fx" OFF \ + if MACH=$(whiptail --backtitle "ProxMenuX" --title "$(translate "MACHINE TYPE")" --radiolist --cancel-button "$(translate "Exit-Script")" "$(translate "Choose Type")" 10 58 2 \ + "q35" "$(translate "Machine q35")" ON \ + "i440fx" "$(translate "Machine i440fx")" OFF \ 3>&1 1>&2 2>&3); then if [ $MACH = q35 ]; then - echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" + echo -e "${DGN}$(translate "Using Machine Type:") ${BGN}$MACH${CL}" FORMAT="" MACHINE=" -machine q35" else - echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" + echo -e "${DGN}$(translate "Using Machine Type:") ${BGN}$MACH${CL}" FORMAT=",efitype=4m" MACHINE="" fi @@ -313,15 +313,15 @@ function advanced_settings() { fi # BIOS Type Selection - if BIOS=$(whiptail --backtitle "ProxMenuX" --title "$(translate "BIOS TYPE")" --radiolist --cancel-button Exit-Script "Choose BIOS Type" 10 58 2 \ + if BIOS=$(whiptail --backtitle "ProxMenuX" --title "$(translate "BIOS TYPE")" --radiolist --cancel-button "$(translate "Exit-Script")" "$(translate "Choose BIOS Type")" 10 58 2 \ "ovmf" "UEFI (OVMF)" ON \ - "seabios" "SeaBIOS (Legacy)" OFF \ + "seabios" "$(translate "SeaBIOS (Legacy)")" OFF \ 3>&1 1>&2 2>&3); then if [ "$BIOS" = "seabios" ]; then - echo -e "${DGN}Using BIOS Type: ${BGN}SeaBIOS${CL}" + echo -e "${DGN}$(translate "Using BIOS Type:") ${BGN}SeaBIOS${CL}" BIOS_TYPE=" -bios seabios" else - echo -e "${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" + echo -e "${DGN}$(translate "Using BIOS Type:") ${BGN}OVMF (UEFI)${CL}" BIOS_TYPE=" -bios ovmf" fi else @@ -329,28 +329,28 @@ function advanced_settings() { fi # Hostname Selection - if VM_NAME=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Hostname")" 8 58 Synology-DSM --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if VM_NAME=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Hostname")" 8 58 Synology-DSM --title "$(translate "HOSTNAME")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z $VM_NAME ]; then HN="Synology-DSM" - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" + echo -e "${DGN}$(translate "Using Hostname:") ${BGN}$HN${CL}" else HN=$(echo ${VM_NAME,,} | tr -d ' ') - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" + echo -e "${DGN}$(translate "Using Hostname:") ${BGN}$HN${CL}" fi else exit_script fi # CPU Type Selection - if CPU_TYPE1=$(whiptail --backtitle "ProxMenuX" --title "$(translate "CPU MODEL")" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \ + if CPU_TYPE1=$(whiptail --backtitle "ProxMenuX" --title "$(translate "CPU MODEL")" --radiolist "$(translate "Choose")" --cancel-button "$(translate "Exit-Script")" 10 58 2 \ "1" "Host" ON \ "0" "KVM64" OFF \ 3>&1 1>&2 2>&3); then if [ $CPU_TYPE1 = "1" ]; then - echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}" + echo -e "${DGN}$(translate "Using CPU Model:") ${BGN}Host${CL}" CPU_TYPE=" -cpu host" else - echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}" + echo -e "${DGN}$(translate "Using CPU Model:") ${BGN}KVM64${CL}" CPU_TYPE="" fi else @@ -358,77 +358,77 @@ function advanced_settings() { fi # Core Count Selection - if CORE_COUNT=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate CPU Cores")" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if CORE_COUNT=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate CPU Cores")" 8 58 2 --title "$(translate "CORE COUNT")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z $CORE_COUNT ]; then CORE_COUNT="2" - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" + echo -e "${DGN}$(translate "Allocated Cores:") ${BGN}$CORE_COUNT${CL}" else - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" + echo -e "${DGN}$(translate "Allocated Cores:") ${BGN}$CORE_COUNT${CL}" fi else exit_script fi # RAM Size Selection - if RAM_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate RAM in MiB")" 8 58 4096 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if RAM_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate RAM in MiB")" 8 58 4096 --title "RAM" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z $RAM_SIZE ]; then RAM_SIZE="4096" - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" + echo -e "${DGN}$(translate "Allocated RAM:") ${BGN}$RAM_SIZE${CL}" else - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" + echo -e "${DGN}$(translate "Allocated RAM:") ${BGN}$RAM_SIZE${CL}" fi else exit_script fi # Bridge Selection - if BRG=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Bridge")" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if BRG=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Bridge")" 8 58 vmbr0 --title "$(translate "BRIDGE")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z $BRG ]; then BRG="vmbr0" - echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" + echo -e "${DGN}$(translate "Using Bridge:") ${BGN}$BRG${CL}" else - echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" + echo -e "${DGN}$(translate "Using Bridge:") ${BGN}$BRG${CL}" fi else exit_script fi # MAC Address Selection - if MAC1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a MAC Address")" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if MAC1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a MAC Address")" 8 58 $GEN_MAC --title "$(translate "MAC ADDRESS")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z $MAC1 ]; then MAC="$GEN_MAC" - echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}" + echo -e "${DGN}$(translate "Using MAC Address:") ${BGN}$MAC${CL}" else MAC="$MAC1" - echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}" + echo -e "${DGN}$(translate "Using MAC Address:") ${BGN}$MAC1${CL}" fi else exit_script fi # VLAN Selection - if VLAN1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Vlan(leave blank for default)")" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if VLAN1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Vlan(leave blank for default)")" 8 58 --title "VLAN" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z $VLAN1 ]; then - VLAN1="Default" + VLAN1="$(translate "Default")" VLAN="" - echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" + echo -e "${DGN}$(translate "Using Vlan:") ${BGN}$VLAN1${CL}" else VLAN=",tag=$VLAN1" - echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" + echo -e "${DGN}$(translate "Using Vlan:") ${BGN}$VLAN1${CL}" fi else exit_script fi # MTU Selection - if MTU1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Interface MTU Size (leave blank for default)")" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if MTU1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Interface MTU Size (leave blank for default)")" 8 58 --title "$(translate "MTU SIZE")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then if [ -z $MTU1 ]; then - MTU1="Default" + MTU1="$(translate "Default")" MTU="" - echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" + echo -e "${DGN}$(translate "Using Interface MTU Size:") ${BGN}$MTU1${CL}" else MTU=",mtu=$MTU1" - echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" + echo -e "${DGN}$(translate "Using Interface MTU Size:") ${BGN}$MTU1${CL}" fi else exit_script @@ -437,15 +437,15 @@ function advanced_settings() { # Confirmation - if (whiptail --backtitle "ProxMenuX" --title "$(translate "ADVANCED SETTINGS COMPLETE")" --yesno "Ready to create a $NAME?" --no-button Do-Over 10 58); then + if (whiptail --backtitle "ProxMenuX" --title "$(translate "ADVANCED SETTINGS COMPLETE")" --yesno "$(printf "$(translate "Ready to create a %s?")" "$NAME")" --no-button "$(translate "Do-Over")" 10 58); then echo -e - echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}" + echo -e "${CUS}$(printf "$(translate "Creating a %s using the above advanced settings")" "$NAME")${CL}" sleep 1 select_disk_type || return 1 else header_info sleep 1 - echo -e "${CUS}Using Advanced Settings${CL}" + echo -e "${CUS}$(translate "Using Advanced Settings")${CL}" advanced_settings fi } @@ -482,13 +482,13 @@ function select_disk_type() { while true; do local choice - choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \ + choice=$(whiptail --backtitle "ProxMenuX" --title "$(translate "STORAGE PLAN")" --menu "$(_build_storage_plan_summary)" 18 78 5 \ "a" "$(translate "Add virtual disk")" \ "b" "$(translate "Add import disk")" \ "c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \ "r" "$(translate "Reset current storage selection")" \ "d" "$(translate "──── [ Finish and continue ] ────")" \ - --ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || { + --ok-button "$(translate "Select")" --cancel-button "$(translate "Cancel")" 3>&1 1>&2 2>&3) || { msg_warn "$(translate "Storage plan selection cancelled.")" return 1 } @@ -560,7 +560,7 @@ function select_virtual_disk() { local VALID VALID=$(pvesm status -content images | awk 'NR>1') if [[ -z "$VALID" ]]; then - msg_error "Unable to detect a valid storage location." + msg_error "$(translate "Unable to detect a valid storage location.")" return 1 fi @@ -577,7 +577,7 @@ function select_virtual_disk() { local DISK_SIZE stop_spinner - DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) || return 0 + DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "$(translate "VIRTUAL DISK")" --cancel-button "$(translate "Cancel")" 3>&1 1>&2 2>&3) || return 0 [[ -z "$DISK_SIZE" ]] && DISK_SIZE="32" VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}") @@ -592,7 +592,7 @@ function select_controller_nvme() { fi elif declare -F _pci_is_iommu_active >/dev/null 2>&1; then if ! _pci_is_iommu_active; then - whiptail --title "Controller + NVMe" --msgbox \ + whiptail --title "$(translate "Controller + NVMe")" --msgbox \ "$(translate "IOMMU is not active on this host.")\n\n$(translate "Controller/NVMe passthrough requires IOMMU enabled in BIOS/UEFI and kernel.")\n\n$(translate "Enable IOMMU, reboot the host, and try again.")" \ 14 90 return 1 @@ -675,12 +675,12 @@ function select_controller_nvme() { msg+="$(translate "Hidden for safety"):\n${blocked_report}" fi fi - whiptail --title "Controller + NVMe" --msgbox "$msg" 18 84 + whiptail --title "$(translate "Controller + NVMe")" --msgbox "$msg" 18 84 return 1 fi local selected - selected=$(whiptail --title "Controller + NVMe" --checklist \ + selected=$(whiptail --title "$(translate "Controller + NVMe")" --checklist \ "$(translate "Select available Controllers/NVMe to add:")" 20 96 10 \ "${menu_items[@]}" 3>&1 1>&2 2>&3) || return 1 @@ -692,7 +692,7 @@ function select_controller_nvme() { if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]] && declare -F _vm_storage_confirm_controller_passthrough_risk >/dev/null 2>&1; then local vm_name_for_notice="${HN:-$NAME}" - if ! _vm_storage_confirm_controller_passthrough_risk "${VMID:-}" "$vm_name_for_notice" "Controller + NVMe"; then + if ! _vm_storage_confirm_controller_passthrough_risk "${VMID:-}" "$vm_name_for_notice" "$(translate "Controller + NVMe")"; then CONTROLLER_NVME_PCIS=() return 1 fi @@ -745,18 +745,18 @@ function prompt_controller_conflict_policy() { function select_loader() { # Ensure the images directory exists if [ ! -d "$IMAGES_DIR" ]; then - msg_info "Creating images directory" + msg_info "$(translate "Creating images directory")" mkdir -p "$IMAGES_DIR" chmod 755 "$IMAGES_DIR" - msg_ok "Images directory created: $IMAGES_DIR" + msg_ok "$(printf "$(translate "Images directory created: %s")" "$IMAGES_DIR")" fi # Create the loader selection menu - LOADER_OPTION=$(whiptail --backtitle "ProxMenuX" --title "SELECT LOADER" --menu "$(translate "Choose a loader for Synology DSM:")" 15 70 4 \ + LOADER_OPTION=$(whiptail --backtitle "ProxMenuX" --title "$(translate "SELECT LOADER")" --menu "$(translate "Choose a loader for Synology DSM:")" 15 70 4 \ "1" "AuxXxilium Arc Loader" \ "2" "RedPill Loader (RROrg - RR)" \ "3" "Alpine RedPill Loader (PeterSuh-Q3 M-shell)" \ - "4" "Custom Loader Image (from $IMAGES_DIR)" \ + "4" "$(printf "$(translate "Custom Loader Image (from %s)")" "$IMAGES_DIR")" \ 3>&1 1>&2 2>&3) if [ -z "$LOADER_OPTION" ]; then @@ -768,28 +768,28 @@ function select_loader() { LOADER_TYPE="arc" LOADER_NAME="AuxXxilium Arc" LOADER_URL="https://github.com/AuxXxilium/arc/" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}" download_loader ;; 2) LOADER_TYPE="redpill" LOADER_NAME="RedPill RR" LOADER_URL="https://github.com/RROrg/rr/" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}" download_loader ;; 3) LOADER_TYPE="tinycore" LOADER_NAME="Alpine RedPill M-shell" LOADER_URL="https://github.com/PeterSuh-Q3/tinycore-redpill/" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}" download_loader ;; 4) LOADER_TYPE="custom" - LOADER_NAME="Custom Image" + LOADER_NAME="$(translate "Custom Image")" LOADER_URL="https://xpenology.com/forum/" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}" select_custom_image ;; esac @@ -800,7 +800,7 @@ function select_custom_image() { IMAGES=$(find "$IMAGES_DIR" -type f -name "*.img" -o -name "*.iso" -o -name "*.qcow2" -o -name "*.vmdk" | sort) if [ -z "$IMAGES" ]; then - whiptail --title "$(translate "No Images Found")" --msgbox "No compatible images found in $IMAGES_DIR\n\nSupported formats: .img, .iso, .qcow2, .vmdk\n\nPlease add some images and try again." 15 70 + whiptail --title "$(translate "No Images Found")" --msgbox "$(printf "$(translate "No compatible images found in %s\n\nSupported formats: .img, .iso, .qcow2, .vmdk\n\nPlease add some images and try again.")" "$IMAGES_DIR")" 15 70 select_loader fi @@ -814,14 +814,14 @@ function select_custom_image() { done <<< "$IMAGES" # Let the user select an image - LOADER_FILE=$(whiptail --backtitle "ProxMenuX" --title "SELECT CUSTOM IMAGE" --menu "$(translate "Choose a custom image:")" 20 70 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3) + LOADER_FILE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "SELECT CUSTOM IMAGE")" --menu "$(translate "Choose a custom image:")" 20 70 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3) if [ -z "$LOADER_FILE" ]; then - msg_error "No custom image selected" + msg_error "$(translate "No custom image selected")" exit_script fi - echo -e "${DGN}${TAB}Using Custom Image: ${BGN}$(basename "$LOADER_FILE")${CL}" + echo -e "${DGN}${TAB}$(translate "Using Custom Image:") ${BGN}$(basename "$LOADER_FILE")${CL}" FILE=$(basename "$LOADER_FILE") } # ========================================================== @@ -840,17 +840,17 @@ function download_loader() { FILE="" LOADER_FILE="" - echo -e "${DGN}${TAB}Retrieving the URL for the ${BGN}$LOADER_NAME loader${CL}" + echo -e "${DGN}${TAB}$(printf "$(translate "Retrieving the URL for the %s loader")" "${BGN}$LOADER_NAME")${CL}" if [[ "$LOADER_TYPE" == "arc" || "$LOADER_TYPE" == "redpill" ]] && ! command -v unzip &> /dev/null; then - msg_info "Installing unzip..." + msg_info "$(translate "Installing unzip...")" apt-get update -qq && apt-get install -y unzip -qq >/dev/null 2>&1 if ! command -v unzip &> /dev/null; then - msg_error "Failed to install unzip" + msg_error "$(translate "Failed to install unzip")" sleep 2 return 1 fi - msg_ok "Installed unzip successfully." + msg_ok "$(translate "Installed unzip successfully.")" fi asset_pattern='^[^/]+[.]img[.]zip$' @@ -927,7 +927,7 @@ function download_loader() { rm -rf "$stage" FILE="$image_name" LOADER_FILE="$IMAGES_DIR/$FILE" - msg_ok "Downloaded ${CL}${BL}${FILE}${CL} to ${IMAGES_DIR}" + msg_ok "$(printf "$(translate "Downloaded %s to %s")" "${CL}${BL}${FILE}${CL}" "${IMAGES_DIR}")" } # ======================================================= @@ -960,7 +960,7 @@ function select_efi_storage() { VALID=$(pvesm status -content images | awk 'NR>1') if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location for EFI disk." >&2 + msg_error "$(translate "Unable to detect a valid storage location for EFI disk.")" >&2 return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then @@ -969,7 +969,7 @@ function select_efi_storage() { else [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" > /dev/null 2>&1 while [ -z "${STORAGE:+x}" ]; do - STORAGE=$(whiptail --backtitle "ProxMenuX" --title "EFI Disk Storage" --radiolist \ + STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "EFI Disk Storage")" --radiolist \ "$(translate "Choose the storage for the EFI disk (4MB):")"$'\n\n'"$(translate "Use Spacebar to select.")" \ 16 $(($MSG_MAX_LENGTH + 23)) 6 \ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || { @@ -1015,7 +1015,7 @@ function select_storage_volume() { VALID=$(pvesm status -content images | awk 'NR>1') if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location." >&2 + msg_error "$(translate "Unable to detect a valid storage location.")" >&2 return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then STORAGE=${STORAGE_MENU[0]} @@ -1048,19 +1048,19 @@ function create_vm() { if ! qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1${BIOS_TYPE}${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \ -name $HN -tags proxmenux,nas -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \ -serial0 socket; then - msg_error "Failed to create base VM. Check VM ID and host configuration." + msg_error "$(translate "Failed to create base VM. Check VM ID and host configuration.")" return 1 fi - msg_ok "Create a $NAME" + msg_ok "$(printf "$(translate "Create a %s")" "$NAME")" # Check if UEFI (OVMF) is being used =================== if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then - msg_info "Configuring EFI disk" + msg_info "$(translate "Configuring EFI disk")" if ! EFI_STORAGE=$(select_efi_storage $VMID); then - msg_error "EFI storage selection failed or was cancelled." + msg_error "$(translate "EFI storage selection failed or was cancelled.")" return 1 fi EFI_DISK_NAME="vm-${VMID}-disk-efivars" @@ -1084,9 +1084,9 @@ function create_vm() { if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:4,efitype=4m,format=raw,pre-enrolled-keys=0" >/dev/null 2>&1; then - msg_ok "EFI disk created (raw) and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" + msg_ok "$(printf "$(translate "EFI disk created (raw) and configured on %s")" "${CL}${BL}$EFI_STORAGE${GN}${CL}")" else - msg_error "Failed to configure EFI disk" + msg_error "$(translate "Failed to configure EFI disk")" ERROR_FLAG=true fi else @@ -1097,13 +1097,13 @@ function create_vm() { if pvesm alloc "$EFI_STORAGE" "$VMID" "$EFI_DISK_NAME" 4M >/dev/null 2>&1; then if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:${EFI_DISK_NAME},pre-enrolled-keys=0" >/dev/null 2>&1; then - msg_ok "EFI disk created and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" + msg_ok "$(printf "$(translate "EFI disk created and configured on %s")" "${CL}${BL}$EFI_STORAGE${GN}${CL}")" else - msg_error "Failed to configure EFI disk" + msg_error "$(translate "Failed to configure EFI disk")" ERROR_FLAG=true fi else - msg_error "Failed to create EFI disk" + msg_error "$(translate "Failed to create EFI disk")" ERROR_FLAG=true fi fi @@ -1116,7 +1116,7 @@ function create_vm() { # Select storage volume for loader ======================= if ! LOADER_STORAGE=$(select_storage_volume $VMID "loader disk"); then - msg_error "Loader storage selection failed or was cancelled." + msg_error "$(translate "Loader storage selection failed or was cancelled.")" return 1 fi @@ -1126,7 +1126,7 @@ function create_vm() { import_pid=$! # Show a simple progress indicator - echo -n "Importing loader disk: " + echo -n "$(translate "Importing loader disk:") " while kill -0 $import_pid 2>/dev/null; do echo -n "." sleep 2.5 @@ -1139,10 +1139,10 @@ function create_vm() { # If the disk was not imported correctly, show an error message but continue if [ -z "$IMPORTED_DISK" ]; then - msg_error "Loader import failed. No disk detected." + msg_error "$(translate "Loader import failed. No disk detected.")" ERROR_FLAG=true else - msg_ok "Loader imported successfully to ${CL}${BL}$LOADER_STORAGE${GN}${CL}" + msg_ok "$(printf "$(translate "Loader imported successfully to %s")" "${CL}${BL}$LOADER_STORAGE${GN}${CL}")" fi @@ -1159,23 +1159,23 @@ function create_vm() { if [[ -n "$IMPORTED_REF" && -n "$IMPORTED_ID" ]]; then if qm set "$VMID" -ide0 "$IMPORTED_REF" >/dev/null 2>&1; then - msg_ok "Configured loader disk as ide0" + msg_ok "$(translate "Configured loader disk as ide0")" qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1 else - msg_error "Failed to assign loader disk to ide0" + msg_error "$(translate "Failed to assign loader disk to ide0")" ERROR_FLAG=true fi else - msg_error "Loader import failed. No disk detected in config." + msg_error "$(translate "Loader import failed. No disk detected in config.")" ERROR_FLAG=true fi else DISK_NAME="vm-${VMID}-disk-0" if qm set "$VMID" -ide0 "$LOADER_STORAGE:${DISK_NAME}" >/dev/null 2>&1; then - msg_ok "Configured loader disk as ide0" + msg_ok "$(translate "Configured loader disk as ide0")" else - msg_error "Failed to assign loader disk" + msg_error "$(translate "Failed to assign loader disk")" ERROR_FLAG=true fi fi @@ -1185,7 +1185,7 @@ function create_vm() { result=$(qm set "$VMID" -boot order=ide0 2>&1) if [[ $? -eq 0 ]]; then - msg_ok "Loader configured as boot device." + msg_ok "$(translate "Loader configured as boot device.")" else ERROR_FLAG=true fi @@ -1217,27 +1217,27 @@ if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then SATA_ID="sata${DISK_SLOT_INDEX}" if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then - msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..." + msg_info "$(printf "$(translate "Creating virtual disk (format=raw) for %s...")" "$STORAGE_TYPE")" if ! qm set "$VMID" -$SATA_ID "$STORAGE:$SIZE,format=raw" >/dev/null 2>&1; then - msg_error "Failed to assign disk $DISK_NUM ($SATA_ID) on $STORAGE" + msg_error "$(printf "$(translate "Failed to assign disk %s (%s) on %s")" "$DISK_NUM" "$SATA_ID" "$STORAGE")" ERROR_FLAG=true continue fi else - msg_info "Allocating virtual disk for $STORAGE_TYPE..." + msg_info "$(printf "$(translate "Allocating virtual disk for %s...")" "$STORAGE_TYPE")" if ! pvesm alloc "$STORAGE" "$VMID" "$DISK_NAME" "$SIZE"G >/dev/null 2>&1; then - msg_error "Failed to allocate virtual disk $DISK_NUM" + msg_error "$(printf "$(translate "Failed to allocate virtual disk %s")" "$DISK_NUM")" ERROR_FLAG=true continue fi if ! qm set "$VMID" -$SATA_ID "$STORAGE:${DISK_REF}$DISK_NAME" >/dev/null 2>&1; then - msg_error "Failed to configure virtual disk as $SATA_ID" + msg_error "$(printf "$(translate "Failed to configure virtual disk as %s")" "$SATA_ID")" ERROR_FLAG=true continue fi fi - msg_ok "Configured virtual disk as $SATA_ID, ${SIZE}GB on ${CL}${BL}$STORAGE${CL} ${GN}" + msg_ok "$(printf "$(translate "Configured virtual disk as %s, %sGB on %s")" "$SATA_ID" "${SIZE}" "${CL}${BL}$STORAGE${CL} ${GN}")" DISK_INFO="${DISK_INFO}

Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE}

" CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE} ($SATA_ID)\n" DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1)) @@ -1259,12 +1259,12 @@ if [[ ${#EFFECTIVE_IMPORT_DISKS[@]} -gt 0 ]]; then DISK_ID="sata${DISK_SLOT_INDEX}" if qm set "$VMID" -${DISK_ID} ${DISK} >/dev/null 2>&1; then - msg_ok "Configured import disk ${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN} as $DISK_ID" + msg_ok "$(printf "$(translate "Configured import disk %s as %s")" "${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN}" "$DISK_ID")" DISK_INFO="${DISK_INFO}

Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE)

" CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE) (${DISK_ID})\n" DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1)) else - msg_error "Failed to configure import disk $DISK as $DISK_ID" + msg_error "$(printf "$(translate "Failed to configure import disk %s as %s")" "$DISK" "$DISK_ID")" ERROR_FLAG=true fi done @@ -1343,14 +1343,14 @@ if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then for PCI_DEV in "${CONTROLLER_NVME_PCIS[@]}"; do if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then if _pci_function_assigned_to_vm "$PCI_DEV" "$VMID"; then - msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + msg_warn "$(printf "$(translate "Controller/NVMe already present in VM config (%s)")" "${PCI_DEV}")" continue fi else local PCI_BDF PCI_BDF="${PCI_DEV#0000:}" if qm config "$VMID" 2>/dev/null | grep -qE "^hostpci[0-9]+:.*(0000:)?${PCI_BDF}([,[:space:]]|$)"; then - msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + msg_warn "$(printf "$(translate "Controller/NVMe already present in VM config (%s)")" "${PCI_DEV}")" continue fi fi @@ -1397,12 +1397,12 @@ if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then fi if qm set "$VMID" --hostpci${HOSTPCI_INDEX} "${PCI_DEV},pcie=1" >/dev/null 2>&1; then - msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}" + msg_ok "$(printf "$(translate "Configured controller/NVMe as hostpci%s: %s")" "${HOSTPCI_INDEX}" "${PCI_DEV}")" DISK_INFO="${DISK_INFO}

Controller/NVMe: ${PCI_DEV}

" CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n" HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1)) else - msg_error "Failed to configure controller/NVMe: ${PCI_DEV}" + msg_error "$(printf "$(translate "Failed to configure controller/NVMe: %s")" "${PCI_DEV}")" ERROR_FLAG=true fi done @@ -1411,7 +1411,7 @@ if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then fi if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#EFFECTIVE_IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then - msg_error "No disks/controllers configured." + msg_error "$(translate "No disks/controllers configured.")" exit_script fi @@ -1424,16 +1424,16 @@ HTML_DESC="

Synology DSM VM

-

Created with ProxMenuX

-

Loader: $LOADER_NAME

+

$(translate "Created with ProxMenuX")

+

$(translate "Loader:") $LOADER_NAME

-Docs -Code -Loader +$(translate "Docs") +$(translate "Code") +$(translate "Loader") Ko-fi

@@ -1442,16 +1442,16 @@ ${DISK_INFO}
" -msg_info "Setting VM description" +msg_info "$(translate "Setting VM description")" if ! qm set "$VMID" -description "$HTML_DESC" >/dev/null 2>&1; then - msg_error "Failed to set VM description" + msg_error "$(translate "Failed to set VM description")" exit_script fi -msg_ok "Configured VM description" +msg_ok "$(translate "Configured VM description")" if [ "$ERROR_FLAG" = true ]; then - msg_error "VM created with errors. Check configuration." + msg_error "$(translate "VM created with errors. Check configuration.")" else if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then WIZARD_GPU_RESULT="cancelled" @@ -1540,7 +1540,7 @@ header_info sleep 1 # Start script -if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then +if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(printf "$(translate "This will create a New %s. Proceed?")" "$NAME")" 10 58; then start_vm_wizard_capture if ! start_script; then stop_vm_wizard_capture diff --git a/tests/fixtures/cli-before-i18n/synology-display.sh b/tests/fixtures/cli-before-i18n/synology-display.sh new file mode 100644 index 00000000..8418de52 --- /dev/null +++ b/tests/fixtures/cli-before-i18n/synology-display.sh @@ -0,0 +1,141 @@ + msg_title "Synology VM Creator" + msg_warn "$(translate "No compatible GPU detected for VM passthrough. Skipping GPU wizard option.")" + msg_warn "$gpu_notice" + msg_info2 "$(translate "Launching GPU passthrough assistant for VM") ${VMID}..." + echo -e "${DEF}Using Default Settings${CL}" + echo -e "${CUS}Using Advanced Settings${CL}" + echo -e " ${TAB}${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}" + echo -e " ${TAB}${DGN}Using Machine Type: ${BGN}q35${CL}" + echo -e " ${TAB}${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" + echo -e " ${TAB}${DGN}Using Hostname: ${BGN}${HN}${CL}" + echo -e " ${TAB}${DGN}Using CPU Model: ${BGN}Host${CL}" + echo -e " ${TAB}${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}" + echo -e " ${TAB}${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}" + echo -e " ${TAB}${DGN}Using Bridge: ${BGN}${BRG}${CL}" + echo -e " ${TAB}${DGN}Using MAC Address: ${BGN}${MAC}${CL}" + echo -e " ${TAB}${DGN}Using VLAN: ${BGN}Default${CL}" + echo -e " ${TAB}${DGN}Using Interface MTU Size: ${BGN}Default${CL}" + echo -e " ${TAB}${DGN}Configuring Serial Port: ${BGN}${SERIAL_PORT}${CL}" + echo -e " ${TAB}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}" + echo -e "${DEF}Creating a $NAME using the above default settings${CL}" + echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" + echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" + echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" + echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" + echo -e "${DGN}Using BIOS Type: ${BGN}SeaBIOS${CL}" + echo -e "${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" + echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" + echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" + echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}" + echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}" + echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" + echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" + echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" + echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" + echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" + echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" + echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}" + echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}" + echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" + echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" + echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" + echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" + echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}" + echo -e "${CUS}Using Advanced Settings${CL}" + echo -e "$summary" + msg_warn "$(translate "Storage plan selection cancelled.")" + msg_ok "$(translate "Virtual Disks Created:")" + msg_ok "$(translate "Import Disks Selected:")" + msg_ok "$(translate "Controllers + NVMe Selected:")" + msg_info "$(translate "Detecting storage for disk images...")" + msg_error "Unable to detect a valid storage location." + msg_info "$(translate "Detecting PCI storage controllers and NVMe devices...")" + msg_info "Creating images directory" + msg_ok "Images directory created: $IMAGES_DIR" + echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" + msg_error "No custom image selected" + echo -e "${DGN}${TAB}Using Custom Image: ${BGN}$(basename "$LOADER_FILE")${CL}" + echo -e "${DGN}${TAB}Retrieving the URL for the ${BGN}$LOADER_NAME loader${CL}" + msg_info "Installing unzip..." + msg_error "Failed to install unzip" + msg_ok "Installed unzip successfully." + msg_error "$(translate "Failed to retrieve loader release metadata.")" + msg_error "$(translate "No unambiguous loader download found in release metadata.")" + msg_error "$(translate "Failed to prepare loader download directory.")" + msg_error "$(translate "Failed to download loader image.")" + msg_error "$(translate "Failed to extract a nonempty loader image.")" + msg_error "$(translate "Failed to save loader image.")" + msg_ok "Downloaded ${CL}${BL}${FILE}${CL} to ${IMAGES_DIR}" + msg_error "Unable to detect a valid storage location for EFI disk." >&2 + msg_warn "$(translate "EFI storage selection cancelled.")" >&2 + msg_error "Unable to detect a valid storage location." >&2 + msg_warn "$(translate "Storage selection cancelled for $purpose.")" >&2 + msg_error "Failed to create base VM. Check VM ID and host configuration." + msg_ok "Create a $NAME" + msg_info "Configuring EFI disk" + msg_error "EFI storage selection failed or was cancelled." + msg_ok "EFI disk created (raw) and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" + msg_error "Failed to configure EFI disk" + msg_ok "EFI disk created and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" + msg_error "Failed to configure EFI disk" + msg_error "Failed to create EFI disk" + msg_error "Loader storage selection failed or was cancelled." + echo -n "Importing loader disk: " + echo -n "." + msg_error "Loader import failed. No disk detected." + msg_ok "Loader imported successfully to ${CL}${BL}$LOADER_STORAGE${GN}${CL}" + msg_ok "Configured loader disk as ide0" + msg_error "Failed to assign loader disk to ide0" + msg_error "Loader import failed. No disk detected in config." + msg_ok "Configured loader disk as ide0" + msg_error "Failed to assign loader disk" + msg_ok "Loader configured as boot device." + msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..." + msg_error "Failed to assign disk $DISK_NUM ($SATA_ID) on $STORAGE" + msg_info "Allocating virtual disk for $STORAGE_TYPE..." + msg_error "Failed to allocate virtual disk $DISK_NUM" + msg_error "Failed to configure virtual disk as $SATA_ID" + msg_ok "Configured virtual disk as $SATA_ID, ${SIZE}GB on ${CL}${BL}$STORAGE${CL} ${GN}" + msg_ok "Configured import disk ${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN} as $DISK_ID" + msg_error "Failed to configure import disk $DISK as $DISK_ID" + msg_warn "$(translate "Skipping SR-IOV device"): ${SRIOV_SKIPPED}" + msg_warn "$(translate "No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.")" + msg_warn "$(translate "IOMMU was configured during this wizard and a reboot is pending.")" + msg_warn "$(translate "Controller + NVMe assignment will be written now and become active after host reboot.")" + msg_error "$(translate "IOMMU is not active. Skipping Controller + NVMe assignment.")" + msg_error "$(translate "Controller + NVMe passthrough requires machine type q35. Skipping controller assignment.")" + msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + msg_warn "$(translate "Controller/NVMe is in use by running VM") ${SRC_VMID} ($(translate "stop source VM first"))" + msg_warn "$(translate "Start on boot disabled for VM") ${SRC_VMID}" + msg_ok "$(translate "Controller/NVMe removed from source VM") ${SRC_VMID} (${PCI_DEV})" + msg_info2 "$(translate "Skipped device"): ${PCI_DEV}" + msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}" + msg_error "Failed to configure controller/NVMe: ${PCI_DEV}" + msg_error "No disks/controllers configured." +msg_info "Setting VM description" + msg_error "Failed to set VM description" +msg_ok "Configured VM description" + msg_error "VM created with errors. Check configuration." + msg_success "$(translate "Completed Successfully with GPU passthrough configured!")" + msg_warn "$(translate "Host VFIO configuration changed (initramfs updated). Reboot required before starting the VM.")" + msg_success "$(translate "Completed Successfully!")" + msg_warn "$(translate "GPU passthrough was skipped (no compatible GPU detected).")" + msg_warn "$(translate "GPU passthrough was not applied.")" +echo -e "${TAB}${GN}$(translate "Next Steps:")${CL}" +echo -e "${TAB}1. $(translate "Start the VM")" +echo -e "${TAB}2. $(translate "Open the VM console and wait for the loader to boot")" +echo -e "${TAB}3. $(translate "In the loader interface, follow the instructions to select your Synology model")" +echo -e "${TAB}4. $(translate "Complete the DSM installation wizard")" +echo -e "${TAB}5. $(translate "Find your device using https://finds.synology.com")" + echo -e "${TAB}- $(translate "If you want to use a physical monitor on the passthrough GPU:")" + echo -e "${TAB}• $(translate "First complete DSM setup and verify Web UI/SSH access.")" + echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")" + msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")" + msg_warn "$(translate "Rebooting the system...")" + msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")" + msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")" + msg_warn "$(translate "VM creation cancelled before disk planning.")" diff --git a/tests/fixtures/cli-before-i18n/update_post_install_function.sh b/tests/fixtures/cli-before-i18n/update_post_install_function.sh new file mode 100644 index 00000000..17690202 --- /dev/null +++ b/tests/fixtures/cli-before-i18n/update_post_install_function.sh @@ -0,0 +1,200 @@ +#!/bin/bash +# ========================================================== +# ProxMenux - Sprint 12B: re-run a single post-install function +# ========================================================== +# Invoked by the Monitor's "Update" buttons (Settings → ProxMenux +# Optimizations) and by the bash menu's update flow. Sources the +# appropriate post_install script and invokes one specific function so +# the user can update a single tool without re-running the whole flow. +# +# Two invocation modes: +# +# SINGLE — set SOURCE_TYPE + FUNCTION_NAME (+ optionally TOOL_KEY): +# SOURCE_TYPE=auto FUNCTION_NAME=install_log2ram_auto +# +# BATCH — set FUNCTIONS_BATCH to a newline-separated list of +# "source:function:tool_key" triples (tool_key optional): +# FUNCTIONS_BATCH="auto:install_log2ram_auto:log2ram +# custom:install_ceph:ceph" +# +# In batch mode the wrapper iterates the list, sourcing each flow once +# and re-using its function definitions. Tools coming from the same +# flow share a single source step so the terminal output stays clean. +# ========================================================== + +set -e + +LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" + +if [[ -f "$UTILS_FILE" ]]; then + # shellcheck disable=SC1090 + source "$UTILS_FILE" +fi + +if command -v load_language >/dev/null 2>&1; then + load_language +fi +if command -v initialize_cache >/dev/null 2>&1; then + initialize_cache +fi + +if command -v show_proxmenux_logo >/dev/null 2>&1; then + show_proxmenux_logo +fi + +SOURCE_TYPE="${SOURCE_TYPE:-}" +FUNCTION_NAME="${FUNCTION_NAME:-}" +TOOL_KEY="${TOOL_KEY:-${FUNCTION_NAME}}" +FUNCTIONS_BATCH="${FUNCTIONS_BATCH:-}" + +# Build the list of (source, function, tool_key) tuples from either the +# single-mode env vars or the batch payload. +declare -a BATCH=() +if [[ -n "$FUNCTIONS_BATCH" ]]; then + while IFS= read -r line; do + [[ -z "$line" ]] && continue + BATCH+=("$line") + done <<< "$FUNCTIONS_BATCH" +elif [[ -n "$SOURCE_TYPE" && -n "$FUNCTION_NAME" ]]; then + BATCH+=("${SOURCE_TYPE}:${FUNCTION_NAME}:${TOOL_KEY}") +else + echo "ERROR: provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode)." + exit 2 +fi + +# ---------------------------------------------------------------------- +# Source the flow scripts on first use. Both auto and customizable +# guard their entry point with `if [[ "${BASH_SOURCE[0]}" == "${0}" ]]`, +# so sourcing loads function definitions without triggering the +# interactive menu or the extremeshok dialog. +# ---------------------------------------------------------------------- +SOURCED_AUTO=0 +SOURCED_CUSTOM=0 +ensure_flow_loaded() { + local source_type="$1" + case "$source_type" in + # `detected` is the synthetic source the registry assigns to a + # tool when a legacy detector finds it on the host but the + # entry was never written to installed_tools.json (e.g. + # proxmox_repos on a pre-1.2.2 install). Treat it as `auto` + # for the source-loading step — the legacy-tool's wrapper + # function lives in auto_post_install.sh, and once the update + # runs successfully `register_tool` overwrites the synthetic + # entry with source="auto" so subsequent runs don't hit this + # branch again. + auto|detected) + if [[ $SOURCED_AUTO -eq 0 ]]; then + # shellcheck disable=SC1091 + source "$LOCAL_SCRIPTS/post_install/auto_post_install.sh" + SOURCED_AUTO=1 + fi + ;; + custom) + if [[ $SOURCED_CUSTOM -eq 0 ]]; then + # shellcheck disable=SC1091 + source "$LOCAL_SCRIPTS/post_install/customizable_post_install.sh" + SOURCED_CUSTOM=1 + fi + ;; + *) + echo "ERROR: invalid source '$source_type' (must be 'auto', 'custom', or 'detected')" + return 2 + ;; + esac +} + +# Suppress the Monitor's `service_fail` notifications while this +# wrapper runs. Some post-install functions legitimately restart their +# services, and systemd can expose a transient "failed" state before the +# fresh unit starts — same class of noise as PVE services restarting +# during an apt full-upgrade. Shared markers with +# scripts/utilities/proxmox_update.sh; consumed by +# AppImage/scripts/notification_events.py::is_apt_active_on_host. +_PROXMENUX_UPDATE_MARKER="/var/run/proxmenux-update-in-progress" +_PROXMENUX_UPDATE_FINISHED_MARKER="/var/run/proxmenux-update-just-finished" +_proxmenux_postinstall_cleanup() { + rm -f "$_PROXMENUX_UPDATE_MARKER" + touch "$_PROXMENUX_UPDATE_FINISHED_MARKER" +} +trap _proxmenux_postinstall_cleanup EXIT +touch "$_PROXMENUX_UPDATE_MARKER" + +# ---------------------------------------------------------------------- +# Run each tool. We don't bail on the first failure — the user marked a +# multi-select, they expect every chosen tool to be attempted. RCs are +# collected and the wrapper exits non-zero if any failed. +# ---------------------------------------------------------------------- +TOTAL=${#BATCH[@]} +FAILED=0 +INDEX=0 +for entry in "${BATCH[@]}"; do + INDEX=$((INDEX + 1)) + IFS=':' read -r src fn tkey <<< "$entry" + [[ -z "$tkey" ]] && tkey="$fn" + + if command -v msg_info2 >/dev/null 2>&1; then + msg_info2 "[$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..." + else + echo "[ProxMenux] [$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..." + fi + + if ! ensure_flow_loaded "$src"; then + FAILED=$((FAILED + 1)) + continue + fi + + if ! declare -F "$fn" >/dev/null 2>&1; then + if command -v msg_error >/dev/null 2>&1; then + msg_error "Function '$fn' is not defined in the ${src} flow." + else + echo "ERROR: function '$fn' is not defined in the ${src} flow." + fi + FAILED=$((FAILED + 1)) + continue + fi + + set +e + "$fn" + RC=$? + set -e + + if [[ $RC -eq 0 ]]; then + if command -v msg_ok >/dev/null 2>&1; then + msg_ok "Update for ${tkey} completed." + else + echo "[ProxMenux] Update for ${tkey} completed." + fi + else + if command -v msg_error >/dev/null 2>&1; then + msg_error "Update for ${tkey} exited with status $RC." + else + echo "ERROR: update for ${tkey} exited with status $RC." + fi + FAILED=$((FAILED + 1)) + fi +done + +if [[ $FAILED -eq 0 ]]; then + if command -v msg_success >/dev/null 2>&1; then + msg_success "All $TOTAL update(s) completed successfully." + else + echo "[ProxMenux] All $TOTAL update(s) completed successfully." + fi +else + if command -v msg_warn >/dev/null 2>&1; then + msg_warn "$FAILED of $TOTAL updates failed — see the messages above." + else + echo "WARNING: $FAILED of $TOTAL updates failed." + fi +fi + +if command -v msg_success >/dev/null 2>&1; then + msg_success "Press Enter to close this terminal..." +else + echo "Press Enter to close this terminal..." +fi +read -r 2>/dev/null || true + +[[ $FAILED -eq 0 ]] && exit 0 || exit 1 diff --git a/tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh b/tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh new file mode 100644 index 00000000..81daf239 --- /dev/null +++ b/tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh @@ -0,0 +1,23 @@ + msg_info "$(translate "Installing dependencies: ${MISSING[*]}")" + msg_ok "$(translate "All dependencies installed and verified.")" + msg_error "$(translate "Failed to install: ${MISSING[*]}")" + msg_error "$(translate "Failed to install: ${MISSING[*]}")" + msg_error "$(translate "Could not determine a valid ISO storage directory.")" + msg_error "$(translate "Failed to create temporary directory:") $TMP_DIR" + msg_warn "$(translate "Cancelled by user or empty URL.")" + msg_error "$(translate "The URL does not contain the required parameters (id, pack, edition).")" + echo -e "\n${BGN}=============== UUP Dump Creator ===============${CL}" + echo -e " ${BGN}🆔 ID:${CL} ${DGN}$BUILD_ID${CL}" + echo -e " ${BGN}🌐 Language:${CL} ${DGN}$LANG${CL}" + echo -e " ${BGN}💿 Edition:${CL} ${DGN}$EDITION${CL}" + echo -e " ${BGN}🖥️ Architecture:${CL} ${DGN}$ARCH${CL}" + echo -e " ${BGN}📁 Destination:${CL} ${DGN}$OUT_DIR${CL}" + echo -e "${BGN}===============================================${CL}\n" + echo -e "\n${GN}=======================================${CL}" + echo -e " 💿 ${GN}Starting ISO conversion...${CL}" + echo -e "${GN}=======================================${CL}\n" + msg_ok "$(translate "ISO created successfully:") $OUT_DIR/$(basename "$ISO_FILE")" + msg_ok "$(translate "Cleaning temporary files...")" + msg_success "$(translate "Press Enter to return to menu...")" + msg_warn "$(translate "No ISO was generated.")" + msg_success "$(translate "Press Enter to return to menu...")" diff --git a/tests/optimization-settings-fixture.cjs b/tests/optimization-settings-fixture.cjs new file mode 100644 index 00000000..68be679e --- /dev/null +++ b/tests/optimization-settings-fixture.cjs @@ -0,0 +1,61 @@ +// Actual Settings handlers and real fetchApi; all responses are inert fixtures. +const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm') +const {execFileSync}=require('node:child_process') +const root=path.resolve(__dirname,'..'),app=path.join(root,'AppImage') +const ts=require(path.join(app,'node_modules/typescript')) +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 find=data=>key.split('.').reduce((item,name)=>item?.[name],data) + return (find(catalogs[locale])??find(catalogs.en)??key).replace(/\{(\w+)\}/g,(all,key)=>params[key]??all) +} +const flatten=node=>!node||typeof node!=='object'?[]:[node,...[node.props?.children].flat(Infinity).flatMap(flatten)] +const text=node=>node==null||node===false?'':typeof node!=='object'?String(node):[node.props?.children].flat(Infinity).map(text).join(' ') +const compile=source=>ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020,jsx:ts.JsxEmit.ReactJSX}}).outputText +const source=fs.readFileSync(path.join(app,'components/settings.tsx'),'utf8') +const descriptions={exports:{},require:name=>({default:require(path.resolve(app,'lib',name))})};vm.runInNewContext(compile(fs.readFileSync(path.join(app,'lib/optimization-descriptions.ts'),'utf8')),descriptions) +const tools=['one','two'].map(key=>({key,name:'Demo '+key,enabled:true,has_source:true,version:'1.0',available_version:'1.1'})) +const sourceBody={success:true,function:'demo',script:'demo.sh',source:'demo() {\n echo "& $HOME"\n}\n',deprecated:false} +function fixture(locale='sk',catalog=tools){ + let cursor=0,states=[],refs=[],effects=[],memo=[],tree,language=locale,reply=()=>sourceBody + const calls=[],events={},timers=[],copies=[];let fallbackCopies=0 + const t=(key,params)=>translate(language)(key,params) + const react={ + useState(value){const i=cursor++;if(!(i in states))states[i]=typeof value==='function'?value():value;return[states[i],v=>{states[i]=typeof v==='function'?v(states[i]):v}]}, + useRef(value){const i=cursor++;return refs[i]??=( {current:value})}, + useEffect(fn,deps){const i=cursor++,old=memo[i];if(!old||deps?.some((v,n)=>v!==old.deps?.[n]))effects.push(()=>{old?.cleanup?.();memo[i]={deps,cleanup:fn()}})}, + } + const response=(body,status=200)=>({ok:status<400,status,statusText:'fixture',headers:{get:()=> 'application/json'},json:async()=>body}) + const fetch=async(url,options={})=>{ + const endpoint=String(url).replace(/^https?:\/\/[^/]+/,'');calls.push({endpoint,method:options.method||'GET'}) + assert.equal(options.method||'GET','GET','Source viewer may never mutate') + if(endpoint.includes('/tool-source/')){const result=await reply(endpoint);if(result instanceof Error)throw result;return result?.http?response(result.body,result.http):response(result)} + if(endpoint.endsWith('/installed-tools'))return response({success:true,installed_tools:catalog,updates_available_count:catalog.filter(tool=>tool.has_update).length}) + return response({categories:[],storages:[],interfaces:[],dismissed:[],success:true,candidates:[],configured:false}) + } + const api={exports:{},fetch,process:{env:{}},console:{error(){}},require:()=>require('./ts-fixture.cjs').loadTypeScript('lib/api-errors.ts')};vm.runInNewContext(compile(fs.readFileSync(path.join(app,'lib/api-config.ts'),'utf8')),api) + const add=(name,handler)=>{(events[name]??=new Set()).add(handler)},remove=(name,handler)=>events[name]?.delete(handler) + const win={addEventListener:add,removeEventListener:remove,setTimeout:fn=>{timers.push(fn);return timers.length},isSecureContext:true} + const doc={addEventListener:add,removeEventListener:remove,body:{appendChild(){},removeChild(){}},createElement:()=>({style:{},focus(){},select(){}}),execCommand:()=>{fallbackCopies++;return true}} + const clipboard={writeText:async value=>copies.push(value)} + const context={exports:{},window:win,document:doc,navigator:{clipboard},console:{error(){}},setTimeout:win.setTimeout, + require:name=>{ + if(name==='react')return react + if(name==='react/jsx-runtime')return{jsx:(type,props)=>({type,props}),jsxs:(type,props)=>({type,props})} + if(name.includes('optimization-descriptions'))return descriptions.exports + if(name.includes('api-config'))return api.exports + if(name.includes('i18n/provider'))return{SUPPORTED_LANGUAGES:[],useI18n:()=>({t,language,setLanguage:value=>language=value})} + if(name.includes('use-platform'))return{useIsPbs:()=>false} + if(name.includes('format-network'))return{getNetworkUnit:()=> 'Bytes'} + return new Proxy({},{get:(_,key)=>key}) + }} + vm.runInNewContext(compile(source),context) + const render=()=>{cursor=0;effects=[];tree=context.exports.Settings();for(const effect of effects)effect();return tree} + const settle=async()=>{for(let i=0;i<24;i++)await Promise.resolve();render()} + const tool=name=>flatten(tree).find(node=>node.props?.title===t('settings.optimizations.sourceTitle')&&text(node).includes(descriptions.exports.optimizationToolName(name,catalog.find(tool=>tool.key===name)?.name||'Demo '+name,t))) + const dialog=()=>flatten(tree).find(node=>node.props?.role==='dialog') + const close=()=>flatten(tree).find(node=>node.props?.['aria-label']===t('actions.close')).props.onClick() + return{render,settle,tool,dialog,close,calls,copies,timers,tree:()=>tree,setReply:value=>reply=value,t, + language:value=>{language=value;render()},escape:()=>events.keydown?.forEach(fn=>fn({key:'Escape'})), + unmount:()=>memo.forEach(effect=>effect?.cleanup?.()),clipboard,win,fallbackCopies:()=>fallbackCopies} +} +module.exports={fixture,flatten,text,translate,catalogs,descriptions} diff --git a/tests/test_api_errors.cjs b/tests/test_api_errors.cjs new file mode 100644 index 00000000..0a4ef081 --- /dev/null +++ b/tests/test_api_errors.cjs @@ -0,0 +1,66 @@ +const assert=require('node:assert/strict') +const {loadTypeScript}=require('./ts-fixture.cjs') +function fixture(language='sk',token='fixture-token'){ + const local=new Map(token?[['proxmenux-auth-token',token]]:[]),session=new Map(),calls=[];let reloads=0,reply=()=>new Response('{}',{headers:{'Content-Type':'application/json'}}) + const storage=map=>({getItem:key=>map.get(key)||null,setItem:(key,v)=>map.set(key,v),removeItem:key=>map.delete(key)}) + const win={localStorage:storage(local),location:{protocol:'https:',hostname:'fixture.invalid',port:'',reload:()=>reloads++}} + const globals={window:win,document:{documentElement:{lang:language}},localStorage:win.localStorage,sessionStorage:storage(session),console:{error(){}},fetch:async(url,options)=>{calls.push({url,options});return reply()}} + const {fetchApi}=loadTypeScript('lib/api-config.ts',globals) + return{fetchApi,calls,reply:value=>reply=value,globals,local,reloads:()=>reloads} +} +async function main(){ + for(const lang of ['sk','en','de']){ + const f=fixture(lang),sk=lang==='sk' + f.reply(()=>new Response('html',{status:502,statusText:'Bad Gateway',headers:{'Content-Type':'text/html'}})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===(sk?'Požiadavka na API zlyhala: 502 Bad Gateway':'API request failed: 502 Bad Gateway')) + f.reply(()=>new Response('',{headers:{'Content-Type':'text/html'}})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===(sk?'Očakávala sa odpoveď JSON, ale prišiel typ text/html':'Expected JSON response but got text/html')) + f.reply(()=>new Response('{',{headers:{'Content-Type':'application/json'}})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===(sk?'Odpoveď z /api/demo obsahuje neplatný JSON':'Invalid JSON response from /api/demo')) + f.reply(()=>({ok:true,headers:{get:()=>null},text:async()=>''})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message.includes(sk?'neznámy typ obsahu':'unknown content type')) + const detail={error:'Original technical detail &',details:{keep:1},suggestion:'Keep diagnostics'} + f.reply(()=>new Response(JSON.stringify(detail),{status:400,headers:{'Content-Type':'application/json'}})) + await assert.rejects(f.fetchApi('/api/demo',{method:'POST',body:'fixture'}),e=>{assert.equal(e.message,detail.error);assert.equal(e.status,400);assert.deepEqual(e.body,detail);return true}) + f.reply(()=>new Response('{',{status:400,statusText:'Bad Request',headers:{'Content-Type':'application/json'}})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message.includes('400 Bad Request')) + const result={ok:true,items:[1,2],empty:null};f.reply(()=>new Response(JSON.stringify(result),{headers:{'Content-Type':'application/json'}})) + assert.deepEqual(await f.fetchApi('/api/demo',{method:'PUT',body:'unchanged',headers:{'X-Fixture':'keep'}}),result) + const call=f.calls.at(-1);assert.equal(call.url,'/api/demo');assert.equal(call.options.method,'PUT');assert.equal(call.options.body,'unchanged');assert.equal(call.options.headers.Authorization,'Bearer fixture-token');assert.equal(call.options.headers['X-Fixture'],'keep');assert.equal(call.options.cache,'no-store') + assert.equal(f.calls.length,7,'Each request calls fetch exactly once, no retries or side calls') + const network=new Error('Original network diagnostic');f.reply(()=>{throw network});await assert.rejects(f.fetchApi('/api/demo'),e=>e===network) + assert.equal(f.calls.length,8);assert.equal(f.reloads(),0) + } + for(const token of ['', 'fixture-token']){ + const f=fixture('sk',token);f.reply(()=>new Response('',{status:401})) + await assert.rejects(f.fetchApi('/api/demo'),/Chýba platné prihlásenie/) + assert.equal(f.local.has('proxmenux-auth-token'),false);assert.equal(f.reloads(),token?1:0) + await assert.rejects(f.fetchApi('/api/demo'),/Chýba platné prihlásenie/);assert.equal(f.reloads(),token?1:0) + assert.equal(f.calls.length,2) + } + const privateMode=loadTypeScript('lib/api-errors.ts',{document:{documentElement:{lang:'sk'}},window:{get localStorage(){throw new Error('denied')}}}) + assert.equal(privateMode.apiFailure('invalidJson',{endpoint:'/api/demo'}),'Odpoveď z /api/demo obsahuje neplatný JSON') + const stored=loadTypeScript('lib/api-errors.ts',{window:{localStorage:{getItem:()=> 'sk'}}}) + assert.equal(stored.apiFailure('unknownContentType'),'neznámy typ obsahu') + // Existing consumer paths, with their actual imports bound to real fetchApi. + const helper=fixture();Object.assign(helper.globals,{AbortSignal,setTimeout:fn=>{fn();return 1}}) + const control=loadTypeScript('lib/vm-control.ts',helper.globals) + helper.reply(()=>new Response(JSON.stringify({status:'stopped',exitstatus:'OK',guest_status:'running'}),{headers:{'Content-Type':'application/json'}})) + await control.waitForVmControlTask(999,'reboot','UPID:fixture');assert.ok(helper.calls.at(-1).url.includes('/control-task/UPID%3Afixture')) + const archive=loadTypeScript('lib/guest-backup-archives.ts',helper.globals),backup={volid:'store:fixture',storage:'store'} + helper.reply(()=>new Response(JSON.stringify({results:[{volid:backup.volid,status:'deleted'}]}),{headers:{'Content-Type':'application/json'}})) + assert.equal((await archive.deleteGuestBackups(999,[backup]))[0].status,'deleted') + const deletion=helper.calls.at(-1);assert.equal(deletion.options.method,'POST');assert.equal(JSON.parse(deletion.options.body).confirm,true) + helper.reply(()=>new Response('fixture',{status:500,statusText:'Fixture'}));const count=helper.calls.length + await assert.rejects(archive.deleteGuestBackups(999,[backup]),/Požiadavka na API zlyhala/);assert.equal(helper.calls.length,count+1,'No retry of delete after a lost response') + const updates=loadTypeScript('lib/lxc-update-observation.ts',helper.globals) + helper.reply(()=>new Response(JSON.stringify({count:2,available:true}),{headers:{'Content-Type':'application/json'}})) + assert.equal((await updates.checkLxcUpdates(999)).count,2);assert.equal(helper.calls.at(-1).url,'/api/vms/999/updates/check') + helper.reply(()=>new Response(JSON.stringify(helper.calls.at(-1).options.method==='POST'?{job_id:'fixture'}:{state:'complete',update_check:{count:0}}),{headers:{'Content-Type':'application/json'}})) + assert.equal((await updates.refreshCheckLxcUpdates(999)).count,0) + const terminal=loadTypeScript('lib/terminal-ws.ts',helper.globals) + helper.reply(()=>new Response('fixture',{status:500}));assert.equal(await terminal.getTicketedWsUrl('wss://fixture.invalid/ws'),'wss://fixture.invalid/ws') + assert.equal(helper.calls.at(-1).url,'/api/terminal/ticket') + console.log('PASS actual fetchApi: EN/SK/fallback, HTTP/type/JSON/401, backend detail/body/status, original requests and success; no extra fetch/retry/mutation') +} +main().catch(error=>{console.error(error);process.exitCode=1}) diff --git a/tests/test_api_import_boundaries.cjs b/tests/test_api_import_boundaries.cjs new file mode 100644 index 00000000..6feb24e3 --- /dev/null +++ b/tests/test_api_import_boundaries.cjs @@ -0,0 +1,18 @@ +const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm') +const root=path.resolve(__dirname,'..'),ts=require(root+'/AppImage/node_modules/typescript') +const code=ts.transpileModule(fs.readFileSync(root+'/AppImage/lib/api-config.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}}).outputText +async function main(){ + let calls=0,reply=()=>new Response(JSON.stringify({ok:true}),{headers:{'Content-Type':'application/json'}}) + // Existing consumers compile the actual transport in VM contexts without imports. + const context={exports:{},process:{env:{}},console:{error(){}},fetch:async()=>{calls++;return reply()}} + vm.runInNewContext(code,context) + assert.deepEqual(await context.exports.fetchApi('/api/fixture'),{ok:true}) + const body={error:'Original backend detail',code:'fixture',details:{keep:true}} + reply=()=>new Response(JSON.stringify(body),{status:400,headers:{'Content-Type':'application/json'}}) + await assert.rejects(context.exports.fetchApi('/api/fixture',{method:'POST',body:'unchanged'}),error=>{assert.equal(error.message,body.error);assert.equal(error.status,400);assert.deepEqual(error.body,body);return true}) + const network=new Error('Original network detail');reply=()=>{throw network} + await assert.rejects(context.exports.fetchApi('/api/fixture'),error=>error===network) + assert.equal(calls,3,'No locale import, retry or extra request on these paths') + console.log('PASS legacy transport imports: success, backend payload/status and network identity without a locale loader; exactly three calls') +} +main().catch(error=>{console.error(error);process.exitCode=1}) diff --git a/tests/test_optimization_labels.cjs b/tests/test_optimization_labels.cjs new file mode 100644 index 00000000..038119ca --- /dev/null +++ b/tests/test_optimization_labels.cjs @@ -0,0 +1,46 @@ +// Actual Settings callbacks; terminal is a JSX stub, never a real updater. +const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path') +const {fixture,flatten,text,translate,catalogs,descriptions}=require('./optimization-settings-fixture.cjs') +async function main(){ + const known={key:'time_sync',name:'Time Synchronization',has_source:true,has_update:true,version:'1.0',available_version:'1.1',function:'configure_time_sync',function_auto:'configure_time_sync',source:'auto',description:catalogs.en.settings.optimizations.toolDescriptions.timeSync} + const unknown={...known,key:'future',name:'Future Tool',function:'future_fn',function_auto:'future_fn',description:'New server wording &'} + for(const locale of ['sk','en','de']){ + const f=fixture(locale,[known,unknown]);f.render();await f.settle() + const expected=translate(locale)('settings.optimizations.toolNames.time_sync') + assert.ok(text(f.tree()).includes(expected));assert.ok(text(f.tree()).includes('Future Tool')) + const open=flatten(f.tree()).find(n=>n.props?.title===f.t('settings.optimizations.viewUpdates'));assert.ok(open) + open.props.onClick();f.render() + assert.ok(text(f.tree()).includes(descriptions.exports.optimizationDescription(known.description,translate(locale)))) + assert.ok(text(f.tree()).includes(unknown.description)) + const count=f.calls.length + flatten(f.tree()).find(n=>n.type==='button'&&text(n)===f.t('actions.cancel')).props.onClick();f.render() + assert.equal(f.calls.length,count,'Cancel has no scan/update effects') + open.props.onClick();f.render() + flatten(f.tree()).find(n=>n.type==='button'&&text(n).trim()===f.t('settings.optimizations.updateSelected')).props.onClick();f.render() + const terminal=flatten(f.tree()).find(n=>n.props?.scriptName==='update_post_install_function') + assert.equal(terminal.props.params.FUNCTIONS_BATCH,'auto:configure_time_sync:time_sync\nauto:future_fn:future') + assert.equal(terminal.props.params.EXECUTION_MODE,'web');assert.equal(f.calls.length,count,'Only an inert terminal node was created') + const single=fixture(locale,[known]);single.render();await single.settle() + const button=flatten(single.tree()).find(n=>n.props?.title===single.t('settings.optimizations.updateToolTitle',{name:expected,version:'1.1'}));assert.ok(button) + button.props.onClick({stopPropagation(){}});single.render() + const one=flatten(single.tree()).find(n=>n.props?.scriptName==='update_post_install_function') + assert.equal(one.props.title,single.t('settings.optimizations.updateOneTitle',{name:expected})) + assert.equal(one.props.params.FUNCTIONS_BATCH,'auto:configure_time_sync:time_sync') + assert.equal([...f.calls,...single.calls].filter(c=>c.method!=='GET'||/scan|script-runner/.test(c.endpoint)).length,0) + } + for(const locale of ['sk','en','de']){ + const t=translate(locale) + for(const [key,name] of Object.entries(catalogs.en.settings.optimizations.toolNames)){ + assert.equal(descriptions.exports.optimizationToolName(key,name,t),locale==='sk'?catalogs.sk.settings.optimizations.toolNames[key]:name) + } + assert.equal(descriptions.exports.optimizationToolName('future','Future &',t),'Future &') + } + const source=path.join(__dirname,'../scripts/post_install') + const actual=new Set(['auto_post_install.sh','customizable_post_install.sh'].flatMap(file=>[...fs.readFileSync(path.join(source,file),'utf8').matchAll(/^\s*# description: (.+)$/gm)].map(m=>m[1].trim()))) + const english=catalogs.en.settings.optimizations.toolDescriptions + assert.deepEqual(new Set(Object.values(english)),actual) + for(const [key,description]of Object.entries(english))assert.equal(descriptions.exports.optimizationDescription(description,translate('sk')),catalogs.sk.settings.optimizations.toolDescriptions[key]) + assert.equal(descriptions.exports.optimizationDescription('Changed version description',translate('sk')),'Changed version description') + console.log('PASS optimization names/descriptions: actual Settings cards/single/batch/cancel, unchanged payload, EN/SK/fallback, exact live-description matching; host calls 0') +} +main().catch(error=>{console.error(error);process.exitCode=1}) diff --git a/tests/test_post_install_i18n.py b/tests/test_post_install_i18n.py new file mode 100644 index 00000000..7878c240 --- /dev/null +++ b/tests/test_post_install_i18n.py @@ -0,0 +1,46 @@ +"""Run the real wrapper only against inert flow scripts and temporary markers.""" +from pathlib import Path +import os,re,shlex,subprocess,tempfile,unittest + +ROOT=Path(__file__).resolve().parents[1] +FILE='scripts/post_install/update_post_install_function.sh' +TRANSLATE=re.search(r'(?ms)^translate\(\) \{.*?^\}',(ROOT/'scripts/utils.sh').read_text()).group() + +def run(source,params,language='en',utilities=True): + with tempfile.TemporaryDirectory(prefix='postinstall-i18n-') as temp: + tmp=Path(temp);base=tmp/'base';flow=base/'scripts/post_install';flow.mkdir(parents=True) + trace=tmp/'trace';trace.write_text('');blocked=tmp/'blocked';blocked.write_text('') + for name in ('auto_post_install.sh','customizable_post_install.sh'): + (flow/name).write_text('fixture_ok() { echo ok >> "$FIXTURE_TRACE"; return 0; }\nfixture_fail() { echo fail >> "$FIXTURE_TRACE"; return 7; }\n') + if utilities: + (base/'utils.sh').write_text('load_language(){ LANGUAGE="$FIXTURE_LANG"; }; initialize_cache(){ :; }; show_proxmenux_logo(){ :; }; LANG_DIR='+shlex.quote(str(ROOT/'lang'))+'\n'+TRANSLATE+'\n'+'\n'.join(name+'(){ printf "%s\\n" "$1"; }' for name in ('msg_info2','msg_error','msg_ok','msg_warn','msg_success'))+'\n') + binary=tmp/'bin';binary.mkdir() + for name in ('qm','pct','pvesh','pvesm','apt-get','curl','wget','mount','systemctl','reboot'): + p=binary/name;p.write_text('#!/bin/sh\necho '+shlex.quote(name)+' >> "$FIXTURE_BLOCKED"\nexit 97\n');p.chmod(0o755) + rewritten=source.replace('/usr/local/share/proxmenux',str(base)).replace('/var/run/proxmenux-update-',str(tmp/'proxmenux-update-')) + script=tmp/'wrapper.sh';script.write_text(rewritten) + env={**os.environ,'PATH':str(binary)+':'+os.environ['PATH'],'FIXTURE_TRACE':str(trace),'FIXTURE_BLOCKED':str(blocked),'FIXTURE_LANG':language,'SOURCE_TYPE':'','FUNCTION_NAME':'','TOOL_KEY':'','FUNCTIONS_BATCH':'',**params} + result=subprocess.run(['bash',str(script)],input='\n',text=True,capture_output=True,env=env,timeout=10) + assert blocked.read_text()=='','Forbidden host command' + assert not (tmp/'proxmenux-update-in-progress').exists(),'Marker cleanup' + return result.returncode,result.stdout+result.stderr,trace.read_text() + +class PostInstallI18n(unittest.TestCase): + def test_existing_single_batch_failures_and_fallback_contract(self): + before=(ROOT/'tests/fixtures/cli-before-i18n/update_post_install_function.sh').read_text();after=(ROOT/FILE).read_text() + cases=[{}, {'SOURCE_TYPE':'auto','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'detected','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'custom','FUNCTION_NAME':'missing_fn','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'wrong','FUNCTION_NAME':'fixture_ok'}, {'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three\ndetected:fixture_ok:four'}] + for utilities in (False,True): + for params in cases: + with self.subTest(utilities=utilities,params=params):self.assertEqual(run(before,params,utilities=utilities),run(after,params,utilities=utilities)) + def test_slovak_missing_failure_summary_and_no_real_host_calls(self): + source=(ROOT/FILE).read_text() + code,text,trace=run(source,{'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three'},language='sk') + self.assertEqual(code,1);self.assertEqual(trace,'ok\nfail\n') + self.assertIn('Aktualizácia one je dokončená.',text) + self.assertIn('Aktualizácia two skončila s návratovým kódom 7.',text) + self.assertIn("Funkcia 'missing_fn' nie je definovaná v postupe auto.",text) + self.assertIn('Zlyhalo 2 z 3 aktualizácií',text) + self.assertIn('Stlač Enter na zatvorenie terminálu',text) + def test_syntax(self):subprocess.run(['bash','-n',str(ROOT/FILE)],check=True) + +if __name__=='__main__':unittest.main() diff --git a/tests/test_remaining_cli_i18n.py b/tests/test_remaining_cli_i18n.py new file mode 100644 index 00000000..36a46b65 --- /dev/null +++ b/tests/test_remaining_cli_i18n.py @@ -0,0 +1,75 @@ +"""Real translate and display-only shell calls; never source a VM creator.""" +from pathlib import Path +import importlib.util,json,os,re,shlex,subprocess,tempfile,unittest + +ROOT=Path(__file__).resolve().parents[1] +SPEC=importlib.util.spec_from_file_location('official_i18n',ROOT/'.github/scripts/build_translation_cache.py') +OFFICIAL=importlib.util.module_from_spec(SPEC);SPEC.loader.exec_module(OFFICIAL) +TRANSLATE=re.search(r'(?ms)^translate\(\) \{.*?^\}',(ROOT/'scripts/utils.sh').read_text()).group() +FILES=('scripts/vm/synology.sh','scripts/utilities/uup_dump_iso_creator.sh') +DISPLAY=re.compile(r'^\s*(?:msg_(?:info2?|ok|warn|error|success|title)|echo -[en])\s+".*"\s*(?:>&2)?\s*$') + +def display_calls(source): + return [line for line in source.splitlines() if DISPLAY.fullmatch(line) and not re.search(r'\$\((?!(?:translate|printf|basename)\b)',line)] + +def render(calls,language): + with tempfile.TemporaryDirectory(prefix='remaining-sk-display-') as tmp: + blocked=Path(tmp)/'blocked';blocked.write_text('') + definitions='\n'.join(name+'() { printf "%s\\n" "$1"; }' for name in ('msg_info','msg_info2','msg_ok','msg_warn','msg_error','msg_success','msg_title')) + forbidden='\n'.join(name+'() { echo '+shlex.quote(name)+' >> '+shlex.quote(str(blocked))+'; return 97; }' for name in ('qm','pct','pvesh','apt-get','curl','wget','mount','reboot','systemctl','rm','touch','mkdir','chmod')) + values={'NAME':'Synology VM','VMID':'999','IMAGES_DIR':'/fixture/images','EFI_STORAGE':'efi-store','LOADER_STORAGE':'loader-store','STORAGE':'data-store','STORAGE_TYPE':'zfspool','LOADER_NAME':'Arc','LOADER_FILE':'/fixture/loader.img','FILE':'loader.img','DISK_NUM':'2','SATA_ID':'sata2','SIZE':'32','DISK_MODEL':'Fixture SSD','DISK_SIZE':'32G','DISK_ID':'sata3','DISK':'/dev/fixture','PCI_DEV':'0000:01:00.0','HOSTPCI_INDEX':'0','HN':'fixture-vm','CORE_COUNT':'2','RAM_SIZE':'4096','BRG':'vmbr0','MAC':'02:00:00:00:00:01','VLAN1':'Default','MTU1':'Default','SERIAL_PORT':'socket','START_VM':'no','MACH':'q35','OUT_DIR':'/fixture/iso','BUILD_ID':'fixture','EDITION':'professional','ARCH':'amd64','DEFAULT_TMP':'/fixture/tmp','TMP_DIR':'/fixture/tmp','ISO_FILE':'fixture.iso','user_path':'/fixture'} + catalog={} if language=='en' else json.loads((ROOT/'lang'/f'{language}.json').read_text()) + values.update({key:catalog.get('Default','Default') for key in ('VLAN1','MTU1')}) + assignments='\n'.join(key+'='+shlex.quote(value) for key,value in values.items()) + script='LANGUAGE='+shlex.quote(language)+'\nLANG_DIR='+shlex.quote(str(ROOT/'lang'))+'\n'+TRANSLATE+'\n'+definitions+'\n'+forbidden+'\n'+assignments+'\n'+'\n'.join(calls)+'\n' + result=subprocess.run(['bash','-c',script],text=True,capture_output=True,timeout=15,check=True) + assert blocked.read_text()=='' + return result.stdout+result.stderr + +class RemainingCliI18n(unittest.TestCase): + def test_real_default_and_advanced_wizard_cancel_before_host_calls(self): + source=(ROOT/'scripts/vm/synology.sh').read_text() + functions='\n'.join(re.search(r'(?ms)^function '+name+r'\(\) \{.*?^\}',source).group() for name in ('default_settings','select_disk_type','_build_storage_plan_summary','advanced_settings','exit_script')) + for action in ('default_settings','advanced_settings'): + with tempfile.TemporaryDirectory(prefix='remaining-sk-cancel-') as tmp: + blocked=Path(tmp)/'blocked';blocked.write_text('');dialog_log=Path(tmp)/'dialog';dialog_log.write_text('') + forbidden='\n'.join(name+'() { echo '+shlex.quote(name)+' >> '+shlex.quote(str(blocked))+'; return 97; }' for name in ('qm','pct','pvesh','pvesm','curl','wget','apt-get','mount','systemctl','reboot','select_loader')) + script='LANGUAGE=sk\nLANG_DIR='+shlex.quote(str(ROOT/'lang'))+'\n'+TRANSLATE+'\n'+forbidden+'\n'+functions+'\nclear(){ :; }; sleep(){ :; }; header_info(){ :; }; msg_warn(){ echo "$1"; }; whiptail(){ printf "%s\\n" "$*" >> '+shlex.quote(str(dialog_log))+'; return 1; }; NAME="Synology VM"; NEXTID=999; GEN_MAC="02:00:00:00:00:01"; '+action + result=subprocess.run(['bash','-c',script],text=True,capture_output=True,timeout=10) + self.assertEqual(blocked.read_text(),'') + self.assertIn('Zrušiť' if action=='default_settings' else 'Ukončiť skript',dialog_log.read_text()) + self.assertNotIn('command not found',result.stderr) + def test_generated_uup_helper_is_localized_and_valid_without_execution(self): + source=(ROOT/FILES[1]).read_text();body=source.split('cat > uup_download_linux.sh <{ + if(name.startsWith('@/'))return loadTypeScript(path.join(app,name.slice(2))+'.ts',globals) + if(!name.startsWith('.'))return require(name) + let target=path.resolve(path.dirname(resolved),name) + if(!fs.existsSync(target))target+='.ts' + return loadTypeScript(target,globals) + }} + const compiled=ts.transpileModule(fs.readFileSync(resolved,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020,esModuleInterop:true}}).outputText + vm.runInNewContext(compiled,context,{filename:resolved}) + return context.module.exports +} +module.exports={loadTypeScript} From dbcc4de6e10b7a01391dfb3fae61133934c8f119 Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:49:16 +0000 Subject: [PATCH 2/3] fix(i18n): address Monitor and CLI review feedback --- AppImage/lib/api-errors.ts | 22 +- AppImage/lib/i18n/catalogs.ts | 23 ++ AppImage/lib/i18n/provider.tsx | 20 +- scripts/vm/synology.sh | 6 +- tests/backup_i18n_seam.cjs | 9 +- .../cli-before-i18n/synology-display.sh | 141 ------------ .../update_post_install_function.sh | 200 ------------------ .../uup_dump_iso_creator-display.sh | 23 -- tests/test_api_errors.cjs | 41 ++++ tests/test_post_install_i18n.py | 19 +- tests/test_remaining_cli_i18n.py | 28 ++- 11 files changed, 121 insertions(+), 411 deletions(-) create mode 100644 AppImage/lib/i18n/catalogs.ts delete mode 100644 tests/fixtures/cli-before-i18n/synology-display.sh delete mode 100644 tests/fixtures/cli-before-i18n/update_post_install_function.sh delete mode 100644 tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh diff --git a/AppImage/lib/api-errors.ts b/AppImage/lib/api-errors.ts index 7e995dee..a80c9add 100644 --- a/AppImage/lib/api-errors.ts +++ b/AppImage/lib/api-errors.ts @@ -1,17 +1,19 @@ -import enMessages from "../messages/en/common.json" -import skMessages from "../messages/sk/common.json" -import { detectBrowserLanguage, LANGUAGE_STORAGE_KEY } from "./i18n/languages" +import { MESSAGE_CATALOG } from "./i18n/catalogs" +import { detectBrowserLanguage, isSupportedLanguage, LANGUAGE_STORAGE_KEY } from "./i18n/languages" -type Failure = keyof typeof enMessages.apiErrors +type Failure = keyof (typeof import("../messages/en/common.json"))["apiErrors"] // fetchApi also runs outside React hooks. Read the provider's current document // language, with its stored/browser preference as a fallback. export function apiFailure(key: Failure, params: Record = {}): string { - let language = typeof document === "undefined" ? "" : document.documentElement?.lang - if (!language && typeof window !== "undefined") { - try { language = window.localStorage.getItem(LANGUAGE_STORAGE_KEY) || "" } catch { /* private browsing */ } + let preference = typeof document === "undefined" ? "" : document.documentElement?.lang + if (!isSupportedLanguage(preference) && typeof window !== "undefined") { + try { preference = window.localStorage.getItem(LANGUAGE_STORAGE_KEY) || "" } catch { /* private browsing */ } } - language ||= detectBrowserLanguage() - const messages = language === "sk" ? skMessages.apiErrors : enMessages.apiErrors - return messages[key].replace(/\{(\w+)\}/g, (match, name) => params[name] === undefined ? match : String(params[name])) + const language = isSupportedLanguage(preference) ? preference : detectBrowserLanguage() + const messages = MESSAGE_CATALOG[language].apiErrors + const localized = messages && typeof messages === "object" ? (messages as Record)[key] : undefined + const fallback = (MESSAGE_CATALOG.en.apiErrors as Record)[key] + const template = typeof localized === "string" ? localized : fallback + return template.replace(/\{(\w+)\}/g, (match, name) => params[name] === undefined ? match : String(params[name])) } diff --git a/AppImage/lib/i18n/catalogs.ts b/AppImage/lib/i18n/catalogs.ts new file mode 100644 index 00000000..3547eb10 --- /dev/null +++ b/AppImage/lib/i18n/catalogs.ts @@ -0,0 +1,23 @@ +import enMessages from "../../messages/en/common.json" +import deMessages from "../../messages/de/common.json" +import esMessages from "../../messages/es/common.json" +import frMessages from "../../messages/fr/common.json" +import itMessages from "../../messages/it/common.json" +import ptMessages from "../../messages/pt/common.json" +import skMessages from "../../messages/sk/common.json" +import svMessages from "../../messages/sv/common.json" +import type { LanguageCode } from "./languages" + +type MessageTree = Record + +// Shared by React translations and error handling outside React hooks. +export const MESSAGE_CATALOG: Record = { + en: enMessages as MessageTree, + de: deMessages as MessageTree, + es: esMessages as MessageTree, + fr: frMessages as MessageTree, + it: itMessages as MessageTree, + pt: ptMessages as MessageTree, + sk: skMessages as MessageTree, + sv: svMessages as MessageTree, +} diff --git a/AppImage/lib/i18n/provider.tsx b/AppImage/lib/i18n/provider.tsx index 04d5b638..ebdc6097 100644 --- a/AppImage/lib/i18n/provider.tsx +++ b/AppImage/lib/i18n/provider.tsx @@ -1,14 +1,7 @@ "use client" import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react" -import enMessages from "../../messages/en/common.json" -import deMessages from "../../messages/de/common.json" -import esMessages from "../../messages/es/common.json" -import frMessages from "../../messages/fr/common.json" -import itMessages from "../../messages/it/common.json" -import ptMessages from "../../messages/pt/common.json" -import skMessages from "../../messages/sk/common.json" -import svMessages from "../../messages/sv/common.json" +import { MESSAGE_CATALOG } from "./catalogs" import { DEFAULT_LANGUAGE, LANGUAGE_STORAGE_KEY, @@ -21,17 +14,6 @@ import { type MessageTree = Record type TranslationParams = Record -const MESSAGE_CATALOG: Record = { - en: enMessages as MessageTree, - de: deMessages as MessageTree, - es: esMessages as MessageTree, - fr: frMessages as MessageTree, - it: itMessages as MessageTree, - pt: ptMessages as MessageTree, - sk: skMessages as MessageTree, - sv: svMessages as MessageTree, -} - interface I18nContextValue { language: LanguageCode setLanguage: (language: LanguageCode) => void diff --git a/scripts/vm/synology.sh b/scripts/vm/synology.sh index 66668b95..b75ff7cc 100644 --- a/scripts/vm/synology.sh +++ b/scripts/vm/synology.sh @@ -1431,9 +1431,9 @@ HTML_DESC="

-$(translate "Docs") -$(translate "Code") -$(translate "Loader") +Docs +Code +Loader Ko-fi

diff --git a/tests/backup_i18n_seam.cjs b/tests/backup_i18n_seam.cjs index 3404efe2..db8ce033 100644 --- a/tests/backup_i18n_seam.cjs +++ b/tests/backup_i18n_seam.cjs @@ -29,10 +29,11 @@ const lookupCode = pure.map(node => printer.printNode(ts.EmitHint.Unspecified, n const catalogs = Object.fromEntries(fs.readdirSync(path.join(app, 'messages')).filter(locale => fs.existsSync(path.join(app, 'messages', locale, 'common.json'))).map(locale => [locale, JSON.parse(read(`messages/${locale}/common.json`))])); -const registered = provider.statements.flatMap(statement => ts.isVariableStatement(statement) ? - statement.declarationList.declarations.filter(declaration => declaration.name.getText(provider) === 'MESSAGE_CATALOG') : []); -assert.equal(registered.length, 1, 'real provider catalog declaration'); -assert.deepEqual(registered[0].initializer.properties.map(property => property.name.getText(provider)).sort(), +const catalogSource = ts.createSourceFile('catalogs.ts', read('lib/i18n/catalogs.ts'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); +const registered = catalogSource.statements.flatMap(statement => ts.isVariableStatement(statement) ? + statement.declarationList.declarations.filter(declaration => declaration.name.getText(catalogSource) === 'MESSAGE_CATALOG') : []); +assert.equal(registered.length, 1, 'real shared catalog declaration'); +assert.deepEqual(registered[0].initializer.properties.map(property => property.name.getText(catalogSource)).sort(), Object.keys(catalogs).sort(), 'test each runtime-registered shipped locale'); assert.ok(catalogs.en, 'English fallback catalog exists'); const translator = (language, messages = catalogs) => diff --git a/tests/fixtures/cli-before-i18n/synology-display.sh b/tests/fixtures/cli-before-i18n/synology-display.sh deleted file mode 100644 index 8418de52..00000000 --- a/tests/fixtures/cli-before-i18n/synology-display.sh +++ /dev/null @@ -1,141 +0,0 @@ - msg_title "Synology VM Creator" - msg_warn "$(translate "No compatible GPU detected for VM passthrough. Skipping GPU wizard option.")" - msg_warn "$gpu_notice" - msg_info2 "$(translate "Launching GPU passthrough assistant for VM") ${VMID}..." - echo -e "${DEF}Using Default Settings${CL}" - echo -e "${CUS}Using Advanced Settings${CL}" - echo -e " ${TAB}${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}" - echo -e " ${TAB}${DGN}Using Machine Type: ${BGN}q35${CL}" - echo -e " ${TAB}${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" - echo -e " ${TAB}${DGN}Using Hostname: ${BGN}${HN}${CL}" - echo -e " ${TAB}${DGN}Using CPU Model: ${BGN}Host${CL}" - echo -e " ${TAB}${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e " ${TAB}${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}" - echo -e " ${TAB}${DGN}Using Bridge: ${BGN}${BRG}${CL}" - echo -e " ${TAB}${DGN}Using MAC Address: ${BGN}${MAC}${CL}" - echo -e " ${TAB}${DGN}Using VLAN: ${BGN}Default${CL}" - echo -e " ${TAB}${DGN}Using Interface MTU Size: ${BGN}Default${CL}" - echo -e " ${TAB}${DGN}Configuring Serial Port: ${BGN}${SERIAL_PORT}${CL}" - echo -e " ${TAB}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}" - echo -e "${DEF}Creating a $NAME using the above default settings${CL}" - echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" - echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" - echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" - echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" - echo -e "${DGN}Using BIOS Type: ${BGN}SeaBIOS${CL}" - echo -e "${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" - echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}" - echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}" - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" - echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" - echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" - echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}" - echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}" - echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" - echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" - echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" - echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" - echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}" - echo -e "${CUS}Using Advanced Settings${CL}" - echo -e "$summary" - msg_warn "$(translate "Storage plan selection cancelled.")" - msg_ok "$(translate "Virtual Disks Created:")" - msg_ok "$(translate "Import Disks Selected:")" - msg_ok "$(translate "Controllers + NVMe Selected:")" - msg_info "$(translate "Detecting storage for disk images...")" - msg_error "Unable to detect a valid storage location." - msg_info "$(translate "Detecting PCI storage controllers and NVMe devices...")" - msg_info "Creating images directory" - msg_ok "Images directory created: $IMAGES_DIR" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" - echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" - msg_error "No custom image selected" - echo -e "${DGN}${TAB}Using Custom Image: ${BGN}$(basename "$LOADER_FILE")${CL}" - echo -e "${DGN}${TAB}Retrieving the URL for the ${BGN}$LOADER_NAME loader${CL}" - msg_info "Installing unzip..." - msg_error "Failed to install unzip" - msg_ok "Installed unzip successfully." - msg_error "$(translate "Failed to retrieve loader release metadata.")" - msg_error "$(translate "No unambiguous loader download found in release metadata.")" - msg_error "$(translate "Failed to prepare loader download directory.")" - msg_error "$(translate "Failed to download loader image.")" - msg_error "$(translate "Failed to extract a nonempty loader image.")" - msg_error "$(translate "Failed to save loader image.")" - msg_ok "Downloaded ${CL}${BL}${FILE}${CL} to ${IMAGES_DIR}" - msg_error "Unable to detect a valid storage location for EFI disk." >&2 - msg_warn "$(translate "EFI storage selection cancelled.")" >&2 - msg_error "Unable to detect a valid storage location." >&2 - msg_warn "$(translate "Storage selection cancelled for $purpose.")" >&2 - msg_error "Failed to create base VM. Check VM ID and host configuration." - msg_ok "Create a $NAME" - msg_info "Configuring EFI disk" - msg_error "EFI storage selection failed or was cancelled." - msg_ok "EFI disk created (raw) and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" - msg_error "Failed to configure EFI disk" - msg_ok "EFI disk created and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" - msg_error "Failed to configure EFI disk" - msg_error "Failed to create EFI disk" - msg_error "Loader storage selection failed or was cancelled." - echo -n "Importing loader disk: " - echo -n "." - msg_error "Loader import failed. No disk detected." - msg_ok "Loader imported successfully to ${CL}${BL}$LOADER_STORAGE${GN}${CL}" - msg_ok "Configured loader disk as ide0" - msg_error "Failed to assign loader disk to ide0" - msg_error "Loader import failed. No disk detected in config." - msg_ok "Configured loader disk as ide0" - msg_error "Failed to assign loader disk" - msg_ok "Loader configured as boot device." - msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..." - msg_error "Failed to assign disk $DISK_NUM ($SATA_ID) on $STORAGE" - msg_info "Allocating virtual disk for $STORAGE_TYPE..." - msg_error "Failed to allocate virtual disk $DISK_NUM" - msg_error "Failed to configure virtual disk as $SATA_ID" - msg_ok "Configured virtual disk as $SATA_ID, ${SIZE}GB on ${CL}${BL}$STORAGE${CL} ${GN}" - msg_ok "Configured import disk ${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN} as $DISK_ID" - msg_error "Failed to configure import disk $DISK as $DISK_ID" - msg_warn "$(translate "Skipping SR-IOV device"): ${SRIOV_SKIPPED}" - msg_warn "$(translate "No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.")" - msg_warn "$(translate "IOMMU was configured during this wizard and a reboot is pending.")" - msg_warn "$(translate "Controller + NVMe assignment will be written now and become active after host reboot.")" - msg_error "$(translate "IOMMU is not active. Skipping Controller + NVMe assignment.")" - msg_error "$(translate "Controller + NVMe passthrough requires machine type q35. Skipping controller assignment.")" - msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" - msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" - msg_warn "$(translate "Controller/NVMe is in use by running VM") ${SRC_VMID} ($(translate "stop source VM first"))" - msg_warn "$(translate "Start on boot disabled for VM") ${SRC_VMID}" - msg_ok "$(translate "Controller/NVMe removed from source VM") ${SRC_VMID} (${PCI_DEV})" - msg_info2 "$(translate "Skipped device"): ${PCI_DEV}" - msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}" - msg_error "Failed to configure controller/NVMe: ${PCI_DEV}" - msg_error "No disks/controllers configured." -msg_info "Setting VM description" - msg_error "Failed to set VM description" -msg_ok "Configured VM description" - msg_error "VM created with errors. Check configuration." - msg_success "$(translate "Completed Successfully with GPU passthrough configured!")" - msg_warn "$(translate "Host VFIO configuration changed (initramfs updated). Reboot required before starting the VM.")" - msg_success "$(translate "Completed Successfully!")" - msg_warn "$(translate "GPU passthrough was skipped (no compatible GPU detected).")" - msg_warn "$(translate "GPU passthrough was not applied.")" -echo -e "${TAB}${GN}$(translate "Next Steps:")${CL}" -echo -e "${TAB}1. $(translate "Start the VM")" -echo -e "${TAB}2. $(translate "Open the VM console and wait for the loader to boot")" -echo -e "${TAB}3. $(translate "In the loader interface, follow the instructions to select your Synology model")" -echo -e "${TAB}4. $(translate "Complete the DSM installation wizard")" -echo -e "${TAB}5. $(translate "Find your device using https://finds.synology.com")" - echo -e "${TAB}- $(translate "If you want to use a physical monitor on the passthrough GPU:")" - echo -e "${TAB}• $(translate "First complete DSM setup and verify Web UI/SSH access.")" - echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")" - msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")" - msg_warn "$(translate "Rebooting the system...")" - msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")" - msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")" - msg_warn "$(translate "VM creation cancelled before disk planning.")" diff --git a/tests/fixtures/cli-before-i18n/update_post_install_function.sh b/tests/fixtures/cli-before-i18n/update_post_install_function.sh deleted file mode 100644 index 17690202..00000000 --- a/tests/fixtures/cli-before-i18n/update_post_install_function.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash -# ========================================================== -# ProxMenux - Sprint 12B: re-run a single post-install function -# ========================================================== -# Invoked by the Monitor's "Update" buttons (Settings → ProxMenux -# Optimizations) and by the bash menu's update flow. Sources the -# appropriate post_install script and invokes one specific function so -# the user can update a single tool without re-running the whole flow. -# -# Two invocation modes: -# -# SINGLE — set SOURCE_TYPE + FUNCTION_NAME (+ optionally TOOL_KEY): -# SOURCE_TYPE=auto FUNCTION_NAME=install_log2ram_auto -# -# BATCH — set FUNCTIONS_BATCH to a newline-separated list of -# "source:function:tool_key" triples (tool_key optional): -# FUNCTIONS_BATCH="auto:install_log2ram_auto:log2ram -# custom:install_ceph:ceph" -# -# In batch mode the wrapper iterates the list, sourcing each flow once -# and re-using its function definitions. Tools coming from the same -# flow share a single source step so the terminal output stays clean. -# ========================================================== - -set -e - -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" -BASE_DIR="/usr/local/share/proxmenux" -UTILS_FILE="$BASE_DIR/utils.sh" - -if [[ -f "$UTILS_FILE" ]]; then - # shellcheck disable=SC1090 - source "$UTILS_FILE" -fi - -if command -v load_language >/dev/null 2>&1; then - load_language -fi -if command -v initialize_cache >/dev/null 2>&1; then - initialize_cache -fi - -if command -v show_proxmenux_logo >/dev/null 2>&1; then - show_proxmenux_logo -fi - -SOURCE_TYPE="${SOURCE_TYPE:-}" -FUNCTION_NAME="${FUNCTION_NAME:-}" -TOOL_KEY="${TOOL_KEY:-${FUNCTION_NAME}}" -FUNCTIONS_BATCH="${FUNCTIONS_BATCH:-}" - -# Build the list of (source, function, tool_key) tuples from either the -# single-mode env vars or the batch payload. -declare -a BATCH=() -if [[ -n "$FUNCTIONS_BATCH" ]]; then - while IFS= read -r line; do - [[ -z "$line" ]] && continue - BATCH+=("$line") - done <<< "$FUNCTIONS_BATCH" -elif [[ -n "$SOURCE_TYPE" && -n "$FUNCTION_NAME" ]]; then - BATCH+=("${SOURCE_TYPE}:${FUNCTION_NAME}:${TOOL_KEY}") -else - echo "ERROR: provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode)." - exit 2 -fi - -# ---------------------------------------------------------------------- -# Source the flow scripts on first use. Both auto and customizable -# guard their entry point with `if [[ "${BASH_SOURCE[0]}" == "${0}" ]]`, -# so sourcing loads function definitions without triggering the -# interactive menu or the extremeshok dialog. -# ---------------------------------------------------------------------- -SOURCED_AUTO=0 -SOURCED_CUSTOM=0 -ensure_flow_loaded() { - local source_type="$1" - case "$source_type" in - # `detected` is the synthetic source the registry assigns to a - # tool when a legacy detector finds it on the host but the - # entry was never written to installed_tools.json (e.g. - # proxmox_repos on a pre-1.2.2 install). Treat it as `auto` - # for the source-loading step — the legacy-tool's wrapper - # function lives in auto_post_install.sh, and once the update - # runs successfully `register_tool` overwrites the synthetic - # entry with source="auto" so subsequent runs don't hit this - # branch again. - auto|detected) - if [[ $SOURCED_AUTO -eq 0 ]]; then - # shellcheck disable=SC1091 - source "$LOCAL_SCRIPTS/post_install/auto_post_install.sh" - SOURCED_AUTO=1 - fi - ;; - custom) - if [[ $SOURCED_CUSTOM -eq 0 ]]; then - # shellcheck disable=SC1091 - source "$LOCAL_SCRIPTS/post_install/customizable_post_install.sh" - SOURCED_CUSTOM=1 - fi - ;; - *) - echo "ERROR: invalid source '$source_type' (must be 'auto', 'custom', or 'detected')" - return 2 - ;; - esac -} - -# Suppress the Monitor's `service_fail` notifications while this -# wrapper runs. Some post-install functions legitimately restart their -# services, and systemd can expose a transient "failed" state before the -# fresh unit starts — same class of noise as PVE services restarting -# during an apt full-upgrade. Shared markers with -# scripts/utilities/proxmox_update.sh; consumed by -# AppImage/scripts/notification_events.py::is_apt_active_on_host. -_PROXMENUX_UPDATE_MARKER="/var/run/proxmenux-update-in-progress" -_PROXMENUX_UPDATE_FINISHED_MARKER="/var/run/proxmenux-update-just-finished" -_proxmenux_postinstall_cleanup() { - rm -f "$_PROXMENUX_UPDATE_MARKER" - touch "$_PROXMENUX_UPDATE_FINISHED_MARKER" -} -trap _proxmenux_postinstall_cleanup EXIT -touch "$_PROXMENUX_UPDATE_MARKER" - -# ---------------------------------------------------------------------- -# Run each tool. We don't bail on the first failure — the user marked a -# multi-select, they expect every chosen tool to be attempted. RCs are -# collected and the wrapper exits non-zero if any failed. -# ---------------------------------------------------------------------- -TOTAL=${#BATCH[@]} -FAILED=0 -INDEX=0 -for entry in "${BATCH[@]}"; do - INDEX=$((INDEX + 1)) - IFS=':' read -r src fn tkey <<< "$entry" - [[ -z "$tkey" ]] && tkey="$fn" - - if command -v msg_info2 >/dev/null 2>&1; then - msg_info2 "[$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..." - else - echo "[ProxMenux] [$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..." - fi - - if ! ensure_flow_loaded "$src"; then - FAILED=$((FAILED + 1)) - continue - fi - - if ! declare -F "$fn" >/dev/null 2>&1; then - if command -v msg_error >/dev/null 2>&1; then - msg_error "Function '$fn' is not defined in the ${src} flow." - else - echo "ERROR: function '$fn' is not defined in the ${src} flow." - fi - FAILED=$((FAILED + 1)) - continue - fi - - set +e - "$fn" - RC=$? - set -e - - if [[ $RC -eq 0 ]]; then - if command -v msg_ok >/dev/null 2>&1; then - msg_ok "Update for ${tkey} completed." - else - echo "[ProxMenux] Update for ${tkey} completed." - fi - else - if command -v msg_error >/dev/null 2>&1; then - msg_error "Update for ${tkey} exited with status $RC." - else - echo "ERROR: update for ${tkey} exited with status $RC." - fi - FAILED=$((FAILED + 1)) - fi -done - -if [[ $FAILED -eq 0 ]]; then - if command -v msg_success >/dev/null 2>&1; then - msg_success "All $TOTAL update(s) completed successfully." - else - echo "[ProxMenux] All $TOTAL update(s) completed successfully." - fi -else - if command -v msg_warn >/dev/null 2>&1; then - msg_warn "$FAILED of $TOTAL updates failed — see the messages above." - else - echo "WARNING: $FAILED of $TOTAL updates failed." - fi -fi - -if command -v msg_success >/dev/null 2>&1; then - msg_success "Press Enter to close this terminal..." -else - echo "Press Enter to close this terminal..." -fi -read -r 2>/dev/null || true - -[[ $FAILED -eq 0 ]] && exit 0 || exit 1 diff --git a/tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh b/tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh deleted file mode 100644 index 81daf239..00000000 --- a/tests/fixtures/cli-before-i18n/uup_dump_iso_creator-display.sh +++ /dev/null @@ -1,23 +0,0 @@ - msg_info "$(translate "Installing dependencies: ${MISSING[*]}")" - msg_ok "$(translate "All dependencies installed and verified.")" - msg_error "$(translate "Failed to install: ${MISSING[*]}")" - msg_error "$(translate "Failed to install: ${MISSING[*]}")" - msg_error "$(translate "Could not determine a valid ISO storage directory.")" - msg_error "$(translate "Failed to create temporary directory:") $TMP_DIR" - msg_warn "$(translate "Cancelled by user or empty URL.")" - msg_error "$(translate "The URL does not contain the required parameters (id, pack, edition).")" - echo -e "\n${BGN}=============== UUP Dump Creator ===============${CL}" - echo -e " ${BGN}🆔 ID:${CL} ${DGN}$BUILD_ID${CL}" - echo -e " ${BGN}🌐 Language:${CL} ${DGN}$LANG${CL}" - echo -e " ${BGN}💿 Edition:${CL} ${DGN}$EDITION${CL}" - echo -e " ${BGN}🖥️ Architecture:${CL} ${DGN}$ARCH${CL}" - echo -e " ${BGN}📁 Destination:${CL} ${DGN}$OUT_DIR${CL}" - echo -e "${BGN}===============================================${CL}\n" - echo -e "\n${GN}=======================================${CL}" - echo -e " 💿 ${GN}Starting ISO conversion...${CL}" - echo -e "${GN}=======================================${CL}\n" - msg_ok "$(translate "ISO created successfully:") $OUT_DIR/$(basename "$ISO_FILE")" - msg_ok "$(translate "Cleaning temporary files...")" - msg_success "$(translate "Press Enter to return to menu...")" - msg_warn "$(translate "No ISO was generated.")" - msg_success "$(translate "Press Enter to return to menu...")" diff --git a/tests/test_api_errors.cjs b/tests/test_api_errors.cjs index 0a4ef081..4a2c76f7 100644 --- a/tests/test_api_errors.cjs +++ b/tests/test_api_errors.cjs @@ -1,5 +1,17 @@ const assert=require('node:assert/strict') +const fs=require('node:fs'),path=require('node:path') const {loadTypeScript}=require('./ts-fixture.cjs') +async function withApiCatalog(language,values,run){ + const original=fs.readFileSync,target=path.resolve(__dirname,'../AppImage/messages',language,'common.json') + fs.readFileSync=function(file,...args){ + const content=original.call(this,file,...args) + if(typeof file==='string'&&path.resolve(file)===target){ + const catalog=JSON.parse(content);catalog.apiErrors=values;return JSON.stringify(catalog) + } + return content + } + try{return await run()}finally{fs.readFileSync=original} +} function fixture(language='sk',token='fixture-token'){ const local=new Map(token?[['proxmenux-auth-token',token]]:[]),session=new Map(),calls=[];let reloads=0,reply=()=>new Response('{}',{headers:{'Content-Type':'application/json'}}) const storage=map=>({getItem:key=>map.get(key)||null,setItem:(key,v)=>map.set(key,v),removeItem:key=>map.delete(key)}) @@ -9,6 +21,35 @@ function fixture(language='sk',token='fixture-token'){ return{fetchApi,calls,reply:value=>reply=value,globals,local,reloads:()=>reloads} } async function main(){ + // Future translations must work without another language-specific code branch. + for(const language of ['de','es','fr','it','pt','sv']){ + await withApiCatalog(language,{http:language+' HTTP {status} {statusText}',unknownContentType:language+' TYPE'},async()=>{ + const f=fixture(language) + f.reply(()=>new Response('html',{status:502,statusText:'Bad Gateway'})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===language+' HTTP 502 Bad Gateway') + // A partial catalog falls back per key, preserving the original endpoint. + f.reply(()=>new Response('{',{headers:{'Content-Type':'application/json'}})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message==='Invalid JSON response from /api/demo') + assert.equal(f.calls.length,2,'Localization never retries or makes its own request') + }) + } + await withApiCatalog('fr',{http:{invalid:'not a message'},invalidJson:'FR {endpoint}'},async()=>{ + const f=fixture('fr');f.reply(()=>new Response('',{status:503,statusText:'Unavailable'})) + await assert.rejects(f.fetchApi('/api/demo'),e=>e.message==='API request failed: 503 Unavailable') + const browser=loadTypeScript('lib/api-errors.ts',{navigator:{language:'fr-FR',languages:['sk']}}) + assert.equal(browser.apiFailure('invalidJson',{endpoint:'/literal?x=&y=1'}),'FR /literal?x=&y=1') + const document={documentElement:{lang:'sk'}};let storedReads=0 + const current=loadTypeScript('lib/api-errors.ts',{document,window:{localStorage:{getItem:()=>{storedReads++;return 'sk'}}}}) + assert.equal(current.apiFailure('invalidJson',{endpoint:'/api/demo'}),'Odpoveď z /api/demo obsahuje neplatný JSON') + document.documentElement.lang='fr' + assert.equal(current.apiFailure('invalidJson',{endpoint:'/api/demo'}),'FR /api/demo','The next error follows a language switch without reloading') + assert.equal(storedReads,0,'The provider document language takes precedence') + document.documentElement.lang='unsupported' + assert.equal(current.apiFailure('unknownContentType'),'neznámy typ obsahu') + assert.equal(storedReads,1,'An invalid document language uses a supported stored preference') + const denied=loadTypeScript('lib/api-errors.ts',{window:{get localStorage(){throw new Error('denied')}},navigator:{language:'fr-FR',languages:[]}}) + assert.equal(denied.apiFailure('invalidJson',{endpoint:'/api/demo'}),'FR /api/demo') + }) for(const lang of ['sk','en','de']){ const f=fixture(lang),sk=lang==='sk' f.reply(()=>new Response('html',{status:502,statusText:'Bad Gateway',headers:{'Content-Type':'text/html'}})) diff --git a/tests/test_post_install_i18n.py b/tests/test_post_install_i18n.py index 7878c240..b0d08b1e 100644 --- a/tests/test_post_install_i18n.py +++ b/tests/test_post_install_i18n.py @@ -26,12 +26,21 @@ def run(source,params,language='en',utilities=True): return result.returncode,result.stdout+result.stderr,trace.read_text() class PostInstallI18n(unittest.TestCase): - def test_existing_single_batch_failures_and_fallback_contract(self): - before=(ROOT/'tests/fixtures/cli-before-i18n/update_post_install_function.sh').read_text();after=(ROOT/FILE).read_text() - cases=[{}, {'SOURCE_TYPE':'auto','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'detected','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'custom','FUNCTION_NAME':'missing_fn','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'wrong','FUNCTION_NAME':'fixture_ok'}, {'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three\ndetected:fixture_ok:four'}] + def test_single_batch_exit_codes_and_fallback_without_host_calls(self): + source=(ROOT/FILE).read_text() + cases=[({},2,''), + ({'SOURCE_TYPE':'auto','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'},0,'ok\n'), + ({'SOURCE_TYPE':'detected','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'},0,'ok\n'), + ({'SOURCE_TYPE':'custom','FUNCTION_NAME':'missing_fn','TOOL_KEY':'fixture'},1,''), + ({'SOURCE_TYPE':'wrong','FUNCTION_NAME':'fixture_ok'},1,''), + ({'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three\ndetected:fixture_ok:four'},1,'ok\nfail\nok\n')] for utilities in (False,True): - for params in cases: - with self.subTest(utilities=utilities,params=params):self.assertEqual(run(before,params,utilities=utilities),run(after,params,utilities=utilities)) + for params,expected_code,expected_trace in cases: + with self.subTest(utilities=utilities,params=params): + code,text,trace=run(source,params,utilities=utilities) + self.assertEqual(code,expected_code) + self.assertEqual(trace,expected_trace) + self.assertNotIn('command not found',text) def test_slovak_missing_failure_summary_and_no_real_host_calls(self): source=(ROOT/FILE).read_text() code,text,trace=run(source,{'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three'},language='sk') diff --git a/tests/test_remaining_cli_i18n.py b/tests/test_remaining_cli_i18n.py index 36a46b65..fea9f5ae 100644 --- a/tests/test_remaining_cli_i18n.py +++ b/tests/test_remaining_cli_i18n.py @@ -1,6 +1,7 @@ """Real translate and display-only shell calls; never source a VM creator.""" from pathlib import Path import importlib.util,json,os,re,shlex,subprocess,tempfile,unittest +from html.parser import HTMLParser ROOT=Path(__file__).resolve().parents[1] SPEC=importlib.util.spec_from_file_location('official_i18n',ROOT/'.github/scripts/build_translation_cache.py') @@ -27,6 +28,25 @@ def render(calls,language): return result.stdout+result.stderr class RemainingCliI18n(unittest.TestCase): + def test_badges_remain_valid_with_hostile_translations_without_host_calls(self): + source=(ROOT/FILES[0]).read_text() + html=re.search(r'(?ms)^HTML_DESC=".*?^
"',source).group() + class Images(HTMLParser): + def __init__(self):super().__init__();self.images=[] + def handle_starttag(self,tag,attrs): + if tag=='img':self.images.append(dict(attrs)) + with tempfile.TemporaryDirectory() as tmp: + blocked=Path(tmp)/'blocked';blocked.write_text('') + forbidden='\n'.join(name+'(){ echo '+shlex.quote(name)+' >> '+shlex.quote(str(blocked))+'; return 97; }' for name in ('qm','pct','pvesh','curl','wget','mount','reboot','systemctl')) + script=forbidden+'\ntranslate(){ printf "%s" "O\'Brien &