Configuration
The saykit.config.ts file, locales, buckets, formatters, transformers
Every SayKit project has a saykit.config.ts (or .js) in its root. It's a single source of truth for locales, which files to extract from, where translations live, and how they're parsed and serialised.
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', 'ja'],
buckets: [
{
include: ['src/**/*.{ts,tsx}'],
output: 'src/locales/{locale}.{extension}',
formatter: po(),
transformer: [js(), jsx()],
},
],
});The first locale in locales is treated as the source locale: the language your code is written in, and the one extraction writes through as-is. Other locales are reconciled against it on every run. The config is validated by Zod at load time, mistakes get caught with a clear error.
defineConfig() doesn't just type the config, it also runs it through the Zod schema and returns
the normalised result. Use it for both the type hints and the validation.
Loading
The CLI and the build-tool plugins both load your config via resolveConfig() from
@saykit/config/features/loader. It searches up from the current working directory for
saykit.config.{ts,mts,cts,js,mjs,cjs} and transpiles TypeScript automatically.
Transpiled output is cached in node_modules/.cache/saykit/config/ and invalidated when:
- the config file's
mtimechanges, or - a
tsconfig.jsonbetween the config and the workspace root changes.
Top-level schema
Prop
Type
Locales
Locales are arbitrary strings. SayKit doesn't care whether you use BCP 47 (en-GB), ISO 639 (en), or your own naming, whatever you pick is the key it uses everywhere.
locales: ['en', 'fr-CA', 'es-419'];The source locale is whichever entry you list first. It's the locale your code is written in, and it's the one SayKit will rewrite into when it generates translation files from scratch. The runtime Say class uses the same convention.
Reordering locales so that a different entry sits first means SayKit will treat your existing
source messages as if they were in the new source locale. Plan this early.
Buckets
A bucket is a unit of "files in → translation file out". Most projects have one bucket. Use more when different parts of your app need their own translation files (UI vs emails, client vs server, …).
Prop
Type
Output template
The output template names where translation files go. Two placeholders are required:
{locale}, replaced with each locale string{extension}, replaced with the formatter's extension (no leading dot)
output: 'src/locales/{locale}.{extension}';
// → src/locales/en.po, src/locales/fr.pooutput: 'translations/{locale}/messages.{extension}';
// → translations/en/messages.po, translations/fr/messages.poMultiple transformers
Pass an array when you want different languages in the same bucket:
transformer: [js(), jsx()];Each transformer decides which files it handles via its match() function. The bucket dispatches files to the matching transformer transparently.
Formatters
A Formatter knows how to parse and stringify a particular translation file format.
import po from '@saykit/format-po';
formatter: po();
formatter: po({ includeReferences: false });PO is the default and what every SayKit example uses. You can also write your own, for example, JSON or YAML.
Transformers
A Transformer knows how to parse a source language. It declares which files it handles, how to extract messages from them, and how to rewrite macros to runtime calls.
import js from '@saykit/transform-js';
import jsx from '@saykit/transform-jsx';
transformer: [js(), jsx()];Most projects use the two built-in transformers. You can also write your own for new file types or DSLs.
Full example: multiple buckets
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', 'ja'],
buckets: [
{
include: ['src/app/**/*.{ts,tsx}'],
exclude: ['**/*.test.*', '**/*.stories.*'],
output: 'src/locales/{locale}.{extension}',
formatter: po(),
transformer: [js(), jsx()],
},
{
include: ['src/emails/**/*.ts'],
output: 'src/emails/locales/{locale}.{extension}',
formatter: po(),
transformer: js(),
},
],
});Next
- Messages, what you can author
- Extraction, running the CLI, watch mode, reconciliation
- Formats, PO and writing your own