← Back to work

bible-viewer

A Bible reader that puts two Korean translations side by side

PersonalSole designer, developer, and operator2026.06 — Present

ReactTypeScriptViteTailwindFirebaseFuse.js

Why I built it

I often compare two Korean Bible translations verse by verse, but every reader I tried made switching translations a tab change. Reading both at once meant opening two windows. I wanted a viewer that lines the two texts up against the same verse.

What I did

Everything — data pipeline, search indexing, the column layout, Firestore sync, deployment.

Data pipeline

The two translations come from different sources, so collection had to split too. The older translation is available from Bolls.life as JSON with verse numbers already attached, but the newer one isn’t on Bolls at all — I had to scrape holybible.or.kr’s raw EUC-KR HTML and parse verse numbers myself from <ol start> values. Both share the same versification lineage, so alignment mostly just works, but the newer translation merges a handful of verses the older one keeps separate — for those I filled the reference key with an empty string rather than dropping it.

Scraping all 66 books / 1,189 chapters is cached per chapter so an interrupted run can resume, and a separate verification script checks book/chapter/verse counts and cross-translation alignment automatically.

What I learned

Importing a 30MB scripture JSON through resolveJsonModule kills tsc with a heap OOM: the type checker expands the entire literal into a type. Switching to ?url plus a runtime fetch fixed it.

I also spent a while deciding whether translations belonged in static JSON or Firestore. Scripture is immutable and large, so it stayed static; only per-user state (bookmarks, reading position) went to Firestore. That is where I learned that Firestore’s last-write-wins semantics will resurrect a deleted document from a stale write on another device unless deletions are modeled as tombstones.

The interesting part

I built column drag-and-reorder with raw Pointer Events, no dnd-kit. The layout is a row-based grid, so a “column” is not one DOM node. I laid a single absolutely-positioned pillar card behind transparent cells and wrote CSS custom properties (--drag-x) straight onto an ancestor, so a pointermove triggers zero re-renders. The other columns yield with translateX(±pitch). It ends up feeling like rearranging icons on an iPhone home screen.

What I’d do differently

To run integration tests without a browser I executed the Firestore logic in Node through Vite’s ssrLoadModule. Honest as that is, it still does not cover IndexedDB persistence in a real browser.