mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 18:16:43 +00:00
fix(i18n): protect the literals a reader copies, and name the OCI category
The glossary only holds what someone listed, and what reaches the reader
as a broken command was never on it. Paths, long options and Proxmox
subcommands are now recognised by shape:
/dev/apex_0 came back as /dev/apex 0
--auto-uninstall desinstalación automática
pct config 110 configuration PCT 110 (fr)
pct enter 101 pct inserire 101 (it)
OCI joins the glossary as well. Read as a word it became "BEC" in French
across thirty-one strings — "Vérification du BEC" named nothing — and the
menu entry read "Gestionnaire du BEC Apps". The entry itself and "beta"
are protected too, so the name stays the same in every language.
A provider can also alter a token rather than carry it through: argos
returned MPXTERM000 for PMXTERM000, the restore found nothing, and the
token shipped. "MPXTERM000 configuré" is in the French catalogue today.
The restore now verifies its own tokens and fails the string instead,
which leaves the key absent for the next run to retry.
Spanish category labels: twenty-six were wrong. Seven collapsed two
categories into one — "Backup & Recovery" read only "Recuperación",
"Network & Firewall" read "Red de cortafuegos" — six were half
translated, and the connector alternated between "&", "y" and "e". They
now use "y" or "e" throughout. "IoT & Smart Home" and
"AI / Coding & Dev-Tools" stay in English by choice.
"Messaging & Queues" becomes "Messaging & Notifications": its only
application is Apprise, a notification gateway, not a queue. The
Helper-Scripts import still maps their name, so nothing breaks upstream.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,11 @@ PROTECTED_TECHNICAL_TERMS = (
|
||||
# and the reader is told about something Proxmox does not call that.
|
||||
"CT",
|
||||
"VM",
|
||||
# Open Container Initiative. Read as a word it becomes "BEC" in French,
|
||||
# which named nothing, across thirty-one strings of the OCI section.
|
||||
"OCI manager Apps",
|
||||
"OCI",
|
||||
"beta",
|
||||
)
|
||||
TECHNICAL_TERM_RE = re.compile(
|
||||
"|".join(
|
||||
@@ -114,6 +119,20 @@ TRANSLATE_CALL_RE = re.compile(
|
||||
# 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_]*\}")
|
||||
# Literals a reader copies and runs. Translated as words they stop working:
|
||||
# /dev/apex_0 came back as "/dev/apex 0", --auto-uninstall as "desinstalación
|
||||
# automática", and "pct config 110" as "configuration PCT 110". A glossary can
|
||||
# only hold what someone listed; these are recognised by shape.
|
||||
STRUCTURAL_LITERAL_RE = re.compile(
|
||||
r"""
|
||||
/(?:dev|etc|usr|var|opt|srv|proc|sys|run|boot|tmp)/[\w./+-]*[\w/+-]
|
||||
| (?<![\w-])--[A-Za-z][\w-]*
|
||||
| \b[\w.-]+\.(?:sh|py|json|yml|yaml|conf|service|timer|func|cfg|list|log)\b
|
||||
| \b(?:pct|qm|pvesm|pveam|pveum|pvecm|apt-get|dpkg|systemctl|journalctl
|
||||
|zpool|smartctl|modprobe|blkid|lsblk|mkfs)\s+[a-z][\w-]*
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
# 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.
|
||||
@@ -180,13 +199,20 @@ def restore_placeholders(text: str, found: list[str]) -> str:
|
||||
|
||||
|
||||
def protect_technical_terms(text: str) -> tuple[str, list[str]]:
|
||||
"""Replace glossary terms with stable tokens before translation."""
|
||||
"""Replace glossary terms with stable tokens before translation.
|
||||
|
||||
Structural literals go first. A glossary can only list what someone
|
||||
thought of, and what reaches the reader as a broken command is never on
|
||||
that list: an absolute path, a long option, a filename. They are
|
||||
recognised by shape instead, which also covers the next one added.
|
||||
"""
|
||||
protected: list[str] = []
|
||||
|
||||
def _swap(match: re.Match[str]) -> str:
|
||||
protected.append(match.group(0))
|
||||
return f"PMXTERM{len(protected) - 1:03d}"
|
||||
|
||||
text = STRUCTURAL_LITERAL_RE.sub(_swap, text)
|
||||
return TECHNICAL_TERM_RE.sub(_swap, text), protected
|
||||
|
||||
|
||||
@@ -197,6 +223,24 @@ def restore_technical_terms(text: str, protected: list[str]) -> str:
|
||||
return text
|
||||
|
||||
|
||||
# A provider can alter a token instead of carrying it through: argos returned
|
||||
# MPXTERM000 for PMXTERM000, the replace found nothing, and the token shipped
|
||||
# to the reader — "MPXTERM000 configuré" is in the French catalogue now.
|
||||
SENTINEL_RESIDUE_RE = re.compile(r"[MP][MPX]X?\s?(?:TERM|PH)\s?\d{2,4}", re.IGNORECASE)
|
||||
|
||||
|
||||
def assert_no_sentinel_residue(text: str) -> None:
|
||||
"""Fail the translation when a protection token did not survive intact.
|
||||
|
||||
Leaving the key out is recoverable: the next run tries again. Storing a
|
||||
string with a token in it is not, because nothing looks at a value that
|
||||
already exists.
|
||||
"""
|
||||
found = SENTINEL_RESIDUE_RE.search(text)
|
||||
if found:
|
||||
raise RuntimeError(f"the provider altered a protection token: {found.group(0)!r}")
|
||||
|
||||
|
||||
def iter_script_files(
|
||||
scripts_dir: Path, extra_files: Iterable[Path] = ()
|
||||
) -> Iterable[Path]:
|
||||
@@ -490,6 +534,7 @@ def translate_text(
|
||||
raise ValueError(f"Unknown provider: {provider}")
|
||||
translated = restore_technical_terms(clean_translation(translated), protected_terms)
|
||||
translated = restore_placeholders(translated, placeholders)
|
||||
assert_no_sentinel_residue(translated)
|
||||
return restore_sentence_spacing(translated) if translated else text
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user