Files
computer-playbook/tests/unit/python/utils/i18n/test_translate.py
T
kevinveenbirkenbachandClaude Opus 5 4c66220c50 style: settle the formatter and linter rewrites of this branch
Every commit here went in with `--no-verify`, so the pre-commit `make test`
never ran and neither the formatter nor the linter saw the change sets. The
quality gate collects that debt now.

`test_aliases.py` built a lookup table by handing a generator to `dict()`,
which ruff C402 rejects and offers no autofix for; a dict comprehension does
the same work. `selections.py` and `test_translate.py` each wrapped a call
across three lines that fits on one, which is the formatter's own rewrite
from the run that then aborted on the C402 above.

All three edits preserve behaviour: the comprehension is equivalent to the
generator it replaces, and the rest is whitespace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 15:33:31 +02:00

422 lines
14 KiB
Python

import json
import re
import threading
import unittest
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import ClassVar
from babel.messages.catalog import Catalog
from utils.annotations.suppress import is_suppressed_at
from utils.i18n.catalog import (
ENGINE,
MACHINE_TRANSLATION,
REFUSAL_PREFIX,
REJECTED_PREFIX,
TRANSLATION_REFUSED,
build_template,
merge,
render,
)
from utils.i18n.libretranslate import LibreTranslate
from utils.i18n.placeholders import Rejected
from utils.i18n.translate import (
URL_SUPPRESSION,
apply,
damaged,
over_shared,
pending,
retry,
taken_translations,
)
TOKEN = re.compile(r'<x id="\d+"></x>')
class FakeLibreTranslate(BaseHTTPRequestHandler):
"""German by prefix; drops every token of a message containing ``lossy``."""
requests: ClassVar[list[dict]] = []
def log_message(self, *args):
return
def _reply(self, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
self._reply([{"code": "en", "targets": ["de", "fr"]}])
def do_POST(self):
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
FakeLibreTranslate.requests.append(payload)
translated = [
TOKEN.sub("", f"DE {text}") if "lossy" in text else f"DE {text}"
for text in payload["q"]
]
self._reply({"translatedText": translated})
class TestTranslate(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), FakeLibreTranslate)
threading.Thread(target=cls.server.serve_forever, daemon=True).start()
cls.client = LibreTranslate(f"http://127.0.0.1:{cls.server.server_port}", 2)
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
cls.server.server_close()
def setUp(self):
FakeLibreTranslate.requests.clear()
def test_waits_for_offered_languages(self):
self.client.wait(["de", "fr"], timeout=1)
with self.assertRaises(TimeoutError):
self.client.wait(["xx"], timeout=0)
def test_requests_html_from_english_into_the_target(self):
self.assertEqual(self.client.translate(["Hello"], "de").values, ["DE Hello"])
self.assertEqual(
{
key: FakeLibreTranslate.requests[0][key]
for key in ("source", "target", "format")
},
{"source": "en", "target": "de", "format": "html"},
)
def test_rules_for_human_fuzzy_empty_and_damaged_entries(self):
catalog = merge(
build_template(
[
("human", "Kept by a person"),
("fuzzy", "Changed source"),
("empty", "New source"),
("lossy", "lossy {count} items"),
],
"core",
),
None,
"de",
)
catalog.get("Kept by a person", context="human").string = "Von Hand"
changed = catalog.get("Changed source", context="fuzzy")
changed.string = "Veraltet"
changed.flags.add("fuzzy")
todo = pending(catalog)
discarded = apply(
todo, self.client.translate([m.id for m in todo], "de").values
)
human = catalog.get("Kept by a person", context="human")
self.assertEqual((human.string, human.user_comments), ("Von Hand", []))
self.assertEqual(changed.string, "DE Changed source")
self.assertFalse(changed.fuzzy)
self.assertEqual(changed.user_comments, [MACHINE_TRANSLATION])
fresh = catalog.get("New source", context="empty")
self.assertEqual(
(fresh.string, fresh.user_comments),
("DE New source", [MACHINE_TRANSLATION]),
)
self.assertEqual(discarded, 1)
lossy = catalog.get("lossy {count} items", context="lossy")
self.assertFalse(lossy.string)
self.assertEqual(
lossy.user_comments,
[
f"{REFUSAL_PREFIX} {ENGINE} lost-token",
URL_SUPPRESSION,
f"{REJECTED_PREFIX} DE lossy items",
],
)
self.assertNotIn(lossy, pending(catalog))
class TestRefusalIsRemembered(unittest.TestCase):
"""A refusal is deterministic for an unchanged source, so it is recorded.
Without the mark every run spends a masking pass, a request and an
inference to reach the same verdict: 34 languages produced 185 requests
and no translation before this landed.
"""
def _entry(self, comments: list[str] | None = None):
catalog = Catalog(locale="de")
catalog.add("lossy {count} items", "", context="lossy")
message = catalog.get("lossy {count} items", context="lossy")
message.user_comments = list(comments or [])
return catalog, message
def test_a_refused_entry_is_not_offered_again(self):
catalog, message = self._entry([TRANSLATION_REFUSED])
self.assertNotIn(message, pending(catalog))
def test_an_entry_without_the_mark_stays_pending(self):
catalog, message = self._entry()
self.assertIn(message, pending(catalog))
def test_the_mark_is_written_once(self):
_, message = self._entry([TRANSLATION_REFUSED])
apply([message], [None])
self.assertEqual(message.user_comments, [TRANSLATION_REFUSED])
def test_a_translation_that_lands_clears_the_mark(self):
_, message = self._entry([TRANSLATION_REFUSED])
apply([message], ["verlustig {count} Einträge"])
self.assertEqual(message.user_comments, [MACHINE_TRANSLATION])
self.assertEqual(message.string, "verlustig {count} Einträge")
def test_retry_offers_the_entry_again(self):
catalog, message = self._entry([MACHINE_TRANSLATION, TRANSLATION_REFUSED])
cleared = retry(catalog)
self.assertEqual(cleared, 1)
self.assertEqual(message.user_comments, [MACHINE_TRANSLATION])
self.assertIn(message, pending(catalog))
def test_the_mark_names_the_engine_that_tried(self):
_, message = self._entry()
apply([message], [None])
self.assertEqual(
message.user_comments, [f"{REFUSAL_PREFIX} {ENGINE} damaged-span"]
)
def test_the_rejected_translation_stays_readable_beside_the_mark(self):
_, message = self._entry()
apply([message], [Rejected("verlustig Einträge", "protected-span")])
self.assertEqual(
message.user_comments,
[
(
f"{REFUSAL_PREFIX} {ENGINE} protected-span "
"lost=['{count}'] gained=[]"
),
URL_SUPPRESSION,
f"{REJECTED_PREFIX} verlustig Einträge",
],
)
self.assertFalse(message.string)
def test_the_catalog_carries_the_rejection_as_a_comment(self):
catalog, message = self._entry()
apply([message], [Rejected("verlustig Einträge", "protected-span")])
written = render(catalog).decode("utf-8")
self.assertIn(f"# {REJECTED_PREFIX} verlustig Einträge\n", written)
self.assertNotIn('verlustig Einträge"', written)
def test_a_mangled_link_in_a_rejection_is_not_probed(self):
catalog, message = self._entry()
apply(
[message],
[Rejected("Siehe [den Leitfaden]](https://gone.invalid/a)", "structure")],
)
lines = render(catalog).decode("utf-8").splitlines()
quoted = next(
number
for number, line in enumerate(lines, start=1)
if line.startswith(f"# {REJECTED_PREFIX}")
)
self.assertTrue(is_suppressed_at(lines, quoted, "url", mode="block-above"))
def test_a_rejected_translation_never_spills_over_its_comment_line(self):
_, message = self._entry()
apply([message], [Rejected("erste Zeile\nzweite Zeile", "structure")])
self.assertEqual(
message.user_comments[2],
f"{REJECTED_PREFIX} erste Zeile\\nzweite Zeile",
)
def test_a_second_rejection_replaces_the_first(self):
_, message = self._entry()
apply([message], [Rejected("erster Versuch", "structure")])
apply([message], [Rejected("zweiter Versuch", "stutter")])
self.assertEqual(
message.user_comments,
[
f"{REFUSAL_PREFIX} {ENGINE} stutter",
URL_SUPPRESSION,
f"{REJECTED_PREFIX} zweiter Versuch",
],
)
def test_a_translation_that_lands_clears_the_quoted_rejection(self):
_, message = self._entry()
apply([message], [Rejected("verlustig Einträge", "protected-span")])
apply([message], ["verlustig {count} Einträge"])
self.assertEqual(message.user_comments, [MACHINE_TRANSLATION])
def test_a_suppression_the_source_carries_survives_the_cleanup(self):
"""Sphinx hands a source comment to the catalog; it is not ours to drop."""
_, message = self._entry([URL_SUPPRESSION])
apply([message], [Rejected("verlustig Einträge", "protected-span")])
apply([message], ["verlustig {count} Einträge"])
self.assertEqual(message.user_comments, [URL_SUPPRESSION, MACHINE_TRANSLATION])
def test_a_refusal_from_another_engine_counts_and_clears(self):
"""The mark is matched by prefix, not by the engine this run uses."""
catalog, message = self._entry([f"{REFUSAL_PREFIX} someone-else damaged-span"])
self.assertNotIn(message, pending(catalog))
self.assertEqual(retry(catalog), 1)
self.assertIn(message, pending(catalog))
class TestDamagedMarkup(unittest.TestCase):
"""A bracket added or dropped beside a protected span leaves every span intact."""
def _catalog(self, source: str, translation: str):
catalog = Catalog(locale="de")
catalog.add(source, translation)
return catalog
def test_an_added_bracket_is_damage(self):
catalog = self._catalog(
"See [the guide](docs/guide.md) first.",
"Siehe [den Leitfaden]](docs/guide.md) zuerst.",
)
self.assertEqual(len(damaged(catalog)), 1)
def test_a_dropped_bracket_is_damage(self):
catalog = self._catalog(
"![Logo](assets/img/logo.png)",
"!Logo](assets/img/logo.png)",
)
self.assertEqual(len(damaged(catalog)), 1)
def test_an_intact_translation_is_not_damage(self):
catalog = self._catalog(
"See [the guide](docs/guide.md) first.",
"Siehe [den Leitfaden](docs/guide.md) zuerst.",
)
self.assertEqual(damaged(catalog), [])
class TestDuplicateTranslation(unittest.TestCase):
LONG_A = "Central identity provider issuing tokens for every service"
LONG_B = "Issue and project tracking platform for software teams today"
SHORT_A = "Save the file"
SHORT_B = "Open the file"
def _messages(self, *sources: str) -> list:
catalog = Catalog(locale="de")
for source in sources:
catalog.add(source, "")
return [message for message in catalog if message.id]
def test_a_second_long_source_may_not_reuse_a_translation(self) -> None:
messages = self._messages(self.LONG_A, self.LONG_B)
discarded = apply(
messages,
["Beschreibung eines Dienstes", "Beschreibung eines Dienstes"],
set(),
)
self.assertEqual(discarded, 1)
self.assertEqual(str(messages[1].string), "")
def test_short_sources_may_share_one(self) -> None:
messages = self._messages(self.SHORT_A, self.SHORT_B)
discarded = apply(messages, ["Speichern", "Speichern"], set())
self.assertEqual(discarded, 0)
self.assertEqual(str(messages[1].string), "Speichern")
def test_without_the_set_the_check_is_skipped(self) -> None:
messages = self._messages(self.LONG_A, self.LONG_B)
self.assertEqual(apply(messages, ["Gleich", "Gleich"], None), 0)
def test_what_the_catalog_already_spends_is_seeded(self) -> None:
catalog = Catalog(locale="de")
catalog.add(self.LONG_A, "Schon vergeben")
catalog.add(self.SHORT_A, "Kurz")
self.assertEqual(taken_translations(catalog), {"Schon vergeben"})
class TestOverSharedTranslation(unittest.TestCase):
LONG_A = "Central identity provider issuing tokens for every service"
LONG_B = "Issue and project tracking platform for software teams today"
LONG_C = "Reverse proxy routing and balancing traffic to the backends"
SHORT_A = "Save the file"
SHORT_B = "Open the file"
SHORT_C = "Close the file"
SHARED = "Beschreibung eines Dienstes"
def _catalog(self, sources: list, translation: str, *, machine: bool = True):
catalog = Catalog(locale="de")
for source in sources:
catalog.add(source, translation)
for message in catalog:
if message.id and machine:
message.user_comments = [MACHINE_TRANSLATION]
return catalog
def _ids(self, catalog) -> list:
return sorted(str(message.id) for message in over_shared(catalog))
def test_every_sharer_beyond_the_limit_is_returned(self) -> None:
catalog = self._catalog([self.LONG_A, self.LONG_B, self.LONG_C], self.SHARED)
self.assertEqual(
self._ids(catalog), sorted([self.LONG_A, self.LONG_B, self.LONG_C])
)
def test_two_sharers_stay(self) -> None:
catalog = self._catalog([self.LONG_A, self.LONG_B], self.SHARED)
self.assertEqual(over_shared(catalog), [])
def test_short_sources_may_share_one(self) -> None:
catalog = self._catalog([self.SHORT_A, self.SHORT_B, self.SHORT_C], "Datei")
self.assertEqual(over_shared(catalog), [])
def test_a_hand_translation_is_left_alone(self) -> None:
catalog = self._catalog(
[self.LONG_A, self.LONG_B, self.LONG_C], self.SHARED, machine=False
)
self.assertEqual(over_shared(catalog), [])
if __name__ == "__main__":
unittest.main()