Projects BibleWeb Personal Translation Builder Word selection persistence
Done

Word selection persistence

Area: Personal Translation Builder Milestone: v3

Context

Problem: Users spend time carefully choosing their preferred glosses for each word. These selections must be saved so they're not lost when the popup closes or the page refreshes.

Solution: Two-layer persistence — localStorage for instant offline access, plus server-side storage for authenticated users so selections sync across devices.

Not included: Cloud sync between multiple accounts, or sharing selections with other users. Selections are personal and per-user.

Functional

When a user selects a gloss in the interlinear popup, the selection is immediately saved. The next time they open the popup for the same verse, their previous selections are restored.

User flow:

  1. User selects a gloss for a word → saved immediately to localStorage
  2. User closes the popup and reopens it later → selection is restored
  3. If authenticated: selection also saved to server via API
  4. On a different device (authenticated): server selections are loaded and merged

Merge behavior: When both localStorage and server have selections for the same verse, server data wins (server is the source of truth for authenticated users).

Edge cases:

  • Anonymous users: localStorage only — no server sync
  • Server sync failures: localStorage selection still preserved locally
  • Deselecting a word (clicking same gloss again): removes from both localStorage and server

UX & Design

No dedicated UI — persistence is invisible. Users simply see their selections restored when they return to a verse.

Technical

Layer 1 — localStorage:

  • Key: bibleweb_translations
  • Value: JSON Record<string, string> where key = ${bookId}-${chapter}-${verse}-${wordPosition}, value = selected gloss
  • Loaded on popup mount via loadTranslations()
  • Written on every selectMeaning() call via persistTranslations()

Layer 2 — Server (authenticated only):

  • On mount: loadFromServer(bookId, chapter, verse) fetches GET /api/word-selections/{bookId}/{chapter}/{verse}
  • Merges server selections into local state (server wins)
  • On selection: syncToServer() calls POST /api/word-selections/... with {wordPosition, selectedGloss}
  • On deselection: DELETE request

DB table: word_selections

  • Columns: userId, bookId, chapter, verse, wordPosition, selectedGloss, updatedAt
  • Unique index: (userId, bookId, chapter, verse, wordPosition)
  • Upsert via onConflictDoUpdate

User ID: Authenticated users use session user ID. Anonymous users get a cookie-based ID via getUserId(cookies).

Files:

  • apps/web/src/lib/components/bible/InterlinearPopup.svelte — load/save logic
  • apps/web/src/routes/api/word-selections/[bookId]/[chapter]/[verse]/+server.ts — GET/POST/DELETE API
  • apps/web/src/lib/server/queries/word-selections.tssaveWordSelection() with upsert

Status

Current: DONE Milestone: v3 Priority: Core — without persistence, gloss selection would be useless

History:

  • localStorage was the first layer implemented (works offline)
  • Server sync added for cross-device support
  • Server-wins merge strategy chosen for simplicity

Dependencies:

  • Requires: per-word gloss selection (DONE), word_selections DB table (DONE)
  • Used by: export assembled translation (IN_PROGRESS)

Screenshots

Feature screenshot