Files
UmbraTrans/umbratrans_web/tests/test_project_status.py
B.D. Murphy 3f05489e25 Parameterise the translation pipeline by target language
The pipeline was hard-coded to Simplified Chinese in three places: the
drafter was told to translate into Chinese, the critic was told to compare
English with Chinese, and the glossary's translated forms were read from a
"zh" key. A production that declared target_language: es-ES was therefore
translated into Chinese and filed under a Spanish name - measured on oldbox,
deal-hunter-es held 3,038 paragraphs of Chinese and was reported complete.

A new registry, tools/translation/languages.py, is the single source of
truth for what a target language is: its names, its script, its punctuation,
its glossary key, its typography and its chapter-numbering convention. Ten
languages are registered. The prompt scaffolding stays in Chinese - that is
what the models read best - and the target is named with its Chinese, English
and native names so there is no room to misread it.

The registry now reaches every stage:

- prompts.py: the analyst, drafter, critic and glossary blocks take the
  target. The hard rules about this book - Deal Hunter is the ship's name,
  the AI's name and the book's title, so it stays in English - are the
  language's own table for Chinese and the global rules for everything else,
  so a language with no table does not lose them. The Chinese rules are
  byte-for-byte what they were.
- pipeline.py: --target-language, a run-directory guard that refuses to
  reuse a directory belonging to another language (from the manifest, or by
  measuring the text it already holds), a draft-keyed-by-target reader, and
  a refusal of a draft that came back in the wrong script.
- book.py: per-language chapter-title and subtitle tables. A language with no
  table falls back to the English title, never to another language's; a
  language with a table and a missing chapter gets a visible marker in its
  own language.
- qa.py: the target's quote marks, punctuation, script count and forbidden
  title forms. The Latin-residue check is skipped for a Latin target, where
  it would flag the whole book.
- align.py: the aligner and verifier prompts name the target, and the
  sentence and clause splitting uses the target's own terminators.
- render.py: the fonts, the language and country codes, the writing
  direction, the chapter headings and the fixed editorial strings around the
  translation are the target's. The Latin-run span is skipped for a Latin
  target, where marking the text as English would tell the renderer it is
  English.
- project_status.py: the dashboard measures a sample of a production's text
  and reports a production whose text is not in the language it declares as
  "mismatched", not "complete". A chapter count is a claim about the files;
  the script check is a measurement of the text.

Also fixes: the dashboard gated every language's production on the source
production existing, so a Spanish production could never be reported even
when it was there.

The Chinese render is byte-identical to before on the real book, and the
Chinese rules are unchanged. Suite: 620 passed, the same 68 pre-existing
environment failures, +49 new tests.
2026-11-07 05:53:17 -08:00

97 lines
3.7 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Tests for the project-status adapter.
The adapter reports a language's state from the productions directory. The
chapter count is a claim about the *files*; the script check is a measurement
of the *text*. Measured on oldbox, ``deal-hunter-es`` held 3,038 paragraphs
of Chinese and was reported as a complete Spanish translation, so the
measurement is the part that matters.
"""
from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
from umbratrans_web.project_status import ProjectStatusAdapter
PROJECT = SimpleNamespace(slug="dealhunter-trans", id="11111111-1111-1111-1111-111111111111")
def _production(root: Path, tag: str, chapters: dict[str, str]) -> Path:
"""Write a production directory with the given chapters of text."""
prod = root / f"deal-hunter-{tag}"
for chapter_id, text in chapters.items():
directory = prod / "runs" / chapter_id
directory.mkdir(parents=True, exist_ok=True)
(directory / "translation.json").write_text(
json.dumps({"paragraphs": {"ch-1-p0000": text}}, ensure_ascii=False),
encoding="utf-8",
)
return prod
def _row(adapter: ProjectStatusAdapter, tag: str) -> dict:
status = adapter.status_for(PROJECT)
row = next(r for r in status["languages"] if r["language_tag"] == tag)
return row["translation"]
def test_a_production_in_its_own_language_is_complete(tmp_path: Path) -> None:
_production(tmp_path, "zh", {"ch-1": "凯因达走到门前。"})
_production(tmp_path, "fr", {"ch-1": "Kainda s'approcha de la porte."})
adapter = ProjectStatusAdapter(tmp_path)
assert _row(adapter, "zh-Hans")["state"] == "complete"
assert _row(adapter, "fr-FR")["state"] == "complete"
assert _row(adapter, "fr-FR")["language_problem"] is None
def test_a_mislabelled_production_is_not_reported_complete(tmp_path: Path) -> None:
"""The defect this check exists for: Chinese filed under a Spanish name."""
_production(tmp_path, "es", {"ch-1": "凯因达走到门前。", "ch-2": "门是关着的。"})
adapter = ProjectStatusAdapter(tmp_path)
row = _row(adapter, "es-ES")
assert row["state"] == "mismatched"
assert row["chapters_done"] == 2
assert "not Spanish" in row["language_problem"]
def test_a_production_with_no_runs_is_not_started(tmp_path: Path) -> None:
adapter = ProjectStatusAdapter(tmp_path)
row = _row(adapter, "de-DE")
assert row["state"] == "not_started"
assert row["language_problem"] is None
def test_a_mislabelled_production_needs_review(tmp_path: Path) -> None:
_production(tmp_path, "es", {"ch-1": "凯因达走到门前。"})
adapter = ProjectStatusAdapter(tmp_path)
status = adapter.status_for(PROJECT)
row = next(r for r in status["languages"] if r["language_tag"] == "es-ES")
assert row["review"]["state"] == "needs_review"
def test_a_language_with_no_table_of_its_own_is_not_borrowed(tmp_path: Path) -> None:
"""A Chinese production must not be accepted as Japanese.
Japanese is written in Han characters *and* kana. A text with no kana at
all is Chinese, however many Han characters it has.
"""
_production(tmp_path, "ja", {"ch-1": "凯因达走到门前。"})
adapter = ProjectStatusAdapter(tmp_path)
row = _row(adapter, "ja-JP")
assert row["state"] == "mismatched"
assert "kana" in row["language_problem"]
def test_every_configured_language_is_reported(tmp_path: Path) -> None:
adapter = ProjectStatusAdapter(tmp_path)
status = adapter.status_for(PROJECT)
tags = {r["language_tag"] for r in status["languages"]}
assert {"en", "zh-Hans", "es-ES", "fr-FR"} <= tags