Files
UmbraTrans/tools/translation/tests/test_render.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

159 lines
6.5 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Tests for the renderer.
The renderer writes the book the reader receives, so its typography is part
of the translation: the fonts, the language codes, the chapter-numbering
convention and the fixed editorial strings all have to be the target
language's. A Spanish edition typeset with Chinese fonts and Chinese chapter
headings is the same defect as a Chinese draft filed under a Spanish name,
one level down and much easier to miss.
"""
from __future__ import annotations
import json
import sys
import tempfile
import unittest
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
from languages import resolve # noqa: E402
from render import build_asciidoc, build_fodt, build_styles # noqa: E402
CHINESE = resolve("zh-Hans")
SPANISH = resolve("es-ES")
ARABIC = resolve("ar-SA")
def _book(target: str = "zh-Hans", subtitle: str = "交易猎人") -> dict:
return {
"title": "Deal Hunter",
"title_target": "Deal Hunter",
"subtitle_target": subtitle,
"target_language": target,
"chapters": [
{
"id": "ch-6",
"index": 6,
"row": 6,
"matter": "body",
"title": "Salvage",
"title_target": "Salvage",
"paragraphs": [
{"id": "ch-6-p0000", "type": "narration", "text": "The chair has"},
{"id": "ch-6-p0001", "type": "dialogue", "text": "Hello."},
],
},
],
}
def _run_dir(root: Path, text: str) -> Path:
chapter = root / "ch-6"
chapter.mkdir(parents=True, exist_ok=True)
(chapter / "translation.json").write_text(
json.dumps({"paragraphs": {"ch-6-p0000": text, "ch-6-p0001": text}}),
encoding="utf-8",
)
return root
class RenderTypographyTests(unittest.TestCase):
"""The renderer's typography is the target language's."""
def test_the_asciidoc_language_is_the_target_language(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "La silla estaba sucia.")
asciidoc = build_asciidoc(_book("es-ES", "Cazador de Ofertas"), run, target=SPANISH)
self.assertIn(":lang: es-ES", asciidoc)
self.assertNotIn("zh-CN", asciidoc)
def test_the_stylesheet_carries_the_target_language_codes(self) -> None:
styles = build_styles(SPANISH)
self.assertIn('fo:language="es"', styles)
self.assertIn('fo:country="ES"', styles)
self.assertIn('style:font-name="Noto Serif"', styles)
self.assertNotIn('fo:language="zh"', styles)
def test_an_rtl_target_gets_a_right_to_left_direction(self) -> None:
self.assertIn('fo:direction="rtl"', build_styles(ARABIC))
self.assertNotIn('fo:direction="rtl"', build_styles(SPANISH))
def test_the_chapter_heading_follows_the_target_convention(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "La silla estaba sucia.")
spanish = build_asciidoc(_book("es-ES"), run, target=SPANISH)
chinese = build_asciidoc(_book(), run, target=CHINESE)
self.assertIn("== Capítulo 1 Salvage", spanish)
self.assertIn("== 第一章 Salvage", chinese)
def test_a_latin_target_does_not_mark_its_text_as_english(self) -> None:
"""The Latin-run span exists so Chinese line-breaking leaves names alone.
For a Spanish paragraph the whole text is Latin, and marking it as
English would tell the renderer that the paragraph is English.
"""
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "La silla estaba sucia.")
spanish = build_fodt(_book("es-ES"), run, target=SPANISH)
chinese = build_fodt(_book(), run, target=CHINESE)
self.assertNotIn('text:style-name="Latin"', spanish)
self.assertIn('text:style-name="Latin"', chinese)
def test_the_fixed_strings_are_the_target_language_s(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "La silla estaba sucia.")
spanish = build_asciidoc(_book("es-ES"), run, target=SPANISH, sample=True)
chinese = build_asciidoc(_book(), run, target=CHINESE, sample=True)
self.assertIn("Muestra de revisión", spanish)
self.assertIn("Traducido por Umbra Tools", spanish)
self.assertNotIn("审阅样本", spanish)
self.assertIn("审阅样本", chinese)
def test_the_contents_heading_is_the_target_language_s(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "La silla estaba sucia.")
spanish = build_fodt(_book("es-ES"), run, target=SPANISH)
self.assertIn(">Índice</text:index-title-template>", spanish)
self.assertNotIn(">目录</text:index-title-template>", spanish)
def test_the_running_header_carries_the_target_subtitle(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "La silla estaba sucia.")
spanish = build_fodt(_book("es-ES", "Cazador de Ofertas"), run, target=SPANISH)
self.assertIn("Deal Hunter · Cazador de Ofertas", spanish)
def test_the_old_book_format_still_renders(self) -> None:
"""A book file written before the subtitle field existed must still work.
The old format stored the subtitle in the book-level ``title_zh``.
"""
book = _book()
del book["subtitle_target"]
book["title_zh"] = "交易猎人"
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "中间的椅子旁。")
asciidoc = build_asciidoc(book, run, target=CHINESE)
fodt = build_fodt(book, run, target=CHINESE)
self.assertIn("= Deal Hunter: 交易猎人", asciidoc)
self.assertIn("交易猎人", fodt)
def test_a_chapter_with_no_target_title_falls_back_to_the_english_one(self) -> None:
"""A missing translation is visible; borrowing another language's is not."""
book = _book("es-ES")
del book["chapters"][0]["title_target"]
with tempfile.TemporaryDirectory() as tmp:
run = _run_dir(Path(tmp), "La silla estaba sucia.")
asciidoc = build_asciidoc(book, run, target=SPANISH)
self.assertIn("Salvage", asciidoc)
self.assertNotIn("打捞", asciidoc)
if __name__ == "__main__":
unittest.main()