Quickstart
Build a SayKit project end-to-end in a few minutes
This walks through a complete SayKit setup (config, source, extraction, runtime) using a small Vite + React app. The exact framework doesn't matter much; only the bundler plugin and provider change between stacks.
The full source for this and other stacks lives in the SayKit examples.
1. Install
pnpm add saykit @saykit/react
pnpm add -D @saykit/config @saykit/format-po @saykit/transform-js @saykit/transform-jsx unplugin-saykit2. Configure SayKit
Create saykit.config.ts in your project root:
import { defineConfig } from '@saykit/config';
import po from '@saykit/format-po';
import js from '@saykit/transform-js';
import jsx from '@saykit/transform-jsx';
export default defineConfig({
locales: ['en', 'fr'],
buckets: [
{
include: ['src/**/*.{ts,tsx}'],
output: 'src/locales/{locale}.{extension}',
formatter: po(),
transformer: [js(), jsx()],
},
],
});This tells SayKit:
- The source locale is
en(the first entry), and we also wantfr. - Look for messages in any
.ts/.tsxfile undersrc/. - Write extracted messages to
src/locales/en.poandsrc/locales/fr.po. - Use the PO formatter and the JS + JSX transformers.
3. Wire up the bundler
For Vite, register unplugin-saykit in your config:
import saykit from 'unplugin-saykit/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [saykit(), react()],
});Using Next.js, React Native, Expo, or another Babel-driven pipeline? Add babel-plugin-saykit
to your Babel config instead. See the Babel integration.
4. Author some messages
Write messages inline using the say tagged template (or <Say> in JSX):
import { Say, SayProvider } from '@saykit/react/client';
import { useState } from 'react';
function Counter({ name }: { name: string }) {
const [count, setCount] = useState(0);
return (
<div>
<p>
<Say>Hello, {name}!</Say>
</p>
<p>
<Say.Plural _={count} one="You have 1 message" other="You have # messages" />
</p>
<button onClick={() => setCount(count + 1)}>
<Say>Add one</Say>
</button>
</div>
);
}You can also use the runtime form when you need a string:
import { useSay } from '@saykit/react/client';
const say = useSay();
const greeting = say`Hello, ${name}!`;5. Extract messages
Run the CLI:
pnpm saykit extractSayKit walks your source, finds every macro, and writes the catalogue files:
src/locales/
en.po # source locale, generated from your source
en.po.d.ts # auto-generated TS declaration
fr.po # other locales, ready for translation
fr.po.d.ts
.gitignore # marks generated declaration filesFor iterative development, use watch mode:
pnpm saykit extract --watchen.po will look something like this:
msgid "Hello, {name}!"
msgstr "Hello, {name}!"
msgid "{count, plural,\n one {You have 1 message}\n other {You have # messages}\n}"
msgstr "{count, plural,\n one {You have 1 message}\n other {You have # messages}\n}"
msgid "Add one"
msgstr "Add one"6. Translate
Open fr.po and fill in msgstr for each entry:
msgid "Hello, {name}!"
msgstr "Bonjour, {name} !"
msgid "Add one"
msgstr "Ajouter un"Future extract runs reconcile new and removed messages without overwriting your translations.
7. Provide a Say at runtime
Create one Say instance for your app:
import { Say } from 'saykit';
import en from './locales/en.po';
import fr from './locales/fr.po';
const say = new Say({
locales: ['en', 'fr'],
messages: { en, fr },
});
export default say;Activate a locale and wrap your tree in SayProvider:
import { SayProvider } from '@saykit/react/client';
import { createRoot } from 'react-dom/client';
import App from './app';
import say from './i18n';
const locale = say.match([navigator.language]);
say.activate(locale);
createRoot(document.getElementById('root')!).render(
<SayProvider locale={locale} messages={say.messages}>
<App />
</SayProvider>,
);That's it. Your app now renders in the matched locale, and switching locales is just an
activate() + re-render away.