SayKit
Core Concepts

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.

saykit.config.ts
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 mtime changes, or
  • a tsconfig.json between 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.po
output: 'translations/{locale}/messages.{extension}';
// → translations/en/messages.po, translations/fr/messages.po

Multiple 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

saykit.config.ts
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

On this page