mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
The gateway routes every request on its own, so a catalog holds the work of several engines, while the mark apply() wrote was the constant translated-by: libretranslate and attributed all of it to one backend. The catalogs on disk show the drift: 1021537 entries carry that constant and 13 carry ollama, written once the gateway began answering from more than one place. The constant becomes machine_translation(engine), which names what the gateway reported and writes unnamed where it reported nothing, plus is_machine_translation(), which recognises any of them by prefix so every mark already on disk keeps matching. Outcome carries an engines tuple aligned with its values, merge pads it, and _reask returns the name alongside the text: a successful re-ask records the engine that answered, not the one whose rejection it replaced, because the catalog does not carry that rejected text. apply() now strips an earlier mark before stamping the current one. clear() removes only refusal prefixes, so an entry re-translated after i18n-prune would otherwise accumulate one mark per engine and the record would be ambiguous again, which is the defect this change exists to remove. This is the half that makes the attribution true. Recording latency in the decider history is the other half; together they let the router weigh quality against speed per engine, which a catalog claiming one engine everywhere cannot support. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
507 lines
17 KiB
Python
507 lines
17 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,
|
|
REFUSAL_PREFIX,
|
|
REJECTED_PREFIX,
|
|
TRANSLATION_REFUSED,
|
|
build_template,
|
|
machine_translation,
|
|
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,
|
|
)
|
|
|
|
MACHINE_TRANSLATION = machine_translation()
|
|
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_the_protected_tokens_travel_aligned_with_their_messages(self):
|
|
"""The gateway applies them per entry, so a flat list would mislabel."""
|
|
self.client.translate(["Run ``make setup`` now", "Plain prose"], "de")
|
|
|
|
payload = FakeLibreTranslate.requests[0]
|
|
|
|
self.assertEqual(len(payload["protected"]), len(payload["q"]))
|
|
self.assertEqual(payload["protected"], [['<x id="0"></x>'], []])
|
|
|
|
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 zweite Zeile",
|
|
)
|
|
|
|
def test_a_rejected_translation_carries_no_backslash(self):
|
|
_, message = self._entry()
|
|
|
|
apply([message], [Rejected("See [test\\_file.py](../x.py)", "structure")])
|
|
|
|
self.assertNotIn("\\", message.user_comments[2])
|
|
|
|
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, "de")), 1)
|
|
|
|
def test_a_dropped_bracket_is_damage(self):
|
|
catalog = self._catalog(
|
|
"",
|
|
"!Logo](assets/img/logo.png)",
|
|
)
|
|
|
|
self.assertEqual(len(damaged(catalog, "de")), 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, "de"), [])
|
|
|
|
|
|
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), [])
|
|
|
|
|
|
class EngineAwareGateway(BaseHTTPRequestHandler):
|
|
"""Loses a token until the engine that loses it is excluded."""
|
|
|
|
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"]}])
|
|
|
|
def do_POST(self):
|
|
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
EngineAwareGateway.requests.append(payload)
|
|
spent = "alpha" in (payload.get("exclude") or [])
|
|
engine = "beta" if spent else "alpha"
|
|
texts = payload["q"]
|
|
self._reply(
|
|
{
|
|
"translatedText": [
|
|
f"DE {text}" if spent else TOKEN.sub("", f"DE {text}")
|
|
for text in texts
|
|
],
|
|
"engine": [engine] * len(texts),
|
|
}
|
|
)
|
|
|
|
|
|
class TestAnotherEngineIsAskedAfterARejection(unittest.TestCase):
|
|
SOURCE: ClassVar[str] = "Run ``make setup`` now"
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), EngineAwareGateway)
|
|
threading.Thread(target=cls.server.serve_forever, daemon=True).start()
|
|
cls.client = LibreTranslate(f"http://127.0.0.1:{cls.server.server_port}", 1)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.server.shutdown()
|
|
cls.server.server_close()
|
|
|
|
def setUp(self):
|
|
EngineAwareGateway.requests.clear()
|
|
|
|
def test_the_rejected_answer_is_replaced_by_the_next_engines(self) -> None:
|
|
outcome = self.client.translate([self.SOURCE], "de")
|
|
|
|
self.assertEqual(outcome.values, ["DE Run ``make setup`` now"])
|
|
self.assertEqual(outcome.damaged, 0)
|
|
|
|
def test_the_engine_that_failed_is_named_in_the_second_request(self) -> None:
|
|
self.client.translate([self.SOURCE], "de")
|
|
|
|
self.assertEqual(
|
|
[request["exclude"] for request in EngineAwareGateway.requests],
|
|
[[], ["alpha"]],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|