fix(i18n): keep {placeholders} out of the translator in the TUI cache

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) <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-09-22 21:45:54 +02:00
co-authored by Claude Opus 5
parent f8fade3c0a
commit eafc472279
8 changed files with 59 additions and 2 deletions
+45 -2
View File
@@ -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<quote>["'])(?P<text>(?:\\.|(?! (?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"(?<![A-Za-z0-9_]){re.escape(term)}(?![A-Za-z0-9_])"
for term in terms
),
re.IGNORECASE,
)
print(f"Protected application names: {len(titles)}", flush=True)
def protect_placeholders(text: str) -> 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
+2
View File
@@ -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...",
+2
View File
@@ -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...",
+2
View File
@@ -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...",
+2
View File
@@ -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...",
+2
View File
@@ -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...",
+2
View File
@@ -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...",
+2
View File
@@ -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...",