diff --git a/lang/sk.json b/lang/sk.json index 4e912c17..95c78843 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -713,7 +713,7 @@ "CPU Cores": "CPU jadrá", "CPU MODEL": "MODEL CPU", "CPU Model": "Model CPU", - "CPU cores": "Jadro procesora", + "CPU cores": "Počet jadier CPU", "CPU priority": "Priorita procesora", "CPU set to host,hidden=1,flags=+pcid": "CPU nastavené na host,hidden=1,flags=+pcid", "CPU vendor (intel/amd):": "Výrobca CPU (intel/amd):", @@ -1311,7 +1311,7 @@ "Core is running, but not responding over HTTP on 80/8123": "Jadro beží, ale neodpovedá cez HTTP na 80/8123", "Core is still on the initial installation page": "Jadro je stále na úvodnej inštalačnej stránke", "Core packages": "Základné balíky", - "Cores": "Jadro", + "Cores": "Počet jadier", "Corrupted gzip layer": "Vrstvená vrstva gzipu", "Could not add": "Nepodarilo sa pridať", "Could not add disk": "Disk sa nepodarilo pridať", @@ -1880,7 +1880,7 @@ "Do you want to update the existing export?": "Chcete aktualizovať existujúci export?", "Do you want to update the existing share?": "Chcete aktualizovať existujúce zdieľanie?", "Do you want to view the selected backup before restoring?": "Chcete si vybranú zálohu pred obnovou pozrieť?", - "Docker Mods are only offered for compatible LinuxServer images": "Docker Mods sú ponúkané len pre kompatibilné LinuxServer obrázky", + "Docker Mods are only offered for compatible LinuxServer images": "Docker Mods sú dostupné iba pre kompatibilné obrazy LinuxServer", "Docker Volume Backup": "Docker Volume Backup", "Docker/CLI not available yet or no valid answer": "Docker/CLI ešte nie je k dispozícii alebo nie je platná odpoveď", "Documents & Notes": "Dokumenty a poznámky", @@ -1931,7 +1931,7 @@ "Dumping AMD GPU ROM BIOS via sysfs...": "Vytváram dump AMD GPU ROM BIOS cez sysfs...", "Duplicate IP addresses found": "Našli sa duplicitné IP adresy", "Duplicate parameters cleaned": "Duplicitné parametre sú vyčistené", - "Duplicate variable in the contract; review it before editing": "Duplikovať premenné v zmluve; preskúmať pred úpravou", + "Duplicate variable in the contract; review it before editing": "Premenná je v definícii kontajnera uvedená viackrát; pred úpravou ju skontrolujte", "Duplicated GPU device in the container": "Duplikované GPU zariadenie v kontajneri", "Duplicated NVIDIA devices": "Duplikované zariadenia NVIDIA", "Duplicated VMID in the Proxmox inventory": "Duplikované VMID v inventári Proxmox", @@ -1942,7 +1942,7 @@ "Duplicati web interface (password only)": "Duplicati webové rozhranie (iba heslo)", "Duration": "Trvanie", "Duration:": "Trvanie:", - "Dynamic NVIDIA is only validated for unprivileged containers. This profile uses static mounts and must be recreated after the host driver changes.": "Dynamická NVIDIA je validovaná len pre neprivilegované kontajnery. Tento profil používa statické pripojenia a musí byť znovu vytvorený po zmene hostiteľského vodiča.", + "Dynamic NVIDIA is only validated for unprivileged containers. This profile uses static mounts and must be recreated after the host driver changes.": "Dynamická NVIDIA je overená iba pre neprivilegované kontajnery. Tento profil používa statické pripojenia; po zmene ovládača na hostiteľovi je potrebné kontajner znovu vytvoriť.", "EFI disk created and configured on": "EFI disk bol vytvorený a nastavený na", "EFI storage selection cancelled.": "Výber úložiska pre EFI bol zrušený.", "EFI storage selection failed or was cancelled. VM creation aborted.": "Výber úložiska pre EFI zlyhal alebo bol zrušený. Vytvorenie VM bolo prerušené.", @@ -2135,7 +2135,7 @@ "Enter the number or type the interface name:": "Zadajte číslo alebo napíšte názov rozhrania:", "Enter the password for Samba user:": "Zadajte heslo pre Samba používateľa:", "Enter the recovery passphrase set when the keyfile was created:": "Zadajte obnovovaciu frázu nastavenú pri vytvorení keyfile:", - "Enter the size in whole GB, for example": "Zadajte veľkosť v celej GB, napríklad", + "Enter the size in whole GB, for example": "Zadajte veľkosť v celých GB, napríklad", "Enter the value again.": "Zadajte hodnotu znova.", "Enter username for Samba server:": "Zadajte používateľské meno pre Samba server:", "Enter username:": "Zadajte používateľské meno:", @@ -2815,7 +2815,7 @@ "Host monitor verified: PID and network namespaces and memory match the host": "Monitor hostiteľa overený: PID a sieťové názvy a pamäť zodpovedajú hostiteľovi", "Host monitor: shared PID/network and privileged access. A compromised image could affect Proxmox. Use only on trusted networks.": "Monitor hostiteľa: spoločná PID/sieť a privilegovaný prístup. Kompromisný obraz by mohol ovplyvniť Proxmox. Používajte len na dôveryhodných sieťach.", "Host monitoring declined": "Monitorovanie hostiteľa sa znížilo", - "Host path for": "Hostiteľ cesta pre", + "Host path for": "Cesta na hostiteľovi pre", "Host paths found in container configs or saved records (not targeted for removal):": "Cesty hostiteľa nájdené v konfiguráciách kontajnerov alebo v uložených záznamoch (neodstraňujú sa):", "Host permissions applied (o+rwx + default ACL) — unprivileged LXCs can read/write through bind-mounts": "Oprávnenia hosta boli nastavené (o+rwx + predvolené ACL) - neprivilegované LXC môžu čítať/zapisovať cez bind mounty", "Host system path": "Cesta k hostiteľskému systému", @@ -2885,7 +2885,7 @@ "IP or hostname of the PVE node hosting the Borg server LXC:": "IP alebo hostname PVE uzla, na ktorom beží Borg server LXC:", "IPv4 address of the container": "IPv4 adresa kontajnera", "IPv4 address of the containers": "IPv4 adresa kontajnerov", - "IPv4 gateway (empty = no outbound route)": "Vstupná brána IPv4 (prázdna = žiadna úniková cesta)", + "IPv4 gateway (empty = no outbound route)": "Brána IPv4 (prázdne = bez odchádzajúcej trasy)", "ISO": "ISO", "ISO created successfully:": "ISO bolo úspešne vytvorené:", "ISO image — installation images": "ISO obraz — inštalačné obrazy", @@ -2938,7 +2938,7 @@ "Image": "Obraz", "Image Source Directory": "Zdrojový priečinok obrazov", "Image access confirmed.\n\n{reference}\n\nArchitecture: {architecture}\nVersion: {version}\nDownload size: {size}\nDigest: {digest}": "Prístup k obrazu bol potvrdený.\n\n{reference}\n\nArchitektúra: {architecture}\nVerzia: {version}\nVeľkosť na stiahnutie: {size}\nDigest: {digest}", - "Image cache": "Comment", + "Image cache": "Vyrovnávacia pamäť obrazov", "Image compatibility restored:": "Kompatibilita s obrázkom obnovená:", "Image compatibility verified:": "Kompatibilita s obrázkom overená:", "Image directory:": "Priečinok s obrazmi:", @@ -3260,7 +3260,7 @@ "Invalid VMID:": "Neplatný VMID:", "Invalid ZFS pool name.": "Neplatný názov ZFS poolu.", "Invalid absolute mount path": "Neplatná absolútna pripojovacia dráha", - "Invalid absolute path; avoid spaces, commas and relative segments": "Neplatná absolútna cesta; vyhnúť sa medzerám, čiarkam a relatívnym segmentom", + "Invalid absolute path; avoid spaces, commas and relative segments": "Neplatná absolútna cesta; nepoužívajte medzery, čiarky ani relatívne časti cesty", "Invalid acceleration profile": "Neplatný profil zrýchlenia", "Invalid access address:": "Neplatná adresa prístupu:", "Invalid action": "Neplatná akcia", @@ -3379,7 +3379,7 @@ "It can be made on another storage of this host. This backup is temporary: it is deleted when the operation ends. The choice is remembered for the next operations of this application.": "Zálohu možno vytvoriť na inom úložisku tohto hostiteľa. Je dočasná a po skončení operácie sa odstráni. Táto voľba sa zapamätá aj pre ďalšie operácie aplikácie.", "It cannot be removed on its own, because the application would stop working: continuing removes the whole application.": "Nemôže sa odstrániť samostatne, pretože žiadosť by prestala fungovať: pokračuje v odstraňovaní celej aplikácie.", "It cannot be removed on its own, because the application would stop working: continuing targets the whole application for removal.": "Nedá sa odstrániť samostatne, pretože aplikácia by prestala fungovať: pokračovanie sa zameriava na odstránenie celej aplikácie.", - "It is created empty; existing data is not migrated automatically.": "Vytvára sa prázdny; existujúce údaje sa automaticky neprenášajú.", + "It is created empty; existing data is not migrated automatically.": "Nové úložisko bude prázdne; existujúce údaje sa doň automaticky neprenesú.", "It is recommended to create a backup before continuing.": "Pred pokračovaním sa odporúča vytvoriť zálohu.", "It is strongly recommended to create a backup of your container before proceeding with the conversion.": "Pred prevodom sa dôrazne odporúča vytvoriť zálohu kontajnera.", "It needs a privileged container, which is not isolated from the host.": "Potrebuje privilegovaný kontajner, ktorý nie je izolovaný od hostiteľa.", @@ -3943,7 +3943,7 @@ "NVIDIA installer not found at": "NVIDIA inštalátor sa nenašiel v", "NVIDIA installer reported an error. Check /tmp/nvidia_install.log": "NVIDIA inštalátor ohlásil chybu. Skontrolujte /tmp/nvidia_install.log", "NVIDIA installer returned error": "NVIDIA inštalátor vrátil chybu", - "NVIDIA is already attached": "NVIDIA je už pripojený", + "NVIDIA is already attached": "Zariadenie NVIDIA je už pripojené", "NVIDIA kernel modules unloaded successfully.": "Kernel moduly NVIDIA boli úspešne uvoľnené.", "NVIDIA libs require approximately 1.5GB of free space.": "NVIDIA knižnice vyžadujú približne 1,5 GB voľného miesta.", "NVIDIA mount with an unauthorized source or target": "Pripojenie NVIDIA s neoprávneným zdrojom alebo cieľom", @@ -4708,7 +4708,7 @@ "Permission error": "Chyba oprávnení", "Permissions:": "Oprávnenia:", "Persist mount in CT /etc/fstab (optional):": "Uložiť pripojenie v CT /etc/fstab (voliteľné):", - "Persistence for": "Pretrvávanie", + "Persistence for": "Trvalé úložisko pre", "Persistence for the new path": "Pretrvávanie novej cesty", "Persistent data:": "Trvalé údaje:", "Persistent disk reused:": "Trvalý disk opätovne použitý:", @@ -5282,7 +5282,7 @@ "Required command not found:": "Požadovaný príkaz sa nenašiel:", "Required if using a VirtIO or SCSI disk.": "Vyžaduje sa pri použití VirtIO alebo SCSI disku.", "Required install helpers not available.": "Potrební pomocníci na inštaláciu nie sú dostupní.", - "Required new path cancelled": "Required nová cesta zrušená", + "Required new path cancelled": "Pridanie novej povinnej cesty bolo zrušené", "Required persistent paths cannot be removed": "Required perzistentné cesty nemožno odstrániť", "Required update helpers unavailable. Update stopped.": "Požadovaní pomocníci s aktualizáciou nie sú k dispozícii. Aktualizácia zastavená.", "Requires acl package. Skip if setfacl is not available.": "Vyžaduje balík acl. Preskočte, ak setfacl nie je dostupný.", @@ -5912,7 +5912,7 @@ "Single GPU Warning": "Upozornenie na jednu GPU", "Single target found — selected automatically:": "Našiel sa jeden target - vybraný automaticky:", "Size": "Veľkosť", - "Size in GB of": "Veľkosť v GB", + "Size in GB of": "Veľkosť v GB pre", "Size of each consume/export volume in GB": "Veľkosť každého spotrebovaného/vývozného objemu v GB", "Size of the /dev/shm shared memory in MB": "Veľkosť zdieľanej pamäte /dev/shm v MB", "Size of the Frigate temporary cache in MB": "Veľkosť Frigate dočasnej vyrovnávacej pamäte v MB", @@ -6434,8 +6434,8 @@ "The current image of the saved channel will be checked and downloaded. Resources, paths and GPU are kept. The CT is stopped during the replacement and a native backup is created first.": "Aktuálny obraz uloženého kanála bude skontrolovaný a stiahnutý. Zdroje, cesty a GPU sú udržiavané. CT je zastavený počas výmeny a natívne zálohovanie je vytvorené ako prvé.", "The current record is missing for": "Aktuálny záznam chýba", "The current template changes the image or identity; an explicit migration is required": "Aktuálna šablóna mení obraz alebo identitu; explicitná migrácia je required", - "The current template changes the template identity or image repository; an explicit migration is required": "Aktuálna šablóna zmení identitu šablóny alebo úložisko obrázkov;vyžaduje sa explicitná migrácia", - "The current template requires a new persistent path:": "Aktuálna šablóna requires novú pretrvávajúcu cestu:", + "The current template changes the template identity or image repository; an explicit migration is required": "Aktuálna šablóna mení identitu šablóny alebo repozitár obrazu; je potrebná výslovná migrácia", + "The current template requires a new persistent path:": "Aktuálna šablóna vyžaduje nové trvalé úložisko pre cestu:", "The custom path cannot hide system directories": "Vlastná cesta nemôže skryť systémové adresáre", "The custom path overlaps another mount": "Vlastná cesta sa prekrýva s iným bodom pripojenia", "The data and document volumes need at least 8 GB": "Údaje a objemy dokumentov potrebujú najmenej 8 GB", @@ -6942,7 +6942,7 @@ "This tool is designed for systems with Intel GPUs.": "Tento nástroj je určený pre systémy s Intel GPU.", "This translator only supports the Nextcloud stack": "Tento prekladateľ podporuje iba Nextcloud stack", "This value is required.": "Táto hodnota je required.", - "This variant requires the device": "Tento variant requires zariadenie", + "This variant requires the device": "Tento variant vyžaduje zariadenie", "This version does not build against the running kernel.": "Táto verzia nie je postavená proti bežiacemu jadru.", "This will RESET the ProxMenux Monitor login credentials on this host:": "Týmto sa vynulujú prihlasovacie údaje do ProxMenux Monitora na tomto serveri:", "This will add the mount to /etc/fstab so it persists after reboot.": "Týmto sa pripojenie pridá do /etc/fstab, takže zostane aj po reštarte.", @@ -7403,7 +7403,7 @@ "Validating credentials with server": "Overujem prihlasovacie údaje na serveri", "Validating disk safety...": "Overujem bezpečnosť disku...", "Validation method: http (port 80 forwarded) or dns (DNS provider plugin)": "Validačná metóda: http (port 80 expedované) alebo dns (DNS modul poskytovateľa)", - "Value for": "Hodnota", + "Value for": "Hodnota pre", "Variable name": "Názov premennej", "Variables": "Premenné", "Variables the installation asks for:": "Premenné, ktoré požaduje inštalácia:", @@ -7475,7 +7475,7 @@ "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:": "Navštívte web, kde nájdete ďalšie skripty, novinky a môžete podporiť projekt:", "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:\n\nhttps://community-scripts.github.io/ProxmoxVE": "Navštívte web, kde nájdete ďalšie skripty, novinky a môžete podporiť projekt:\n\nhttps://community-scripts.github.io/ProxmoxVE", "Vivaldi is a Norwegian freeware, cross-platform web browser with a built-in email client developed by Vivaldi Technologies.": "Vivaldi je nórsky freeware, cross-platform webový prehliadač s vstavaným e-mailovým klientom vyvinutým Vivaldi Technologies.", - "Volume configuration cancelled": "Konfigurácia hlasitosti zrušená", + "Volume configuration cancelled": "Nastavenie dátového úložiska bolo zrušené", "Volume options are not yet supported": "Možnosti objemu zatiaľ nie sú podporované", "Volume size in GB": "Veľkosť dátového disku v GB", "Volumes attached": "Pripojené objemy", @@ -7751,7 +7751,7 @@ "could not be compiled for kernel": "nepodarilo sa skompilovať pre jadro", "could not validate NVIDIA; exit code": "nemohol potvrdiť NVIDIA; kód výstupu", "cpuunits must be between 8 and 10000": "cpujednotky musia byť od 8 do 10000", - "custom": "zvyk", + "custom": "vlastné", "custom dependency commands are not yet supported": "Vlastné príkazy závislosti ešte nie sú podporované", "custom path(s) saved.": "uložených vlastných ciest.", "darktable is an open source photography workflow application and raw developer. A virtual lighttable and darkroom for photographers. It manages your digital negatives in a database, lets you view them through a zoomable lighttable and enables you to develop raw images and enhance them.": "darktable je aplikácia fotografií s otvoreným zdrojom a surový vývojár. Virtuálna svetelná a tmavá miestnosť pre fotografov. Spravuje vaše digitálne negatíva v databáze, umožňuje vám ich prezerať pomocou zoomovateľného svetelného zariadenia a umožňuje vám vyvinúť surové obrázky a zlepšiť ich.", @@ -7807,7 +7807,7 @@ "for this policy and may fail after first use or on subsequent VM starts.": "podľa týchto pravidiel a môže zlyhať po prvom použití alebo pri ďalších štartoch VM.", "formatted as": "naformátované ako", "found": "nájdené", - "free": "bez cla", + "free": "voľné", "from": "zo siete", "from Proxmox web interface (you will be asked)": "z webového rozhrania Proxmoxu (ešte sa vás opýta)", "from container": "z kontajnera", @@ -7831,7 +7831,7 @@ "has only": "má iba", "health and persistence profile not yet defined": "profil zdravia a pretrvávania, zatiaľ nedefinovaný", "hiSHtory is a better shell history. It stores your shell history in context (what directory you ran the command in, whether it succeeded or failed, how long it took, etc). This is all stored locally and end-to-end encrypted for syncing to to all your other computers.": "HISHtory je lepšia história mušlí. Ukladá históriu vášho shellu v kontexte (v akom adresári ste spustili príkaz, či sa to podarilo alebo zlyhalo, ako dlho to trvalo, atď). Toto všetko je uložené lokálne a end-to-end zašifrované pre synchronizáciu so všetkými ostatnými počítačmi.", - "host directory": "adresár host", + "host directory": "adresár hostiteľa", "host fstab only (not registered as Proxmox storage)": "iba host fstab (nie je zaregistrované ako úložisko Proxmoxu)", "hostpci entries for all IOMMU group devices": "hostpci položky pre všetky zariadenia IOMMU skupiny", "hostpci entries for selected GPU functions (full IOMMU group will be enforced after reboot)": "hostpci položky pre vybrané funkcie GPU (celá IOMMU skupina sa vynúti po reštarte)", @@ -7853,7 +7853,7 @@ "iSCSI storage provides raw block devices for VM disk images.": "iSCSI úložisko poskytuje surové blokové zariadenia pre disky virtuálnych strojov.", "iSCSI tools installed": "iSCSI nástroje sú nainštalované", "iftop usage": "Použitie iftop", - "image cache on": "Comment", + "image cache on": "vyrovnávacia pamäť obrazov na", "image itself": "samotného obrazu", "imported:": "importované:", "in CT": "v CT", @@ -7951,7 +7951,7 @@ "may be closed — trying discovery anyway...": "môže byť zatvorený - aj tak skúšam vyhľadanie...", "melonDS aims at providing fast and accurate Nintendo DS emulation.": "Cieľom melonDS je poskytovať rýchle a accurate Nintendo DS emulácia.", "members:": "členovia:", - "minimum": "minimálna", + "minimum": "minimum", "missing": "chýba", "mkfs.btrfs not found. Install btrfs-progs and retry.": "mkfs.btrfs sa nenašiel. Nainštalujte btrfs-progs a skúste to znova.", "more": "ďalších", @@ -7997,7 +7997,7 @@ "of free disk space.": "voľného miesta na disku.", "older firmware may increase passthrough instability": "starší firmvér môže zvýšiť nestabilitu priameho priradenia", "oldest driver offered:": "najstarší ponúkaný ovládač:", - "on": "o", + "on": "na", "on SSD/NVMe pools that support discard": "na SSD/NVMe pooloch, ktoré podporujú discard", "one of its services declares no image": "jedna zo služieb nemá zadaný žiadny obraz", "one of its services is not written as a service": "jedna z jej služieb nie je napísaná ako služba", @@ -8154,7 +8154,7 @@ "umount the path if currently mounted": "odpojiť cestu, ak je práve pripojená", "unknown include:": "neznámy include:", "unknown mount hook:": "neznámy hook pripojenia:", - "unprivileged LXC": "LXC unprivileged", + "unprivileged LXC": "neprivilegovaný LXC", "unsupported credential generator": "nepodporovaný credential generátor", "unsupported dependency condition": "stav bez podpory závislosti", "unsupported external credential or boolean": "nepodporované externé credential alebo boolean", @@ -8264,5 +8264,8 @@ "Invalid Compose volume path:": "Neplatná cesta dátového úložiska v súbore Compose:", "Invalid Compose port:": "Neplatný port v súbore Compose:", "Invalid application identifier:": "Neplatný identifikátor aplikácie:", + "No template is available for '{app_id}'": "Pre aplikáciu '{app_id}' nie je dostupná šablóna", + "The application '{app_id}' is not in the index": "Aplikácia '{app_id}' nie je v indexe", + "The generated template does not match the schema:": "Vygenerovaná šablóna nezodpovedá schéme:", "If a newer application image is available, the container is stopped and backed up, and the backup integrity is verified. The container is then updated using the new image.\n\nIf the update fails after the container was changed, ProxMenux attempts to restore the previous installation from the backup. If the operation remains incomplete, select the application in OCI management and choose \"Recover the previous installation\".\n\nHost directories mounted in the container are not restored by the container backup.": "Ak je dostupný novší obraz aplikácie, kontajner sa zastaví, vytvorí sa jeho záloha a overí sa jej integrita. Potom sa kontajner aktualizuje pomocou nového obrazu.\n\nAk aktualizácia po zmene kontajnera zlyhá, ProxMenux sa pokúsi obnoviť predchádzajúcu inštaláciu zo zálohy. Ak operácia zostane nedokončená, v správe OCI vyberte danú aplikáciu a možnosť „Obnoviť predchádzajúcu inštaláciu“.\n\nPripojené adresáre hostiteľa sa zo zálohy kontajnera neobnovujú." } diff --git a/oci/src/proxmenux_oci/recreation.py b/oci/src/proxmenux_oci/recreation.py index e1f3d897..af271ba7 100644 --- a/oci/src/proxmenux_oci/recreation.py +++ b/oci/src/proxmenux_oci/recreation.py @@ -3,7 +3,7 @@ import copy import re from pathlib import Path, PurePosixPath -from .i18n import translate +from .i18n import N_, translate from .ui import BacktrackUI, RestartWizard, UserCancelled @@ -243,8 +243,29 @@ def edit_recreation(record, ui): candidate = edit_section(candidate, ui, selected) +def _catalog_message(error): + """Localize legacy catalog errors here, preserving the catalog API.""" + text = str(error) + patterns = ( + (r"No template is available for '(.*)'", N_("No template is available for '{app_id}'")), + (r"The application '(.*)' is not in the index", N_("The application '{app_id}' is not in the index")), + ) + for pattern, source in patterns: + match = re.fullmatch(pattern, text) + if match: + translated = translate(source) + return text if translated == source else translated.format(app_id=match[1]) + prefix = 'La plantilla generada no cumple el esquema:' + if text.startswith(prefix): + source = N_('The generated template does not match the schema:') + translated = translate(source) + return text if translated == source else translated + text[len(prefix):] + return text + + def refresh_template(candidate, ui): from .catalog import Catalog + from .converter import ConversionError old = candidate.get('template', {}) template_id = old.get('id', '') if not re.fullmatch(r'[a-z0-9][a-z0-9-]+', template_id): @@ -258,7 +279,10 @@ def refresh_template(candidate, ui): if len(matching) != 1: return name = matching[0]['id'] - latest = catalog.compose(name) + try: + latest = catalog.compose(name) + except ConversionError as error: + raise ConversionError(_catalog_message(error)) from error if latest == old: return if not ui.confirm(translate('Apply options from the current catalog template? New required paths and settings may be requested. Review the resulting configuration before recreating the CT.'), True): diff --git a/oci/tests/test_recreation_i18n.py b/oci/tests/test_recreation_i18n.py new file mode 100644 index 00000000..b1413857 --- /dev/null +++ b/oci/tests/test_recreation_i18n.py @@ -0,0 +1,216 @@ +"""Single-container editor wording, real helper paths and no-apply boundaries.""" +import copy +import json +from pathlib import Path +import subprocess +import unittest +from unittest.mock import patch + +from test_modify_menu import MenuUI, record +from proxmenux_oci import i18n, installer, management, recreation +from proxmenux_oci.catalog import Catalog +from proxmenux_oci.cli import _deployment_summary_text +from proxmenux_oci.converter import ConversionError +from proxmenux_oci.ui import UserCancelled + +ROOT = Path(__file__).resolve().parents[2] + + +class EditorUI(MenuUI): + def info(self, text): + self.events.append(('info', text)) + + def password(self, text, **kwargs): + self.events.append(('password', text)) + return next(self.answers) + + +class SlovakEditorTests(unittest.TestCase): + def setUp(self): + translations = json.loads((ROOT / 'lang/sk.json').read_text()) + for mock in (patch.object(i18n, '_language', 'sk'), + patch.object(i18n, '_cache', translations)): + mock.start() + self.addCleanup(mock.stop) + self.forbidden = [] + # The actual editor/helpers run, but any external action fails the test. + for target, name in ((management, '_run_lifecycle'), (subprocess, 'run'), + (subprocess, 'Popen'), (subprocess, 'check_output'), + (Path, 'write_text'), (Path, 'write_bytes')): + mock = patch.object(target, name, side_effect=AssertionError('editor performed an external action')) + self.forbidden.append(mock.start()) + self.addCleanup(mock.stop) + self.original = record() + self.saved = copy.deepcopy(self.original) + + def tearDown(self): + self.assertEqual(self.original, self.saved) + for mock in self.forbidden: + mock.assert_not_called() + + def text(self, ui): + return '\n'.join(str(e[1:]) for e in ui.events) + + def test_cpu_minimum_then_invalid_number_can_be_corrected_and_discarded(self): + ui = EditorUI(['cores', 'discard'], ['0', 'two', '3'], confirms=[True]) + self.assertIsNone(recreation.edit_recreation(self.original, ui)) + text = self.text(ui) + self.assertIn('Počet jadier CPU', text) + self.assertIn('Počet jadier: minimum 1', text) + self.assertIn('Hodnota musí byť číslo:', text) + self.assertIn('Zadajte hodnotu znova.', text) + self.assertIn('Zahodiť nepoužité zmeny?', text) + self.assertNotIn('private-test-secret', text) + + def test_ram_boundary_and_declined_review_do_not_apply_a_proposal(self): + ui = EditorUI(['memory', 'apply', 'discard'], ['127', '128'], + reviews=[False], confirms=[True]) + self.assertIsNone(recreation.edit_recreation(self.original, ui)) + self.assertIn('RAM v MiB: minimum 128', self.text(ui)) + summary = next(e[1] for e in ui.events if e[0] == 'review') + self.assertIn('128 MB RAM', summary) + self.assertIn('rootfs 8 GB na local-zfs', summary) + self.assertIn('vyrovnávacia pamäť obrazov na', summary) + self.assertIn('neprivilegovaný LXC', summary) + self.assertNotIn('private-test-secret', summary) + + def test_no_change_and_cancelled_section_keep_the_existing_record(self): + for choices, answers in ((['apply'], []), (['cores', 'discard'], [UserCancelled()])): + with self.subTest(choices=choices): + ui = EditorUI(choices, answers) + self.assertIsNone(recreation.edit_recreation(self.original, ui)) + self.assertFalse(any(e[0] == 'review' for e in ui.events)) + + def test_environment_duplicate_is_an_error_not_an_instruction_to_duplicate(self): + self.original['deployment']['environment'] *= 2 + self.saved = copy.deepcopy(self.original) + ui = EditorUI([], ['PASSWORD', UserCancelled()], confirms=[True]) + result = recreation.edit_section(self.original, ui, 'environment') + self.assertIs(result, self.original) + self.assertIn('Premenná je v definícii kontajnera uvedená viackrát; pred úpravou ju skontrolujte', self.text(ui)) + + def test_invalid_variable_name_and_value_are_rejected_before_draft_is_returned(self): + for name, value, expected in (('INVALID NAME', None, 'Neplatný názov premennej'), + ('VISIBLE', 'bad\x00value', 'Hodnota obsahuje nepodporovaný znak')): + with self.subTest(name=name): + answers = [name] + ([value] if value is not None else []) + [UserCancelled()] + ui = EditorUI([], answers, confirms=[True, False]) + self.assertIs(recreation.edit_section(self.original, ui, 'environment'), self.original) + self.assertIn(expected, self.text(ui)) + + def test_new_secret_prompt_and_summary_do_not_expose_the_value(self): + candidate = copy.deepcopy(self.original) + ui = EditorUI([], ['NEW_SECRET', 'fixture-new-secret'], confirms=[True, True, False]) + recreation.edit_setting(candidate, ui, 'environment') + self.assertIn(('password', 'Nová hodnota pre NEW_SECRET'), ui.events) + self.assertNotIn('fixture-new-secret', self.text(ui)) + self.assertNotIn('fixture-new-secret', _deployment_summary_text(candidate['template'], candidate['deployment'])) + + def test_storage_picker_reports_free_space_not_customs_duty(self): + ui = EditorUI(['local-zfs']) + rows = [{'storage': name, 'type': 'zfspool', 'avail': 8 * 1024**3} + for name in ('local-zfs', 'other')] + with patch.object(installer.host, 'storages', return_value=rows): + self.assertEqual(installer.ask_storage(ui, 'Úložisko', 'rootdir', 'local-zfs'), 'local-zfs') + self.assertIn('8 GB voľné', self.text(ui)) + + def test_shared_host_path_prompt_changes_only_the_draft(self): + candidate = copy.deepcopy(self.original) + candidate['template']['proxmox']['installer_profile']['shared_directories'] = { + 'container_root': '/shares'} + ui = EditorUI(['host-bind'], ['media', '/fixture/nonexistent'], confirms=[True, False]) + with patch.object(installer.os.path, 'isdir', return_value=False): + recreation.edit_setting(candidate, ui, 'shares') + self.assertIn('Cesta na hostiteľovi pre /shares/media', self.text(ui)) + self.assertEqual(candidate['deployment']['mounts'][-1]['source'], '/fixture/nonexistent') + + def test_extra_path_cannot_cover_system_directories(self): + ui = EditorUI(['host-bind'], ['/etc', '/fixture/host', UserCancelled()], + confirms=[True, False]) + self.assertIs(recreation.edit_section(self.original, ui, 'paths'), self.original) + self.assertIn('Vlastná cesta nemôže skryť systémové adresáre', self.text(ui)) + + def test_network_gateway_explains_outbound_route(self): + ui = EditorUI(['static'], ['vmbr0', '192.0.2.42/24', ''], confirms=[]) + with patch.object(installer.host, 'bridges', return_value=[]), \ + patch.object(installer.host, 'address_answers', return_value=False): + recreation.edit_setting(copy.deepcopy(self.original), ui, 'network') + self.assertIn('Brána IPv4 (prázdne = bez odchádzajúcej trasy)', self.text(ui)) + + def test_incompatible_docker_mods_are_localized_without_gpu_or_image_probe(self): + self.original['template']['proxmox']['installer_profile']['hardware_acceleration'] = { + 'profiles': [{'id': 'gpu', 'environment': [{'name': 'DOCKER_MODS', 'value': 'mod'}]}]} + self.saved = copy.deepcopy(self.original) + ui = EditorUI([]) + self.assertIs(recreation.edit_section(self.original, ui, 'acceleration'), self.original) + self.assertIn('Docker Mods sú dostupné iba pre kompatibilné obrazy LinuxServer', self.text(ui)) + + def test_duplicate_usb_device_is_rejected_without_attaching_it(self): + self.original['deployment']['devices'] = [{'host_path': '/dev/ttyACM0'}] + self.saved = copy.deepcopy(self.original) + ui = EditorUI(['usb'], ['/dev/ttyACM0', UserCancelled()], confirms=[True]) + with patch.object(installer.host, 'usb_devices', return_value=[]): + self.assertIs(recreation.edit_section(self.original, ui, 'devices'), self.original) + self.assertIn('Toto zariadenie je už pripojené', self.text(ui)) + + def catalog_refresh(self, error=None, latest=None): + candidate = copy.deepcopy(self.original) + candidate['template']['container_contract']['image']['repository'] = 'example/app' + ui = EditorUI(['managed-volume'], ['local-zfs', '4'], confirms=[True]) + index = {'applications': [{'id': 'example', 'template_id': 'example-app'}]} + with patch.object(Catalog, 'load_index', return_value=index), \ + patch.object(Catalog, 'compose', side_effect=error, return_value=latest): + recreation.refresh_template(candidate, ui) + return candidate, ui + + def test_required_catalog_path_has_a_clear_persistence_prompt(self): + latest = copy.deepcopy(self.original['template']) + latest['container_contract']['image']['repository'] = 'example/app' + latest['container_contract']['volumes'] = [{'id': 'new', 'container_path': '/new-data'}] + candidate, ui = self.catalog_refresh(latest=latest) + self.assertIn('Aktuálna šablóna vyžaduje nové trvalé úložisko pre cestu: /new-data', self.text(ui)) + self.assertIn('Nové úložisko bude prázdne; existujúce údaje sa doň automaticky neprenesú.', self.text(ui)) + self.assertIn('Trvalé úložisko pre /new-data', self.text(ui)) + self.assertEqual(candidate['deployment']['mounts'][-1]['container_path'], '/new-data') + + def test_template_identity_change_is_a_localized_migration_error(self): + latest = copy.deepcopy(self.original['template']) + latest['container_contract']['image']['repository'] = 'different/app' + with self.assertRaisesRegex(ValueError, 'Aktuálna šablóna mení identitu šablóny alebo repozitár obrazu; je potrebná výslovná migrácia'): + self.catalog_refresh(latest=latest) + + def test_real_catalog_missing_file_error_is_localized_only_in_editor(self): + index = {'applications': [{'id': 'example', 'template_id': 'example-app'}]} + item = dict(index['applications'][0], template_family='curated-profile', curated_path='absent-template.json') + with patch.object(Catalog, 'load_index', return_value=index), patch.object(Catalog, 'find_item', return_value=item): + with self.assertRaisesRegex(ConversionError, "No template is available for 'example'"): + Catalog(ROOT / 'oci').compose('example') + with self.assertRaisesRegex(ConversionError, "Pre aplikáciu 'example' nie je dostupná šablóna"): + recreation.refresh_template(copy.deepcopy(self.original), EditorUI([])) + + def test_real_schema_diagnostic_preserves_field_paths_and_exception_type(self): + with self.assertRaises(ConversionError) as raw: + Catalog(ROOT / 'oci').validate({}) + self.assertTrue(str(raw.exception).startswith('La plantilla generada no cumple el esquema:')) + details = str(raw.exception).split(':', 1)[1] + with self.assertRaises(ConversionError) as localized: + self.catalog_refresh(error=raw.exception) + self.assertEqual(str(localized.exception), 'Vygenerovaná šablóna nezodpovedá schéme:' + details) + + def test_missing_index_entry_preserves_the_application_identifier(self): + with self.assertRaisesRegex(ConversionError, "Aplikácia 'example' nie je v indexe"): + self.catalog_refresh(error=ConversionError("The application 'example' is not in the index")) + + def test_english_missing_translation_and_unknown_diagnostics_are_preserved(self): + for message in ("No template is available for 'example'", "The application 'example' is not in the index", + 'La plantilla generada no cumple el esquema: field: required', 'unknown technical failure'): + for language, cache in (('en', {}), ('sk', {})): + with self.subTest(message=message, language=language), \ + patch.object(i18n, '_language', language), patch.object(i18n, '_cache', cache): + with self.assertRaises(ConversionError) as error: + self.catalog_refresh(error=ConversionError(message)) + self.assertEqual(str(error.exception), message) + + +if __name__ == '__main__': + unittest.main()