Files
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
..
2026-10-04 20:14:53 -07:00

UmbraTrans web application

The browser client and the account/organization API it talks to.

This is not a mock. Every page calls the real backend under /api. The three routes below are served from the same origin as the API by umbratrans_web, because the session cookie is HttpOnly and the CSRF token travels as a header, and the application deliberately installs no CORS middleware. A separately hosted frontend could not call it at all.

What is real

Surface Backend
Sign in, forced first-login password change, sign out Real, Argon2id and server-side sessions
Accounts: list, filter, provision, suspend, reactivate, deactivate, revoke sessions Real
Organizations: list, create, rename, re-slug, suspend, reactivate, archive Real
Audit log Real
Dashboard counts Real, scoped to the caller
Project workspace contents, maintenance, statistics Not built yet

The unbuilt areas render as explicit empty states that name the missing backend work. No page displays a fabricated name, count, or metric.

Running it

In development the API serves this directory. From the repository root:

cd /home/umbra/src/UmbraTrans
UMBRA_ENV=development \
UMBRATRANS_REPOSITORY=json \
UMBRATRANS_REPOSITORY_PATH=.local/data/development/umbratrans-web/dev-accounts.json \
UMBRATRANS_STATIC_ROOT=/home/umbra/src/UmbraTrans/web \
UMBRATRANS_ALLOWED_HOSTS=127.0.0.1,localhost \
  /home/umbra/.local/bin/uv run python -m umbratrans_web serve \
    --host 127.0.0.1 --port 8400 --repository json

There is no first administrator until one is provisioned through the operator command, which reads the one-time password from a hidden prompt:

UMBRA_ENV=development \
  /home/umbra/.local/bin/uv run umbratrans-bootstrap-admin \
  --username Umbraadmin \
  --repository-path .local/data/development/umbratrans-web/dev-accounts.json

--repository json keeps accounts across restarts. --repository memory starts empty every time.

Routes

  • / — sign in, and the mandatory password change for a temporary-password account
  • /uadmin/ — control panel: dashboard, accounts, organizations, audit
  • /workspace.html — project workspace

Security properties the client relies on

  • No secret is ever sent by a client. POST /api/uadmin/accounts has no password field; the service generates the one-time password. The request schema refuses unknown fields, so a submitted password is rejected before a handler sees it.
  • A one-time password is shown exactly once, in the otp-modal panel. It is held in one variable, cleared when the panel closes and on pagehide, and is never written to localStorage, sessionStorage, a cookie, the URL, the page title, or the console.
  • Every unsafe request carries X-CSRF-Token, read fresh from the readable umbratrans_csrf cookie. GET /api/health on page load bootstraps that cookie, because the middleware does not issue one on an unsafe method and POST /api/auth/login is exempt.
  • No inline script, no inline style, no innerHTML. The server sends script-src 'self'; style-src 'self', so an inline <script> or a style= attribute would be refused by the browser. All server-supplied text is rendered with textContent through the el() helper in api.js.
  • Nothing is trusted from the browser. The UI hides controls a role cannot use, but the service re-checks every one of them; a forged identifier is refused with the same generic error as any other forbidden request.

Not a production deployment

The application fails closed in production: the PostgreSQL adapter does not exist, so create_app raises rather than starting against an unreviewed store. Serving this directory is therefore a development arrangement, and the reviewed migration in umbratrans_web/migrations/ has not been applied.

Systemd

umbratrans-web-prototype.service currently serves this directory with python3 -m http.server, which is static only and cannot reach the API. It exists so the port stays occupied and the public hostname keeps answering. See systemd/umbratrans-web-prototype.service.