From 11fd8d80605c6910d148fcc473cb801339ff5338 Mon Sep 17 00:00:00 2001 From: Umbra Tools Date: Fri, 6 Nov 2026 21:22:09 -0800 Subject: [PATCH] Keep the row for an unverifiable paragraph, so the gap says why A paragraph whose translation could not be verified produced no rows at all, which is indistinguishable from one that was never translated: the review lane showed a missing utterance with no explanation. The project's own rule for a gap is "the row is kept, the gap is visible, no clip is generated", and it already applies to the collapse orphans. An unverifiable paragraph is the same kind of gap, so it now gets the same treatment: a voiced=false row carrying the reason. Measured on the real ch-15 window: the ten drifted paragraphs now produce thirteen explicit not-voiced rows instead of vanishing, and the reviewer can see that the translation was dropped because it renders another paragraph rather than because nobody translated it. 60 tests, ruff clean. --- tools/translation/align.py | 19 +++++++++++++++++++ tools/translation/tests/test_align.py | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/tools/translation/align.py b/tools/translation/align.py index d72160d..bc36b7e 100644 --- a/tools/translation/align.py +++ b/tools/translation/align.py @@ -880,6 +880,25 @@ def build_delivery( for alignment in alignments: paragraph = bundle_ids.get(alignment.paragraph_id) or {} sources = {source.source_id: source for source in source_utterances(paragraph, bundle)} + if alignment.method == "mismatch": + # The paragraph's translation could not be trusted - it + # renders another paragraph. The utterances still get a row, + # marked as not voiced and carrying the reason, so the gap is + # visible in the review lane and says why. Silence would be + # indistinguishable from "never translated". + reason = "; ".join(alignment.problems) or "the translation could not be verified" + for source in sources.values(): + if source.row_id is None: + continue + utterances.append( + { + "utterance_id": source.row_id, + "voiced": False, + "reason": reason, + "source_ref": source_ref or alignment.paragraph_id, + } + ) + continue for segment in alignment.segments: source = sources.get(segment.source_id) if source is None or source.row_id is None: diff --git a/tools/translation/tests/test_align.py b/tools/translation/tests/test_align.py index 5ff5157..cd59784 100644 --- a/tools/translation/tests/test_align.py +++ b/tools/translation/tests/test_align.py @@ -333,6 +333,28 @@ class DeliveryTests(unittest.TestCase): self.assertIn("absorbed", collapsed[0]["reason"]) self.assertNotIn("text", collapsed[0]) + def test_an_unverifiable_paragraph_is_delivered_as_a_visible_gap(self) -> None: + """The row is kept and says why, rather than vanishing. + + A delivery that simply omits the utterances is indistinguishable + from one that was never translated. The review lane needs to see the + gap and its reason. + """ + alignment = align_paragraph(paragraph_fixture(), "这艘船很大。设计师很用心。", bundle_fixture()) + alignment.method = "mismatch" + alignment.segments = [] + alignment.problems = ["translation belongs to another paragraph: it renders the next one"] + delivery = build_delivery( + {"target_language": "zh"}, + [({"id": "ch-1", "paragraphs": [paragraph_fixture()]}, [alignment])], + bundle_fixture(), + ) + self.assertEqual(len(delivery["utterances"]), 2) + self.assertTrue(all(not u["voiced"] for u in delivery["utterances"])) + self.assertEqual([u["utterance_id"] for u in delivery["utterances"]], [100, 101]) + self.assertIn("another paragraph", delivery["utterances"][0]["reason"]) + self.assertNotIn("text", delivery["utterances"][0]) + class CommandTests(unittest.TestCase): def test_the_cache_directory_is_created_before_the_first_call(self) -> None: