Files
church-translate/docs/PLAN-language-columns.md
T
christianandClaude Opus 5 c9eef0672c Church Translate, as it runs at the campuses
Live sermon translation into 100+ languages, running entirely on one Mac per
campus: audio in, voice detection, Whisper on the GPU, NLLB-200 in its own
process, and a built-in web server the congregation reads from on their
phones. No audio leaves the building.

This is the source only. What used to sit beside it — 14 GB of speech and
translation models, 8 GB of voice bundles, 6 GB of natural voices, the Electron
caches, and the app data holding the operator PIN and every service transcript
— is machine state and downloaded assets, and stays in the NAS backup where it
belongs. The vendored node, cloudflared and Windows ffmpeg binaries are out too
(533 MB, re-fetched for packaging); the browser-side runtime beside them is
kept, so a checkout still serves the congregation page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 15:41:21 -04:00

4.2 KiB
Raw Permalink Blame History

Plan — side-by-side language columns with a real move animation

The ask

"Make this portion side by side with one side being the active languages and the other side being the disabled languages. There needs to be a nice animation of the languages moving sides, so that they don't seem like they are teleporting instantly to the other side."

What exists now

One scrolling list of 105 rows, each a <label class="langrow"> with a checkbox, English name, native name and a voice/captions tag. Toggling calls toggleFeatured() which mutates opConfig.featured, re-renders the whole list with renderOpLangList(), and debounces a save. The row does not move — only its checkbox state changes. Filter and "with a voice only" narrow the list; "Turn all off" empties it.

Design

┌─ ON (3) ───────────────┐  ┌─ OFF (102) ─────────────┐
│ Español (América Latina)│  │ Afrikaans               │
│ Português (Brasil)      │  │ Albanian      Shqip     │
│ Português (Portugal)    │  │ Amharic       አማርኛ      │
└─────────────────────────┘  └─────────────────────────┘
  • Two columns, each independently scrollable, each with a count in its header.
  • One filter box and one "with a voice only" toggle drive BOTH columns.
  • Clicking a row moves it to the other column. The checkbox goes away: in a two-column layout the column IS the state, and a checkbox next to it would say the same thing twice.
  • Empty column shows a short instruction rather than blank space.

The animation — FLIP, because it is the only technique that is not a lie

Naively re-rendering makes a row vanish from one column and appear in the other: the "teleporting" the ask names. FLIP animates the real transition:

  1. First — before mutating state, record getBoundingClientRect() for every currently visible row, keyed by language code.
  2. Last — mutate and re-render. Rows are now in their final positions.
  3. Invert — for each row that both existed before and exists now, apply transform: translate(dx, dy) putting it visually back where it was.
  4. Play — on the next frame, clear the transform with a transition. The row travels from its old position to its new one.

Constraints taken from the UX guidance:

  • transform/opacity ONLY. Never animate width/height/top/left — they force layout on every frame.
  • Batch reads then writes. 105 rows × 2 measurements will thrash layout if interleaved. All reads first, then all writes.
  • Only animate rows that are actually on screen. A row scrolled far out of view should not fly hundreds of pixels; it just appears in place.
  • Respect prefers-reduced-motion — skip the animation entirely, keep the move. Rated High severity.
  • Interruptible. Clicking three languages quickly must not queue three animations or leave a row stuck mid-flight.
  • ~260ms, ease-out, matching the app's existing paneIn feel.
  • The row in flight gets a raised z-index so it passes over its neighbours rather than under them.

What must not regress

  • toggleFeatured()'s optimistic update + 500ms debounced save, and its revert on failure. Ticking twenty languages must stay one write.
  • The filter, the "with a voice only" toggle, "Turn all off", and the count line.
  • opConfig.featured stays the single source of truth; the columns are a view.
  • Alphabetical order inside each column, and the voice/captions tag per row.
  • No horizontal overflow — the card is ~470px wide inside the settings column.

Open questions for the audit

  1. FLIP across two DIFFERENT scroll containers — what breaks?
  2. What happens when the destination column is scrolled such that the row's final position is outside the visible area?
  3. Rapid clicking: how do in-flight transforms interact with a re-render?
  4. Is 105 rows × 2 rect reads per toggle acceptable on this machine?
  5. Does removing the checkbox lose anything — keyboard operability, screen reader state, the "I can see what's off" affordance?
  6. Two independently scrolling columns inside a page that also scrolls — does that create a scroll trap?