mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-09 13:34:14 +02:00
Merge pull request #1409 from bhangart/fix/mirror-to-english
Mirror qet_en.ts instead of qet_fr.ts now that English is the source
This commit is contained in:
+10
-8
@@ -364,19 +364,21 @@ would happen under `-j`/parallel builds) can corrupt a `.ts` file mid-write,
|
||||
causing `lrelease` to fail with "Premature end of document". Run it on its
|
||||
own, review the diff, and commit the updated `.ts` files separately.
|
||||
|
||||
The strings in the code are French, so `lang/qet_fr.ts` is the source
|
||||
The strings in the code are English, so `lang/qet_en.ts` is the source
|
||||
language's own file. After `update_translations`, run
|
||||
|
||||
```sh
|
||||
python3 misc/i18n/mirror_source_language.py lang/qet_fr.ts
|
||||
python3 misc/i18n/mirror_source_language.py lang/qet_en.ts
|
||||
```
|
||||
|
||||
before committing: it copies every new French source string into its own
|
||||
translation, so `qet_fr.qm` stays complete and French wording can later be
|
||||
before committing: it copies every new English source string into its own
|
||||
translation, so `qet_en.qm` stays complete and English wording can later be
|
||||
corrected in the `.ts` alone, without changing the key that the other
|
||||
languages are attached to. Forgetting it breaks nothing (an empty entry
|
||||
falls back to the code text); `--check` reports whether a run is needed.
|
||||
A French correction typed into `qet_fr.ts` is lost like any other
|
||||
translation when the code string is reworded; the vanished entry keeps the
|
||||
old text for recovery. Never add `-removeidentical` to the `lrelease`
|
||||
options: it would strip these entries again.
|
||||
The script refuses any other language's file, such as `qet_fr.ts`, where it
|
||||
would copy English into a translation. An English correction typed into
|
||||
`qet_en.ts` is lost like any other translation when the code string is
|
||||
reworded; the vanished entry keeps the old text for recovery. Never add
|
||||
`-removeidentical` to the `lrelease` options: it would strip these entries
|
||||
again.
|
||||
|
||||
+6
-6
@@ -2032,17 +2032,17 @@ Continue?</translation>
|
||||
<message>
|
||||
<location filename="../sources/ui/diagrameditorhandlersizewidget.ui" line="31"/>
|
||||
<source>x 0.25</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>x 0.25</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/diagrameditorhandlersizewidget.ui" line="36"/>
|
||||
<source>x 0.5</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>x 0.5</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/diagrameditorhandlersizewidget.ui" line="41"/>
|
||||
<source>x 0.75</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>x 0.75</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/diagrameditorhandlersizewidget.ui" line="46"/>
|
||||
@@ -19699,7 +19699,7 @@ The other fields are not used.</translation>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/terminaleditor.cpp" line="279"/>
|
||||
<source>Change the potential of a terminal</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Change the potential of a terminal</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/terminaleditor.cpp" line="298"/>
|
||||
@@ -19798,12 +19798,12 @@ The other fields are not used.</translation>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/terminaleditor.ui" line="84"/>
|
||||
<source>Potential:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Potential:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/terminaleditor.ui" line="91"/>
|
||||
<source>No effect unless "Potential isolation" is checked (all terminals of the block then stay connected). When it is checked, terminals sharing the same value stay connected to each other and the others are isolated.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>No effect unless "Potential isolation" is checked (all terminals of the block then stay connected). When it is checked, terminals sharing the same value stay connected to each other and the others are isolated.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/terminaleditor.ui" line="98"/>
|
||||
|
||||
@@ -18,30 +18,35 @@
|
||||
mirror_source_language.py — give every source-language string its own
|
||||
translation in the source language's .ts file.
|
||||
|
||||
python3 misc/i18n/mirror_source_language.py lang/qet_fr.ts
|
||||
python3 misc/i18n/mirror_source_language.py --check lang/qet_fr.ts
|
||||
python3 misc/i18n/mirror_source_language.py lang/qet_en.ts
|
||||
python3 misc/i18n/mirror_source_language.py --check lang/qet_en.ts
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
The strings in the code are French, and they are both the translation
|
||||
key and the text the French UI shows. lang/qet_fr.ts was almost empty:
|
||||
an empty entry falls back to the code text at run time, so French never
|
||||
needed a translation. The price is that a French wording fix is a key
|
||||
change, which orphans the translation of that string in every other
|
||||
language file.
|
||||
The strings in the code are English (French until #1390), and they are
|
||||
both the translation key and the text the English UI shows. An empty
|
||||
entry in lang/qet_en.ts falls back to the code text at run time, so the
|
||||
source language never strictly needs a translation. The price is that an
|
||||
English wording fix in the code is a key change, which orphans the
|
||||
translation of that string in every other language file.
|
||||
|
||||
With every French string mirrored into qet_fr.ts, the French UI is
|
||||
served from qet_fr.qm like any other language, and French wording can
|
||||
be corrected in the .ts alone. The key in the code then only has to
|
||||
With every source string mirrored into its own .ts file, the source
|
||||
language is served from its .qm like any other language, and its wording
|
||||
can be corrected in the .ts alone. The key in the code then only has to
|
||||
stay stable.
|
||||
|
||||
Only the source language's own file may be mirrored: a run on any other
|
||||
file would copy English into it. The script therefore refuses a file
|
||||
whose <TS> header names a language different from its sourcelanguage
|
||||
(lang/qet_fr.ts says language="fr_FR" sourcelanguage="en").
|
||||
|
||||
WHAT IT DOES
|
||||
|
||||
For every message whose translation is empty, the source text is copied
|
||||
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
|
||||
rendering of a source in another language, identical to the source, or marked
|
||||
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
|
||||
@@ -87,6 +92,39 @@ class SourceError(ValueError):
|
||||
"""A <source> that cannot be copied verbatim."""
|
||||
|
||||
|
||||
class LanguageError(ValueError):
|
||||
"""The file is a translation into another language, not a mirror."""
|
||||
|
||||
|
||||
TS_HEADER_RE = re.compile(r"<TS\b[^>]*>")
|
||||
ATTR_RE = r'\b{}="([^"]*)"'
|
||||
|
||||
|
||||
def _base_language(code: str) -> str:
|
||||
return code.replace("-", "_").split("_")[0].lower()
|
||||
|
||||
|
||||
def check_languages(text: str) -> None:
|
||||
"""Refuse a .ts whose language is not its source language.
|
||||
|
||||
A file without a sourcelanguage attribute is accepted: lupdate writes
|
||||
one only when it knows the source language, and the fixtures and old
|
||||
files have none.
|
||||
"""
|
||||
header = TS_HEADER_RE.search(text)
|
||||
if header is None:
|
||||
return
|
||||
lang = re.search(ATTR_RE.format("language"), header.group(0))
|
||||
source = re.search(ATTR_RE.format("sourcelanguage"), header.group(0))
|
||||
if lang is None or source is None:
|
||||
return
|
||||
if _base_language(lang.group(1)) != _base_language(source.group(1)):
|
||||
raise LanguageError(
|
||||
f'the file is a translation into "{lang.group(1)}" of '
|
||||
f'"{source.group(1)}" text; only the source language\'s own '
|
||||
"file can be mirrored")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Stats:
|
||||
mirrored_messages: int = 0
|
||||
@@ -195,6 +233,7 @@ def _mirror_message(match: re.Match, forms: int, stats: Stats) -> str:
|
||||
|
||||
def mirror_text(text: str, forms: int = 2) -> tuple[str, Stats]:
|
||||
"""Return the mirrored .ts text and what was done to it."""
|
||||
check_languages(text)
|
||||
stats = Stats()
|
||||
new = MESSAGE_RE.sub(lambda m: _mirror_message(m, forms, stats), text)
|
||||
return new, stats
|
||||
@@ -204,12 +243,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Copy each empty translation's source text into the "
|
||||
"translation of a source-language .ts file.")
|
||||
parser.add_argument("ts", type=Path, help="the .ts file, e.g. lang/qet_fr.ts")
|
||||
parser.add_argument("ts", type=Path, help="the .ts file, e.g. lang/qet_en.ts")
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="change nothing; exit 1 if a run would change the file")
|
||||
parser.add_argument("--forms", type=int, default=2,
|
||||
help="plural forms to write when a plural message has "
|
||||
"none (default 2, French)")
|
||||
"none (default 2, as in English and French)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
with open(args.ts, encoding="utf-8", newline="") as f:
|
||||
@@ -217,7 +256,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
try:
|
||||
new, stats = mirror_text(text, args.forms)
|
||||
ET.fromstring(new.encode("utf-8"))
|
||||
except (SourceError, ET.ParseError) as e:
|
||||
except (SourceError, LanguageError, ET.ParseError) as e:
|
||||
print(f"{args.ts}: {e}", file=sys.stderr)
|
||||
return 2
|
||||
print(f"{args.ts}: {stats.summary()}")
|
||||
|
||||
@@ -10,7 +10,7 @@ byte for byte. Every message class the script distinguishes appears
|
||||
once in the pair, so a change in behaviour shows up as a diff in a
|
||||
known place.
|
||||
|
||||
The last test looks at the repository's own lang/qet_fr.ts and fails
|
||||
The last test looks at the repository's own lang/qet_en.ts and fails
|
||||
when it needs a mirror run: that is the maintenance rule from
|
||||
INSTALL.md, kept honest.
|
||||
"""
|
||||
@@ -33,6 +33,7 @@ import mirror_source_language as m # noqa: E402
|
||||
SCRIPT = HERE / "mirror_source_language.py"
|
||||
BEFORE = HERE / "fixtures" / "mirror_before.ts"
|
||||
AFTER = HERE / "fixtures" / "mirror_after.ts"
|
||||
REPO_EN = HERE.parent.parent / "lang" / "qet_en.ts"
|
||||
REPO_FR = HERE.parent.parent / "lang" / "qet_fr.ts"
|
||||
|
||||
|
||||
@@ -153,6 +154,27 @@ class MirrorText(unittest.TestCase):
|
||||
self.assertNotIn("unfinished", out)
|
||||
ET.fromstring(out)
|
||||
|
||||
def test_translation_into_another_language_is_refused(self):
|
||||
for header in ('<TS version="2.1" language="fr_FR" sourcelanguage="en">',
|
||||
'<TS language="de" sourcelanguage="en_US">'):
|
||||
text = (header + '<context><name>C</name><message>\n'
|
||||
' <source>Open</source>\n'
|
||||
' <translation></translation>\n'
|
||||
' </message></context></TS>')
|
||||
with self.assertRaises(m.LanguageError, msg=header):
|
||||
m.mirror_text(text)
|
||||
|
||||
def test_source_language_file_is_accepted(self):
|
||||
for header in ('<TS version="2.1" language="en_US" sourcelanguage="en">',
|
||||
'<TS version="2.1" language="fr_FR">'): # no sourcelanguage
|
||||
text = (header + '<context><name>C</name><message>\n'
|
||||
' <source>Open</source>\n'
|
||||
' <translation></translation>\n'
|
||||
' </message></context></TS>')
|
||||
out, stats = m.mirror_text(text)
|
||||
self.assertEqual(stats.mirrored_messages, 1, header)
|
||||
self.assertIn("<translation>Open</translation>", out)
|
||||
|
||||
def test_source_with_child_element_is_refused(self):
|
||||
text = ('<TS><context><name>C</name><message>\n'
|
||||
' <source>a<byte value="7"/>b</source>\n'
|
||||
@@ -198,12 +220,24 @@ class CommandLine(unittest.TestCase):
|
||||
self.assertIn("child element", r.stderr)
|
||||
self.assertEqual(read(tmp), bad)
|
||||
|
||||
@unittest.skipUnless(REPO_FR.is_file(), "lang/qet_fr.ts not found next to the tool")
|
||||
def test_repository_french_file_is_mirrored(self):
|
||||
"""The maintenance rule: run the mirror after update_translations."""
|
||||
r = self.run_script("--check", str(REPO_FR))
|
||||
@unittest.skipUnless(REPO_EN.is_file(), "lang/qet_en.ts not found next to the tool")
|
||||
def test_repository_source_language_file_is_mirrored(self):
|
||||
"""The maintenance rule: run the mirror after update_translations.
|
||||
|
||||
Until 2026-10-09 this checked lang/qet_fr.ts, which #1390 had turned
|
||||
into an ordinary translation; it passed while the rule was wrong."""
|
||||
r = self.run_script("--check", str(REPO_EN))
|
||||
self.assertEqual(r.returncode, 0,
|
||||
f"lang/qet_fr.ts needs a mirror run:\n{r.stdout}")
|
||||
f"lang/qet_en.ts needs a mirror run:\n{r.stdout}")
|
||||
|
||||
@unittest.skipUnless(REPO_FR.is_file(), "lang/qet_fr.ts not found next to the tool")
|
||||
def test_repository_translation_file_is_refused(self):
|
||||
"""A run on the French translation would copy English into it."""
|
||||
before = REPO_FR.read_bytes()
|
||||
r = self.run_script(str(REPO_FR))
|
||||
self.assertEqual(r.returncode, 2, r.stdout)
|
||||
self.assertIn("only the source language", r.stderr)
|
||||
self.assertEqual(REPO_FR.read_bytes(), before)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user