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 {count} 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.d.po.ts # auto-generated TS declaration
fr.po # other locales, created empty (header only)
fr.d.po.tsExtraction only writes the source locale (en). Other locales are created empty the first time and then left untouched, translated content is owned by your translation management system, and untranslated keys fall back to the source string automatically. See extraction for the full picture.
For 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
Your fr.po starts empty. A translation management system fills it in for you, or, if you translate by hand, copy the msgid lines you want to translate out of en.po and write the msgstr:
msgid "Hello, {name}!"
msgstr "Bonjour, {name} !"
msgid "Add one"
msgstr "Ajouter un"You only need the entries you have actually translated, anything missing falls back to the source string.
extract only ever writes the source locale, so your translations are never touched. When source strings get removed or reworded, run saykit clean to strip the entries in fr.po that no longer match anything in en.po:
pnpm saykit clean7. 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.