React
Setup & Boot Modes
React adapter for @secundus-studio/stift-core@0.4.0. Core stays headless; this package is the useSyncExternalStore-backed surface (via @tanstack/react-store) on top of the same store, pack manager, and config the vanilla API reads — every hook is a thin wrapper over an existing headless method.
TanStack-style unified surface: @secundus-studio/stift-react@0.4.0 re-exports @secundus-studio/stift-core@0.4.0, so a React app installs this one package and imports everything from it — createStift, the Stift type, RegisteredLocale, PersisterAdapter, hooks, providers, all of it.
pnpm add @secundus-studio/stift-react@0.4.0 @secundus-studio/stift-vite-plugin@0.4.0
Provider
import { createStift, StiftProvider, BootFallback, useTranslation } from '@secundus-studio/stift-react'
import { createStiftRuntime } from 'virtual:stift/runtime'
const Stift = createStift({ ...createStiftRuntime(), persister })
const locale = await Stift.detectLocale()
<StiftProvider Stift={Stift} boot={() => Stift.initialize({ locale })}>
<BootFallback fallback={<Spinner />}>
<App />
</BootFallback>
</StiftProvider>
function Cart() {
const { t, ready } = useTranslation('checkout')
return <h1>{t('title')}</h1>
}
Instance wiring (detectors, localePersistence, persister): Quickstart. Generated artifacts: Build.
Boot modes
Three supported modes — pick one per app (or per subtree):
fallback(splash):bootprop +<BootFallback fallback={…}>. Renders the fallback until boot settles, then children. The fallback must not require translation. A rejectedbootstill releases the gate and reports throughonBootError(defaults toconsole.error).suspense(per-component): default.useTranslation(ns)suspends until its namespaces load — wrap in<Suspense>. Nothing suspends for cached or seeded namespaces.none(immediate): omitboot/BootFallback, pass{ suspense: false }, gate onready. Renders instantly with raw-key fallback while loads stream in — suits hybrid client/server pages and client islands.
Boot imperatively in the client entry (as the examples do) by omitting boot and calling initialize() yourself.
SSR hydration
No SSR boot mode — the server renders translated HTML and the client adopts it:
// client
const Stift = createStift({ ...runtime, hydrate: window.__STIFT_STATE__ })
<StiftProvider Stift={Stift}> {/* no boot prop, no BootFallback */}
<App /> {/* seeded namespaces never suspend */}
</StiftProvider>
The server renders t() results directly (@secundus-studio/stift-core/server: resolveLocale + FsJsonSource), then serializes { locale, namespaces } into the HTML (e.g. window.__STIFT_STATE__). Set <html lang> from the server-resolved locale; the client never re-detects.
Hooks by concern
The hooks are deliberately split by internationalization domain:
| Hook | Domain | Returns | Headless equivalent |
|---|---|---|---|
useStift() |
i18n (system root) | Stift instance |
createStift() |
useTranslation(ns?) |
i18n (messages) | { t, ready } (t.rich / t.markdown on t) |
Stift.scope / Stift.t |
usePackStatus() |
i18n (resources) | { pendingLocale, isBooting, packInstalls, getStatus } |
Stift.state + subscribe |
useLocale() |
l10n (locale) | { locale, direction, setLocale, context } |
Stift.getLocale/getDirection/setLocale |
useFormatter() |
l10n (conventions) | formatter functions only | @secundus-studio/stift-format@0.4.0 / Intl |
useConfig() |
g11n (system config) | Readonly<StiftRuntimeConfig> |
Stift.config |
The three-letter words
- g11n (globalization) is the umbrella — i18n + l10n together. In this adapter it's the
Stiftinstance anduseConfig(): system configuration and strategy (supported locales, currencies, numbering systems, params/context declarations, memory, loading strategy, detection, security tier). - i18n (internationalization) is the enabling engineering: message keys,
t, ICU, RTL support, detection, and the translation resources (packs).useStift,useTranslation,usePackStatus. - l10n (localization) is the adaptation for one locale: the active locale, switching, and national conventions (date/number formatting).
useLocale,useFormatter.
A "pack" is translation files — i18n resources, not a locale concern — so usePackStatus reports pack download/install state, not a locale state.
Strict per-concern returns
Hooks never return overlapping meta:
useTranslationreturns only{ t, ready }.readyistrueonce every requested namespace is cached for the active locale (i18next-style flag); pair it with{ suspense: false }to gate instead of suspending.useLocalereturns locale state only — the instance belongs touseStift(), config touseConfig().useFormatterreturns formatting functions only; locale/direction come fromuseLocale().
Runtime config
Stift.config (headless) and useConfig() (React) expose the effective runtime config — normalized from the generated virtual:stift/runtime config object plus any loose createStift overrides, shallow-frozen for the instance lifetime. security.staticKey never crosses the runtime boundary. Declaring it: stift.config.ts.
Next
- Rendering messages, values, rich text, agreement context → Translating & Values
- Measurement units → Units
- Offline packs and
usePackStatuspatterns → Offline & Persistence