Runtime
Offline & Persistence
Packs are per-locale+namespace compiled blobs (Uint8Array), kept in an in-memory cache and optionally an async PersisterAdapter so they survive reloads without the network.
Persister
import { createBestEffortPersister } from '@secundus-studio/stift-persisters'
export const persister = await createBestEffortPersister()
// OPFS → IndexedDB → Cache Storage → memory: first supported wins, never throws.
Pin one backend instead of the chain:
| Import | Backend | Notes |
|---|---|---|
@secundus-studio/stift-persisters/opfs |
Origin Private FS | Fastest; needs secure context |
@secundus-studio/stift-persisters/idb-keyval |
IndexedDB | Broad fallback |
@secundus-studio/stift-persisters/cache-api |
Cache Storage | Shares the browser cache quota |
All implement PersisterAdapter = { get, set, del, keys } over Uint8Array — custom backends implement the same interface directly, no registration needed. Wire via createStift({ persister }). With no persister, packs are memory-only (re-fetched each boot). Stift.pack.durability reports 'durable' | 'memory' | 'none' — gate install/wipe UI on it: a memory persister looks healthy but evaporates on reload.
Install → activate lifecycle
await Stift.installLocale('ar') // fetch + cache the pack(s); returns boolean
await Stift.activateLocale('ar') // load into memory + make it the active locale
Stift.isLocaleInstalled('ar') // → boolean
const size = await Stift.getPackSize('ar') // PackSizeInfo: byte budget vs. estimate
activateLocale is the reactive switch behind useLocale().setLocale for offline-resident locales. Use getPackSize to warn before installing a large pack on metered storage. Wipes are dialect-safe (wiping ar never wipes ar-SY): Dialects.
Boot offline-first
const locale = await Stift.detectLocale() // custom (saved) > navigator > fallback
Stift.setLocale(locale)
void Stift.initialize({ locale }) // unawaited: first paint stays off the network
void Stift.pack.checkForUpdates(locale) // revalidate in the background
initialize fast-paths returning visitors: installed packs verify + activate from the persister with zero network, and the reactive packInstalls status rehydrates from disk so it reads installed from the first notification. Strategy choice: Loading Strategies.
Packs never need a service worker. Locale packs, detection, and catalog/update checks all work without one. A SW only buys app-shell precaching (a fully offline reload of the shell); if you ship one, activate new versions with skipWaiting + clients.claim() so refreshes pull freshly-published packs without a hard reload.
Wiring checklist
- Suspense boundary: components that suspend on a namespace load (
useTranslation) must sit under a<Suspense>boundary, or the first render before anything is cached drops the tree. Boot modes: Setup & Boot Modes. - Error surfacing: wire
hooks.onErrortoconsole.error(or inline UI) so failed pack downloads show the real error instead of failing silently. - Reset:
Stift.pack.wipePersisted({ except: locale })clears all downloaded packs except the active locale. - OPFS requires a secure context (localhost fine; prod needs HTTPS); if unavailable, fall back to IndexedDB.
Next
- Detecting changed packs with one ping → Updates & Invalidation
- Pack state in React (
usePackStatus) → Setup & Boot Modes - Watching it all happen → Devtools