Files
computer-playbook/tests/unit/python/utils/i18n/test_placeholders.py
T
kevinveenbirkenbachandClaude Opus 5 4c98624e07 feat(i18n): gettext catalogs with LibreTranslate machine translation
Core and the documentation get gettext catalogs for all 184 ISO 639-1
languages listed in meta/languages.yml, 47 of which LibreTranslate can fill.

`make i18n-extract` merges the current sources into locale/<code>/
LC_MESSAGES/{core,docs}.po: role descriptions, category and menu titles and
the logout strings for the core domain, the Sphinx gettext output for the
docs domain. A changed source keeps its previous translation as a fuzzy
entry, and catalogs are written deterministically so an unchanged merge
leaves the files untouched.

`make i18n-translate domain=<core|docs> [languages=<codes>]` fills only the
empty and fuzzy entries with a short-lived LibreTranslate container (the
image web-svc-libretranslate pins) and removes it afterwards. It starts the
container, and loads models, only for languages that still have pending
entries. Code spans, placeholders, URLs, tags and dotted names such as
Infinito.Nexus are masked for the request and a translation that damages
one is discarded. An unreachable server stops the run instead of
discarding every remaining entry, so a rerun continues where it stopped;
machine translations carry the translator comment `libretranslate`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 18:22:03 +02:00

73 lines
2.3 KiB
Python

import unittest
from utils.i18n.placeholders import mask, unmask
class TestMaskRoundTrip(unittest.TestCase):
def test_translation_with_every_token_restores_the_spans(self):
source = "Run ``make setup`` for {role}, see https://docs.infinito.nexus/?a=1&b=2 & more."
masked = mask(source)
self.assertEqual(len(masked.spans), 3)
self.assertNotIn("make setup", masked.text)
translated = masked.text.replace("Run", "Starte").replace(
"&amp; more", "&amp; mehr"
)
self.assertEqual(
unmask(translated, masked, source),
"Starte ``make setup`` for {role}, see https://docs.infinito.nexus/?a=1&b=2 & mehr.",
)
def test_tokens_may_move(self):
source = "{done} of {total} services signed out"
masked = mask(source)
translated = (
f'Abgemeldet: {masked.text.split(" of ")[0]} von <x id="1"></x> Diensten'
)
self.assertEqual(
unmask(translated, masked, source),
"Abgemeldet: {done} von {total} Diensten",
)
def test_dotted_names_stay_whole(self):
source = "Infinito.Nexus serves main.yml, e.g. here."
masked = mask(source)
self.assertEqual(masked.spans, ("Infinito.Nexus", "main.yml"))
self.assertIn("e.g. here", masked.text)
class TestMaskRejects(unittest.TestCase):
def test_dropped_token_discards_the_translation(self):
source = "Signed out, except {failed} of {total} services."
masked = mask(source)
self.assertIsNone(
unmask('Abgemeldet, außer <x id="0"></x> Diensten.', masked, source)
)
def test_duplicated_token_discards_the_translation(self):
source = "Open {url}"
masked = mask(source)
self.assertIsNone(
unmask('<x id="0"></x> <x id="0"></x> öffnen', masked, source)
)
def test_unknown_token_discards_the_translation(self):
source = "Open {url}"
masked = mask(source)
self.assertIsNone(unmask('<x id="7"></x> öffnen', masked, source))
def test_translator_inventing_a_placeholder_discards_the_translation(self):
source = "Plain sentence."
self.assertIsNone(unmask("Satz mit {extra}.", mask(source), source))
if __name__ == "__main__":
unittest.main()