{% 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 %}
Applies to the whole book — every chapter and every speaker.
{% endblock %} {% block content %}
MOCKUP — spec 30. Read-only. The language selector sits in the header row, beside Active project.
{# ------------------------------------------------------------------ coverage #}

{{ language_name }} — progress

{{ text_have }}
of {{ total }} utterances translated
{{ '%.2f' % text_pct }}%
{{ audio_have }}
of {{ total }} utterances voiced
{{ '%.2f' % audio_pct }}%

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 %}
{{ audio_blocked_count }} utterance{{ 's' if audio_blocked_count != 1 else '' }} translated but not voiced. Audio is behind translation, which is the expected state before a render pass.
{% endif %}
{# -------------------------------------------------------------------- issues #}

Needs attention

{% if issue_count == 0 %}

Nothing outstanding for {{ language_name }}.

{% elif issue_count <= issue_threshold %} {# Small enough to read in one place - list it. #} {% for i in issues %} {% endfor %}
Chapter # Kind Text
Ch {{ i.chapter }} {{ i.index }} {{ i.kind }} {{ i.text }}
{% else %} {# Too many to be useful inline - summarise and link to the chapters. #}

{{ issue_count }} issues across {{ issue_chapters }} chapters — too many to list usefully here.

{% for ch in issue_by_chapter %} {% endfor %}
Chapter Issues Kinds
Chapter {{ ch.chapter }} {{ ch.count }} {{ ch.kinds }} Review
{% endif %}

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.

{# ------------------------------------------------------------------- content #}

Content — {{ language_name }}

{% if is_source %}
Source language. Content is the editable original — no translation layer involved.
{% else %}
Utterances render in {{ language_name }}. Navigation, buttons and statuses stay English.
{% endif %} {% if not rows %}

No {{ language_name }} content yet.

{% else %} {% if not is_source and show_source_ref %}{% endif %} {% for r in rows %} {% if not is_source and show_source_ref %} {% endif %} {% endfor %}
# Speaker What will be voicedEnglish sourceAudio
{{ r.index }} {{ r.speaker }} {% if r.type == 'narration' %} narration {% else %} dialogue {% endif %}
{{ r.target_text }}
{{ r.source_text }} {% if r.has_audio %} voiced {% else %} none {% endif %}
{% endif %}
{# -------------------------------------------------------------------- design #}
What the real build changes
AreaChange
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.
Open questions
  1. Chapter titles are content, so they would translate — but they are also used in filenames.
  2. Speaker names are proper nouns; samples transliterate them. Same treatment?
  3. Should a missing translation block generation, or generate English and mark it fallback? Proposal: block it.
{% endblock %}