From eafc472279a6393d68250ba7b68782bda6abbec8 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 22 Sep 2026 21:45:54 +0200 Subject: [PATCH 1/2] fix(i18n): keep {placeholders} out of the translator in the TUI cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Monitor's generator protects them; this one did not, so a provider read the word inside the braces and translated it. "{count}" came back as "{conta}" in Portuguese and "{počet}" in Slovak, and the consumer — finding no placeholder to substitute — fell back to English, which is every message the recent batches introduced. The token carries no underscores: argos splits on them and hands back "PMX PH 0". Two further fixes this depends on: - protect_catalog_titles rebuilt the glossary pattern without the word boundaries the module-level one has, so a short term matched inside longer words once the catalogue was loaded. - CT and VM join the glossary. Read as initials they get reordered — "CT" comes back as "TC" in Spanish — naming something Proxmox does not. Co-Authored-By: Claude Opus 5 (1M context) --- .github/scripts/build_translation_cache.py | 47 +++++++++++++++++++++- lang/de.json | 2 + lang/es.json | 2 + lang/fr.json | 2 + lang/it.json | 2 + lang/pt.json | 2 + lang/sk.json | 2 + lang/sv.json | 2 + 8 files changed, 59 insertions(+), 2 deletions(-) diff --git a/.github/scripts/build_translation_cache.py b/.github/scripts/build_translation_cache.py index 3a5b70cd..90af39e8 100644 --- a/.github/scripts/build_translation_cache.py +++ b/.github/scripts/build_translation_cache.py @@ -94,6 +94,11 @@ PROTECTED_TECHNICAL_TERMS = ( "GPU", "CPU", "AMD", + # Proxmox's own words for a container and a virtual machine. Read as + # initials they get reordered — "CT" comes back as "TC" in Spanish — + # and the reader is told about something Proxmox does not call that. + "CT", + "VM", ) TECHNICAL_TERM_RE = re.compile( "|".join( @@ -106,6 +111,9 @@ TRANSLATE_CALL_RE = re.compile( r"""translate\s+(?P["'])(?P(?:\\.|(?! (?P=quote) ).)*?)(?P=quote)""", re.VERBOSE | re.DOTALL, ) +# Same shape the Monitor's generator uses, so a message reads identically +# whichever of the two catalogues it comes from. +PLACEHOLDER_RE = re.compile(r"\{[A-Za-z_][A-Za-z0-9_]*\}") # Providers that answer the same thing every time for the same input, so a # second attempt cannot produce a different result. `appimage` shells out to a # binary that may reach a network service, so it is not on the list. @@ -133,11 +141,44 @@ def protect_catalog_titles(directories) -> None: if not titles: return terms = tuple(sorted(set(PROTECTED_TECHNICAL_TERMS) | titles, key=len, reverse=True)) + # Same word boundaries as the module-level pattern. Without them a short + # term matches inside longer words, and every application title in the + # catalogue joins the glossary here. TECHNICAL_TERM_RE = re.compile( - "|".join(re.escape(term) for term in terms), re.IGNORECASE) + "|".join( + rf"(? tuple[str, list[str]]: + """Replace each ``{name}`` with a token before translation. + + A provider reads the name inside the braces as a word and translates + it: ``{count}`` comes back as ``{conta}`` in Portuguese and ``{počet}`` + in Slovak. The consumer then finds no placeholder to substitute and + falls back to English, so the translation was paid for and never used. + The token carries no underscores because argos splits on them. + """ + found: list[str] = [] + + def _swap(match: re.Match[str]) -> str: + found.append(match.group(0)) + return f"PMXPH{len(found) - 1:03d}" + + return PLACEHOLDER_RE.sub(_swap, text), found + + +def restore_placeholders(text: str, found: list[str]) -> str: + """Put the original ``{name}`` back where each token landed.""" + for index, original in enumerate(found): + text = text.replace(f"PMXPH{index:03d}", original) + return text + + def protect_technical_terms(text: str) -> tuple[str, list[str]]: """Replace glossary terms with stable tokens before translation.""" protected: list[str] = [] @@ -433,7 +474,8 @@ def translate_text( timeout: int, appimage_path: Path, ) -> str: - protected_text, protected_terms = protect_technical_terms(text) + protected_text, placeholders = protect_placeholders(text) + protected_text, protected_terms = protect_technical_terms(protected_text) if provider == "argos": translated = translate_argos(protected_text, dest_lang) elif provider == "googletrans": @@ -447,6 +489,7 @@ def translate_text( else: raise ValueError(f"Unknown provider: {provider}") translated = restore_technical_terms(clean_translation(translated), protected_terms) + translated = restore_placeholders(translated, placeholders) return restore_sentence_spacing(translated) if translated else text diff --git a/lang/de.json b/lang/de.json index b2f2065d..23010491 100644 --- a/lang/de.json +++ b/lang/de.json @@ -892,6 +892,8 @@ "Completed with errors —": "Mit Fehlern abgeschlossen –", "Completed.": "Vollendet.", "Completed. Devices added to VM {vmid}: {count}.": "Abgeschlossen. Zur VM {vmid} hinzugefügte Geräte: {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Abgeschlossen. Zu CT {ctid} hinzugefügte Festplatten: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Abgeschlossen. Zu VM {vmid} hinzugefügte Festplatten: {count}.", "Completed. Press Enter to return to menu...": "Vollendet. Drücken Sie die Eingabetaste, um zum Menü zurückzukehren...", "Completing its final cleanup...": "Abschluss der endgültigen Bereinigung...", "Completing pending package configurations...": "Ausstehende Paketkonfigurationen werden abgeschlossen...", diff --git a/lang/es.json b/lang/es.json index 53c41aec..289477a2 100644 --- a/lang/es.json +++ b/lang/es.json @@ -893,6 +893,8 @@ "Completed with errors —": "Completado con errores.", "Completed.": "Terminado.", "Completed. Devices added to VM {vmid}: {count}.": "Completado. Dispositivos agregados a la VM {vmid}: {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Completado. Discos añadidos a CT {ctid}: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Completado. Discos añadidos a VM {vmid}: {count}.", "Completed. Press Enter to return to menu...": "Terminado. Presione Enter para regresar al menú...", "Completing its final cleanup...": "Completando su limpieza final...", "Completing pending package configurations...": "Completando las configuraciones de paquetes pendientes...", diff --git a/lang/fr.json b/lang/fr.json index e1ebd6fc..6f8f0f80 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -893,6 +893,8 @@ "Completed with errors —": "Complété avec des erreurs —", "Completed.": "Complété.", "Completed. Devices added to VM {vmid}: {count}.": "Terminé. Appareils ajoutés à la VM {vmid} : {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Terminé. Disques ajoutés à CT {ctid}: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Terminé. Disques ajoutés à VM {vmid}: {count}.", "Completed. Press Enter to return to menu...": "Complété. Appuyez sur Entrée pour revenir au menu...", "Completing its final cleanup...": "Compléter son nettoyage final...", "Completing pending package configurations...": "Finalisation des configurations de packages en attente...", diff --git a/lang/it.json b/lang/it.json index 165303f8..630ca727 100644 --- a/lang/it.json +++ b/lang/it.json @@ -893,6 +893,8 @@ "Completed with errors —": "Completato con errori —", "Completed.": "Completato.", "Completed. Devices added to VM {vmid}: {count}.": "completato. Dispositivi aggiunti alla VM {vmid}: {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Completato. I dischi aggiunti a CT {ctid}: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Completato. I dischi aggiunti a VM {vmid}: {count}.", "Completed. Press Enter to return to menu...": "Completato. Premere Invio per tornare al menu...", "Completing its final cleanup...": "Completa la sua pulizia finale...", "Completing pending package configurations...": "completamento delle configurazioni dei pacchetti in sospeso...", diff --git a/lang/pt.json b/lang/pt.json index 8f661e3a..904d4076 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -893,6 +893,8 @@ "Completed with errors —": "Concluído com erros -", "Completed.": "Concluído.", "Completed. Devices added to VM {vmid}: {count}.": "Concluído. Dispositivos adicionados à VM {vmid}: {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Concluído. Discos adicionados ao CT {ctid}: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Concluído. Discos adicionados ao VM {vmid}: {count}.", "Completed. Press Enter to return to menu...": "Concluído. Pressione Enter para retornar ao menu...", "Completing its final cleanup...": "Completando sua limpeza final...", "Completing pending package configurations...": "Concluindo configurações de pacotes pendentes...", diff --git a/lang/sk.json b/lang/sk.json index 78611a9e..c4ddd318 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -892,6 +892,8 @@ "Completed with errors —": "Dokončené s chybami -", "Completed.": "Dokončené.", "Completed. Devices added to VM {vmid}: {count}.": "Dokončené. Zariadenia pridané do VM {vmid}: {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Hotovo. Disky pridané k CT {ctid}: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Hotovo. Disky pridané k VM {vmid}: {count}.", "Completed. Press Enter to return to menu...": "Dokončené. Stlačte Enter pre návrat do menu...", "Completing its final cleanup...": "Dokončiť záverečné čistenie...", "Completing pending package configurations...": "Dokončenie čakajúcich konfigurácií balíka...", diff --git a/lang/sv.json b/lang/sv.json index 9dd932e9..c38cea90 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -891,6 +891,8 @@ "Completed with errors —": "Kompletterad med fel —", "Completed.": "Avslutad.", "Completed. Devices added to VM {vmid}: {count}.": "Klar. Enheter som har lagts till VM {vmid}: {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Slutfört. Disker läggs till CT {ctid}: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Slutfört. Disker läggs till VM {vmid}: {count}.", "Completed. Press Enter to return to menu...": "Avslutad. Tryck på Enter för att återgå till menyn...", "Completing its final cleanup...": "Slutför sin sista rengöring...", "Completing pending package configurations...": "Slutför väntande paketkonfigurationer...", From 75f571ecdb9691b39cf4b7261677e85e0fd12f6c Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 22 Sep 2026 21:48:42 +0200 Subject: [PATCH 2/2] i18n(sk): match the wording already used for the sibling message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other Slovak string renders "Completed" as "Dokončené", including the device message these two sit beside. They came back with "Hotovo" and "k" instead of "do", which reads as a different hand in a list of four lines that differ only by the noun. Co-Authored-By: Claude Opus 5 (1M context) --- lang/sk.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/sk.json b/lang/sk.json index c4ddd318..12af4325 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -892,8 +892,8 @@ "Completed with errors —": "Dokončené s chybami -", "Completed.": "Dokončené.", "Completed. Devices added to VM {vmid}: {count}.": "Dokončené. Zariadenia pridané do VM {vmid}: {count}.", - "Completed. Disks added to CT {ctid}: {count}.": "Hotovo. Disky pridané k CT {ctid}: {count}.", - "Completed. Disks added to VM {vmid}: {count}.": "Hotovo. Disky pridané k VM {vmid}: {count}.", + "Completed. Disks added to CT {ctid}: {count}.": "Dokončené. Disky pridané do CT {ctid}: {count}.", + "Completed. Disks added to VM {vmid}: {count}.": "Dokončené. Disky pridané do VM {vmid}: {count}.", "Completed. Press Enter to return to menu...": "Dokončené. Stlačte Enter pre návrat do menu...", "Completing its final cleanup...": "Dokončiť záverečné čistenie...", "Completing pending package configurations...": "Dokončenie čakajúcich konfigurácií balíka...",