Skip to content
Skip to Content

Reference / CLI

verbatra init

Create a verbatra config and .env example for this project, detected from your locale files, interactively or fully unattended.

Scaffold a project: init writes a verbatra.config.ts, a .env.example naming your provider's API key variable (never a key value), and the .gitignore entries for .env, .env.local, .verbatra-local/ and verbatra.cache.json. It reads the locale files already in the project first, so usually the provider is all you have to tell it.

Synopsis

verbatra init [flags]

Examples

# prompt for anything not passed or detected
verbatra init --provider gemini

# fully unattended, one JSON document on stdout
verbatra init --provider gemini --yes --json

# choose the format and pattern yourself
verbatra init --provider deepl --format yaml --path "config/locales/{locale}.yml" --yes

# a local server that needs no key
verbatra init --provider openai-compatible --base-url http://localhost:11434/v1 --model llama3.1 --yes

# a self-hosted LibreTranslate server, no language model
verbatra init --provider libretranslate --base-url http://localhost:5000 --yes

# human-only: no provider, no API key
verbatra init --provider none --yes

# also set the project up for coding agents
verbatra init --provider gemini --yes --agent

# wire Cursor and VS Code, and only show what would change
verbatra init --agent --client cursor,vscode --dry-run

Flags

FlagArgumentDefaultEffect
--provider<id>none (required unless asked)provider to scaffold: anthropic, openai, gemini, deepl, google-translate, opencode, openai-compatible, libretranslate, or none
--format<id>detected, else i18next-jsonthe locale file format, one of the ids on Formats
--source<locale>detected, else enthe source locale your strings are written in
--targets<locales>detected, else decomma-separated target locales to translate into
--path<pattern>detected, else the format's usual layout (Detection)locale file pattern; must contain the {locale} token
--model<name>the scaffold defaultthe model; required for openai-compatible, optional for anthropic, openai, gemini and opencode
--base-url<url>nonethe server URL; openai-compatible and libretranslate only, and required there
--api-key-env-var<name>OPENAI_COMPATIBLE_API_KEYthe variable openai-compatible reads its key from
--yesnoneoffnever prompt; take the default for anything neither passed nor detected
--forcenoneoffoverwrite an existing verbatra.config.ts whose content differs
--agentnoneoffalso write the coding-agent section to AGENTS.md or CLAUDE.md and the verbatra MCP server to each detected client's project config (Set up coding agents)
--client<ids>detectedcomma-separated clients --agent wires instead of the detected ones: claude, cursor, vscode, codex, gemini, or all; needs --agent
--dry-runnoneoffreport every file init would write or change, and write nothing (Dry run)

Also takes the global flags --cwd and --json, but not --config. Here --cwd is the directory to scan and write to, and it must already exist. --json never prompts.

Behavior

Prompts

A flag always wins. Anything you leave out is detected from the project, then prompted for when stdin is a terminal and neither --yes nor --json is given, and otherwise taken from its default only under --yes. Pressing Enter accepts the suggested value. A flag with an empty value, such as --targets '', is a usage error (INVALID_OPTION), never a request for the default. If stdin ends at a prompt (Ctrl-D), init exits 2 with MISSING_OPTIONS.

Each answer is checked as you give it. An invalid one is explained on stderr and asked again, up to three times, keeping your earlier answers; a third invalid answer stops init with that answer's error code (such as INVALID_LOCALE), names the flag to pass instead, and writes nothing.

Detection

init scans the directory for files a format adapter claims, skipping hidden directories, node_modules and build output, and finds the part of each path that spells a locale, such as de.json, messages_pt_BR.properties, de.lproj/ or Android's values-de/. The file pattern covering the most locales wins, and it fills in:

  • format: the one format whose adapter reads every matched file. Every JSON adapter reads .json, so for JSON files a single matching dependency in package.json (i18next, vue-i18n, next-intl or @ngx-translate/core) decides.
  • files.pattern and files.localeStyle: posix when the files spell pt_BR, android for values-* directories, literal otherwise.
  • sourceLocale: the source language of an .xcstrings catalogue, else en or the single en-* locale found, else the only locale found.
  • targetLocales: every other locale found.

A file beside the locale files that holds strings under no locale name, such as messages.properties, Strings.resx or a gettext app.pot, is reported as unqualifiedSourceFile. verbatra cannot read the source from such a file, so init does not guess the source locale: pass --source, even with --yes, and the next steps name the file to rename or copy the base file to.

With no locale file at all, the dependencies alone decide the format, and the pattern follows the format's usual layout, the one listed per format on Pick your stack.

An apple-xcstrings catalogue is one file shared by every locale, and verbatra never creates it: create it in Xcode first. --format, --path and --source narrow the detection instead of being overridden by it, so --format yaml only looks at YAML files.

init never guesses between equals. Several formats that read the files (plain JSON with no i18n dependency) exit 2 with FORMAT_AMBIGUOUS, and several tied file patterns (one file per namespace) exit 2 with LAYOUT_AMBIGUOUS. Both list the candidates; pass --format or --path to choose. At a terminal it asks instead.

Unattended runs

Without a terminal, or with --json, init never prompts. A value that is neither passed, detected, nor defaulted by --yes is an error: init exits 2 with MISSING_OPTIONS and names every missing flag. An ambiguity or an invalid flag also lists the flags still missing, so one run tells an agent everything it has to pass. --provider has no default, and neither do --base-url and --model for openai-compatible or --base-url for libretranslate. This is the command an agent runs:

npx @verbatra/cli init --provider gemini --yes --json

JSON output

Schema: init-envelope.json validates the whole envelope; Schemas lists the others.

With --json, stdout carries exactly one envelope. Its result:

interface InitResult {
  configPath: string;                  // absolute path of verbatra.config.ts, or of the config --agent kept
  files: { path: string; action: "created" | "overwritten" | "updated" | "unchanged" }[];
  dryRun: boolean;                     // true under --dry-run: files holds the planned actions
  config: {                            // the config that was validated and written
    sourceLocale: string;
    targetLocales: string[];
    format: string;
    files: { pattern: string; localeStyle?: "posix" | "android" };
    provider: { id: string; options?: Record<string, unknown> };
    sensitiveData?: { mode: "warn" };
  } | null;
  sources: {                           // where each value came from
    provider: "flag" | "prompt";
    format: "flag" | "prompt" | "files" | "dependencies" | "default";
    pattern: "flag" | "prompt" | "detected" | "default";
    sourceLocale: "flag" | "prompt" | "detected" | "default";
    targetLocales: "flag" | "prompt" | "detected" | "default";
  } | null;
  apiKeyEnvVar: string | null;         // the variable to set; null for provider none
  detection: {
    format: { id: string; from: "input" | "files" | "dependencies" } | null;
    layout: {
      pattern: string;
      localeStyle: "literal" | "posix" | "android";
      locales: string[];
      sourceLocale: string | null;
      unqualifiedSourceFile: string | null;
      files: string[];
    } | null;
    ambiguities: { subject: "format" | "layout"; candidates: string[] }[];
    confidence: "high" | "medium" | "low" | "none";
    reasons: string[];                 // one sentence per decision or doubt
  } | null;
  agent: {                             // null without --agent
    instructionsFile: "AGENTS.md" | "CLAUDE.md";
    mcpServer: "added" | "present" | "differs" | "skipped" | null; // Claude Code; null when not wired
    configKept: boolean;               // true when --agent kept an existing config
    clients: {
      id: "claude" | "cursor" | "vscode" | "codex" | "gemini";
      file: string;                    // the client's config file, such as .mcp.json
      server: "added" | "present" | "differs" | "skipped";
      reason: "plugin" | "symlink" | null; // why the client was skipped
      selectedBy: "flag" | "markers" | "default";
      markers: string[];               // the client's marker files found in the project
    }[];
  } | null;
  nextSteps: { description: string; command: string | null }[];
}

confidence is high when the files alone decided everything, medium when a convention or a dependency filled a gap (such as taking en as the source) or a gap stayed open, low when no locale file backed the format, and none when nothing was detected. A default in sources is the value to double-check. nextSteps ends with the exact commands to run next.

An error envelope for FORMAT_AMBIGUOUS, LAYOUT_AMBIGUOUS, INVALID_PROVIDER or INVALID_FORMAT also carries candidates, the values to choose from. Every usage error that leaves flags unanswered carries missing, naming them, such as "--source <locale>".

Providers

  • --provider none scaffolds a human-only config with no options and writes no .env.example, since no API key is ever read.
  • --provider openai-compatible scaffolds a local or self-hosted server from --base-url and --model, with maxOutputTokens: 4096. Its key is optional: a server that needs none leaves OPENAI_COMPATIBLE_API_KEY unset, and --api-key-env-var names a different variable, in upper case such as LLM_API_KEY. A rejected value is never echoed back, in case it was the key itself.
  • --provider libretranslate scaffolds a self-hosted LibreTranslate server from --base-url alone, such as http://localhost:5000, and refuses --model and --api-key-env-var. .env.example names LIBRETRANSLATE_API_KEY for a server started with --api-keys.

init writes a variable's name into the config and .env.example, never a value, and refuses a base URL with credentials, a query string or a fragment (https://user:key@host, https://host/v1?api-key=...), where a key could end up in the committed config.

Existing files

Running init again with the same answers changes nothing and succeeds. A verbatra.config.ts with different content is refused with CONFIG_EXISTS unless you pass --force, and that refusal comes before any prompt or other error. Another verbatra config in the directory (a .verbatrarc.json, or a verbatra property in package.json) is refused even under --force, because it would be read first. Nothing is written when init refuses. A parent directory's config covering this one draws a stderr warning (dry run too): the new verbatra.config.ts replaces it here.

A directory init cannot write to is refused with INIT_UNWRITABLE, naming the file and any file already written. A verbatra.config.ts, .env.example or .gitignore that is a symbolic link leading outside the project, or nowhere, is refused with INIT_UNWRITABLE before anything is written. An existing .env.example is kept; when it does not name the key variable yet, that one line is appended, and under --force a header line init wrote itself is rewritten for the new provider. .gitignore gets only the entries it is missing.

Dry run

--dry-run runs every check and refusal of a real run except a failed write, such as a read-only directory, prints would create, would update, would overwrite or unchanged for each file, and writes nothing. The JSON result lists the planned actions and sets dryRun to true.

Set up coding agents

--agent also prepares the project for coding agents. It writes no key value, never writes through a link out of the project, and adds:

  • The instruction file. A verbatra section between <!-- verbatra:start --> and <!-- verbatra:end --> markers, word for word the snippet from Tell your agent about verbatra. It goes to AGENTS.md, or to CLAUDE.md when that is the only instruction file; when both exist, AGENTS.md wins. In a file that already holds the section, the text between the markers is replaced with the current one and reported updated, or unchanged when it already matches.
  • Each client's MCP config. The verbatra server entry from Connect an MCP client, npx -y @verbatra/mcp without --allow-spend or VERBATRA_MCP_ALLOW_SPEND, so the spend tools stay hidden. @verbatra/cli exports these entries as AGENT_CLIENT_CONFIGS.
Client--clientFileDetected from
Claude Codeclaude.mcp.jsonCLAUDE.md, .claude/, .mcp.json
Cursorcursor.cursor/mcp.json.cursor/, .cursorrules
VS Codevscode.vscode/mcp.json, under servers.vscode/mcp.json
Codexcodex.codex/config.toml, as [mcp_servers.verbatra].codex/
Gemini CLIgemini.gemini/settings.json.gemini/, GEMINI.md

init wires every client it detects, and only Claude Code when it finds no marker. AGENTS.md picks the instruction file, not a client. A .vscode folder without mcp.json is not wired: a next step names the command that wires it. Gemini CLI reads GEMINI.md: unless its settings or GEMINI.md load the instruction file, a next step says how. A detected client whose config path runs through a symbolic link is skipped with reason symlink and a next step. --client replaces the detection; an unknown id, an empty list or --client without --agent exits 2 with INVALID_OPTION and the candidates.

Nothing outside the markers is touched, and other servers and keys keep their bytes, indentation and line endings when the file is laid out one entry per line; otherwise it is rewritten with the same content. The Codex table is appended. An existing verbatra server is never touched: the file is reported unchanged, and the client's server is present when the entry matches the scaffold, or differs when it does not, which also adds a nextSteps note.

When .claude/settings.json or .claude/settings.local.json enables the verbatra Claude Code plugin, init skips .mcp.json: Claude Code is reported skipped with reason plugin and a next step, and an existing entry is never deleted. A settings file behind a symbolic link is ignored.

With an existing config, init --agent keeps it, writes only the agent files, prints kept verbatra.config.ts, never prompts and detects nothing. The JSON result lists the config as unchanged, sets agent.configKept to true, and carries null for config, sources, apiKeyEnvVar and detection. A config flag or --force puts the config back into the run, as without --agent.

init refuses before writing anything, with AGENT_FILE_INVALID, when a client file is not plain JSON (it never rewrites comments or trailing commas), not an object, repeats a key, or has a servers value that is not an object; when .codex/config.toml sets mcp_servers or the verbatra server inline, with dotted keys or as an array of tables, repeats that table or a key in it, or has an unterminated string, array or header; when a client that --client names has a symbolic link on its config path, or an instruction file links outside the project; or when the instruction file has an unpaired, misordered or repeated marker. The message never quotes the file.

Exit codes

CodeMeaning
0files written, or already up to date
2a usage error: MISSING_OPTIONS, INVALID_PROVIDER, INVALID_FORMAT, INVALID_OPTION, INVALID_LOCALE, INVALID_LOCALES, FORMAT_AMBIGUOUS, LAYOUT_AMBIGUOUS, CONFIG_EXISTS, INIT_UNWRITABLE, AGENT_FILE_INVALID, or CONFIG_INVALID when the answers would not make a valid config

See the exit-code contract for how every command uses these codes.

Edit on GitHub

On this page