@secundus-studio/stift-core
v0.4.32
Published
Headless stift core: reactive store, locale/dialect model, detector pipeline, formatter engine interface, pack manager.
Readme
@secundus-studio/stift-core
Headless Stift runtime: reactive store, locale/dialect model, detector
pipeline, ICU MessageFormat engine, pack manager. No React, no DOM � works in
Node, browsers, workers, anywhere TypeScript runs. (React apps: install
@secundus-studio/stift-react instead � it re-exports all of this.)
pnpm add @secundus-studio/stift-core1. Create the instance
import { createStift } from '@secundus-studio/stift-core'
import { createStiftRuntime } from 'virtual:stift/runtime' // emitted by @secundus-studio/stift-vite-plugin
const Stift = createStift({
...createStiftRuntime(), // locales, detection, state backend, CompiledArtifactSource, catalog
persister, // offline pack storage: OPFS / IndexedDB / Cache API (optional but recommended)
hooks: { onError: (e) => console.error(e) }, // pack failures, missing keys
})createStift is synchronous and side-effect-free. Persisted user state (the
locale preference plus setParam/setUnit overrides) is configured in
stift.config.ts (state: { prefix: '__stift_', storage: 'webStorage' }) and
read synchronously at construction — no flash. persister is where downloaded
pack bytes live. Without a persister, packs are memory-only (re-fetched each
boot) — Stift.pack.durability reports 'durable' | 'memory' | 'none' so UI
can gate install affordances honestly.
2. Boot (offline-first)
const locale = await Stift.detectLocale() // explicit ? persisted ? navigator ? fallback
await Stift.initialize({ locale }) // strategy decides what loads wheninitialize fast-paths returning visitors: installed packs verify + activate
from the persister with zero network, and the reactive packInstalls status
is rehydrated from disk so it reads installed from the first notification.
Loading strategies (loadingStrategy): 'lazy-per-namespace' (default, load
on first t()), 'critical-then-full-background' (load critical, swap,
persist the rest in the background), 'boot-all' (everything pinned before
first paint).
3. Translate
Stift.t('checkout', 'title') // unscoped: namespace + key
Stift.scope('checkout')('title') // scoped: bare keys
Stift.scope(['common', 'checkout'])('login') // multi: first namespace with the key wins
Stift.t('checkout', 'title', { count: 3 }, 'ar') // render for a locale without switchingKeys, param shapes, and locales are literal types once the generator emits
stift.gen.d.ts � typos and wrong params are compile errors. A cache miss
returns the raw key, records it in store.missingKeys, reports
hooks.onError({ code: 'missing-key' }), and kicks a background load so the
next read resolves. ICU select/plural/offset work through the default engine;
swap it via createStift({ formatter }).
Stift.subscribe((state) => �) fires on any store mutation (locale, cache,
packInstalls, missingKeys�); read Stift.state / Stift.getLocale() /
Stift.getDirection() synchronously anytime.
4. Offline packs
await Stift.installLocale('ar') // download + verify + activate (boolean)
await Stift.activateLocale('ar') // verify + activate from disk, no network
await Stift.isLocaleInstalled('ar')
await Stift.getPackSize('ar') // byte budget vs download-needed
await Stift.pack.checkAllForUpdates() // ONE catalog fetch, diff every installed locale
await Stift.pack.refreshStale('ar') // re-download only stale namespaces
await Stift.pack.wipeLocale('ar') // delete one locale (honest error status on partial failure)Update checks compare per-namespace content hashes against a freshly fetched
catalog.json � one ping detects every change. Statuses that lie are bugs:
install/refreshStale write __meta per namespace (a failed write never
corrupts the previous good pack), wipes report error instead of fake idle,
and byte-true bytesReceived/bytesTotal come from the catalog.
Optional persisterTtlMs: packs older than the budget report
reason: 'ttl-expired' (still fully usable offline) and refresh in the
background on activate. Content hashes stay the real invalidation signal.
5. SSR hydration (no SSR boot mode)
The server renders t() results directly (@secundus-studio/stift-core/server:
resolveLocale + FsJsonSource), then serializes what it rendered:
// server, after rendering
const state = serializeStiftState(Stift) // { locale, namespaces }
// ? inject into HTML as window.__STIFT_STATE__, set <html lang>// client � adopt it: no re-detection, no fallback flash, no re-fetch
const Stift = createStift({ ...runtime, hydrate: window.__STIFT_STATE__ })
await Stift.initialize({ locale: state.locale })There is deliberately no 'ssr' boot strategy � hydration is the SSR story.
6. Persisters
One package, three backends, plus a best-effort chain:
import { createBestEffortPersister } from '@secundus-studio/stift-persisters'
// OPFS → IndexedDB → Cache Storage → memory: first supported wins
const Stift = createStift({ ...runtime, persister: await createBestEffortPersister() })| 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.
