Browse docs

React

Quickstart

ReactVanilla

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 from locales/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: tsc fails. That is the whole point.

Where next?