SayKit
Core Concepts

Formats

PO and other translation file formats, and how to write your own

A formatter in SayKit is a pluggable adapter that knows how to parse and stringify one translation file format. The bucket calls it during extraction (to write) and during build (to read), but it's completely up to the formatter what file format ends up on disk.

saykit.config.ts
import po from '@saykit/format-po';

buckets: [
  {
    // ...
    formatter: po(),
  },
];

The default and only first-party formatter is PO (Gettext Portable Object). It's the format every SayKit example uses, and the one most translation tools and translators already understand.

PO

PO files look like this:

src/locales/fr.po
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"X-Generator: saykit\n"

#. Translator comment from the source
#: src/app/page.tsx:14
#: src/app/inbox.tsx:8
msgid "Hello, {name}!"
msgstr "Bonjour, {name} !"

msgid "Inbox"
msgctxt "noun"
msgstr "Boîte de réception"

Each entry has:

  • msgid, the source string (or ICU MessageFormat for plural/select)
  • msgstr, the translation (empty for new entries)
  • msgctxt, the descriptor's context, if any
  • #., translator comments (from // TRANSLATORS: lines)
  • #:, source references (file:line)
  • #. id:xxxx, SayKit's stable id (for messages without a custom id)

Why PO?

Universal

Decades of tooling. POEdit, Crowdin, Lokalise, Weblate, Transifex all speak PO natively.

Human-readable

A reviewer can read a PO diff. Translators can edit them in any text editor if needed.

Diff-friendly

Line-based and entry-aligned. PRs reviewing translations are sensible to read.

Tooling

Lots of CLI utilities exist (msgmerge, msgfmt, msgcat) if you ever need to munge PO files outside SayKit.

Options

Prop

Type

formatter: po({ includeReferences: true, includeLineNumbers: false });

Dropping line numbers is a popular choice once a project is large, references still tell translators which files use a string, but .po diffs no longer churn every time a line moves.

Other formats

PO is the only formatter shipped today. If you need JSON, YAML, XLIFF, or a custom format, you can write one yourself.

A Formatter is just an object:

import type { Formatter } from '@saykit/config';

const json: Formatter = {
  extension: '.json',
  parse(content) {
    /* return Message[] */
  },
  stringify(messages, { locale, existingContent }) {
    /* return string */
  },
};

See the custom formatter guide for a complete walkthrough.

The Message shape

Formatters work with this shape:

Prop

Type

Your formatter receives an array of these on stringify, and must return an array of them on parse. Anything you can map back-and-forth, you can support.

Next

On this page