React
Quickstart
Eight steps from empty Vite app to first translated string. Each step links the page that owns the detail.
1. Install
pnpm add @secundus-studio/stift-react@0.4.0 @secundus-studio/stift-vite-plugin@0.4.0
Package variants: Installation.
2. Wire the Vite plugin
// vite.config.ts
import { stiftPlugin } from '@secundus-studio/stift-vite-plugin'
export default defineConfig({
plugins: [
// ...your existing plugins
stiftPlugin({ devtools: { allowFileWrites: true } }),
],
})
devtools.allowFileWrites: true mounts the /__stift_devtools/write bridge so the panel can persist edits back into locales/**. Leave it off unless you're demoing live editing — with the default false there is zero code path that can write to disk. Full options: Build: Vite Plugin & CLI.
3. Write the config
// stift.config.ts
import { defineConfig } from '@secundus-studio/stift-generator'
export default defineConfig({
locales: { default: 'en', supported: ['en', 'ar'] },
namespaces: { dir: 'locales', discovery: 'convention', default: ['common'] },
messageFormat: 'icu-mf1',
detection: { order: ['explicit', 'custom', 'navigator'], fallback: 'en' },
loadingStrategy: 'critical-then-full-background',
})
Start minimal; add params.context, units, and security.tier once the feature calls for them. Full schema: stift.config.ts.
4. Add the first namespace
Create locales/en/common.json:
{ "greeting": "Hello, {name}!" }
The default locale is the schema — new keys propagate to every other locale automatically. Never hand-sync locale files. Rules: Messages & Keys.
5. Create the instance
// src/stift.ts
import { createStiftApp } from '@secundus-studio/stift-react'
import { createStiftRuntime } from 'virtual:stift/runtime'
import { createBestEffortPersister } from '@secundus-studio/stift-persisters'
export const { stift, boot } = await createStiftApp({
...createStiftRuntime(), // generated config + catalog + source
persister: createBestEffortPersister, // offline packs — see Offline & Persistence
})
Everything declarative lives in stift.config.ts: detection order (including the reserved 'persisted' slot) and the state block ({ prefix: '__stift_', storage: 'webStorage' }) that persists the locale + setParam/setUnit overrides. The plugin emits two generated artifacts: src/stift.gen.d.ts (type augmentation — keys, locales, params become literal types) and virtual:stift/runtime (createStiftRuntime()). Offline wiring: Offline & Persistence.
6. Boot
// src/main.tsx
import { stift, boot } from './stift'
await boot() // persisted > navigator > fallback; loads per loadingStrategy
render(
<StiftProvider stift={stift}>
<Suspense fallback={<FullPageLoader />}>
<App />
</Suspense>
</StiftProvider>,
)
Boot modes (splash, suspense, immediate): Setup & Boot Modes. Loading strategies: Loading Strategies.
7. Translate in a component
const { t } = useTranslation('common')
t('greeting', { name: user.name })
Typos and wrong params are compile errors once the typegen runs. Values, rich text, grammatical agreement: Translating & Values.
8. Verify
pnpm dev, open the app: the greeting renders fromlocales/en/common.json.- Add the devtools panel (Devtools), switch locale, edit a string: it writes back to disk and the UI updates in place — no full page reload. The terminal logs
[stift] devtools write: <locale>/<ns>#<key> -> <file>. - Mistype a key in a component:
tscfails. That is the whole point.
Where next?
- Writing real messages (ICU, dialects) → Messages & Keys
- Runtime config, params, units, security tiers → stift.config.ts
- Making it work offline → Offline & Persistence