{% extends "base.html" %} {# The selector belongs beside Active project, but base.html's header is a single flex row with no room for it next to a long project name - it clipped off the right edge. It gets its own row below the header instead, which is also where it stays put when the project name is short. #} {% block header_bar %}
Two separate numbers because they fail independently. Text can arrive before any audio is produced, and audio can be missing because a clip failed rather than because the translation does not exist. One combined percentage hides which of the two is stuck.
{% if audio_blocked %}Nothing outstanding for {{ language_name }}.
{% elif issue_count <= issue_threshold %} {# Small enough to read in one place - list it. #}| Chapter | # | Kind | Text |
|---|---|---|---|
| Ch {{ i.chapter }} | {{ i.index }} | {{ i.kind }} | {{ i.text }} |
{{ issue_count }} issues across {{ issue_chapters }} chapters — too many to list usefully here.
| Chapter | Issues | Kinds | |
|---|---|---|---|
| Chapter {{ ch.chapter }} | {{ ch.count }} | {{ ch.kinds }} | Review |
Up to {{ issue_threshold }} issues are listed inline. Beyond that the list stops being scannable, so it groups by chapter and links out instead of hiding anything.
No {{ language_name }} content yet.
{% else %}| # | Speaker | What will be voiced | {% if not is_source and show_source_ref %}English source | {% endif %}Audio |
|---|---|---|---|---|
| {{ r.index }} | {{ r.speaker }} |
{% if r.type == 'narration' %}
narration
{% else %}
dialogue
{% endif %}
{{ r.target_text }}
|
{% if not is_source and show_source_ref %}
{{ r.source_text }} | {% endif %}{% if r.has_audio %} voiced {% else %} none {% endif %} |
| Area | Change |
|---|---|
| Selection | Dropdown in the header beside Active project. Backed by a separate
active_language cookie, defaulting to the project source language,
so switching language cannot disturb project selection. Fully reversible. |
| Database | New utterance_translations table. English stays editable in
utterances.text; translations are confirm-only.
Rejected: a
language column on utterances — one row holds one language,
so it forces 10× duplication or overwrites the source. |
| Audio on disk | chapter_audio/ch6/utt_0001.wav (English, unchanged) →
chapter_audio_de/ch6/utt_0001.wav.
Required: the
staleness marker records spoken text, so two languages sharing a filename would
each mark the other stale and overwrite it in a loop. |
| Marker | Marker JSON gains "language": "de", checked by
is_current(), so an export can prove what it shipped. |
| Handoff | Export (utterance_id, chapter, index, speaker, source_text, context_before,
context_after); import returns the same plus language_code and
text. Idempotent. Context matters — “Yes.” is untranslatable alone. |
| Server | /v1/tts accepts any string as language today. Validate
against the 10 supported codes and fail fast rather than producing wrong audio. |