Files
computer-playbook/tests/unit/python/cli/build/i18n/test_main.py
T
kevinveenbirkenbachandClaude Opus 5 52d356544b refactor(i18n): let the table name languages and the gateway pick the engine
meta/languages.yml carried `libretranslate: true|false` per entry, which
answered two different questions with one field: which languages the
repository translates, and which engine renders them. The first is the
table's business; the second is not. The gateway offers every backend
that serves a pair, falls through when one refuses or mangles the
answer, drops an engine whose failures pile up, and settles a request
class on the engine that wins its comparisons - and `signature()` is
"{source}:{target}:{length bucket}:{markup|plain}", so that learning is
already per target language. A static column beside it is a second
opinion that goes stale the moment the router learns something.

So the field is gone, 184 lines, and the table now holds name, endonym
and direction. Every language it names carries a catalog in every
domain.

What one engine can do is declared with that engine instead:
services.libretranslate-engine.serves, 47 codes. Not a detour - both
readers run before any engine answers. The lane prefetches one model per
code, and the readiness probe is itself the thing that would have asked.

`translatable()` split along that seam. Catalog-scoped callers take
`domain_languages()`, engine-scoped ones take the new
`served_languages()`: the CI matrix, extraction and translation the
first, the model prefetch and the tuning sweep the second. Collapsing
the two is what this change is guarding against - `baked_languages()`
renders identically before and after, verified by diffing both outputs.

`translate()` also stops dying on a language whose catalog has not been
extracted yet. It reads a missing catalog as nothing pending rather than
raising, because 136 docs catalogs do not exist until the next
extraction and refusing those languages would quietly shrink the set the
repository promises to translate.

The oracle suite follows: tests/oracle/test_translation_score.py asks
utils/i18n/judge.py about every language instead of routing a laya
checkpoint per language. It cannot run yet - the tools lane carries no
LiteLLM - and its skip message says exactly that.

test_catalogs_complete_on_gpu now measures against the table rather than
one engine's coverage. It is live on a GPU host and will report the 136
missing catalogs as the gap they are, which is the point: narrowing it
to the engine would hide exactly the languages worth watching.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-18 01:26:35 +02:00

124 lines
4.2 KiB
Python

from __future__ import annotations
import contextlib
import importlib
import io
import json
import unittest
import unittest.mock as mock
from pathlib import Path
from tempfile import TemporaryDirectory
from utils.i18n.catalog import catalog_path, new_catalog, write_catalog
from utils.i18n.languages import LANGUAGES_FILE
from utils.i18n.libretranslate import SERVICE_KEY, SERVICES_FILE, Outcome
cli = importlib.import_module("cli.build.i18n.__main__")
LANGUAGES = """\
en: {name: English, native: English, direction: ltr}
de: {name: German, native: Deutsch, direction: ltr}
fr: {name: French, native: Français, direction: ltr}
zu: {name: Zulu, native: isiZulu, direction: ltr}
"""
SERVES = f"""\
{SERVICE_KEY}:
serves: [de, fr]
"""
def _catalog(root: Path, code: str, translation: str) -> None:
catalog = new_catalog("core", code)
catalog.add("Hello", translation, context="menu:x:title")
path = catalog_path(root, code, "core")
path.parent.mkdir(parents=True)
write_catalog(path, catalog)
def _checkout(case: unittest.TestCase) -> Path:
"""Return a checkout holding the language table and the engine's own coverage.
Args:
case: the test whose lifetime the temporary directory follows.
"""
tmp = TemporaryDirectory()
case.addCleanup(tmp.cleanup)
root = Path(tmp.name)
(root / LANGUAGES_FILE).parent.mkdir()
(root / LANGUAGES_FILE).write_text(LANGUAGES, encoding="utf-8")
(root / SERVICES_FILE).parent.mkdir(parents=True)
(root / SERVICES_FILE).write_text(SERVES, encoding="utf-8")
return root
class TestLanguages(unittest.TestCase):
def setUp(self) -> None:
self.root = _checkout(self)
def test_the_matrix_gets_every_language_the_table_names(self) -> None:
"""Not the one engine's coverage: the gateway falls through to a chat
backend that declares no catalogue, so a language outside
``serves`` is translated rather than skipped."""
with (
mock.patch.object(cli, "PROJECT_ROOT", self.root),
mock.patch("sys.stdout", new_callable=io.StringIO) as out,
):
status = cli.languages(["core"])
self.assertEqual(status, 0)
self.assertEqual(json.loads(out.getvalue()), ["de", "fr", "zu"])
class TestTranslate(unittest.TestCase):
def setUp(self) -> None:
self.root = _checkout(self)
_catalog(self.root, "de", "Hallo")
def _run(self, requested: list[str] | None = None) -> tuple[int, mock.Mock, mock.Mock]:
started = mock.Mock(return_value=contextlib.nullcontext("http://lt"))
client = mock.Mock()
client.return_value.translate.side_effect = lambda texts, code: Outcome(
["Bonjour"] * len(texts), 0, 0, ""
)
with (
mock.patch.object(cli, "PROJECT_ROOT", self.root),
mock.patch.object(cli, "server", started),
mock.patch.object(cli, "LibreTranslate", client),
):
return cli.translate(["core"], requested or []), started, client
def test_a_code_the_table_does_not_name_is_refused_before_any_deploy(self) -> None:
status, started, _client = self._run(["xx"])
self.assertEqual(status, 2)
started.assert_not_called()
def test_a_language_whose_catalog_is_not_extracted_yet_starts_nothing(self) -> None:
"""``zu`` is in the table and has no catalog on disk. Reading one would
raise, and refusing the language would drop it from the set the
repository promises to translate; it simply has nothing pending."""
status, started, _client = self._run()
self.assertEqual(status, 0)
started.assert_not_called()
def test_only_languages_with_pending_entries_are_awaited(self) -> None:
_catalog(self.root, "fr", "")
status, _started, client = self._run()
self.assertEqual(status, 0)
self.assertEqual(client.return_value.wait.call_args.args[0], ["fr"])
def test_nothing_pending_starts_no_container(self) -> None:
_catalog(self.root, "fr", "Bonjour")
status, started, _client = self._run()
self.assertEqual(status, 0)
started.assert_not_called()
if __name__ == "__main__":
unittest.main()