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 <noreply@anthropic.com>
This commit is contained in:
Beat Hangartner
2026-10-07 10:08:03 +02:00
parent 287689d185
commit f32d9365e5
4 changed files with 63 additions and 3 deletions
+13
View File
@@ -28,6 +28,19 @@
<source>Brouillon</source>
<translation type="unfinished">Brouillon (à revoir)</translation>
</message>
<message>
<location filename="../sources/alpha.cpp" line="15"/>
<source>Copier</source>
<translation>Copier</translation>
</message>
<message numerus="yes">
<location filename="../sources/alpha.cpp" line="16"/>
<source>%n page(s)</source>
<translation>
<numerusform>%n page(s)</numerusform>
<numerusform>%n page(s)</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../sources/alpha.cpp" line="20"/>
<source>%n élément(s)</source>
+13
View File
@@ -28,6 +28,19 @@
<source>Brouillon</source>
<translation type="unfinished">Brouillon (à revoir)</translation>
</message>
<message>
<location filename="../sources/alpha.cpp" line="15"/>
<source>Copier</source>
<translation type="unfinished">Copier</translation>
</message>
<message numerus="yes">
<location filename="../sources/alpha.cpp" line="16"/>
<source>%n page(s)</source>
<translation type="unfinished">
<numerusform>%n page(s)</numerusform>
<numerusform>%n page(s)</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../sources/alpha.cpp" line="20"/>
<source>%n élément(s)</source>
+24 -2
View File
@@ -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 <numerusform>s are filled;
- the file stays byte-identical outside the rewritten <translation>
@@ -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"<translation{tattrs}>",
f"<translation{TYPE_RE.sub('', tattrs)}>", 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:
+13 -1
View File
@@ -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("<translation> Ligne 1\nLigne 2\n</translation>", out)