translationCore Mobile — an offline-first PWA that aims to be the "world's simplest mobile audio notebook and pencil" editor for oral communities doing translation.
There is no pathway for translation production in communities that cannot use text-based modalities. Oral communicators have no "pencil and paper." This is an attempt at one.
The product mockups of 22 Aug 2026 — which arrived about an hour after work began — set the domain model the app uses today. The initial scaffold was replaced rather than evolved:
was: Project -> Chapter -> Section -> Segment -> Take
now: Book -> Chapter -> Segment (-> Take, hidden, 1:1)
A segment is the unit of work — one recording, edited in place.
The issues and the docs call that replacement the pivot, and the word is
load-bearing: it names the umbrella issue, the batch numbering, and the
@pivotpending tag in the source.
docs/design/pivot-plan.md is the plan of record,
#25 is the umbrella
issue, and the work is nine batches, B0–B8 — all landed except the Template
Library half of B7.
npm install
npm run dev # http://localhost:5173
npm run dev:lan # bind to 0.0.0.0 — open it from a phone on the same Wi-Fi
⚠️ Microphone access requires a secure context.localhostcounts;http://192.168.x.xdoes not. To test on a phone over LAN you need HTTPS — deploy to a staging URL, or use a tunnel (cloudflared tunnel --url http://localhost:5173).
npm run verify # format:check + lint + knip + typecheck + test + buildIndividually: npm run lint, npm run knip, npm run typecheck, npm test,
npm run format, npm run build.
feature -> develop -> staging -> main
(default) (staging) (production)
| Branch | Purpose | Deploys to |
|---|---|---|
feature/*, develop |
dev and local testing | a preview version with its own URL |
staging |
what testers use | tc-mobile-staging |
main |
production | tc-mobile |
Each promotion is a PR. The staging -> main PR is the production gate.
Live staging: https://tc-mobile-staging.unfoldingword.workers.dev
Live production: https://tcmobile.app. The tc-mobile Worker's own URL,
https://tc-mobile.unfoldingword.workers.dev, stays live too, with no
redirect: browser storage is per origin, so recordings made there exist
only there, and a redirect would hide them (#1295; the reasoning is in
AGENTS.md → "Cloudflare Workers Builds owns deployment").
Cloudflare Workers Builds deploys the PWA straight from the repo — no
Actions workflow deploys the web app. (.github/ holds three native lanes, all
on push to staging/main and none touching Cloudflare: ios-testflight.yml
and android-apk.yml build on a promotion merge, android-play.yml uploads a
native bundle to Google Play only when vars.PLAY_UPLOAD_ENABLED is true —
see AGENTS.md → "Cloudflare Workers Builds owns deployment".)
Workers Builds is configured per Worker, so the repo is connected twice:
tc-mobile builds from main, tc-mobile-staging builds from staging with
--env staging.
Open a deployed URL on the device. It is HTTPS, which matters —
getUserMedia refuses to run outside a secure context, so a LAN address like
http://192.168.x.x cannot record audio no matter what else is correct.
Add it to the home screen to exercise the installed PWA (standalone display and safe-area insets behave differently there than in a browser tab). Share Chapter and Share Book hand an MP3 (or a zip of them) to the OS share sheet.
ci.yml: full-history secret scan, format, lint, knip, typecheck, test, build,
and a check that the PWA service worker, manifest, and version.json were
emitted. It deploys nothing. (.github/ also holds three native lanes, none
of which touch Cloudflare: ios-testflight.yml and android-apk.yml build
on a promotion merge to staging/main (and on a manual dispatch from one of
those two), signing in a release-signing environment restricted to those
two branches once #1281's runbook has run (it replaces #321's required
reviewer; until then the reviewer still gates every run);
android-play.yml triggers on push to staging/main and uploads a signed
.aab to Google Play only when vars.PLAY_UPLOAD_ENABLED is true, from a
play-upload environment with the same branch restriction and no reviewers.)
The repo is unfoldingWord/tc-mobile, in the unfoldingWord org, public since
2026-09-13. Keep it public: environments and their protection rules exist
only on public repositories for this org's plan — docs/native/README.md §4a
step 4 has the detail.
Onion layers, enforced by ESLint no-restricted-imports — imports never go
"upward":
src/
├── types/ Domain types (no internal imports)
├── lib/ Pure audio + storage core (imports: types)
│ ├── audio/ PCM edit, peaks, MP3 — no DOM, unit-tested in Node
│ └── storage/ IndexedDB repositories
├── hooks/ Browser boundary (imports: lib, types)
│ the ONLY place MediaRecorder / Web Audio appear
├── components/ UI components (imports: hooks, lib, types)
└── app/ Screens (imports: everything)
The split is deliberate. The requirements owner said from the start that the UI
would need extensive changes that were not yet specified — the pivot is that
rewrite arriving. Keeping lib/ DOM-free is what lets the UI layer be replaced
without touching the audio core.
MediaRecorder (webm/opus on Android, mp4/aac on iOS)
→ decodeAudioData + OfflineAudioContext resample
→ canonical mono 16-bit PCM @ 44.1 kHz ← everything internal is this
→ edit: cut / insert / paste / concat (pure Int16Array functions)
→ export: MP3 (lamejs, in a Web Worker) (Share Chapter / Share Book)
Fifty OBS stories (598 illustrated frames) are bundled as beta content. Before
the pivot they mapped onto the domain model directly — a story a Chapter, a
frame a Section, one Section per frame built by src/hooks/use-chapter.ts.
Section is gone from the model as of B1–B4, and so is that loader: the pivot
screens (Books → Segments → Recorder) start from an empty Books shelf (G2),
and the bundled OBS catalog is not yet imported into the Book model — that
wiring is later pivot work.
node scripts/build-obs-catalog.mjs # refresh src/data/obs-catalog.json from Door43
node scripts/build-obs-thumbs.mjs # rebuild public/obs/thumbs/ from the 360px framesStory text and frame metadata are bundled (230 KB), and so are the 128px
thumbnails — 598 of them for 2.5 MB. They ship in the build but are excluded
from the service-worker precache until a screen reads them (#177): no shipped
screen draws them yet, so precaching 2.5 MB of unused pictures only delayed
offline-readiness. jpg is restored to the precache when the Template Library
(#33) wires a reader — imports/calls thumbUrl, or otherwise references the
/obs/thumbs/ path — the bundle-and-precache decision itself stands (ADR 0006,
2026-09-04 amendment).
The 360px frames are not bundled, and after B0 (#26) they are not cached
either: the on-demand IndexedDB fetch for full-size artwork is gone. The
pre-pivot recording view that rendered a frame's CDN <img> is gone too, removed
with the rest of the pre-pivot UI in B2–B4.
Two things about this content changed with the pivot. Artwork is an optional
per-segment illustration rather than the thing that decides the browse layout
(D6), and no mockup screen draws it — so B0 removed the media cache outright
(Q4 answered no; #1 closed as moot). Reference audio is out of Phase 1 (D5), so
the narration path — the narrationUrl helper and the reference control — is
gone too. See ADR 0006.
unfoldingWord® Open Bible Stories is made available under a Creative Commons Attribution-ShareAlike 4.0 International License. Artwork is © Sweet Publishing under CC BY-SA 3.0. This repository's own source is MIT; the OBS content and this code are separate works in mere aggregation.
Settled, not yet implemented. The OBS licence treats a translation as a derivative work, so recordings produced against OBS content are CC BY-SA and must not carry the unfoldingWord® trademark. The requirements owner confirmed that reading on 2026-08-23 (#15 closed). Nothing in the export path implements it yet: Share Chapter and Share Book carry no attribution (#252), and the data model still cannot tell an OBS-derived recording from a user-authored one. ADR 0006.
Read docs/research/prior-art.md before designing
anything. In short: Shema Studio has already shipped essentially this entire
v1 feature list (its source is not public — someone needs to ask the Shema
Studio developer), and a uW Scripture Burrito prototype already proved this
exact stack — Vite + PWA + IndexedDB — on low-end Android inside uW.
docs/design/pivot-plan.md |
The plan of record for the pivot — #25 |
docs/design/ |
Screen design passes and design notes |
docs/research/prior-art.md |
Shema Studio, passage-recorder-app, Scripture Burrito |
docs/decisions/ |
ADRs |
AGENTS.md |
Contributor and agent guide |
MIT — see LICENSE. One LGPL-3.0 dependency, lamejs: settled
2026-08-23, keep it — ADR 0003.
Every bundled open-source component is disclosed inside the app, reachable
on the phone under Menu → About & licenses (#36): each web-bundle dependency
and Workbox with its licence and copyright, and the verbatim licence texts (MIT,
the collected third-party notices, GNU LGPL v3, GNU GPL v3) read in-drawer. Those
texts also ship and precache for offline under
public/licenses/, so a translator in the field is not sent
to node_modules to find them. The About screen links this repository at the
build's full commit id, and the exact source of lamejs 1.2.7 is kept in it
under third_party/lamejs-1.2.7/ (where it came
from is in that folder's PROVENANCE.md); the lamejs row links that folder at
the same commit. That is the LGPL §4(d)(0) mechanism the DRI selected
(ADR 0003); it has not had qualified legal
review. The in-app notice
covers the web bundle, including the @capacitor/* JavaScript packages the app
imports; the Capacitor native shell's own attribution (the Gradle / CocoaPods /
native tree) is tracked separately (#477).