Skip to content

Guides

Keys and catalogs

A catalog is a JSON file per language and a key is the name a message answers to. The default is that you never write one: you write text, and the compiler names it for you. This page is about the times you want the key to be yours, which is most projects that already have one.

Five ways, one catalog

They mix freely in the same project and end up in the same file. Pick per message, not per app.

You writeThe key isReach for it when
t`Hola {name}`A hash of the textAlways, unless you have a reason not to. You never name anything and nothing drifts
t.id('inbox.title')`…`The id you gave itYou want to read the catalog, or a translator asked you to
t('inbox.title')The key you wroteThe text lives in the catalog, and you only write its key where you use it
defineKeys({ … })The id you declaredThe key comes from data, or is shared with something outside this app, and its text lives only in the catalog
data-verbaly="inbox.title"The attribute valueThe text is in HTML you do not compile, so nothing extracts it

The default: no key at all

Write the sentence. The compiler hashes the text into a short stable id, writes it to your source catalog and generates the types. Change the wording and you get a new key, which is the point: a translation of the old sentence never silently stands in for the new one.

t`Hola {name}`;

// locales/es.json
"eYJLZELq": "Hola {name}"

Readable ids

When you want to open the catalog and recognise things, name the message where you write it with t.id. The text stays in your code and the id is what lands in the file:

t.id('inbox.title')`Hello {name}`;
// extracted under "inbox.title" and rewritten to:
t('inbox.title', { name });

The dots are a naming convention, and your catalog can spell them either way: one flat "inbox.title" key, or a title inside an inbox group. Every command reads both and writes back the shape your file already has, so nothing reformats your catalog for you. Dynamic ids (t.id(someVar)) are left untouched, and one id written with two texts is reported with every place that disagrees, because all of them show the one written first, by file path, whatever order your files load in.

Text that lives in the catalog

You can also keep the text in the catalog and write only the key in your code. A key written as a literal counts as used, so extract --prune keeps it and check verifies it exists. Pick this when the wording changes more often than the code, or when someone edits the texts without opening a component.

// locales/es.json
"about.bio": "Escribo software en Lima."

t('about.bio');

When the key comes from data, a map by slug or by id, declare it with defineKeys, below, so the compiler sees it and your editor checks it.

Every key is typed

Every t in your project checks its keys against your catalog: the one from virtual:verbaly, useT() in React, Vue and Svelte, getT() in Next.js, and the t of any instance. A key you write that no catalog has does not compile, the error names it, and a message with a {name} asks for it.

t('about.bio');   // ✓
t('about.boi');   // ✗ '"about.boi"' is not assignable to … VerbalyKey
t(item.key);      // ✓ a key from data passes as it is

A key that comes from data, a string or a template with a part from data like t(`status.${s}`), passes as it is, since no catalog can tell which key it will be. A variable typed as a union of keys, VerbalyKey itself included, is data too: each key it can be has to exist, and its params are left open. Declare such keys with defineKeys, below, when you want check to verify they exist and extract --prune to keep them.

Keys you already own

Sometimes the key is the point: it is shared with a mobile app or a translation memory, it outlives any one component, and its text lives only in the catalog. Declare those with defineKeys and Verbaly treats them as used, so extract --prune keeps them and check verifies every one exists:

texts.ts
import { defineKeys } from 'virtual:verbaly';

export const BannerText = defineKeys({
  title: 'alerts_banner_title',
  button: 'alerts_banner_button',
});

The types come from your own catalog, so a key that does not exist is an error right there, on the line that declares it, and your editor completes the ones that do. Read them anywhere with t(BannerText.title). Groups nest as deep as you like, and an empty value is skipped because empty means untranslated.

Who owns a text

A message keeps its text in one place, and that place is where you change it. A text written in your code, with t`…` or t.id, belongs to the code: extract, the dev servers and the build all use it, so an edit of it in the source catalog is replaced, and Verbaly tells you which file holds the text. A text that lives only in the catalog is edited there. Every command that reads a source text reads the one that ships: check, status, review, translate, export, import, Studio and agents.

A translation remembers which text it was written for. For readable keys, Verbaly keeps a short fingerprint of the source text and of each translation in .verbaly-state.json, next to your catalogs, taken the moment the translation is written, whoever writes it. Change a source text and its translations show up as outdated in status, check and doctor until someone updates them, or keeps them with verbaly review --approve. Editing a translation clears it on its own. Hashed keys never need this: new wording is a new key.

Keys in plain HTML

Text that lives in HTML nobody compiles has no call site to extract, so the key travels on the element. Write the catalog first and point at it. This site is built that way, and Plain HTML covers the whole interpreter.

<h1 data-verbaly="inbox.title">Inbox</h1>

Two shapes of catalog

Flat is what extract writes. Nested is what a person writes. Both are read everywhere, and every command writes back the shape the file already had, so a project never ends up half one and half the other.

// flat
{ "inbox.title": "Bandeja" }

// nested, the same key
{ "inbox": { "title": "Bandeja" } }

Which languages you have

If you do not list locales, every JSON file in your catalogs directory is a language, except the pseudo-locale one: drop a pt.json in and it is picked up. List locales and that list is the whole list, so a file left behind by an earlier run is ignored. npx verbaly doctor tells you which of the two is happening.

↑↓to moveEnterto open
Copied to clipboard