From f32d9365e5e6a81db08dd025101efcda38cf6e77 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Wed, 7 Oct 2026 10:08:03 +0200 Subject: [PATCH] Finish unfinished French entries that equal their source After a lupdate refresh, the same-text heuristic fills a new string with the translation of an existing identical one and marks it unfinished. In the source-language file that guess is the source text itself, which is exactly what a mirror run writes, but the script left it alone as "work in progress": 39 such entries stayed unfinished in qet_fr.ts after refreshing it on 2026-10-07. The script now finishes an unfinished translation (or plural with all forms) that is identical to its source. Unfinished translations with other text stay untouched. Two fixture messages and a test cover it. Co-Authored-By: Claude Opus 5.5 --- misc/i18n/fixtures/mirror_after.ts | 13 ++++++++++++ misc/i18n/fixtures/mirror_before.ts | 13 ++++++++++++ misc/i18n/mirror_source_language.py | 26 ++++++++++++++++++++++-- misc/i18n/test_mirror_source_language.py | 14 ++++++++++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/misc/i18n/fixtures/mirror_after.ts b/misc/i18n/fixtures/mirror_after.ts index c83caeb28..0309fd987 100644 --- a/misc/i18n/fixtures/mirror_after.ts +++ b/misc/i18n/fixtures/mirror_after.ts @@ -28,6 +28,19 @@ Brouillon Brouillon (à revoir) + + + Copier + Copier + + + + %n page(s) + + %n page(s) + %n page(s) + + %n élément(s) diff --git a/misc/i18n/fixtures/mirror_before.ts b/misc/i18n/fixtures/mirror_before.ts index 48e1aaf4d..613011833 100644 --- a/misc/i18n/fixtures/mirror_before.ts +++ b/misc/i18n/fixtures/mirror_before.ts @@ -28,6 +28,19 @@ Brouillon Brouillon (à revoir) + + + Copier + Copier + + + + %n page(s) + + %n page(s) + %n page(s) + + %n élément(s) diff --git a/misc/i18n/mirror_source_language.py b/misc/i18n/mirror_source_language.py index 2f7c7e469..dab5a9b1a 100755 --- a/misc/i18n/mirror_source_language.py +++ b/misc/i18n/mirror_source_language.py @@ -42,7 +42,11 @@ into the translation and the entry is finished. Nothing else changes: - a translation with text is never touched, whether it is a French rendering of an English source, identical to the source, or marked - unfinished (a translator's work in progress); + unfinished (a translator's work in progress), with one exception: + an unfinished translation identical to its source is finished. + That is what lupdate's same-text heuristic leaves behind when a new + string repeats an existing one, and it is what a mirror run would + have written anyway; - vanished and obsolete entries are left to lupdate; - in a plural message only the empty s are filled; - the file stays byte-identical outside the rewritten @@ -87,6 +91,7 @@ class SourceError(ValueError): class Stats: mirrored_messages: int = 0 mirrored_forms: int = 0 + finished_identical: int = 0 unfinished_cleared: int = 0 kept_translated: int = 0 kept_identical: int = 0 @@ -94,12 +99,13 @@ class Stats: kept_unfinished_with_text: int = 0 def changed(self) -> bool: - return self.mirrored_messages > 0 + return self.mirrored_messages > 0 or self.finished_identical > 0 def summary(self) -> str: return ( f"mirrored {self.mirrored_messages} message(s), " f"{self.mirrored_forms} plural form(s), " + f"finished {self.finished_identical} identical unfinished, " f"cleared {self.unfinished_cleared} unfinished flag(s); kept " f"{self.kept_translated} translated, {self.kept_identical} identical, " f"{self.kept_vanished} vanished, " @@ -111,6 +117,18 @@ def _translation_type(attrs: str) -> str: return m.group("type") if m else "" +def _finish(whole: str, body: str, tm: re.Match, tattrs: str, + stats: Stats) -> str: + """Drop the unfinished flag of a translation equal to its source.""" + stats.finished_identical += 1 + stats.unfinished_cleared += 1 + start = whole.index(body) + tm.start() + old = tm.group(0) + new = old.replace(f"", + f"", 1) + return whole[:start] + new + whole[start + len(old):] + + def _mirror_message(match: re.Match, forms: int, stats: Stats) -> str: whole = match.group(0) attrs, body = match.group("attrs"), match.group("body") @@ -134,6 +152,8 @@ def _mirror_message(match: re.Match, forms: int, stats: Stats) -> str: found = FORM_RE.findall(tbody) empty = [f for f in found if f == ""] if found and not empty: + if ttype == "unfinished" and all(f == source for f in found): + return _finish(whole, body, tm, tattrs, stats) if ttype == "unfinished": stats.kept_unfinished_with_text += 1 elif all(f == source for f in found): @@ -153,6 +173,8 @@ def _mirror_message(match: re.Match, forms: int, stats: Stats) -> str: stats.mirrored_forms += forms else: if tbody != "": + if ttype == "unfinished" and tbody == source: + return _finish(whole, body, tm, tattrs, stats) if ttype == "unfinished": stats.kept_unfinished_with_text += 1 elif tbody == source: diff --git a/misc/i18n/test_mirror_source_language.py b/misc/i18n/test_mirror_source_language.py index 92aa1ffa2..7b462d4e3 100755 --- a/misc/i18n/test_mirror_source_language.py +++ b/misc/i18n/test_mirror_source_language.py @@ -66,6 +66,7 @@ class MirrorText(unittest.TestCase): self.assertEqual(out, after) self.assertEqual(stats.mirrored_messages, 0) self.assertEqual(stats.mirrored_forms, 0) + self.assertEqual(stats.finished_identical, 0) self.assertEqual(stats.unfinished_cleared, 0) def test_before_becomes_after(self): @@ -73,7 +74,8 @@ class MirrorText(unittest.TestCase): self.assertEqual(out, read(AFTER)) self.assertEqual(stats.mirrored_messages, 8) self.assertEqual(stats.mirrored_forms, 3) - self.assertEqual(stats.unfinished_cleared, 2) + self.assertEqual(stats.finished_identical, 2) + self.assertEqual(stats.unfinished_cleared, 4) self.assertEqual(stats.kept_translated, 1) self.assertEqual(stats.kept_identical, 1) self.assertEqual(stats.kept_vanished, 2) @@ -113,6 +115,16 @@ class MirrorText(unittest.TestCase): if key[0] == "Beta": self.assertEqual(tr.text, msg.findtext("source"), key) + def test_unfinished_identical_to_source_is_finished(self): + """lupdate's same-text heuristic leaves these behind after a refresh; + 39 of them appeared in lang/qet_fr.ts on 2026-10-07.""" + out, _ = m.mirror_text(read(BEFORE)) + msgs = messages(out) + for key in [("Alpha", "Copier"), ("Alpha", "%n page(s)")]: + self.assertIsNone(msgs[key].find("translation").get("type"), key) + self.assertEqual(msgs[("Alpha", "Brouillon")].find("translation").get("type"), + "unfinished", "a different unfinished text stays unfinished") + def test_multiline_and_spaces_kept(self): out, _ = m.mirror_text(read(BEFORE)) self.assertIn(" Ligne 1\nLigne 2\n", out)