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.
97 lines
3.7 KiB
Python
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
|