Skip to content
Skip to Content

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 --json

Flags

FlagArgumentDefaultEffect
--out<path>verbatra-types.d.tswhere to write the declaration, relative to the working directory and inside the project root; missing directories are created
--checknoneoffcompare 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 argument

What 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 messageWhat is declared
no placeholdersVerbatraNoArguments, 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 arbnumber
an ICU date or time argument ({d, date, short}) in next-intl-json or arbDate | number
an ICU select argument ({gender, select, ...}) in next-intl-json or arbstring
an i18next number formatter ({{count, number}}) in i18next-json, ngx-translate-json or yamlnumber
an i18next datetime formatter ({{d, datetime}}) in the same three formatsDate | number
an i18next unescaped interpolation ({{- name}})the name behind the prefix, name
a MessageFormat number, plural or choice argument ({0,number}) in propertiesnumber
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 forVerbatraUnknownArguments, and the key is named in the output
a message that mixes named, numbered and anonymous argumentsVerbatraUnknownArguments, 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_one and cart.item_other 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_ordinal_one 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_OUTPUT_CONFLICT when it:

  • names no file, including one that ends in a path separator such as generated/ (an empty --out fails earlier, with INVALID_OUT),
  • is absolute,
  • climbs out of the project root with ..,
  • is not a TypeScript file, meaning its name does not end in .ts, .mts or .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.cjs or verbatra.config.ts,
  • is the config file the run actually loaded, including one you named with --config,
  • is the glossary file your config points glossary at,
  • is an existing file that does not begin with the // Generated by verbatra from header 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_UNWRITABLE, 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=lf

JSON 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:

Output
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 types

See Terminal output for color, the spinner and the variables that control them.

Exit codes

CodeMeaning
0the 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)
2could not run: a usage error, a config or source problem, a refused output path (TYPES_OUTPUT_CONFLICT), or a declaration that could not be written (TYPES_UNWRITABLE)

See Exit codes for the contract every command follows.

Edit on GitHub

On this page