Compile-time i18n for JavaScript, TypeScript, React, Next.js, TanStack Start, Carbon, and more.
SayKit is a framework-agnostic internationalisation toolkit built around compile-time message extraction, typed configuration, and small runtime primitives. You write messages inline with tagged templates or JSX, and SayKit takes care of extracting them into translation files, generating stable identifiers, and rendering them at runtime.
Warning
SayKit is under active development. APIs may change before the stable 1.0 release.
- Tagged-template and JSX authoring:
say`Hello, ${name}!`or<Say>Hello, {name}!</Say> - Compile-time extraction from JS, TS, JSX, and TSX via Babel or unplugin
- ICU MessageFormat support for plurals, ordinals, and select
- Typed
saykit.config.tswith a Zod-validated schema - A small core runtime, with optional framework adapters
- Import translation files directly: no build-time replacement, no shipped extraction
Full docs live at saykit.js.org (or run the website workspace locally). The site covers:
- Introduction and installation
- Messages, configuration, and the runtime
- React and Carbon integrations
This is a pnpm monorepo. The published packages live in packages/*.
| Package | Description |
|---|---|
saykit |
Core runtime: the Say class, macros, and ICU formatting |
@saykit/config |
Config schema (defineConfig) and the saykit CLI |
@saykit/react |
React integration: <Say>, SayProvider, server helpers |
@saykit/carbon |
Carbon Discord-bot integration |
unplugin-saykit |
Universal bundler plugin (Vite, Rollup, Webpack, esbuild, Rspack, …) |
babel-plugin-saykit |
Babel plugin for SayKit |
@saykit/transform-js |
JS/TS macro transformer (used by plugins) |
@saykit/transform-jsx |
JSX/TSX macro transformer (used by plugins) |
@saykit/format-po |
Gettext PO formatter |
@saykit/format-json |
JSON formatter, with ARB and WebExtension dialects |
End-to-end examples live in examples/*. Each one is a small but complete app, with
a README explaining which parts of SayKit it exercises and why.
| Example | Stack | Read it for |
|---|---|---|
vanilla |
TypeScript + Vite | The whole picture end to end — start here |
react |
React 19 SPA + Vite | SayProvider, rich-text messages, code-split catalogues |
nextjs |
Next.js App Router + Babel | Server Components, getSay, middleware locale detection |
tanstack-start |
TanStack Start + Vite | Fallback chains (en-NZ → en-GB → en), SSR locale negotiation |
expo |
Expo / React Native + Metro | The whitespace prop, device locale |
carbon |
Carbon Discord bot on Cloudflare Workers | Per-locale command registration, interaction.say / guild.say |
browser-extension |
Chrome MV3 + Vite | The webextension JSON dialect, writing into _locales/ |
custom-formatter |
Node CLI + tsdown | Writing your own Formatter and Transformer |
Between them they cover the plural shapes that actually catch people out — English and French
(one/other), Polish (one/few/many/other), and Japanese (other only) — across Gettext
PO, plain JSON, the WebExtension JSON dialect, and a hand-written YAML format.
pnpm add saykit @saykit/config @saykit/format-po @saykit/transform-jsimport { defineConfig } from '@saykit/config';
import po from '@saykit/format-po';
import js from '@saykit/transform-js';
export default defineConfig({
locales: ['en', 'fr'],
buckets: [
{
include: ['src/**/*.ts'],
output: 'src/locales/{locale}.{extension}',
formatter: po(),
transformer: js(),
},
],
});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 } });
say.activate('en');
console.log(say`Hello, ${'world'}!`);pnpm saykit extractExtraction only writes the source locale (the first entry in locales); new locales get an empty
placeholder file and existing translation files are left untouched, keeping diffs small and leaving
translated content to your TMS. Untranslated keys fall back to a configurable fallback chain (and
ultimately the source string) at load time, and saykit clean can prune orphaned and untranslated
entries from other locale files when your TMS doesn't do it for you.
For framework-specific setup, see the React and Carbon integration docs.
pnpm install
pnpm build # build all packages
pnpm test # run package tests
pnpm check # type-check all packages
pnpm lint # oxlint
pnpm format # oxfmtThe repo uses Turborepo for task orchestration and Changesets for versioning and publishing.