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 write | The key is | Reach for it when |
|---|---|---|
t`Hola {name}` | A hash of the text | Always, unless you have a reason not to. You never name anything and nothing drifts |
t.id('inbox.title')`…` | The id you gave it | You want to read the catalog, or a translator asked you to |
t('inbox.title') | The key you wrote | The text lives in the catalog, and you only write its key where you use it |
defineKeys({ … }) | The id you declared | The 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 value | The 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.
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:
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.
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.
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:
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.
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.
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.