Reference / CLI
verbatra types
Generate TypeScript declarations for your catalog keys and their message arguments, without calling a provider or reading an API key.
Turn a misspelled key or a forgotten interpolation argument into a compile error. types reads your source catalog through the configured format adapter and writes one declaration file: a union of every key and, per key, the arguments its message takes. It calls no provider, reads no API key, and never touches a target locale file.
Synopsis
verbatra types [flags]Examples
# write verbatra-types.d.ts from the source catalog
verbatra types
# write it where the app already imports from
verbatra types --out src/generated/messages.d.ts
# CI gate: exit 1 if the committed declaration is stale
verbatra types --check
# machine-readable result for a script
verbatra types --jsonFlags
| Flag | Argument | Default | Effect |
|---|---|---|---|
--out | <path> | verbatra-types.d.ts | where to write the declaration, relative to the working directory and inside the project root; missing directories are created |
--check | none | off | compare the committed declaration with a fresh generation byte for byte, writing nothing; exit 1 when it is out of date or missing |
Also takes the global flags --cwd, --config and --json.
Behavior
What it generates
Given a locales/en.json like this:
{
"app": { "title": "Verbatra" },
"greeting": "Hello {{name}}",
"cart": { "item_one": "{{count}} item", "item_other": "{{count}} items" }
}you get:
// Generated by verbatra from locales/en.json (i18next-json). Do not edit by hand.
// Re-run `verbatra types` after the source catalog changes.
/** An argument whose type the source catalog does not record. */
export type VerbatraArgument = string | number;
/** The arguments of a message that takes none: passing any argument is a type error. */
export type VerbatraNoArguments = Record<string, never>;
/**
* The arguments of a message verbatra could not analyse, so nothing is claimed about them.
* Every key carrying this type is listed by `verbatra types` when it generates this file.
*/
export type VerbatraUnknownArguments = Record<string, VerbatraArgument>;
/** Every key in the source catalog, mapped to the arguments its message takes. */
export interface VerbatraMessages {
"app.title": VerbatraNoArguments;
"greeting": { readonly "name": VerbatraArgument };
"cart.item": { readonly "count": VerbatraArgument };
"cart.item_one": { readonly "count": VerbatraArgument };
"cart.item_other": { readonly "count": VerbatraArgument };
}
/** Every key in the source catalog. */
export type VerbatraMessageKey = keyof VerbatraMessages;
/** The arguments the message at `Key` takes. */
export type VerbatraMessageArguments<Key extends VerbatraMessageKey> =
VerbatraMessages[Key];
/** The keys the source catalog marks as carrying plural forms. */
export type VerbatraPluralMessageKey = "cart.item_one" | "cart.item_other";Type your own translation function against it and the compiler does the rest:
import type { VerbatraMessageArguments, VerbatraMessageKey } from "./verbatra-types.js";
declare function t<Key extends VerbatraMessageKey>(
key: Key,
args: VerbatraMessageArguments<Key>,
): string;
t("greeting", { name: "Ada" }); // fine
t("greting", { name: "Ada" }); // error: not a key in the catalog
t("greeting", {}); // error: the name argument is required
t("app.title", { name: "Ada" }); // error: this message takes no argumentWhat it will and will not claim
The keys are exactly what the format adapter produced, in document order, so the same catalog always produces the same bytes. Arguments come from the placeholder tokens the adapter extracted; next-intl-json and arb messages are read with the adapter's ICU parser, so an argument only some branches use is still declared. Nothing is guessed, and nothing is declared as any.
| Your message | What is declared |
|---|---|
| no placeholders | VerbatraNoArguments, so passing anything is a type error |
named placeholders ({{name}}, {name}, %(name)s) | an object shape with those names |
numbered or anonymous placeholders ({0}, %@, %1$d) | a readonly tuple, one position per argument |
an argument only some select or plural branches use (next-intl-json, arb) | an optional member, name?:, or an optional tuple position at the end ([number, VerbatraArgument?]); one every branch uses stays required |
an ICU number, plural or selectordinal argument in next-intl-json or arb | number |
an ICU date or time argument ({d, date, short}) in next-intl-json or arb | Date | number |
an ICU select argument ({gender, select, ...}) in next-intl-json or arb | string |
an i18next number formatter ({{count, number}}) in i18next-json, ngx-translate-json or yaml | number |
an i18next datetime formatter ({{d, datetime}}) in the same three formats | Date | number |
an i18next unescaped interpolation ({{- name}}) | the name behind the prefix, name |
a MessageFormat number, plural or choice argument ({0,number}) in properties | number |
a numeric printf conversion (%d, %1$d) | number |
a printf string conversion (%s, %(name)s) | string |
any other placeholder, including a plain ICU argument ({name}) | VerbatraArgument, an alias for string | number |
invalid message syntax, which only next-intl-json and arb check for | VerbatraUnknownArguments, and the key is named in the output |
| a message that mixes named, numbered and anonymous arguments | VerbatraUnknownArguments, and the key is named in the output |
a numbered argument below position 0 or at position 64 or higher (%0$s, {64}) | VerbatraUnknownArguments, and the key is named in the output |
A format whose placeholders carry no name (Apple .strings, Android strings.xml, positional gettext conversions) gets a tuple, never invented names. A name used with different types gets the union of them: {d, date, short} next to a plain {d} becomes Date | number | string. Keys are always quoted string literals, so a key with a dot, a reserved word, a leading digit or a quote is declared verbatim. A key the adapter excluded from translation (a non-string leaf, an Android translatable="false" resource) is never declared and is named in the output.
A key whose own name contains a dot
The declared key is spelled the way verbatra spells it in verbatra.lock.json, not in a lookup syntax your i18n library understands. A JSON catalog holding {"a.b": "..."} carries the key a\.b, deliberately distinct from the nested path a.b:
export interface VerbatraMessages {
"a\\.b": VerbatraNoArguments;
}i18next has no escape for its key separator, so t("a\\.b") does not look up that key. If your catalog carries such keys, set keySeparator to another character (or false for a flat catalog) and map the declared key to the runtime key in your own typed wrapper.
The one case where the declaration is wrong rather than merely silent
Only next-intl-json and arb validate message syntax. In every other format, a broken value such as "Hello {name" yields no placeholder token, so the message is declared as taking no arguments and passing the argument it really needs is a compile error. Nothing reports it, because nothing detected it. Fix the catalog, not the declaration.
Plural keys
Nothing is collapsed. i18next carries plural as a key suffix, so cart.item_ and cart.item_ are declared as written, and next to them the base key cart.item that t("cart.item", { count }) looks up, requiring count and every argument any form takes. i18next's own typings accept both spellings too. An ordinal group such as place_ declares place, and a base key the catalog already holds keeps its own declaration. VerbatraPluralMessageKey lists the keys the adapter marked as carrying plural forms, the suffixed ones, so you can narrow to them.
Where the declaration goes
By default it lands at verbatra-types.d.ts in your project root. Commit it: a teammate or a CI job type-checks against it without running verbatra, and --check needs a committed file to compare against. Nothing adds it to .gitignore.
An output path is refused with TYPES_ when it:
- names no file, including one that ends in a path separator such as
generated/(an empty--outfails earlier, withINVALID_),OUT - is absolute,
- climbs out of the project root with
.., - is not a TypeScript file, meaning its name does not end in
.ts,.mtsor.cts, - names a configured locale file, source or target, so generation can never overwrite a catalog,
- is the lock file,
verbatra.lock.json, - is the provenance file,
verbatra.provenance.json, - is the translation-memory cache,
verbatra.cache.json, - is a file verbatra searches for its configuration:
package.json,.verbatrarc,.verbatrarc.json,.verbatrarc.yaml,.verbatrarc.yml,.verbatrarc.js,.verbatrarc.cjs,.verbatrarc.ts,verbatra.config.js,verbatra.config.cjsorverbatra.config.ts, - is the config file the run actually loaded, including one you named with
--config, - is the glossary file your config points
glossaryat, - is an existing file that does not begin with the
// Generated by verbatra fromheader line verbatra writes at the top of every declaration (a leading byte order mark and blank lines are ignored), or is too large to verify.
File names are compared regardless of case, and the checks run again after symbolic links are resolved, so a linked directory cannot carry the declaration outside the project root or onto a protected file. The last check runs only when a run would write, and leaves your file untouched; --check never refuses an existing file. A path that passes every check can still fail while writing (a read-only directory, a full disk): the run stops with TYPES_, naming the file and the file-system code such as EACCES.
Keeping it honest in CI
Run types --check beside verbatra check in the same job: check catches locales drifting behind the source, types --check catches your types drifting behind it. The comparison is byte for byte and the declaration is always written with line-feed endings, so pin the endings in .gitattributes, or a checkout that rewrites them leaves the file permanently out of date:
verbatra-types.d.ts text eol=lfJSON output
Schema: types-envelope.json validates the whole envelope; Schemas lists the others.
With --json, the envelope carries one result:
interface GenerateTypesResult {
path: string; // absolute path of the declaration file
sourcePath: string; // the source catalog, relative to the project root
keys: number; // how many keys are declared
withArguments: number; // keys taking at least one determined argument
unresolved: { key: string; reason: "invalid-message-syntax" | "mixed-argument-styles" | "argument-index-out-of-range" }[];
excluded: string[]; // keys the adapter excluded, never declared
plural: string[]; // keys carrying plural forms
written: boolean; // false under --check and when the file already matched
stale: boolean; // the file on disk differed when the run started
missing: boolean; // no file existed when the run started
check: boolean; // whether this was a --check run
}Terminal feedback
The summary goes to stdout; a spinner line and, after a write, a hint to commit the file go to stderr (--check suggests nothing, and --quiet drops both). A generating run, an unchanged re-run and a stale --check:
verbatra: generating the declarations... done (0.2s)
verbatra types
214 keys declared, 63 of them taking arguments, from locales/en.json
arguments not determined (1):
legal.notice invalid-message-syntax
excluded by the adapter (1): meta.version
wrote verbatra-types.d.ts
next: commit verbatra-types.d.ts (verbatra types --check compares against it in CI)
verbatra types
214 keys declared, 63 of them taking arguments, from locales/en.json
unchanged verbatra-types.d.ts
verbatra types
215 keys declared, 63 of them taking arguments, from locales/en.json
verbatra-types.d.ts is out of date, re-run verbatra typesSee Terminal output for color, the spinner and the variables that control them.
Exit codes
| Code | Meaning |
|---|---|
0 | the declaration was generated, whether or not the file changed; or --check found it current |
1 | --check found the committed declaration out of date, or missing (is missing, run verbatra types to create it) |
2 | could not run: a usage error, a config or source problem, a refused output path (TYPES_), or a declaration that could not be written (TYPES_) |
See Exit codes for the contract every command follows.
Related
verbatra extractadds the keys your code uses to the catalog;typesthen declares them.verbatra checkis the CI gate for locale drift, next to this one for type drift.- Formats lists the placeholder syntax each format uses.
- Exit codes and JSON output covers the envelope and the exit-code contract.