Skip to content

Latest commit

 

History

History
3678 lines (2537 loc) · 158 KB

File metadata and controls

3678 lines (2537 loc) · 158 KB

DeepL CLI - API Reference

Version: 2.0.0 Last Updated: August 9, 2026

Complete reference for all DeepL CLI commands, options, and configuration.


Table of Contents


Global Options

Options that work with all commands:

--version, -V       Show version number
--help              Show help message
--quiet, -q         Suppress all non-essential output (errors and results only)
--verbose, -v       Show extra information (source language, timing, cache status)
--config, -c FILE   Use alternate configuration file
--no-input          Disable all interactive prompts (abort instead of prompting)
--timeout MS        HTTP request timeout in milliseconds (default: 30000)
--max-retries N     Maximum automatic retries for retryable requests (default: 3)

Automatic retries apply to idempotent requests, and to rate-limited (429) responses for every method; a request that submits work is otherwise never replayed. Retries share a wall-clock budget of twice --timeout, so raising --max-retries alone does not extend how long the CLI waits on an unresponsive endpoint.

--timeout is an enforced wall-clock deadline per attempt, not merely a socket inactivity timeout: an endpoint that keeps a response body trickling is aborted at the deadline just like one that never answers at all. Responses are additionally capped at 32 MiB, raised to 128 MiB for the document download, which must accommodate a whole translated file. Exceeding either bound is a NetworkError (exit code 5); a size-cap rejection is deterministic and is never retried.

Examples:

# Show version
deepl --version

# Get help
deepl --help
deepl translate --help

# Quiet mode - suppress informational messages, keep errors and results
deepl --quiet translate "Hello" --to es
# Output: Hola (no "Translation (ES):" label)

deepl -q cache stats
# Shows cache statistics without decorative output

# Quiet mode with batch operations (no spinners or progress indicators)
deepl --quiet translate docs/ --to es --output docs-es/
# Shows final statistics only, no progress updates

# Use custom config file
deepl --config ~/.deepl-work.json translate "Hello" --to es

# Give a large document more time, and disable automatic retries
deepl --timeout 120000 --max-retries 0 translate report.pdf --to de

# Use custom config directory (via environment variable)
export DEEPL_CONFIG_DIR=/path/to/config
deepl translate "Hello" --to es

# Disable cache
deepl cache disable
deepl translate "Hello" --to es
deepl cache enable

Quiet Mode Behavior:

  • Always shown: Errors and their Suggestion: remediation lines, essential output (translation results, JSON data, command output)
  • Suppressed: Warnings, informational messages, success confirmations, progress spinners, status updates
  • 🎯 Use cases: CI/CD pipelines, scripting, parsing output, quiet automation

Non-Interactive Mode (--no-input):

# Abort instead of prompting for confirmation
deepl --no-input cache clear
# Output: Aborted.

# Combine with --yes to skip prompts and proceed
deepl --no-input cache clear --yes

# Interactive-only commands error with exit code 6
deepl --no-input init
deepl --no-input write "text" --interactive
  • Commands that require confirmation (e.g., cache clear) abort with "Aborted." instead of prompting
  • --yes takes precedence over --no-input — the combination proceeds without prompting
  • Interactive-only commands (init, write --interactive) exit with code 6 (InvalidInput)
  • Use cases: CI/CD pipelines, scripts, automation where a forgotten prompt would hang

Example comparison:

# Normal mode
$ deepl cache enable
✓ Cache enabled

# Quiet mode
$ deepl --quiet cache enable
(no output - command succeeded silently)

# Normal mode with errors
$ deepl translate "Hello" --to invalid
Error: Invalid target language: invalid

# Quiet mode with errors (errors always shown)
$ deepl --quiet translate "Hello" --to invalid
Error: Invalid target language: invalid

Custom Configuration Files:

The --config flag allows you to use alternate configuration files for different projects, environments, or accounts:

# Use work configuration
deepl --config ~/.deepl-work.json translate "Hello" --to es

# Use project-specific configuration
deepl --config ./project/.deepl.json translate docs/ --to fr --output docs-fr/

# Use test environment configuration
deepl --config /path/to/test-config.json usage

Use cases:

  • Multiple API keys: Switch between free and paid accounts
  • Project isolation: Different settings per project (glossaries, formality, etc.)
  • Environment separation: Separate configs for dev/staging/production
  • Testing: Use test configurations without affecting default settings

Precedence: --config replaces the config file only, overriding DEEPL_CONFIG_DIR for configuration. The cache location is unaffected — it still follows DEEPL_CONFIG_DIR > legacy ~/.deepl-cli/ > XDG resolution.

Command Suggestions:

Mistype a command? The CLI suggests the closest match:

$ deepl transalte "Hello" --to es
Unknown command: transalte
Did you mean: deepl translate?

Run deepl --help to see available commands.

Commands

Commands are organized into six groups, matching the deepl --help output:

Group Commands Description
Core Commands translate, write, correct, voice Translation, writing enhancement, spelling and grammar correction, and speech translation
Resources glossary, tm Manage translation glossaries and translation memory
Workflow watch, sync, hooks File watching, project sync, and git hook automation
Configuration init, auth, config, cache, style-rules Setup wizard, authentication, settings, caching, and style rules
Information usage, languages, detect, completion API usage, supported languages, language detection, and shell completions
Administration admin Organization key management and usage analytics

translate

Translate text, files, or directories.

Synopsis

deepl translate [OPTIONS] [TEXT|FILE|DIRECTORY]
deepl t [OPTIONS] [TEXT|FILE|DIRECTORY]        # alias

Description

Translate text directly, from stdin, from files, or entire directories. Supports multiple target languages, code preservation, and context-aware translation.

Input Sources:

  • Direct text argument: deepl translate "Hello" --to es
  • From stdin: echo "Hello" | deepl translate --to es
  • Single file: deepl translate README.md --to es --output README.es.md
  • Directory: deepl translate docs/ --to es --output docs-es/

Note: When reading from stdin or translating files, omit the text argument.

Options

Target Language:

  • --to, -t LANGS - Target language(s), comma-separated (e.g., es, es,fr,de). Required unless defaults.targetLangs is configured.

Source Options:

  • --from, -f LANG - Source language (auto-detect if omitted)
  • --context TEXT - Additional context for better translation

Output Options:

  • --output, -o PATH - Output file or directory (required for file/directory translation, optional for text). A directory receives <name>.<lang>.<ext>deepl translate README.md --to es --output docs/ writes docs/README.es.md, and a trailing slash creates the directory if it does not exist. Use - for stdout (text-based files only)
  • --output-format FORMAT - Convert PDF to DOCX during translation. Valid choices: docx (only supported conversion)
  • --enable-minification - Enable document minification for PPTX/DOCX files (reduces file size)
  • --format FORMAT - Output format: text, json, table (default: text)

Translation Options:

  • --formality LEVEL - Formality: default, more, less, prefer_more, prefer_less, formal, informal
  • --model-type TYPE - Model type: quality_optimized, prefer_quality_optimized, latency_optimized (no CLI default; the API server selects the model when omitted)
  • --preserve-code - Preserve code blocks (markdown, etc.)
    • Known limitation: code blocks and variable placeholders ({name}, %s, etc.; always preserved) are protected internally with __CODE_n__/__VAR_n__ tokens, so source text that literally contains such tokens may be rewritten during restoration.
  • --preserve-formatting - Preserve line breaks and whitespace formatting
  • --split-sentences LEVEL - Sentence splitting: on (default), off, nonewlines
  • --tag-handling MODE - XML tag handling: xml, html
  • --outline-detection BOOL - Control automatic XML structure detection: true (default), false (requires --tag-handling xml)
  • --splitting-tags TAGS - Comma-separated XML tags that split sentences (requires --tag-handling xml)
  • --non-splitting-tags TAGS - Comma-separated XML tags that should not be used to split sentences (requires --tag-handling xml)
  • --ignore-tags TAGS - Comma-separated XML tags with content to ignore (requires --tag-handling xml)
  • --tag-handling-version VERSION - Tag handling version: v1, v2. v2 improves XML/HTML structure handling (requires --tag-handling). Defaults to v2, sent explicitly on every --tag-handling request rather than left to the API's own default, which is documented as moving from v1 to v2 at some point — pinning keeps output from shifting on DeepL's timetable. Pass --tag-handling-version v1 for the older behaviour, which DeepL documents as heading for deprecation
  • --glossary NAME-OR-ID - Use glossary by name or ID for consistent terminology. Repeatable, up to 5 per request; entries are merged, so terms unique to each glossary all apply. When several glossaries define the same source term, which mapping wins is the API's choice and does not follow flag order, so position is not a way to override a term. Passing a 6th exits 6 (ValidationError). A source language is required, because the API rejects a glossary without one: supply --from, or set defaults.sourceLang and it is used automatically. With neither, the command exits 6 before any request.
  • --translation-memory NAME-OR-UUID - Use translation memory by name or UUID (forces quality_optimized model). Requires --from because TMs are pinned to a specific source→target language pair. Invalid use exits 6 (ValidationError); unresolvable/misconfigured TM exits 7 (ConfigError).
  • --tm-threshold N - Minimum match score 0–100 (default 75, requires --translation-memory). Invalid use exits 6 (ValidationError); unresolvable/misconfigured TM exits 7 (ConfigError).
  • --custom-instruction INSTRUCTION - Custom instruction for translation (repeatable, max 10, max 300 chars each). Forces quality_optimized model. Cannot be used with latency_optimized.
  • --style-id UUID - Style rule ID for translation (Pro API only). Forces quality_optimized model. Cannot be used with latency_optimized. Use deepl style-rules list to see available IDs.
  • --no-cache - Bypass cache for this translation (useful for testing/forcing fresh translation)
  • --dry-run - Show what would be translated without performing the operation

API Options:

  • --api-url URL - Custom API endpoint URL (for testing or private instances). Any host other than api.deepl.com and api-free.deepl.com prints a warning to stderr naming the origin the key is about to be sent to — see "Non-standard endpoint notice" below
  • --show-billed-characters - Request and display actual billed character count for cost transparency

Non-standard endpoint notice. The API key is attached to every request, so whatever host the CLI is pointed at receives it along with the text being translated. Whenever the resolved endpoint is not one of the two standard DeepL hosts, the CLI prints an unconditional warning to stderr naming the origin (scheme, host, port — never a path or query, which the redirect may control) and stating where the redirect came from: set by --api-url, or set by api.baseUrl in <config file> with the actual file path, since several config locations are searched and -c can override them. Regional endpoints such as api-jp.deepl.com are included, as is loopback — a co-tenant process listening on 127.0.0.1 receives the key just as a remote host does, the same reasoning the TMS destination-trust gate applies. The notice is not gated behind --verbose, because it is the user's only signal that a substituted config file has redirected their key; it is suppressed by --quiet like every other warning, and it goes to stderr so deepl translate ... > out.txt still captures only translation output. It is printed once per run, not once per API client. Under --verbose, each request line also names the resolved origin ([verbose] HTTP POST https://api.deepl.com/v2/translate completed in 12ms (status 200)).

Batch Options (for directories):

  • --no-recursive - Do not recurse into subdirectories (recursive is the default)
  • --pattern GLOB - File pattern (e.g., *.md, **/*.txt)
  • --concurrency N - Number of parallel translations (default: 5)

Examples

Basic text translation:

# Single language
deepl translate "Hello, world!" --to es

# Multiple languages
deepl translate "Hello, world!" --to es,fr,de

# With source language
deepl translate "Bonjour" --from fr --to en

From stdin:

# Pipe text
echo "Hello" | deepl translate --to es

# From file via stdin
cat README.md | deepl translate --to fr

File translation:

# Single file
deepl translate README.md --to es --output README.es.md

# Multiple languages
deepl translate README.md --to es,fr,de --output translations/

# With code preservation
deepl translate tutorial.md --to es --output tutorial.es.md --preserve-code

Output to stdout:

# Pipe translated file content to stdout
deepl translate README.md --to es --output -

# Combine with shell tools
deepl translate README.md --to es --output - | wc -l

# Redirect to a file
deepl translate README.md --to es --output - > translated.md

Note: --output - only works with text-based files (.txt, .md, .html, .srt, .xlf). Binary documents (PDF, DOCX, etc.), structured files (JSON, YAML), and multi-target translations do not support stdout output.

Smart caching for text-based files:

Small text-based files are automatically routed to the cached text API for faster, more efficient translations:

# Text files under 100 KiB are automatically cached
deepl translate README.md --to es --output README.es.md
# First translation: Makes API call
# Subsequent identical translations: Instant (from cache)

# HTML files also benefit from caching
deepl translate index.html --to fr --output index.fr.html

# Subtitle files
deepl translate subtitles.srt --to ja --output subtitles.ja.srt

# XLIFF localization files
deepl translate strings.xlf --to de --output strings.de.xlf

Structured file formats (i18n):

The following structured formats are parsed to extract only string values, translated via the batch text API, then reassembled preserving keys, nesting, non-string values, indentation, and YAML comments:

  • .json - JSON files (i18n locale files, config files)
  • .yaml, .yml - YAML files (Rails i18n, config files)

An empty string value is copied to the output unchanged and is never sent to the API, so a placeholder key you have not written copy for yet costs nothing and stays in the file. Requests are capped at 50 strings each; if one of them fails, no output file is written and the command exits with that failure's own code (for example 3 for a rate limit), because a partly translated file is indistinguishable from a complete one.

Size ceiling: 10 MiB. A structured file above that is refused with a ValidationError (exit 6) naming its size, before it is read, and a directory run fails just that file. The limit is lower than the 30 MB document ceiling because these two routes cost very different amounts of memory: a document is streamed to the API once, while a structured file is parsed into an object graph — roughly 7-13x its size resident — and the multi-target path (--to de,fr,es) builds a fresh copy per language, up to 5 at a time. 10 MiB matches the ceiling sync.limits.max_file_bytes can never be configured above, so nothing deepl sync accepts is refused here. For anything larger, split the file or use deepl sync, which walks a locale directory file by file and translates only the keys that changed.

# Translate JSON locale file
deepl translate en.json --to es --output es.json

# Translate YAML locale file (comments preserved)
deepl translate en.yaml --to de --output de.yaml

Cached text-based formats:

The following formats use the cached text API when files are under 100 KiB:

  • .txt - Plain text files
  • .md - Markdown files
  • .html, .htm - HTML files
  • .srt - Subtitle files
  • .xlf, .xliff - XLIFF localization files

Large file automatic fallback:

When text-based files exceed 100 KiB, they automatically fall back to the document API:

# Large text file (>100 KiB) - uses document API
deepl translate large-document.txt --to es --output large-document.es.txt
# ⚠ File exceeds 100 KiB limit for cached translation (150.5 KiB), using document API instead
# Translated large-document.txt -> large-document.es.txt

Benefits of smart caching:

  • Performance: Only small text files (<100 KiB) benefit from instant cached translations
  • Efficiency: Reduces API calls and character usage for small text files
  • Cost savings: Only small text files avoid repeated API quota consumption
  • Automatic: No configuration needed - works out of the box
  • Transparent: Warning shown when falling back to document API

Important: Large text files (≥100 KiB) and all binary documents use the document API, which is NOT cached. Repeated translations of large files always make fresh API calls.

Document translation:

# Translate PDF document
deepl translate document.pdf --to es --output document.es.pdf

# Translate PowerPoint with formality
deepl translate presentation.pptx --to de --formality more --output presentation.de.pptx

# Translate Excel spreadsheet
deepl translate report.xlsx --to fr --output report.fr.xlsx

# Translate HTML file
deepl translate website.html --to ja --output website.ja.html

# Convert format during translation (PDF to DOCX - only supported conversion)
deepl translate document.pdf --to es --output document.es.docx --output-format docx

# Enable document minification for smaller file size (PPTX/DOCX only)
deepl translate presentation.pptx --to de --output presentation.de.pptx --enable-minification
deepl translate report.docx --to fr --output report.fr.docx --enable-minification

# Apply a glossary (--from is required for document glossaries)
deepl translate report.docx --from en --to de --output report.de.docx --glossary tech-terms

# Repeat --glossary for up to 5; entries merge, and a term defined in
# several is resolved by the API, not by flag order
deepl translate report.docx --from en --to de --output report.de.docx \
  --glossary base-terms --glossary project-overrides

Supported Document Formats:

  • .pdf - PDF documents (up to 30MB) - Document API only
  • .docx, .doc - Microsoft Word - Document API only
  • .pptx - Microsoft PowerPoint - Document API only
  • .xlsx - Microsoft Excel - Document API only
  • .jpg, .jpeg - JPEG images - Document API only
  • .png - PNG images - Document API only
  • .html, .htm - HTML files - Smart routing (cached text API <100 KiB, document API ≥100 KiB)
  • .txt - Plain text files (up to 30MB) - Smart routing (cached text API <100 KiB, document API ≥100 KiB)
  • .srt - Subtitle files - Smart routing (cached text API <100 KiB, document API ≥100 KiB)
  • .xlf, .xliff - XLIFF localization files - Smart routing (cached text API <100 KiB, document API ≥100 KiB)
  • .md - Markdown files - Cached text API (all sizes)
  • .json - JSON files - Structured file API (string extraction + batch translation)
  • .yaml, .yml - YAML files - Structured file API (string extraction + batch translation, comments preserved)

Document Translation Notes:

  • Structured files: .json, .yaml, .yml are parsed to extract string values, translated via batch text API, and reassembled preserving structure
  • Smart routing: Text-based files (.txt, .md, .html, .srt, .xlf, .xliff) under 100 KiB automatically use the cached text API for better performance
  • Binary formats (PDF, DOCX, PPTX, XLSX) and image formats (JPEG, PNG) always use the document API regardless of size
  • Documents are translated on DeepL servers using async processing
  • Progress updates show status (queued → translating → done)
  • Billed characters are displayed after completion
  • Formatting, structure, and layout are automatically preserved
  • Large documents may take several seconds to translate
  • Maximum file sizes: 30MB (document API, all formats), 100 KiB (cached text API)
  • Document minification (--enable-minification): Reduces file size for PPTX and DOCX files only. Useful for large presentations and documents.
  • Glossaries: --glossary applies to documents and is repeatable up to 5, resolved exactly as for text translation — entries are merged, and when several glossaries define the same source term which mapping wins is the API's choice and does not follow flag order. A source language is required — the API rejects a document glossary without one ("source_lang has to be specified in order to use a glossary") — so pass --from, or set defaults.sourceLang and it is used automatically. Glossary matching is context-dependent exactly as it is for text: a term may be applied in one sentence and left alone in another, and a bare newline-separated word list often gets few terms applied. --translation-memory remains unsupported for documents.

Directory translation:

# Translate all supported files
deepl translate docs/ --to es --output docs-es/

# With glob pattern
deepl translate docs/ --to es --output docs-es/ --pattern "*.md"

# Recursive (default) with custom concurrency
deepl translate src/ --to es,fr --output translations/ --concurrency 10

Context-aware translation:

# Add context for better disambiguation
deepl translate "Bank" --to es --context "Financial institution"
# → "Banco" (not "Orilla" for riverbank)

deepl translate app.json --to es --context "E-commerce checkout flow"

Formality levels:

# Formal
deepl translate "How are you?" --to de --formality more
# → "Wie geht es Ihnen?" (formal)

# Informal
deepl translate "How are you?" --to de --formality less
# → "Wie geht es dir?" (informal)

Sentence splitting:

# Default behavior (sentences split on punctuation and newlines)
deepl translate "Hello. How are you?" --to es
# → "Hola. ¿Cómo estás?"

# Disable sentence splitting (treat as one unit)
deepl translate "Hello. How are you?" --to es --split-sentences off
# → May produce different translation

# Split only on punctuation, not newlines
deepl translate "Line 1\nLine 2" --to es --split-sentences nonewlines
# → Preserves line breaks while splitting sentences

Tag handling (XML/HTML):

# Basic XML tag preservation
deepl translate "<p>Hello world</p>" --to es --tag-handling xml
# → "<p>Hola mundo</p>"

# Translate HTML content
deepl translate "<div><span>Welcome</span></div>" --to de --tag-handling html
# → "<div><span>Willkommen</span></div>"

# Useful for localizing markup files
deepl translate content.html --to fr --tag-handling html --output content.fr.html

# Advanced XML tag handling: Disable automatic structure detection
deepl translate "<doc><p>Text</p></doc>" --to es --tag-handling xml --outline-detection false
# Forces manual tag handling instead of automatic detection

# Specify tags that split sentences (useful for custom XML formats)
deepl translate "<article><br/>Content<hr/>More</article>" --to es --tag-handling xml --splitting-tags "br,hr"
# Treats <br/> and <hr/> as sentence boundaries

# Specify tags for non-translatable content (like code blocks)
deepl translate "<doc><code>let x = 1;</code><p>Text</p></doc>" --to es --tag-handling xml --non-splitting-tags "code,pre"
# Content in <code> and <pre> tags won't be split into sentences

# Ignore specific tags and their content (e.g., scripts, styles)
deepl translate file.html --to es --tag-handling xml --ignore-tags "script,style,noscript" --output file.es.html
# Content in <script>, <style>, and <noscript> tags is not translated

# Combine multiple XML tag handling options
deepl translate complex.xml --to de --tag-handling xml \
  --outline-detection false \
  --splitting-tags "br,hr,div" \
  --non-splitting-tags "code,pre,kbd" \
  --ignore-tags "script,style" \
  --output complex.de.xml
# Fine-tuned control for complex XML/HTML documents

Glossary usage:

# Use glossary for consistent terminology
deepl translate "API documentation" --from en --to es --glossary tech-terms

# Use glossary by ID
deepl translate README.md --from en --to fr --glossary abc-123-def-456 --output README.fr.md

Multiple glossaries on one request:

Repeat --glossary to apply up to 5 glossaries to a single request. Their entries are merged, so terms unique to each glossary all apply. When more than one glossary defines the same source term, which mapping wins is the API's choice and does not follow flag order — position is not a way to override a term, so avoid relying on one glossary to shadow another. The order is still sent as given and never sorted, because it is part of the cache key: reordering the flags is a different request with its own cache entry. Names and UUIDs can be mixed; each value is resolved independently. A 6th --glossary exits 6 (ValidationError). A name that cannot be resolved — unknown, ambiguous, or covering a different language pair than the one requested — exits 7 (ConfigError) without sending a translation request.

# Shared base terminology combined with project-specific terms; terms unique
# to each apply, and a term defined in both is resolved by the API
deepl translate "Hello world" --from en --to de --glossary base-terms --glossary project-overrides

# Names and UUIDs can be mixed
deepl translate README.md --from en --to fr --output README.fr.md \
  --glossary abc-123-def-456 --glossary house-style

A single --glossary is still sent as the API's glossary_id, so existing commands and their cached results are unaffected.

Translation memory usage:

Translation memories (TMs) are pinned to a source→target language pair, so --from is required. Passing --translation-memory forces quality_optimized model type; combining it with --model-type latency_optimized (or prefer_quality_optimized) exits 6 (ValidationError). TM files are authored and uploaded via the DeepL web UI; this CLI resolves the name-or-UUID against GET /v3/translation_memories and caches the resolution per run.

# Use translation memory by name (requires --from for pair resolution)
deepl translate "Welcome to our product." --from en --to de --translation-memory my-tm

# Use translation memory by UUID with a custom threshold
deepl translate "Welcome to our product." --from en --to de \
  --translation-memory 3f2504e0-4f89-41d3-9a0c-0305e82c3301 --tm-threshold 80

# Combine glossary and translation memory on a single call
deepl translate "Welcome to our product." --from en --to de \
  --glossary tech-terms --translation-memory my-tm --tm-threshold 85

Multi-target file translation with glossary / TM:

Both --glossary and --translation-memory apply to multi-target file translation (e.g. --to en,fr,es) and in that mode --from is required. Resolving either by name checks that the resource covers every requested language pair before any translation request goes out, and exits 7 (ConfigError) naming what it does cover if not:

deepl translate "hello" --from en --to de --glossary "EN-ES Test Glossary"
# Error: Glossary "EN-ES Test Glossary" does not support the requested language pair
# Suggestion: Glossary covers en→es; requested en→de.

A multilingual glossary satisfies this when it holds a dictionary for each requested pair; matching is per dictionary, so a glossary holding en→es and de→fr does not count as covering en→fr. Each translation memory in DeepL is scoped to one source→target pair, so a TM name with differing multi-targets cannot satisfy it at all — pass the TM UUID for multi-target TM use.

Passing a UUID skips the check and lets the API decide, which is the escape hatch if the check is ever wrong: the API answers with No dictionary found for language pair EN-DE in glossary <id>.

# Glossary across multiple targets (name resolution works for all targets)
deepl translate README.md --from en --to fr,es,it --glossary tech-terms --output ./out

# Translation memory across multiple targets: pass a UUID to avoid the
# single-pair name-resolution constraint
deepl translate README.md --from en --to fr,es,it --output ./out \
  --translation-memory 3f2504e0-4f89-41d3-9a0c-0305e82c3301

Cache control:

# Bypass cache for fresh translation
deepl translate "Hello" --to es --no-cache

# Useful for testing or when you need the latest translation
deepl translate document.md --to es --output document.es.md --no-cache

Cost transparency:

# Show actual billed character count (Pro API only)
deepl translate "Hello, world!" --to es --show-billed-characters
# Hola, mundo!
#
# Billed characters: 13

# Use with multiple languages
deepl translate "Hello" --to es,fr,de --show-billed-characters
# [es] Hola
# [fr] Bonjour
# [de] Hallo
#
# Billed characters: 15

# Useful for budget tracking and cost analysis
deepl translate document.md --to es --output document.es.md --show-billed-characters

Note: The --show-billed-characters feature is only available with Pro API accounts. Free API accounts will display "N/A" for character counts.

JSON output:

# Get machine-readable JSON output
deepl translate "Hello" --to es --format json
# {"text":"Hola","detectedSourceLang":"en","targetLang":"es","cached":false}

# JSON output may include modelTypeUsed when the API reports which model was used
deepl translate "Hello" --to es --format json --no-cache
# {"text":"Hola","detectedSourceLang":"en","targetLang":"es","modelTypeUsed":"quality_optimized"}

# Useful for scripting and automation
deepl translate "Test" --to es,fr,de --format json

Table output:

# Display translations in structured table format (multiple languages)
deepl translate "Hello, world!" --to es,fr,de --format table
# ┌──────────┬──────────────────────────────────────────────────────────────────────┐
# │ Language │ Translation                                                          │
# ├──────────┼──────────────────────────────────────────────────────────────────────┤
# │ es       │ ¡Hola mundo!                                                         │
# │ fr       │ Bonjour le monde!                                                    │
# │ de       │ Hallo Welt!                                                          │
# └──────────┴──────────────────────────────────────────────────────────────────────┘

# Add --show-billed-characters to display the Characters column
deepl translate "Cost tracking" --to es,fr,de --format table --show-billed-characters --no-cache
# ┌──────────┬────────────────────────────────────────────────────────────────┬────────────┐
# │ Language │ Translation                                                    │ Characters │
# ├──────────┼────────────────────────────────────────────────────────────────┼────────────┤
# │ es       │ Seguimiento de costes                                          │ 16         │
# │ fr       │ Suivi des coûts                                                │ 16         │
# │ de       │ Kostenverfolgung                                               │ 16         │
# └──────────┴────────────────────────────────────────────────────────────────┴────────────┘

# Long translations automatically wrap in the Translation column
deepl translate "This is a very long sentence that demonstrates word wrapping." --to es,fr --format table
# Wider Translation column (70 chars) when Characters column is not shown

# Useful for:
# - Comparing translations side-by-side across multiple languages
# - Monitoring billed characters per translation for cost transparency (with --show-billed-characters)
# - Human-readable output for reports and documentation
# - Quality assurance - spot-checking consistency across languages

Notes:

  • Table format is only available when translating to multiple target languages. For single language translations, use default plain text or JSON format.
  • The Characters column is only shown when using --show-billed-characters flag.
  • Without --show-billed-characters, the Translation column is wider (70 characters vs 60) for better readability.
  • Model type, when the API reports it, is carried in JSON output as modelTypeUsed. Billed characters are not: a single-target --format json run emits {text, targetLang, detectedSourceLang?, modelTypeUsed?, cached?} and no character count, whatever --show-billed-characters is set to. Multi-target JSON (--to es,fr,de --format json) does carry billedCharacters per translation. In plain text output both are appended below the translated text.

write

Improve text with DeepL Write API (grammar, style, tone enhancement).

Synopsis

deepl write [OPTIONS] TEXT
deepl w [OPTIONS] TEXT        # alias

Description

Enhance text quality with AI-powered grammar checking, style improvement, and tone adjustment. Supports 14 target languages.

File Detection: The command automatically detects if the text argument is a file path. If a file exists at that path, it operates on the file; otherwise, it treats the argument as text to improve.

Options

Language:

  • --lang, -l LANG - Target language: de, en, en-gb, en-us, es, fr, it, ja, ko, pt, pt-br, pt-pt, zh, zh-hans. Optional — omit to auto-detect the language and rephrase in the original language.
  • --to LANG - Long-only alias of --lang. Accepts the same language values. Provided for muscle-memory consistency with deepl translate --to; the short form -t is intentionally not bound here (it would collide with deepl translate -t, --to). Specifying both --to and --lang with different values exits with a ValidationError.

Style Options (mutually exclusive with tone):

  • --style STYLE - Writing style:
    • default - No style modification (API default)
    • simple - Simpler, more accessible language
    • business - Professional business language
    • academic - Formal academic language
    • casual - Conversational, informal language
    • prefer_simple, prefer_business, etc. - Soft preferences

Tone Options (mutually exclusive with style):

  • --tone TONE - Tone:
    • default - No tone modification (API default)
    • enthusiastic - More enthusiastic and positive
    • friendly - Warmer, more approachable
    • confident - More assertive and certain
    • diplomatic - More careful and tactful
    • prefer_enthusiastic, prefer_friendly, etc. - Soft preferences

Supported target-language / style-and-tone combinations:

Target language --style --tone
en, en-gb, en-us, de
es, fr, it, pt, pt-br, pt-pt
ja, ko, zh, zh-hans

When --style or --tone is set for a target language that does not support it, the server returns a 4xx; the CLI converts that response into a ValidationError (exit code 6) that names the unsupported combination and points back to this table.

This table is maintained by hand and reflects what the API accepts, which is not always what its metadata reports: GET /v3/languages?resource=write omits writing_style for en, yet --style with --lang en succeeds and returns what --lang en-us returns. Do not narrow this table to the metadata without re-checking behaviour.

Where the target-language list comes from:

The 14 languages are generated from GET /v3/languages?resource=write into src/data/language-entries.ts by npm run generate:languages, alongside the translation list, and npm run check:languages reports drift in either. The WriteLanguage type is derived from that same list, so a language added upstream widens it on regenerate rather than needing a second hand edit.

write and correct check --lang against this list locally and name every valid option, because the set is small enough to enumerate in an error. The list is a snapshot, though, so a code that is shaped like a language tag but absent from it is sent to the API with the list as a warning rather than rejected — otherwise a language DeepL adds is unreachable until the snapshot is regenerated. Input that is not shaped like a language tag still exits 6 locally.

Output Options:

  • --alternatives, -a - Show all improvement alternatives. With --format json, emits them as an array
  • --interactive, -i - Interactive mode: choose from multiple alternatives
  • --diff, -d - Show diff between original and improved text. With --format json, emits the diff payload with an uncoloured patch
  • --check - Check if text needs improvement without modifying (exits with 0 if no changes, 8 if improvements suggested). With --format json, emits the check result payload on stdout
  • --fix - Auto-fix files in place
  • --output, -o FILE - Write output to file
  • --in-place - Edit file in place
  • --backup, -b - Create backup before fixing (use with --fix)
  • --format FORMAT - Output format: text, json (default: text)

Advanced:

  • --no-cache - Bypass cache for this request (always call API)

Supported Languages

  • de - German
  • en - English (generic, defaults to American English)
  • en-gb - British English
  • en-us - American English
  • es - Spanish
  • fr - French
  • it - Italian
  • ja - Japanese
  • ko - Korean
  • pt - Portuguese (generic, defaults to Brazilian Portuguese)
  • pt-br - Brazilian Portuguese
  • pt-pt - European Portuguese
  • zh - Chinese (generic, defaults to Simplified Chinese)
  • zh-hans - Simplified Chinese

Examples

Basic improvement (auto-detect language):

deepl write "Me and him went to store."
# → "He and I went to the store."

With explicit language:

deepl write "Me and him went to store." --lang en-us
# → "He and I went to the store."

With writing style:

# Business style
deepl write "We want to tell you about our product." --lang en-us --style business
# → "We are pleased to inform you about our product."

# Casual style
deepl write "The analysis demonstrates significant findings." --lang en-us --style casual
# → "The analysis shows some pretty big findings."

With tone:

# Confident tone
deepl write "I think this might work." --lang en-us --tone confident
# → "This will work."

# Diplomatic tone
deepl write "Your approach is wrong." --lang en-us --tone diplomatic
# → "Perhaps we could consider an alternative approach."

Show alternatives:

deepl write "This is good." --lang en-us --alternatives

File operations:

# Improve file and save to new location
deepl write document.txt --lang en-us --output improved.txt

# Edit file in place
deepl write document.txt --lang en-us --in-place

# Auto-fix with backup
deepl write document.txt --lang en-us --fix --backup

Interactive mode:

# Choose from multiple alternatives interactively
deepl write "Text to improve." --lang en-us --interactive

Check mode:

# Check if file needs improvement (exit code 8 if changes needed)
deepl write document.md --lang en-us --check

--check reports a result rather than an error, so --format json gives it a success shape rather than the error envelope:

deepl write document.md --check --format json
# {"ok":true,"mode":"write","needsChanges":true,"changes":3,"file":"/abs/path/document.md"}

deepl write "This is fine." --check --format json
# {"ok":true,"mode":"write","needsChanges":false,"changes":0}
Field Meaning
ok Always true — the check ran. A failure emits the error envelope.
mode write or correct, so one parser serves both commands.
needsChanges The verdict, repeated by the exit code (0 clean, 8 needs changes).
changes Number of word-level changes the API would make.
file Absolute path of the checked file; absent when the input was text.

The payload goes to stdout and replaces the human report — the File: … line and the ⚠ Text needs improvement / ✓ Text looks good verdict stay on stderr in text mode only. The exit code is unchanged either way, so a CI job may branch on the code, the payload, or both.

Diff view:

# Show differences between original and improved
deepl write file.txt --lang en-us --diff

JSON output:

# Get machine-readable JSON output
deepl write "This are good." --lang en-us --format json
# {"original":"This are good.","improved":"This is good.","changes":1,"language":"en-us"}

Each output mode has its own JSON shape, because each answers a different question. --check and the two below carry ok: true, marking them as results rather than the ok: false error envelope; the plain improvement payload above predates the envelope and keeps its shape, so it has no ok field.

# --diff: the same three things the text report shows, with an uncoloured patch
deepl write "This are good." --diff --format json
# {"ok":true,"original":"This are good.","improved":"This is good.",
#  "diff":"Index: text\n===...\n-This are good.\n+This is good.\n"}

# --alternatives: every improvement the API offered, as an array
deepl write "This are good." --alternatives --format json
# {"ok":true,"original":"This are good.","alternatives":["This is good.","These are good."]}

--diff's diff field is the unified patch with no colour escapes, whatever the terminal — the text report colours the same patch for a human, and that colouring never reaches the payload. --diff ignores --output/--in-place, so its payload only ever goes to stdout.

--output <file> and --in-place receive whatever the improvement renders to, so under --format json they write the payload rather than the improved text — that is true of the plain payload and of --alternatives alike. To put improved text in a file, leave --format at its default.

Bypass cache:

# Force a fresh API call, skipping cached results
deepl write "Improve this text." --lang en-us --no-cache

correct

Correct spelling and grammar with the DeepL Write API, without rewording.

Synopsis

deepl correct [OPTIONS] TEXT
deepl c [OPTIONS] TEXT        # alias

Description

Fixes spelling and grammar only, avoiding the broader rewording that deepl write performs. Uses the Write API's /v2/write/correct endpoint. Supports the same 14 target languages as write.

File Detection: The command automatically detects if the text argument is a file path. If a file exists at that path, it operates on the file; otherwise, it treats the argument as text to correct.

Options

correct accepts the same options as write except --style and --tone (the correct endpoint does not restyle text):

Language:

  • --lang, -l LANG - Target language: de, en, en-gb, en-us, es, fr, it, ja, ko, pt, pt-br, pt-pt, zh, zh-hans. Optional — omit to auto-detect the language and correct in the original language.
  • --to LANG - Long-only alias of --lang, as on write.

Output Modes:

  • --alternatives, -a - Show all alternative corrections. With --format json, emits them as an array
  • --output, -o FILE - Write corrected text to file
  • --in-place - Edit file in place (use with file input)
  • --interactive, -i - Review the correction before accepting
  • --diff, -d - Show diff between original and corrected text. With --format json, emits the diff payload with an uncoloured patch

Fix Operations:

  • --check - Check if text needs correction (exit 0 if clean, exit 8 if corrections needed). With --format json, emits the check result payload on stdout
  • --fix - Automatically fix file in place
  • --backup, -b - Create backup file before fixing (use with --fix)

Advanced:

  • --no-cache - Bypass cache for this request
  • --format FORMAT - Output format: text, json (default: text)

Examples

Basic correction:

deepl correct "This is an test."
# This is a test.

# Using the alias
deepl c "Their going too the store."

Grammar gate in CI (exit code 8 if corrections needed):

deepl correct README.md --check

With --format json the check emits the same result payload write --check does, with mode set to correct (the field table is under write's Check mode above):

deepl correct README.md --check --format json
# {"ok":true,"mode":"correct","needsChanges":true,"changes":2,"file":"/abs/path/README.md"}

Fix a file in place with a backup:

deepl correct essay.md --fix --backup

Diff view:

deepl correct document.txt --diff

Pipe via stdin:

cat notes.txt | deepl correct

JSON output:

deepl correct "Their going too the store." --format json
# {"original":"Their going too the store.","improved":"They're going to the store.","changes":1,"language":"auto-detected"}

--check, --diff and --alternatives each carry their own payload, identical to write's (see JSON output under write above); only --check's differs, in that its mode reads correct.


voice

Translate audio using the DeepL Voice API with real-time WebSocket streaming.

Synopsis

deepl voice [options] <file>

Arguments

Argument Description
file Audio file to translate. Use - for stdin.

Options

Option Short Description Default
--to <languages> -t Target language(s), comma-separated, max 5 (required) -
--from <language> -f Source language (auto-detect if not specified) auto
--formality <level> Formality level: default, formal, more, informal, less, prefer_more, prefer_less default
--glossary <name-or-id> Use glossary by name or ID (single glossary; not repeatable) -
--content-type <type> Audio content type (auto-detected from file extension) auto
--chunk-size <bytes> Audio chunk size in bytes 6400
--chunk-interval <ms> Interval between audio chunks in milliseconds 200
--no-stream Disable live streaming output, collect and print at end -
--no-reconnect Disable automatic reconnection on WebSocket drop -
--max-reconnect-attempts <n> Maximum reconnect attempts on WebSocket drop 3
--source-language-mode <mode> Source language detection mode: auto, fixed -
--format <format> Output format: text, json text

Note: All formality values (default, formal, informal, more, less, prefer_more, prefer_less) are accepted. The voice API natively uses formal/informal (in addition to more/less), while the translate API uses prefer_more/prefer_less.

Note: If the server ends the stream after transcribing the audio but without sending a translation for one of --to's languages, the command fails with exit code 9 and names the languages, rather than printing an empty translation line and exiting 0. Audio containing no speech transcribes to nothing and is translated to nothing, which is not an error and still exits 0.

Note: the Voice API supports a smaller language set than translate, and the CLI checks --to/--from against it locally — an unlisted code exits 6 before any request, naming every valid option.

Targets (39): ar, bg, cs, da, de, el, en, en-gb, en-us, es, et, fi, fr, he, hu, id, it, ja, ko, lt, lv, nb, nl, pl, pt, pt-br, pt-pt, ro, ru, sk, sl, sv, th, tr, uk, vi, zh, zh-hans, zh-hant

Sources (30): ar, bg, cs, da, de, el, en, es, et, fi, fr, hu, id, it, ja, ko, lt, lv, nb, nl, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh

Codes are matched case-insensitively, so the lowercase spelling deepl languages prints is accepted; the CLI canonicalizes to the casing the Voice API expects before sending.

Supported Audio Formats

Extension Content Type
.ogg, .opus audio/opus;container=ogg
.webm audio/opus;container=webm
.mka audio/opus;container=matroska
.flac audio/flac
.mp3 audio/mpeg
.pcm, .raw audio/pcm;encoding=s16le;rate=16000

Examples

# Basic audio translation
deepl voice recording.ogg --to de

# Multiple target languages
deepl voice meeting.mp3 --to de,fr,es

# With source language and formality
deepl voice audio.flac --to ja --from en --formality more

# Pipe from ffmpeg
ffmpeg -i video.mp4 -f ogg - | deepl voice - --to es --content-type 'audio/opus;container=ogg'

# Pipe raw PCM from stdin
cat audio.pcm | deepl voice - --to es --content-type 'audio/pcm;encoding=s16le;rate=16000'

# JSON output for scripting
deepl voice speech.ogg --to de --format json | jq .targets[0].text

# Disable live streaming
deepl voice speech.ogg --to de --no-stream

JSON Output Format

{
  "sessionId": "session-abc123",
  "source": {
    "lang": "en",
    "text": "Hello world",
    "segments": [{ "text": "Hello world", "startTime": 0, "endTime": 1.5 }]
  },
  "targets": [
    {
      "lang": "de",
      "text": "Hallo Welt",
      "segments": [{ "text": "Hallo Welt", "startTime": 0, "endTime": 1.5 }]
    }
  ]
}

Notes

  • The Voice API requires a DeepL Pro or Enterprise plan.
  • Maximum 5 target languages per session.
  • Maximum audio chunk size: 100KB, recommended pacing: 200ms between chunks.
  • Sessions have a 30-second inactivity timeout and 1-hour maximum duration.
  • The Voice API uses the same endpoint resolution as other commands: :fx keys use api-free.deepl.com, others use api.deepl.com, and custom regional URLs are always honored.

watch

Watch files or directories for changes and auto-translate.

Synopsis

deepl watch [OPTIONS] PATH

Description

Monitor files or directories for changes and automatically translate them. Supports debouncing, glob patterns, and multiple target languages.

Behavior:

  • Runs continuously until interrupted (Ctrl+C)
  • Shows translation statistics on exit, and exits 12 rather than 0 when the session recorded any failed translation or failed auto-commit
  • Detects file changes using filesystem watch
  • Debounces rapid changes to avoid duplicate translations
  • Translates one version of a file at a time. An edit arriving while a file's translation is still running queues exactly one re-translation, which starts after the running one has written. Two translations of the same file never overlap, so a slower translation of older content cannot overwrite a newer one, and an edit storm costs two translations rather than one per event
  • Skips files inside the output directory whose name carries a target-language segment (doc.es.md with --to es), so the CLI's own output does not re-trigger the watcher. The check is limited to the output directory, so a source file named that way — pricing.es.md translated to es — is translated normally as long as it lives outside it. When such a file is skipped and the CLI did not write it, the reason is printed once per file
  • Mirrors each source file's directory under the output directory, relative to the watched path: watching docs/ writes docs/a/index.md to <output>/a/index.es.md and docs/b/index.md to <output>/b/index.es.md. This is the same layout deepl translate <dir> --output <dir> produces, and it is what keeps two same-named files in different directories from writing one output path. A file at the top of the watched directory, and a watched path that is a single file, write straight into the output directory as before

Options

Watch Options:

  • --to, -t LANGS - Target language(s), comma-separated (uses configured defaults.targetLangs if omitted)
  • --output, -o DIR - Output directory (default: <path>/translations for directories, same dir for files)
  • --pattern GLOB - File pattern filter (e.g., *.md, **/*.json)
  • --debounce MS - Debounce delay in milliseconds. The flag wins, then the configured watch.debounceMs, then the default of 500
  • --concurrency NUM - Maximum parallel translations across different files (default: 5). A single file is always translated one version at a time, whatever this is set to

Translation Options:

  • --from, -f LANG - Source language (auto-detect if omitted)
  • --formality LEVEL - Formality level: default, more, less, prefer_more, prefer_less, formal, informal
  • --preserve-code - Preserve code blocks
  • --preserve-formatting - Preserve line breaks and whitespace formatting
  • --glossary NAME-OR-ID - Use glossary by name or ID for consistent terminology. A source language is required, because the API refuses a glossary without one ("Use of a glossary requires the source_lang parameter to be specified"): pass --from, or set defaults.sourceLang and it is used automatically. With neither, the command exits 6 before the watcher starts rather than failing on every file change. Unlike translate, watch takes a single glossary.

Git Integration:

  • --auto-commit - Auto-commit translations to git after each change. One commit per translated source file, queued one at a time: git holds .git/index.lock for the duration of an add or a commit, so parallel translations cannot commit in parallel. A failed commit is reported and counted, and the session exits 12 rather than 0 (see below). The commit goes to the repository holding the output directory, whatever directory the CLI was started in — git can only commit files inside its own working tree, so that is the only repository the translations can reach. When the output directory is in no repository at all, the command exits 6 before the watcher starts rather than warning once per translated file
  • --git-staged - Only watch git-staged files (snapshot taken once at startup). The index read is the one belonging to the watched path's repository, so the CLI need not be started from its root
  • --dry-run - Show what would be watched without starting the watcher

Examples

Watch single file:

# Basic watching
deepl watch README.md --to es

# With custom output
deepl watch README.md --to es,fr --output translations/

# With options
deepl watch tutorial.md --to es --preserve-code --formality more

Watch directory:

# Watch all supported files
deepl watch docs/ --to es

# Watch with pattern
deepl watch docs/ --to es,fr --pattern "*.md"

# With custom debounce (wait 1 second after changes)
deepl watch docs/ --to es --debounce 1000

With auto-commit:

# Automatically commit translations
deepl watch docs/ --to es --auto-commit

With git-staged filtering:

# Only translate files currently staged in git
deepl watch . --to es --git-staged

# Combine with dry-run to preview staged files
deepl watch . --to es,fr --git-staged --dry-run

# Pre-commit workflow: stage files, then watch only those
git add docs/guide.md docs/faq.md
deepl watch docs/ --to de,ja --git-staged --auto-commit

Note: --git-staged takes a one-time snapshot of staged files at startup. Files staged after the watcher starts are not included. The watched path must be inside a git repository — the command exits with an error otherwise — and it is that repository's index that is read, not the one belonging to the directory the CLI was started in.


sync

Continuous localization engine for i18n file translation.

Synopsis

deepl sync [OPTIONS]
deepl sync init [OPTIONS]
deepl sync status [OPTIONS]
deepl sync validate [OPTIONS]
deepl sync audit [OPTIONS]
deepl sync export [OPTIONS]
deepl sync resolve [OPTIONS]
deepl sync push [OPTIONS]
deepl sync pull [OPTIONS]

Description

Scan, translate, and sync i18n resource files. The sync engine reads .deepl-sync.yaml for project configuration, diffs source strings against .deepl-sync.lock to detect changes, translates only new and modified strings via the DeepL API, and writes properly formatted target files.

Supported formats: JSON, YAML, TOML, Gettext PO, Android XML, iOS Strings, Xcode String Catalog (.xcstrings), ARB, XLIFF, Java Properties, Laravel PHP arrays (.php).

Behavior:

  • Reads configuration from .deepl-sync.yaml in the current directory
  • Tracks translation state in .deepl-sync.lock for incremental sync
  • Preserves format-specific structure (indentation, comments, metadata)
  • Displays per-locale progress as each translation completes
  • Exits with code 10 when --frozen detects translation drift
  • Bounds context.scan_paths at sync.max_scan_files files (default 50,000) to prevent a misconfigured glob from wedging the CLI on huge source trees. Exceeding the cap throws a ValidationError with a suggestion to narrow the pattern or raise the cap; see docs/SYNC.md.

Options

Sync Mode:

  • --dry-run - Preview changes without translating

  • --frozen - Fail (exit 10) if translations are missing or outdated; no API calls

  • --ci - Alias for --frozen

  • --force - Re-translate all strings, ignoring the lockfile. WARNING: also bypasses the sync.max_characters cost-cap preflight in .deepl-sync.yaml, so a forced run can re-bill every translated key and incur unexpected API costs. Run deepl sync --dry-run first to see the character estimate before forcing.

    Billing safety guards:

    • --watch --force is rejected at startup with a ValidationError (exit 6) to prevent unbounded billing from a forced re-translation on every file save.
    • In an interactive terminal, --force prompts for confirmation before bypassing the cost cap. Pass --yes (-y) to skip the prompt in scripts.
    • In CI environments (CI=true), --force requires an explicit --yes; otherwise the process exits 6 with an actionable hint naming the missing flag.
    • Anywhere else the prompt cannot be shown — a git hook, cron job, make target, container entrypoint, deepl sync --force < /dev/null, or --no-input--force is refused with exit 6 rather than assumed to be confirmed. --yes is the only way to run it unattended. --force overwrites every target file, including translations edited by hand, and no .deepl.bak survives a successful run, so there is nothing to recover from afterwards.
  • --yes, -y - Skip the --force confirmation prompt (required when CI=true, and whenever there is no terminal to prompt on)

  • --break-lock - Take the sync lock even when .deepl-sync.lock.pidfile names a process that looks alive, reporting the holder it removed. Also accepted by sync pull and sync resolve, which take the same lock. Use it only when that run is definitely not running: two concurrent syncs write the same target files and the same lockfile. Not carried into --watch cycles — it applies to the run you asked for, then the lock arbitrates normally again. See Concurrent sync

Filtering:

  • --locale LANGS - Sync only specific target locales (comma-separated). Note the split: sync --locale is a filter over locales already declared in .deepl-sync.yaml#target_locales — it narrows which configured targets a run acts on. deepl translate --to is an invocation-time specifier — it names the target languages for a one-shot text translation. The sync engine owns the locale mapping via .deepl-sync.yaml; translate does not. A locale not in target_locales passed to sync --locale exits with a ConfigError; an unrecognized code passed to translate --to exits with InvalidInput.

Translation Quality:

  • --formality LEVEL - Override formality: default, more, less, prefer_more, prefer_less, formal, informal
  • --model-type TYPE - Override model type: quality_optimized, prefer_quality_optimized, latency_optimized
  • --glossary NAME-OR-ID - Override glossary name or ID
  • --scan-context / --no-scan-context - Enable or disable source-code context scanning. Matches both string literal and template literal t() calls. When enabled, key paths are parsed into natural-language context descriptions, and HTML element types are detected from surrounding source code. Element types feed into instruction_templates (configured in .deepl-sync.yaml) for auto-generated custom_instructions. Scope: these flags override context.enabled in .deepl-sync.yaml only; all other context.* settings (include, exclude, max_files, etc.) continue to apply when scanning is enabled. Note: bare --context / --no-context on deepl sync is rejected with a ValidationError (exit 6) — the string-valued --context "<text>" flag only applies to deepl translate; sync's boolean toggle was renamed to --scan-context to avoid the collision.

Note: deepl sync deliberately exposes no --translation-memory / --tm-threshold CLI override; configure translation memory via translation.translation_memory (and optional translation.translation_memory_threshold) in .deepl-sync.yaml, with per-locale overrides under translation.locale_overrides.

Performance:

  • --concurrency NUM - Max parallel locale translations (default: 5)
  • --batch - Force plain batch mode (fastest, no context or instructions). All keys in batch API calls.
  • --no-batch - Force per-key mode (slowest, individual context per key). Default: section-batched context (~3.4x faster than per-key while preserving disambiguation context).

Git:

  • --auto-commit - Auto-commit translated files after sync (requires git)

Review:

  • --flag-for-review - Mark translations as machine_translated in lock file for human review

Watch:

  • --watch - Watch source files and auto-sync on changes
  • --debounce MS - Debounce delay for watch mode (default: 500ms)

Output:

  • --format FORMAT - Output format: text (default), json

Config:

  • --sync-config PATH - Path to .deepl-sync.yaml (default: auto-detect)

Subcommands

init

Interactive setup wizard that creates .deepl-sync.yaml by scanning the project for i18n files.

Auto-detected project types: i18next / react-intl / vue-i18n / next-intl (JSON under locales/ or i18n/), Rails (config/locales/en.yml), generic YAML i18n, Django / generic gettext (locale/*/LC_MESSAGES/*.po), Android (res/values/strings.xml), iOS / macOS (*.lproj/Localizable.strings), Xcode String Catalog (Localizable.xcstrings / *.xcstrings), Flutter (pubspec.yaml + l10n/app_en.arb or *_en.arb), Angular / CAT tools (XLIFF under src/locale/ or root), go-i18n (locales/en.toml or i18n/en.toml), Java / Spring (src/main/resources/messages_en.properties), and Laravel (composer.json + lang/en/*.php or resources/lang/en/*.php). Detection is filesystem-only — no package manifests are parsed. See docs/SYNC.md for the full detection matrix. Layouts outside these conventions need the four flags above.

Options:

  • --source-locale CODE - Source locale code
  • --target-locales CODES - Target locales (comma-separated)
  • --file-format TYPE - File format: json, yaml, toml, po, android_xml, ios_strings, xcstrings, arb, xliff, properties, laravel_php
  • --path GLOB - Source file path or glob pattern
  • --format FORMAT - Output format: text (default), json. Under json, success emits the envelope described below and failure emits the shared error envelope, both on stdout
  • --sync-config PATH - Path to .deepl-sync.yaml

--source-lang and --target-langs were accepted as deprecated aliases during 1.x and were removed in 2.0.0; use --source-locale / --target-locales. deepl translate --target-lang is unchanged — it operates on strings and stays aligned with the DeepL API's wire name.

Examples:

# Interactive auto-detection
deepl sync init

# Non-interactive
deepl sync init --source-locale en --target-locales de,fr,es --file-format json --path "locales/en.json"
status

Show translation coverage for all target locales.

Options:

  • --locale LANGS - Show status for specific locales only
  • --format FORMAT - Output format: text (default), json
  • --sync-config PATH - Path to .deepl-sync.yaml

JSON output contract (stable within a major version):

{
  "sourceLocale": "en",
  "totalKeys": 142,
  "skippedKeys": 1,
  "locales": [
    {
      "locale": "de",
      "complete": 140,
      "missing": 2,
      "outdated": 0,
      "unwritten": 0,
      "needsReview": 0,
      "coverage": 98
    }
  ],
  "unwrittenByLocale": []
}

unwritten, needsReview and unwrittenByLocale are always present, even when zero or empty — see the two paragraphs at the end of this subcommand for what each counts.

skippedKeys counts entries the parser tagged as untranslatable and excluded from the translation batch — currently only Laravel pipe-pluralization values (|{n}, |[n,m], |[n,*]). Included in totalKeys.

stdout/stderr split (stable contract): The JSON payload — the success result, or the error envelope below when the command fails — is written to stdout, so deepl sync status --format json > status.json produces a parseable file in both cases. Diagnostic/progress logs and warnings stay on stderr, which means stdout parses even when the CLI or the Node runtime had something to say. The same stdout/stderr split applies to deepl sync --format json, deepl sync validate --format json, and deepl sync audit --format json.

Error envelope (shared across every command with a JSON mode): On failure, --format json emits the following JSON envelope to stdout — the envelope is the command's result in the failure case; the non-zero exit code is the failure signal — and exits with the typed exit code:

{
  "ok": false,
  "error": {
    "code": "ConfigError",
    "message": ".deepl-sync.yaml not found in current directory or any parent",
    "suggestion": "Run `deepl sync init` to create one."
  },
  "exitCode": 7
}

The error.code field matches the error class name (ConfigError, ValidationError, SyncConflict, AuthError, etc.). error.suggestion is present when the underlying DeepLCLIError carries one. exitCode matches the process exit code, so a caller can branch on either field.

Which commands emit it: every command whose effective --format is json — the nine sync subcommands plus translate, write, correct, voice, usage, languages, detect, and those subcommands of glossary, tm, cache, config, hooks, admin and style-rules that declare the flag. The shape and the stream are identical everywhere, so a script wrapping several deepl commands needs one failure path. Notes on the edges:

  • A command with no --format flag, and any run in text or table mode, is unchanged: Error: / Suggestion: prose on stderr.
  • config get/config list default to json, so their failures carry the envelope with no flag passed.
  • A result that is not an error keeps its own shape: write --check / correct --check exit 8 to report that text needs changes, and emit their own ok: true result payload rather than an envelope; --diff and --alternatives carry ok: true payloads of their own too. Discriminate on ok: false is a failure, true is a result whose detail is in the payload. The one exception is write/correct's plain improvement payload ({original, improved, changes, language}), which predates the envelope and carries no ok field.
  • A malformed invocation commander rejects before the command runs (unknown option, a missing argument) still prints commander's own message on stderr and exits 6. The envelope covers command failures, not parse failures.

sync init --format json success envelope: For scripted project bootstrap, deepl sync init --format json emits a success envelope on stdout instead of the plain text confirmation:

{
  "ok": true,
  "created": {
    "configPath": "/absolute/path/.deepl-sync.yaml",
    "sourceLocale": "en",
    "targetLocales": ["de", "fr"],
    "keys": 128
  }
}

keys is present only when the wizard counted the source strings while scanning; a fully non-interactive sync init omits it. Treat it as optional.

Casing convention: every sync JSON payload uses camelCase, while the on-disk .deepl-sync.lock and .deepl-sync.yaml use snake_case. The two are deliberately kept separate — JSON output is a consumer contract; the files are authored configuration. Outside sync, commands that pass a DeepL API response straight through keep the API's own snake_case (glossary, tm), as does detect (detected_language, language_name). The two are deliberately kept separate — JSON output is a consumer contract; the files are authored configuration.

Examples:

deepl sync status

# JSON output
deepl sync status --format json

Sample output:

Source: en (142 keys)

  de  [###################.]  98%  (2 missing, 0 outdated)
  fr  [####################]  100%  (0 missing, 0 outdated)
  es  [###################.]  97%  (4 missing, 0 outdated)
  ja  [##################..]  91%  (12 missing, 0 outdated)

A locale also reports needsReview keys — a translation is present but its format marks it as not ready to ship, which means a gettext #, fuzzy msgstr or an XLIFF review state such as needs-review-translation (1.2) or initial (2.0). msgfmt leaves a fuzzy entry out of the compiled catalog, so the application shows the source string; counting such a key complete would report a project as done while it ships untranslated text. An XLIFF unit with no state attribute, or one carrying a value outside the recognised sets, counts as complete — see docs/SYNC.md for the two tables. Such a key is never re-translated and the file is never rewritten for it — the value is a reviewer's draft — so the count is a report, not a repair, and nothing is billed. The suffix , N needs review and its explanatory line appear only when the count is non-zero, and --format json always carries needsReview on each locale. deepl sync push skips such a key under a needs_review skip reason rather than uploading it as an approved translation. sync --frozen deliberately does not treat it as drift: such a marker is a normal, transient state in a review workflow that only a human clears, and nothing the CLI did caused it.

A locale also reports unwritten keys — recorded as translated in .deepl-sync.lock but not in that locale's target file — when there are any, followed by a line naming the file and the keys. The suffix is omitted when the count is zero, so the line above is unchanged for a healthy project. --format json always carries unwritten on each locale plus a top-level unwrittenByLocale array of {locale, file, keys}, each entry gaining an unusable field when the target file could not be read or parsed at all. See docs/SYNC.md for what is and is not counted.

validate

Check translations for placeholder integrity and format consistency.

Options:

  • --locale LANGS - Validate specific locales only
  • --format FORMAT - Output format: text (default), json
  • --sync-config PATH - Path to .deepl-sync.yaml

Examples:

deepl sync validate

# Validate only German
deepl sync validate --locale de

Sample output:

$ deepl sync validate
Checked 4 translations

  ERROR  de/greeting: Missing placeholders in translation: {name}
  WARN  de/bye: Translation is identical to source text

1 error(s), 1 warning(s)

One line per issue, ERROR or WARN followed by <locale>/<key> and the check's message. A run with no issues prints the header and All translations passed validation.

JSON output contract (stable within a major version):

{
  "totalChecked": 3,
  "passed": 2,
  "warnings": 0,
  "errors": 1,
  "issues": [
    {
      "locale": "de",
      "file": "locales/de.json",
      "key": "errors.count",
      "source": "Hi %s",
      "translation": "Hi",
      "severity": "error",
      "issues": [
        {
          "check": "placeholders",
          "severity": "error",
          "message": "Missing placeholders in translation: %s",
          "details": { "expected": ["%s"], "actual": [] }
        }
      ]
    }
  ]
}

totalChecked counts source/target pairs that were validated; a key absent from the target file is not a pair and is not counted. passed is totalChecked minus the number of pairs carrying any issue, so a pair with only warnings is not "passed". errors is what drives the exit code — see 8 — CheckFailed for which check values are error severity and which are warn-only. A file-level unusable_target issue counts toward errors but not toward totalChecked, and its key/file are both the target path rather than a translation key.

audit

Analyze translation consistency and detect terminology inconsistencies across target locales. "Audit" here means translation-consistency audit (detecting term divergence across locales), not security audit in the npm audit sense.

Options:

  • --format FORMAT - Output format: text (default), json
  • --sync-config PATH - Path to .deepl-sync.yaml

Note: Prior to the 1.1.0 release, this subcommand was prototyped as glossary-report; it never shipped in a tagged release under that name. The old name is rejected with an error pointing at the new form; no alias is kept.

JSON output sample:

{
  "totalTerms": 1,
  "inconsistencies": [
    {
      "sourceText": "Dashboard",
      "locale": "de",
      "translations": ["Armaturenbrett", "Dashboard"],
      "files": ["locales/en/common.json", "locales/en/admin.json"]
    }
  ],
  "missingTargets": []
}

missingTargets lists target files that could not be read or parsed and were therefore excluded from the comparison. A non-empty array means the audit's verdict covers fewer locales than the project has; the text output prints the same list under N target(s) could not be read and were excluded from the comparison:.

The translations array contains the actual translated strings read from target files. If a target file is missing, the content hash falls back in its place.

export

Export source strings to XLIFF 1.2 for CAT tool handoff.

Options:

  • --locale LANGS - Filter by locale (comma-separated)
  • --output PATH - Write to file instead of stdout. Path must stay within the project root; intermediate directories are created automatically
  • --overwrite - Required to overwrite an existing --output file. Without it, an existing file causes a non-zero exit and no write occurs
  • --format FORMAT - Output format: text (default), json. Success output is always XLIFF 1.2 regardless of format; json affects the error envelope on stdout (matching other sync subcommands) so script consumers can parse failure shape uniformly
  • --sync-config PATH - Path to .deepl-sync.yaml

Examples:

# Print XLIFF to stdout (pipe to CAT tool, clipboard, etc.)
deepl sync export

# Write to a file (creates reports/ if needed)
deepl sync export --output reports/handoff.xlf

# Overwrite an existing file
deepl sync export --output reports/handoff.xlf --overwrite

# Rejected: path escapes the project root
deepl sync export --output ../elsewhere.xlf
resolve

Resolve git merge conflicts in .deepl-sync.lock.

Options:

  • --format FORMAT - Output format: text (default), json
  • --dry-run - Preview conflict decisions without writing the lockfile
  • --sync-config PATH - Path to .deepl-sync.yaml
  • --break-lock - Take the sync lock even when .deepl-sync.lock.pidfile names a process that looks alive

JSON success envelope (stable within a major version): { "ok": true, "resolved": <n>, "decisions": [...] }

push

Push local translations to a TMS for human review.

Options:

  • --locale LANGS - Push specific locales only
  • --format FORMAT - Output format: text (default), json
  • --sync-config PATH - Path to .deepl-sync.yaml

Requires TMS integration. Add a tms: block to .deepl-sync.yaml (at minimum enabled: true, server, project_id) and supply credentials via the TMS_API_KEY or TMS_TOKEN environment variable. Running push without a configured tms: block exits 7 (ConfigError). See docs/SYNC.md#tms-rest-contract for the full field reference and REST contract.

TMS destination trust. tms.server is chosen by .deepl-sync.yaml, which lives in the checkout, while TMS_API_KEY / TMS_TOKEN come from your environment. Before an environment-supplied credential is attached to a request, the destination hostname must be one you have approved:

  • Approved if the hostname appears in the user-level tms.allowedServers list (deepl config set tms.allowedServers tms.example.com, comma-separate several).
  • Otherwise, in an interactive terminal, the CLI names the host and what would be sent and asks once. Answering yes records the hostname in user config (~/.config/deepl-cli/config.json), never in the repository.
  • Otherwise — under --no-input, or on a non-TTY such as CI — the run fails closed with exit 7 (ConfigError) naming the host and the exact deepl config set tms.allowedServers ... command. No credential and no translated string is sent.

The gate applies only to environment-supplied credentials. A credential inlined as tms.api_key / tms.token in .deepl-sync.yaml is not gated: it belongs to the same file that chose the destination, so nothing of yours leaks. Loopback hosts (localhost, 127.0.0.1) are not exempt — a co-tenant process listening locally is still an exfiltration sink.

Both commands print the resolved destination origin on success, in text and JSON output, so a redirected destination is visible in logs even when the host was already approved.

JSON success envelope (stable within a major version): { "ok": true, "pushed": <n>, "skipped": [...], "server": "<origin>" }

Each skipped entry carries a reason. push emits untranslated (the target file lists the key but holds no translation for it — pushing it would upload source text as the locale's approved translation), needs_review (the translation is marked as needing review, a gettext #, fuzzy msgstr or an XLIFF review state, so it is a reviewer's draft rather than an approved translation), pipe_pluralization (Laravel pipe-pluralization, never sent to a TMS), and target_missing (the target file does not exist yet, common on a first push). push never emits pull's reasons and pull never emits these.

pull

Pull approved translations from a TMS back into local files.

Options:

  • --locale LANGS - Pull specific locales only
  • --format FORMAT - Output format: text (default), json
  • --sync-config PATH - Path to .deepl-sync.yaml
  • --dry-run - Preview what the pull would change without writing any file
  • --break-lock - Take the sync lock even when .deepl-sync.lock.pidfile names a process that looks alive

Requires TMS integration. Add a tms: block to .deepl-sync.yaml (at minimum enabled: true, server, project_id) and supply credentials via the TMS_API_KEY or TMS_TOKEN environment variable. Running pull without a configured tms: block exits 7 (ConfigError). See docs/SYNC.md#tms-rest-contract for the full field reference and REST contract.

TMS destination trust. tms.server is chosen by .deepl-sync.yaml, which lives in the checkout, while TMS_API_KEY / TMS_TOKEN come from your environment. Before an environment-supplied credential is attached to a request, the destination hostname must be one you have approved:

  • Approved if the hostname appears in the user-level tms.allowedServers list (deepl config set tms.allowedServers tms.example.com, comma-separate several).
  • Otherwise, in an interactive terminal, the CLI names the host and what would be sent and asks once. Answering yes records the hostname in user config (~/.config/deepl-cli/config.json), never in the repository.
  • Otherwise — under --no-input, or on a non-TTY such as CI — the run fails closed with exit 7 (ConfigError) naming the host and the exact deepl config set tms.allowedServers ... command. No credential and no translated string is sent.

The gate applies only to environment-supplied credentials. A credential inlined as tms.api_key / tms.token in .deepl-sync.yaml is not gated: it belongs to the same file that chose the destination, so nothing of yours leaks. Loopback hosts (localhost, 127.0.0.1) are not exempt — a co-tenant process listening locally is still an exfiltration sink.

Both commands print the resolved destination origin on success, in text and JSON output, so a redirected destination is visible in logs even when the host was already approved.

JSON success envelope (stable within a major version): { "ok": true, "pulled": <n>, "replaced": <n>, "skipped": [...], "server": "<origin>", "dryRun": <bool> }

replaced counts keys whose existing local translation the pull overwrote with the TMS version — check it before trusting a pull that ran over hand-edited files. dryRun is true when --dry-run was passed, in which case nothing was written and pulled/replaced are what a real run would do.

Each skipped entry carries a reason. pull emits unusable_target (the target file could not be read), key_collision (the target file's keys collide), shared_target (a target file another sync configuration's .deepl-sync.lock accounts for keys in, left untouched rather than rebuilt from this configuration's keys alone), plural_entry (one exported string cannot fill a gettext msgstr[N] or Android <plurals> entry's forms, so the entry is carried forward as it stands), and no_matches (no matching keys). See docs/SYNC.md#two-configurations-writing-one-file.

Examples

Basic sync:

# Sync all configured locales
deepl sync

# Preview what would be translated
deepl sync --dry-run

CI/CD (frozen mode):

# Fail if translations are out of date (exit code 10)
deepl sync --frozen

Locale filtering:

# Sync only German and French
deepl sync --locale de,fr

Force re-translation:

# Re-translate everything, ignoring the lockfile
deepl sync --force

JSON output for scripting:

deepl sync --format json
deepl sync status --format json

deepl sync --format json output contract (stable within a major version):

The success payload is written to stdout as a single JSON object. The following fields are guaranteed stable and will not be renamed or removed within the same major version:

Field Type Description
ok boolean true if the sync completed without errors
totalKeys number Total translation keys discovered across all source files
translated number Keys translated during this run (summed across all locales)
skipped number Keys skipped (already up-to-date, summed across all locales)
failed number Keys that could not be translated (summed across all locales)
targetLocaleCount number Number of target locales processed
estimatedCharacters number Characters estimated for billing this run
estimatedCost string | undefined Human-readable cost estimate at Pro rate (e.g. ~$0.05), omitted when zero
rateAssumption "pro" Always "pro" — cost estimate uses the DeepL Pro per-character rate
dryRun boolean true when --dry-run was passed; no translations were written
perLocale Array<{locale, translated, skipped, failed}> Per-locale breakdown; each entry aggregates all files for that locale

No other fields appear in the output. Fields not listed above are internal and may change without notice.

Notes

  • The --frozen flag makes no API calls. It compares the lockfile against source files, and each target file against what the lockfile claims about it, and exits with code 10 if any translations are missing, outdated, or recorded as translated while absent from the target file. This is the recommended mode for CI/CD pipelines.
  • The --dry-run flag makes no API calls and writes nothing — no target file, no lockfile, no backup. It still requires an API key, though: the client is constructed before the run decides it has nothing to send, so deepl sync --dry-run without a key exits 2. It reads each target file the lockfile claims translations for, so its estimate covers the same work the real run does: keys the lockfile calls translated that the target file no longer holds are included (the run re-translates and re-bills them, and unwrittenKeys reports the count), and a locale whose target file is on disk and unreadable is excluded and named in a warning, because the run refuses that locale, bills nothing for it and exits 12. Reading the target files is what makes the estimate faithful and is the flag's main cost: measured on a 20,000-key, 6-locale project (2.83 MiB source, 17.3 MiB of target files, 24 MiB lockfile), --dry-run went from 0.44 s to 0.75 s, alongside 0.77 s for sync status and 0.79 s for sync --frozen. The read is skipped entirely for a locale the lockfile claims nothing for, so a project mid-first-sync pays nothing.
  • The sync.max_characters cost cap quotes from the same estimate as --dry-run, so a run the preview prices above the cap is a run the cap refuses. That costs one pass over the target files before the cap decides: on the fixture above, a translating run took 1.54 s with no cap configured and 1.91 s with one. The cap is unaffected for projects that do not set it.
  • The lockfile (.deepl-sync.lock) should be committed to version control. It enables incremental sync by tracking content hashes.
  • The push and pull subcommands require a TMS that implements the REST contract documented in docs/SYNC.md. All other commands work with the standard DeepL Translation API.
  • By default, keys with extracted context are grouped by i18n section and translated in section batches. Use --no-batch to force individual per-key context translation. Use --batch to force all keys into plain batch calls (no context).
  • See docs/SYNC.md for the complete sync guide including configuration schema, CI/CD recipes, and troubleshooting.

hooks

Manage git hooks for translation workflow automation.

Synopsis

deepl hooks <SUBCOMMAND>

Description

Install, uninstall, and manage git hooks that validate translations before committing or pushing.

Subcommands

install <hook-type>

Install a git hook.

Arguments:

  • hook-type - Hook type: pre-commit, pre-push, commit-msg, post-commit

Options:

  • -y, --yes - Skip the confirmation prompt when this repository sends hooks outside the working tree

Hooks directory outside the repository: the hook is written wherever git reads hooks from, which a repository-local core.hooksPath can point anywhere — including an absolute path outside the checkout. Because that setting travels with the repository rather than coming from you, an install that would land outside the working tree names the configured value and the resolved directory and asks first. Declining, or running without a terminal, exits 6 and writes nothing; --yes installs there and prints the same notice to stderr. A core.hooksPath that stays inside the working tree (.husky/_, for example) is not affected, and neither are linked worktrees or submodules, where git legitimately reads hooks from the repository that owns them.

Examples:

deepl hooks install pre-commit
deepl hooks install pre-push
deepl hooks install commit-msg
deepl hooks install post-commit

# Accept a hooks directory outside the working tree without prompting
deepl hooks install pre-commit --yes
uninstall <hook-type>

Uninstall a git hook.

Examples:

deepl hooks uninstall pre-commit
deepl hooks uninstall pre-push
deepl hooks uninstall commit-msg
deepl hooks uninstall post-commit
list

List all hooks and what the file at each hook path is.

Each hook installed by this CLI carries a marker line recording the SHA-256 of its body, and list checks it. Four states are reported:

State Meaning
installed A versioned marker is present and the body hashes to the value it records
modified A marker is present and the body does not hash to it — an edit made after installation, or a forged marker
unverified A legacy (pre-1.0) marker with no recorded hash to check against
not-installed No hook file, or a file carrying no DeepL marker

The hash is unkeyed, so installed establishes that a hook has not changed since its marker was written — not that this CLI wrote it. Anyone who can write the hook can write a marker that agrees with their own content. Treat the marker as detection of changes to a hook, not as proof of its origin.

Options:

  • --format <format> - Output format: text, json (default: text)

Examples:

deepl hooks list

# JSON output for CI/CD scripting
deepl hooks list --format json
# { "pre-commit": "installed", "pre-push": "modified", "commit-msg": "unverified", "post-commit": "not-installed" }

The JSON values are these state strings, not booleans. Gate on state === "installed"; a truthiness test passes for every state, including "not-installed".

path <hook-type>

Show the path to a hook file.

Examples:

deepl hooks path pre-commit

glossary

Manage translation glossaries using the DeepL v3 Glossary API.

The v3 API supports both single-target glossaries (one source → one target language) and multilingual glossaries (one source → multiple target languages).

Synopsis

deepl glossary <SUBCOMMAND>

Subcommands

create <name> <source-lang> <target-lang> <file>

Create a new glossary from a TSV or CSV file.

Arguments:

  • name - Glossary name
  • source-lang - Source language code (e.g., en, de, fr)
  • target-lang - Target language code, comma-separated for multiple (e.g., es, de,fr,es)
  • file - Path to TSV or CSV file with term pairs

File Format:

  • TSV (Tab-Separated Values): source_term<TAB>target_term
  • CSV (Comma-Separated Values): source_term,target_term
  • One term pair per line
  • No header row required

Example file (glossary.tsv):

API	API
authentication	autenticación
cache	caché

Examples:

# Create single-target glossary from TSV file
deepl glossary create tech-terms en es glossary.tsv
# ✓ Glossary created: tech-terms (ID: abc123...)
# Source language: en
# Target languages: es
# Type: Single target
# Total entries: 3

# Create multilingual glossary with comma-separated target languages
deepl glossary create tech-terms en de,fr,es glossary.tsv

# Create glossary from CSV file
deepl glossary create product-names en fr terms.csv
list

List all glossaries with their IDs, language pairs, and entry counts.

Options:

  • --format <format> - Output format: text, json (default: text)

Output Format (text):

  • Single-target glossaries: 📖 name (source→target) - N entries
  • Multilingual glossaries: 📚 name (source→N targets) - N entries

Example:

deepl glossary list
# 📖 tech-terms (en→de) - 3 entries
# 📚 multilingual-terms (en→3 targets) - 15 entries

# JSON output for CI/CD scripting
deepl glossary list --format json
# [{ "glossary_id": "abc123...", "name": "tech-terms", ... }]
show <name-or-id>

Show glossary details including name, ID, languages, creation date, and entry count.

Options:

  • --format <format> - Output format: text, json (default: text)

Output includes:

  • Name and ID
  • Source language
  • Target languages (comma-separated for multilingual glossaries)
  • Type (Single target or Multilingual)
  • Total entry count
  • Language pairs (for multilingual glossaries)
  • Creation timestamp

Example:

deepl glossary show tech-terms
# Name: tech-terms
# ID: abc123...
# Source language: en
# Target languages: de
# Type: Single target
# Total entries: 3
# Created: 2024-10-07T12:34:56.000Z

# Multilingual glossary example
deepl glossary show multilingual-terms
# Name: multilingual-terms
# ID: def456...
# Source language: en
# Target languages: es, fr, de
# Type: Multilingual
# Total entries: 15
#
# Language pairs:
#   en → es: 5 entries
#   en → fr: 5 entries
#   en → de: 5 entries
# Created: 2024-10-08T10:00:00.000Z
delete <name-or-id>

Delete a glossary by name or ID.

Options:

  • -y, --yes - Skip confirmation prompt
  • --dry-run - Show what would be deleted without performing the operation

Example:

deepl glossary delete tech-terms
deepl glossary delete abc-123-def-456
deepl glossary delete tech-terms --dry-run
entries <name-or-id> [--target-lang <lang>]

Get glossary entries in TSV format (suitable for backup or editing).

Arguments:

  • name-or-id - Glossary name or ID

Options:

  • --target-lang <lang> - Target language (required for multilingual glossaries, optional for single-target)
  • --format <format> - Output format: text, json (default: text)

Behavior:

  • For single-target glossaries: --target-lang flag is optional (automatically uses the single target language)
  • For multilingual glossaries: --target-lang flag is required to specify which language pair to retrieve

Example:

# Single-target glossary (no --target-lang needed)
deepl glossary entries tech-terms > backup.tsv

# View entries
deepl glossary entries tech-terms
# API → API
# REST → REST
# authentication → Authentifizierung

# Multilingual glossary (--target-lang required)
deepl glossary entries multilingual-terms --target-lang es
# API → API
# cache → caché
# ...

deepl glossary entries multilingual-terms --target-lang fr
# API → API
# cache → cache
# ...
languages

List all supported glossary language pairs.

Description: Shows which source-target language combinations are available for glossary creation. Not all language pairs supported by DeepL translation are available for glossaries.

Example:

deepl glossary languages
# en → de
# en → es
# en → fr
# de → en
# ...
add-entry <name-or-id> <source> <target> [--target-lang <lang>]

Add a new entry to an existing glossary.

Arguments:

  • name-or-id - Glossary name or ID
  • source - Source language term
  • target - Target language translation

Options:

  • --target-lang <lang> - Target language (required for multilingual glossaries, optional for single-target)

Behavior:

  • Uses v3 PATCH endpoint for efficient updates (no delete+recreate)
  • Glossary ID remains unchanged
  • Preserves all other entries
  • Fails if entry already exists

Examples:

# Add entry to single-target glossary
deepl glossary add-entry tech-terms "database" "Datenbank"

# Add entry to multilingual glossary (--target-lang required)
deepl glossary add-entry multilingual-terms "cache" "caché" --target-lang es
deepl glossary add-entry multilingual-terms "cache" "cache" --target-lang fr

# Add phrase
deepl glossary add-entry tech-terms "user interface" "Schnittstelle"

Note: v3 API uses PATCH for efficient updates. The glossary ID remains unchanged.

update-entry <name-or-id> <source> <new-target> [--target-lang <lang>]

Update an existing entry in a glossary.

Arguments:

  • name-or-id - Glossary name or ID
  • source - Source language term to update
  • new-target - New target language translation

Options:

  • --target-lang <lang> - Target language (required for multilingual glossaries, optional for single-target)

Behavior:

  • Updates existing entry's target text using v3 PATCH endpoint
  • Glossary ID remains unchanged
  • Fails if entry doesn't exist

Examples:

# Update entry in single-target glossary
deepl glossary update-entry tech-terms "API" "API (Programmierschnittstelle)"

# Update entry in multilingual glossary (--target-lang required)
deepl glossary update-entry multilingual-terms "API" "API (Interfaz)" --target-lang es
deepl glossary update-entry multilingual-terms "API" "API (Interface)" --target-lang fr

Note: v3 API uses PATCH for efficient updates. The glossary ID remains unchanged.

remove-entry <name-or-id> <source> [--target-lang <lang>]

Remove an entry from a glossary.

Arguments:

  • name-or-id - Glossary name or ID
  • source - Source language term to remove

Options:

  • --target-lang <lang> - Target language (required for multilingual glossaries, optional for single-target)

Behavior:

  • Removes entry from glossary using v3 PATCH endpoint
  • Glossary ID remains unchanged
  • Fails if entry doesn't exist
  • Fails if removing the last entry (delete glossary instead)

Examples:

# Remove entry from single-target glossary
deepl glossary remove-entry tech-terms "obsolete-term"

# Remove entry from multilingual glossary (--target-lang required)
deepl glossary remove-entry multilingual-terms "deprecated" --target-lang es

Note: You cannot remove the last entry from a glossary. If you need to remove all entries, use deepl glossary delete instead.

rename <name-or-id> <new-name>

Rename a glossary.

Arguments:

  • name-or-id - Glossary name or ID
  • new-name - New name for the glossary

Behavior:

  • Changes glossary name using v3 PATCH endpoint
  • Glossary ID remains unchanged
  • Preserves all entries and language pairs
  • Fails if new name matches current name

Examples:

# Rename by glossary name
deepl glossary rename tech-terms "Technical Terminology v2"

# Rename by glossary ID
deepl glossary rename abc-123-def-456 "Product Names 2024"

Note: v3 API uses PATCH for efficient rename. The glossary ID remains unchanged and all entries are preserved.

update <name-or-id> [--name <name>] [--target-lang <lang>] [--file <path>]

Update a glossary's name and/or dictionary entries in a single request.

Arguments:

  • name-or-id - Glossary name or ID

Options:

  • --name <name> - New glossary name
  • --target-lang <lang> - Target language for dictionary update (required when using --file)
  • --file <path> - TSV/CSV file with entries for dictionary update

Behavior:

  • At least one of --name or --file (with --target-lang) must be provided
  • When both --name and --file are given, the rename and dictionary update happen in a single PATCH request
  • --target-lang is required when --file is specified
  • Uses v3 PATCH endpoint for efficient updates
  • Glossary ID remains unchanged

Examples:

# Rename only
deepl glossary update my-terms --name "Updated Terms"

# Update dictionary entries only
deepl glossary update my-terms --target-lang de --file updated.tsv

# Rename and update dictionary in one request
deepl glossary update my-terms --name new-name --target-lang de --file updated.tsv
replace-dictionary <name-or-id> <target-lang> <file>

Replace all entries in a glossary dictionary from a TSV/CSV file (v3 API only). Unlike updating individual entries (which merges), this replaces the entire dictionary contents.

Arguments:

  • name-or-id - Glossary name or ID
  • target-lang - Target language of the dictionary to replace (e.g., es, fr, de)
  • file - TSV/CSV file path with replacement entries

Examples:

# Replace all Spanish entries from a new file
deepl glossary replace-dictionary my-glossary es new-entries.tsv
# ✓ Dictionary replaced successfully (es)

# Replace by glossary ID
deepl glossary replace-dictionary abc-123-def-456 fr updated-fr.tsv

Notes:

  • Replaces the entire dictionary via the v3 PUT endpoint (not a merge)
  • All existing entries for the specified language pair are removed and replaced with the file contents
  • The file format is the same as for glossary create (TSV or CSV)

delete-dictionary <name-or-id> <target-lang>

Delete a specific language pair from a multilingual glossary (v3 API only).

Arguments:

  • name-or-id - Glossary name or ID
  • target-lang - Target language of the dictionary to delete (e.g., es, fr, de)

Options:

  • -y, --yes - Skip confirmation prompt

Behavior:

  • Removes a specific language pair from a multilingual glossary using v3 DELETE endpoint
  • Glossary ID remains unchanged
  • Other language pairs in the glossary are preserved
  • Fails if glossary is single-target (use glossary delete instead)
  • Fails if this would be the last dictionary in the glossary (use glossary delete instead)
  • Fails if the dictionary doesn't exist in the glossary

Examples:

# Delete Spanish dictionary from multilingual glossary
deepl glossary delete-dictionary multilingual-terms es
# ✓ Dictionary deleted successfully (es)
# Other language pairs (fr, de) remain intact

# Delete by glossary ID
deepl glossary delete-dictionary abc-123-def-456 fr
# ✓ Dictionary deleted successfully (fr)

Notes:

  • Multilingual glossaries only: This command only works with multilingual glossaries that have multiple target languages. For single-target glossaries, use deepl glossary delete to remove the entire glossary.
  • Preserves glossary: Unlike glossary delete, this command preserves the glossary and only removes one language pair.
  • Cannot delete last dictionary: If the glossary would have zero dictionaries after deletion, the command fails. Use glossary delete to remove the entire glossary instead.

tm

Manage translation memories. TM files are authored and uploaded via the DeepL web UI; this command surfaces the account's TMs so you can copy a name or UUID into a translate or sync invocation without leaving the terminal.

Synopsis

deepl tm list [options]

Subcommands

list

List all translation memories on the account.

Options:

  • --format <format> - Output format: text, json (default: text)

Output Format (text):

  • Per-TM: name (source → target[, target...]) — e.g. brand-terms (en → de, fr, ja). Control chars and zero-width codepoints are stripped from the rendered name to prevent a malicious API-returned name from corrupting the terminal via ANSI escape sequences.
  • Empty list: No translation memories found

Output Format (JSON):

Raw TranslationMemory[] as returned by GET /v3/translation_memories — fields: translation_memory_id, name, source_language, target_languages (array).

Example:

deepl tm list
# brand-terms (en → de, fr, ja)
# legal-phrases (en → fr)

deepl tm list --format json | jq '.[] | select(.name == "brand-terms") | .translation_memory_id'
# "3f2504e0-4f89-41d3-9a0c-0305e82c3301"

Related:

  • deepl translate --translation-memory <name-or-uuid> — use a listed TM on a single translate call.
  • .deepl-sync.yaml translation.translation_memory — configure a TM for a sync run (see sync).

cache

Manage translation cache.

Synopsis

deepl cache <SUBCOMMAND>

Subcommands

stats

Show cache statistics (status, entries count, size, percentage used).

Options:

  • --format <format> - Output format: text, json, table (default: text). In non-TTY output, table falls back to text with a WARN line on stderr.
clear

Clear all cache entries (displays: "✓ Cache cleared successfully").

Options:

  • -y, --yes - Skip confirmation prompt
  • --dry-run - Show cache stats that would be cleared without performing the operation
enable

Enable cache (displays: "✓ Cache enabled").

Options:

  • --max-size <size> - Maximum cache size (e.g., 100M, 1G, 500MB)

Examples:

# Enable cache with default size
deepl cache enable

# Enable cache with custom size
deepl cache enable --max-size 100M
deepl cache enable --max-size 1G

Note: You can also configure max cache size separately: deepl config set cache.maxSize <bytes>

disable

Disable cache (displays: "✓ Cache disabled").


config

Manage CLI configuration.

Synopsis

deepl config <SUBCOMMAND>

Subcommands

list

List all configuration values (same as get without arguments).

Options:

  • --format <format> - Output format: text, json (default: json)

Examples:

# JSON output (default)
deepl config list

# Human-readable key=value pairs
deepl config list --format text
# auth.apiKey = "xxxx...xxxx"
# cache.enabled = true
# cache.maxSize = 1073741824
get [key]

Get a specific configuration value, or all values if key is omitted.

Arguments:

  • key (optional) - Configuration key in dot notation (e.g., cache.maxSize, auth.apiKey)

Options:

  • --format <format> - Output format: text, json (default: json)

Examples:

# Get all configuration (JSON)
deepl config get

# Get specific value
deepl config get cache.maxSize

# Human-readable output
deepl config get cache.maxSize --format text
# cache.maxSize = 1073741824
set <key> <value>

Set a configuration value.

Arguments:

  • key - Configuration key in dot notation
  • value - Value to set

Examples:

deepl config set cache.maxSize 52428800
deepl config set defaults.formality more
reset

Reset configuration to defaults (keeps API key).

Options:

  • -y, --yes - Skip confirmation prompt

usage

Show API usage statistics.

Synopsis

deepl usage [OPTIONS]

Description

Display your DeepL API character usage and remaining quota. Helps you monitor consumption and avoid exceeding your account limits.

Options

  • --format FORMAT - Output format: text, json, table (default: text). In non-TTY output, table falls back to text with a WARN line on stderr.

Examples

# Show usage statistics (Free account)
deepl usage
# Character Usage:
#   Used: 123,456 / 500,000 (24.7%)
#   Remaining: 376,544

# Pro account output (additional sections)
deepl usage
# Character Usage:
#   Used: 2,150,000 / 20,000,000 (10.8%)
#   Remaining: 17,850,000
#
# Billing Period:
#   2025-04-24 to 2025-05-24
#
# API Key Usage:
#   Used: 1,880,000 / unlimited
#
# Product Breakdown:
#   translate: 900,000 characters (API key: 880,000)
#   write: 1,250,000 characters (API key: 1,000,000)
#   speech_to_text: 12m 34s (API key)

Output Fields:

  • Used: Number of characters translated this billing period
  • Limit: Total character limit for your account
  • Percentage: Usage as a percentage of total quota
  • Remaining: Characters remaining in your quota

Pro accounts show additional fields:

  • Billing Period: Start and end dates of the current billing cycle
  • API Key Usage: Characters used by this specific API key (vs. the whole account)
  • Product Breakdown: Per-product character counts (translate, write) and durations (speech_to_text) with the API-key-level figure alongside. A duration-billed product shows (API key) when the response carries no account-wide total, rather than repeating the key's own figure as if it were one.

Notes:

  • Usage resets monthly for most accounts
  • Free tier: typically 500,000 characters/month
  • Pro accounts: varies by subscription level; additional sections shown automatically
  • Shows warning when usage exceeds 80%

languages

List supported source and target languages.

Synopsis

deepl languages [OPTIONS]

Description

Display all 125 supported languages grouped by category. Core and regional languages are shown first, followed by extended languages. When an API key is configured, language names are fetched from the DeepL API; otherwise, the local language registry is used.

You can filter to show only source languages, only target languages, or both (default).

Options

  • --source, -s - Show only source languages
  • --target - Show only target languages
  • --features - Show which features each language supports (requires an API key; the local registry carries no feature data)
  • --format FORMAT - Output format: text, json, table (default: text). In non-TTY output, table falls back to text with a WARN line on stderr.

Examples

# Show all supported languages (both source and target)
deepl languages
# Source Languages:
#   ar    Arabic
#   bg    Bulgarian
#   ...
#   zh    Chinese
#
#   Extended Languages (quality_optimized only, no formality/glossary):
#   ace   Acehnese
#   af    Afrikaans
#   ...
#
# Target Languages:
#   ar        Arabic [F]
#   ...
#   en-gb     English (British)
#   en-us     English (American)
#   ...
#
#   Extended Languages (quality_optimized only, no formality/glossary):
#   ace       Acehnese
#   ...
#
#   [F] = supports formality parameter

# Show only source languages
deepl languages --source

# Show only target languages
deepl languages --target

# Show which features each language supports
deepl languages --target --features
# Target Languages:
#   de        German — formality, glossary, style rules, translation memory, auto detection
#   pt        Portuguese — formality, glossary, auto detection
#   en-gb     English (British) — glossary, style rules, translation memory
#   ...
#   Extended Languages (quality_optimized only, no formality/glossary):
#   hi        Hindi — auto detection
#   th        Thai — style rules, translation memory, auto detection
#   ...
#
#   All listed languages also support: tag handling.

# The same matrix as columns
deepl languages --target --features --format table

# Works without API key (shows local registry data)
deepl languages
# Note: No API key configured. Showing local language registry only.

Output Format:

  • Languages are grouped: core/regional first, then extended in a separate section
  • Extended languages are annotated with "quality_optimized only, no formality/glossary"
  • Target languages that support the --formality parameter are marked with [F] (requires API key)
  • Language codes are left-aligned and padded for readability

Feature matrix (--features):

  • Feature support comes from GET /v3/languages; a feature is supported when the API reports it for that language
  • Which features are shown is derived from the response, not a fixed list. A feature only appears when its support differs across the languages listed; one supported by all of them is reported once as All listed languages also support: ... instead of being repeated on every row
  • Because of that, the columns differ between listings: auto detection appears under --target (target-only variants lack it) but is uniform under --source
  • A feature that is not yet generally available shows its status instead of yes, e.g. glossary (beta)
  • --features replaces the [F] shorthand, since formality is one of the reported features
  • --format json includes a raw features object with each feature's status, but only when --features is passed

Where the language list comes from:

  • GET /v3/languages is the authority on which languages exist. The CLI bundles a snapshot of it so that listing and validating languages works offline and without an API key
  • The snapshot is generated, not hand-maintained (npm run generate:languages; npm run check:languages reports drift). Tiers are derived from the response — glossary support separates extended from the rest, source usability separates core from regional — so they are not a separate judgement that can disagree with the API
  • Because the snapshot can lag the API, a well-formed language code it does not list is accepted and sent to the API, which accepts or rejects it authoritatively. Input that is not shaped like a language tag is still rejected locally, with a pointer to deepl languages. This applies to translate, sync and to language values in the config file
  • The listing itself is the union of the API response and the snapshot, so a language DeepL offers is never hidden even if the snapshot predates it

Notes:

  • Source and target language lists differ: 11 regional variants (de-ch, de-de, en-gb, en-us, es-419, fr-ca, fr-fr, pt-br, pt-pt, zh-hans, zh-hant) are target-only
  • Extended languages (82 codes) only support quality_optimized model type and do not support formality or glossary features
  • The API reports the same display name for a bare code and its explicit-region variant — both de and de-de are "German", both fr and fr-fr are "French". The CLI mirrors the API rather than inventing distinct names; the code column distinguishes them
  • The extended tier is a coarser signal than the feature matrix: some extended languages do support style rules and translation memory even though they support neither formality nor glossary
  • Without an API key, the command shows all languages from the local registry with a warning; --features additionally warns that it needs a key

detect

Detect the language of text using DeepL API.

Synopsis

deepl detect [OPTIONS] [text]

Description

Detect the language of the given text. Under the hood, this command calls the DeepL translate API with a dummy target language and returns only the detected_source_language field from the response.

Text can be provided as a positional argument or piped via stdin.

Options

  • --format <format> - Output format: text, json (default: text)

Examples

# Detect language of French text
deepl detect "Bonjour le monde"
# Detected language: French (fr)

# Detect language with JSON output
deepl detect "Hallo Welt" --format json
# {
#   "detected_language": "de",
#   "language_name": "German"
# }

# Pipe text via stdin
echo "Ciao mondo" | deepl detect
# Detected language: Italian (it)

# Use in a script
LANG=$(deepl detect "Hola" --format json | jq -r '.detected_language')
echo "$LANG"  # es

Notes:

  • Requires an API key (the detection uses a translate API call)
  • Each detection call consumes character quota (the text is translated to produce the detection)
  • Very short text (single characters or words) may produce unreliable detection results
  • Supports all 125 languages recognized by the DeepL API (core, regional, and extended)

completion

Generate shell completion scripts for bash, zsh, or fish.

Synopsis

deepl completion <shell>

Arguments

  • shell - Shell type: bash, zsh, fish

Examples

# Generate and install bash completions
deepl completion bash > /etc/bash_completion.d/deepl

# Generate and install zsh completions
deepl completion zsh > "${fpath[1]}/_deepl"

# Generate and install fish completions
deepl completion fish > ~/.config/fish/completions/deepl.fish

# Or source directly in your current session:
source <(deepl completion bash)
eval "$(deepl completion zsh)"
deepl completion fish | source

init

Interactive setup wizard for first-time users.

Synopsis

deepl init

Description

Guides through API key setup, default target language selection, and basic configuration. Validates the API key against the DeepL API before saving.

Examples

# Run the interactive setup wizard
deepl init

# Output:
# Welcome to DeepL CLI! Let's get you set up.
# Enter your DeepL API key: ****
# ✓ API key validated (DeepL API Free)
# Select default target language: es
# ✓ Configuration saved
# You're ready! Try: deepl translate "Hello" --to es

auth

Manage API authentication.

Synopsis

deepl auth <SUBCOMMAND>

Subcommands

set-key [api-key]

Set your DeepL API key and validate it with the DeepL API.

Arguments:

  • api-key (optional) - Your DeepL API authentication key. If omitted, reads from stdin.

Options:

  • --from-stdin - Read API key from stdin
  • --no-verify - Store the key without validating it against the API. Use on offline or proxied networks, where validation cannot reach the API and the key would otherwise be discarded. Exports of DEEPL_API_KEY also bypass validation entirely.

Examples:

# Pipe key from stdin (recommended - avoids exposing key in process listings)
echo "YOUR-API-KEY" | deepl auth set-key --from-stdin

# Read from file
deepl auth set-key --from-stdin < ~/.deepl-api-key

# Provide key as argument
deepl auth set-key YOUR-API-KEY-HERE
# ✓ API key saved and validated successfully

# Store without a network round-trip (offline, or behind an unconfigured proxy)
echo "YOUR-API-KEY" | deepl auth set-key --from-stdin --no-verify
# ✓ API key saved without validation

Security Note: Prefer --from-stdin over passing the key as a command argument. Command arguments are visible to other users via process listings (ps aux).

Deprecation: Passing the API key as a positional argument is deprecated and will emit a warning. Use --from-stdin instead for secure key input.

show

Show current API key (masked for security).

Output Format: API Key: abcd...xyz1 (first 4 and last 4 characters visible)

Examples:

deepl auth show
# API Key: 1234...abcd

deepl auth show
# No API key set
clear

Clear stored API key from configuration.

Examples:

deepl auth clear
# ✓ API key removed

style-rules

Manage DeepL style rules (Pro API only). Style rules carry a list of configured rule ids and optional custom instructions, and apply to translations via their style id.

Synopsis

deepl style-rules <SUBCOMMAND>

Subcommands

list

List all available style rules.

Options:

  • --detailed - Show detailed information including configured rules and custom instructions
  • --page NUMBER - Page number for pagination
  • --page-size NUMBER - Number of results per page (1-25)
  • --format FORMAT - Output format: text, json, table (default: text). In non-TTY output, table falls back to text with a WARN line on stderr.

Examples:

deepl style-rules list
deepl style-rules list --detailed
deepl style-rules list --format json
deepl style-rules list --format table
deepl style-rules list --page 1 --page-size 10
create

Create a new style rule list.

Options:

  • --name NAME - Style rule name (required)
  • --language LANG - Target language (required)
  • --rules JSON - Configured rules as a JSON object of category → settings (optional). The DeepL API models configured rules as a two-level dictionary; arrays are not accepted. See "Configured rules shape" below.
  • --format FORMAT - Output format: text, json (default: text)

Examples:

deepl style-rules create --name "Corporate" --language en
deepl style-rules create --name "Québécois" --language fr \
  --rules '{"punctuation":{"quotation_mark":"use_guillemets"}}'

Configured rules shape:

The DeepL API expects configured_rules as a nested object — a category name maps to a settings object, and each setting maps to a string value. Empty rules are {}. Example for fr-CA:

{
  "punctuation": {
    "quotation_mark": "use_guillemets",
    "spacing_and_punctuation": "do_not_use_space"
  },
  "spelling_and_grammar": {
    "accents_and_cedillas": "use_even_on_capital_letters"
  }
}

The available categories, settings, and accepted values are defined by the DeepL API; consult DeepL's API documentation for the current rule schema.

show

Show a single style rule.

Arguments:

  • <id> - Style rule id (required)

Options:

  • --detailed - Include configured rules and custom instructions
  • --format FORMAT - Output format: text, json (default: text)

Examples:

deepl style-rules show sr-abc123
deepl style-rules show sr-abc123 --detailed --format json
update

Update a style rule — rename and/or replace configured rules. At least one of --name / --rules is required. When both are provided, the rename (PATCH) runs first, then the rules replacement (PUT /configured_rules).

Arguments:

  • <id> - Style rule id (required)

Options:

  • --name NAME - New name
  • --rules JSON - Replace configured rules with a JSON object of category → settings (see "Configured rules shape" under create)
  • --format FORMAT - Output format: text, json (default: text)

Examples:

deepl style-rules update sr-abc123 --name "Renamed"
deepl style-rules update sr-abc123 --rules '{"punctuation":{"quotation_mark":"use_guillemets"}}'
deepl style-rules update sr-abc123 --name "New" --rules '{"spelling_and_grammar":{"accents_and_cedillas":"use_even_on_capital_letters"}}'
delete

Delete a style rule. Prompts for confirmation on a TTY; use --yes to skip or --dry-run to preview.

Arguments:

  • <id> - Style rule id (required)

Options:

  • -y, --yes - Skip confirmation prompt
  • --dry-run - Show what would be deleted without performing the operation

Examples:

deepl style-rules delete sr-abc123
deepl style-rules delete sr-abc123 --yes
deepl style-rules delete sr-abc123 --dry-run
instructions

List custom instructions attached to a style rule. Synthesized from the detailed show response.

Arguments:

  • <style-id> - Style rule id (required)

Options:

  • --format FORMAT - Output format: text, json, table (default: text). In non-TTY output, table falls back to text with a WARN line on stderr.

Examples:

deepl style-rules instructions sr-abc123
deepl style-rules instructions sr-abc123 --format json
deepl style-rules instructions sr-abc123 --format table
add-instruction

Add a custom instruction to a style rule.

Arguments:

  • <style-id> - Style rule id (required)
  • <label> - Instruction label, unique within the rule (required)
  • <prompt> - Instruction prompt text (required)

Options:

  • --source-language LANG - Source language code (optional)
  • --format FORMAT - Output format: text, json (default: text)

Examples:

deepl style-rules add-instruction sr-abc123 tone "Be formal"
deepl style-rules add-instruction sr-abc123 register "Use first person" --source-language en
update-instruction

Update the prompt and/or source language of an existing custom instruction. The label cannot be changed (it is the identifier); rename by removing and re-adding.

Arguments:

  • <style-id> - Style rule id (required)
  • <label> - Instruction label (required)
  • <prompt> - New instruction prompt text (required)

Options:

  • --source-language LANG - Source language code (optional)
  • --format FORMAT - Output format: text, json (default: text)

Examples:

deepl style-rules update-instruction sr-abc123 tone "Be friendlier"
remove-instruction

Remove a custom instruction from a style rule. Prompts for confirmation on a TTY.

Arguments:

  • <style-id> - Style rule id (required)
  • <label> - Instruction label (required)

Options:

  • -y, --yes - Skip confirmation prompt
  • --dry-run - Show what would be removed without performing the operation

Examples:

deepl style-rules remove-instruction sr-abc123 tone --yes
deepl style-rules remove-instruction sr-abc123 tone --dry-run

Notes

  • Style rules are Pro API only and datacenter-specific (EU and US rules don't cross).
  • Use the style id with deepl translate --style-id <uuid> to apply a rule at translation time.
  • Style rules force the quality_optimized model type.
  • Text-format output of stored user strings (rule names, instruction labels, instruction prompts) passes through a terminal-escape sanitizer to prevent injection of control characters from the API response. JSON-format output preserves raw strings (JSON-escaped at the encoding layer).

admin

Admin API for managing API keys and viewing organization usage analytics. Requires an admin-level API key.

Synopsis

deepl admin <SUBCOMMAND>

Subcommands

keys list

List all API keys in the organization.

Options:

  • --format FORMAT - Output format: text, json (default: text)

Examples:

# List all API keys
deepl admin keys list

# JSON output
deepl admin keys list --format json
keys create

Create a new API key.

Options:

  • --label LABEL - Label for the new key
  • --format FORMAT - Output format: text, json (default: text)

Examples:

# Create a key with a label
deepl admin keys create --label "Production Key"

# Create a key without a label
deepl admin keys create

# JSON output
deepl admin keys create --label "CI Key" --format json
keys deactivate

Deactivate an API key (permanent, cannot be undone).

Arguments:

  • <key-id> - Key ID to deactivate (required)

Options:

  • -y, --yes - Skip confirmation prompt

Examples:

deepl admin keys deactivate abc123-def456
deepl admin keys deactivate abc123-def456 --yes
keys rename

Rename an API key.

Arguments:

  • <key-id> - Key ID to rename (required)
  • <label> - New label (required)

Examples:

deepl admin keys rename abc123-def456 "New Label"
keys set-limit

Set character usage limit for an API key.

Arguments:

  • <key-id> - Key ID (required)
  • <characters> - Character limit (number or "unlimited") (required)

Options:

  • --stt-limit <milliseconds> - Speech-to-text milliseconds limit (number or unlimited)

Examples:

# Set a limit of 1 million characters
deepl admin keys set-limit abc123-def456 1000000

# Remove the limit
deepl admin keys set-limit abc123-def456 unlimited

# Set character limit and speech-to-text limit together
deepl admin keys set-limit abc123-def456 1000000 --stt-limit 3600000
usage

View organization usage analytics with per-product character breakdowns.

Options:

  • --start DATE - Start date in YYYY-MM-DD format (required)
  • --end DATE - End date in YYYY-MM-DD format (required)
  • --group-by GROUPING - Group results: key, key_and_day
  • --format FORMAT - Output format: text, json (default: text)

Output includes:

  • Total characters across all products
  • Text translation characters — characters used for /v2/translate
  • Document translation characters — characters used for document translation
  • Text improvement characters — characters used for DeepL Write

Examples:

# View total usage for a date range
deepl admin usage --start 2024-01-01 --end 2024-12-31

# Group usage by key
deepl admin usage --start 2024-01-01 --end 2024-12-31 --group-by key

# Daily usage per key
deepl admin usage --start 2024-01-01 --end 2024-01-31 --group-by key_and_day

# JSON output
deepl admin usage --start 2024-01-01 --end 2024-12-31 --format json

Example output:

Period: 2024-01-01 to 2024-01-31

Total Usage:
  Total:       10,000
  Translation: 7,000
  Documents:   2,000
  Write:       1,000

Per-Key Usage (2 entries):

  Staging Key
    Total:       6,000
    Translation: 4,000
    Documents:   1,500
    Write:       500

  Production Key
    Total:       4,000
    Translation: 3,000
    Documents:   500
    Write:       500

Notes

  • Admin API endpoints require an admin-level API key (not a regular developer key)
  • Key deactivation is permanent and cannot be undone
  • Usage analytics show per-product character breakdowns (translation, documents, write)
  • The --group-by option provides granular breakdowns for cost allocation

Configuration

Configuration file location:

The CLI resolves configuration and cache paths using the following priority order:

Priority Condition Config path Cache path
1 DEEPL_CONFIG_DIR set $DEEPL_CONFIG_DIR/config.json $DEEPL_CONFIG_DIR/cache.db
2 ~/.deepl-cli/ exists ~/.deepl-cli/config.json ~/.deepl-cli/cache.db
3 XDG env vars set $XDG_CONFIG_HOME/deepl-cli/config.json $XDG_CACHE_HOME/deepl-cli/cache.db
4 Default ~/.config/deepl-cli/config.json ~/.cache/deepl-cli/cache.db

Existing ~/.deepl-cli/ installations continue to work with no changes needed.

Configuration Schema

{
  "auth": {
    "apiKey": "your-api-key"
  },
  "api": {
    "baseUrl": "https://api.deepl.com",
    "usePro": true
  },
  "defaults": {
    "sourceLang": null,
    "targetLangs": [],
    "formality": "default",
    "preserveFormatting": true
  },
  "cache": {
    "enabled": true,
    "maxSize": 1073741824,
    "ttl": 2592000
  },
  "output": {
    "format": "text",
    "verbose": false,
    "color": true
  },
  "watch": {
    "debounceMs": 500,
    "autoCommit": false,
    "pattern": "*.md"
  },
  "tms": {
    "allowedServers": []
  }
}

Configuration Notes:

  • baseUrl — when set to a custom/regional endpoint (e.g. https://api-jp.deepl.com), it overrides all auto-detection. Standard DeepL URLs (api.deepl.com, api-free.deepl.com) are treated as tier defaults and do not override key-based auto-detection. By default, the endpoint is auto-detected from the API key: keys ending with :fx use the Free API (api-free.deepl.com), all others use the Pro API (api.deepl.com). The usePro flag serves as a backward-compatible fallback for non-:fx keys.
  • tms.allowedServers — hostnames approved as TMS destinations for an environment-supplied TMS_API_KEY / TMS_TOKEN. Empty by default, so no destination is trusted implicitly. Entries must be bare hostnames (no scheme, port, path, or wildcard) because they are matched against a parsed URL hostname, exactly and case-insensitively — a listed example.com does not approve tms.example.com. Set it with deepl config set tms.allowedServers a.example.com,b.example.com; a single host is still stored as a one-element array. See sync push for how the gate behaves.
  • Most users configure settings via deepl config set command rather than editing the file directly.

Terminal Output Safety

Translations, i18n keys, glossary entries and API error messages can all contain bytes the CLI did not author. Terminal control sequences hidden in that text can set the window title, write the clipboard (OSC 52), erase the screen, or forge a plausible-looking result line. The CLI neutralizes them, replacing each with ?:

Stream When Behavior
stderr (progress, warnings, errors) Always Control sequences replaced. Non-TTY stderr is still rendered by CI log viewers that interpret ANSI.
stdout, when it is a terminal Always Control sequences replaced.
stdout, when redirected to a file, a pipe or command substitution Never Byte-for-byte identical to what the API returned, so deepl translate ... > out.txt is lossless.
Report lines that interpolate untrusted values (for example the key in deepl sync validate) Always Control sequences replaced whether or not stdout is a terminal, since these are diagnostics, not data.

Colour (SGR) sequences are preserved so deepl output stays readable; use NO_COLOR to turn colour off. Tabs, newlines, carriage returns and Unicode formatting characters that are legitimate translation content — zero-width joiners, and the bidi marks used in Arabic, Hebrew and Persian text — are never altered on stdout.

deepl sync export writes XLIFF rather than terminal output. There, tab, newline and carriage return are emitted as the character references &#9;, &#10; and &#13; so a key survives XML attribute-value normalization unchanged, and the remaining C0 control characters — which XML 1.0 cannot represent at all — are replaced with ?.


Environment Variables

DEEPL_API_KEY

Set your API key via environment variable.

export DEEPL_API_KEY="your-api-key"
deepl translate "Hello" --to es

DEEPL_CONFIG_DIR

Override config and cache directory. Takes highest priority over all other path resolution.

export DEEPL_CONFIG_DIR="/custom/path"

XDG_CONFIG_HOME

Override XDG config base directory (default: ~/.config). Config is stored at $XDG_CONFIG_HOME/deepl-cli/config.json. Only used when DEEPL_CONFIG_DIR is unset and legacy ~/.deepl-cli/ does not exist.

export XDG_CONFIG_HOME="$HOME/.config"

XDG_CACHE_HOME

Override XDG cache base directory (default: ~/.cache). Cache is stored at $XDG_CACHE_HOME/deepl-cli/cache.db. Only used when DEEPL_CONFIG_DIR is unset and legacy ~/.deepl-cli/ does not exist.

export XDG_CACHE_HOME="$HOME/.cache"

NO_COLOR

Disable colored output.

export NO_COLOR=1

FORCE_COLOR

Force colored output even when the terminal doesn't appear to support it. Note: NO_COLOR takes priority if both are set. Useful in CI environments.

export FORCE_COLOR=1

TERM

When set to dumb, disables colored output and progress spinners. This is automatically set by some CI environments and editors.

export TERM=dumb

HTTP_PROXY

Route outbound DeepL API requests through an HTTP proxy. Accepts a full URL including optional username:password@ credentials. Also recognized as lowercase http_proxy.

export HTTP_PROXY="http://proxy.example.com:3128"

HTTPS_PROXY

Route outbound DeepL API requests through an HTTPS proxy. Takes precedence over HTTP_PROXY when both are set. Also recognized as lowercase https_proxy.

export HTTPS_PROXY="http://proxy.example.com:3128"

NO_PROXY

Comma-separated list of hosts that bypass HTTP_PROXY / HTTPS_PROXY. Standard semantics apply: * bypasses everything, a leading dot or *. matches subdomains, and an entry may carry a host:port that must agree with the target port. Also recognized as lowercase no_proxy.

export NO_PROXY="localhost,127.0.0.1,.internal.example.com"

TMS_API_KEY

API key used by deepl sync push and deepl sync pull to authenticate against the external translation management system configured under tms.server in .deepl-sync.yaml. See docs/SYNC.md for setup details.

export TMS_API_KEY="your-tms-api-key"

TMS_TOKEN

Bearer token alternative to TMS_API_KEY. Used by deepl sync push and deepl sync pull when the configured TMS server expects token-based auth. See docs/SYNC.md for setup details.

export TMS_TOKEN="your-tms-token"

Exit Codes

Every deepl command returns a specific exit code so CI/CD pipelines and shell scripts can react programmatically to failure modes. This appendix is the single source of truth for every code the CLI emits; per-command sections above surface exit codes inline where a flag has a code-specific contract (for example, sync --frozen exits 10 on drift).

Exit codes come from three paths:

  1. Typed errors thrown in services, API clients, and commands subclass DeepLCLIError, each carrying a fixed exitCode. The CLI's top-level handleError uses that value directly.
  2. HTTP responses from the DeepL API are mapped to typed errors inside the HTTP client (401 → AuthError, 429 → RateLimitError, 456 → QuotaError, 503 → NetworkError).
  3. Untyped errors (plain Error instances that escape service boundaries) are classified by message against a curated list of substrings. When nothing matches, the CLI returns 1 (general error).

Retryable codes are 3 (rate limit) and 5 (network); everything else should be treated as fatal by calling scripts.

Quick reference

Code Name Meaning Retryable
0 Success Command completed successfully N/A
1 GeneralError Unclassified failure (error escaped every typed handler and matched no classifier heuristic) No
2 AuthError Authentication failed or API key missing No
3 RateLimitError Rate limit exceeded (HTTP 429) Yes
4 QuotaError Monthly character quota exhausted (HTTP 456) No
5 NetworkError Connection timeout, refused, reset, truncated response body, or 503 Service Unavailable Yes
6 InvalidInput Missing or malformed arguments, unsupported format No
7 ConfigError Configuration file or value invalid No
8 CheckFailed A check-style command found actionable issues No
9 VoiceError Voice API unavailable or session failed No
10 SyncDrift sync --frozen detected translations out of date No
11 SyncConflict sync resolve could not auto-resolve lockfile conflicts No
12 PartialFailure deepl sync completed with at least one failed key, or a deepl watch session ended with a failed translation or auto-commit Yes (per-locale retry)

Code details

0 — Success

Command completed without error. Every deepl subcommand uses this code on success. Do not rely on stdout being non-empty — successful commands may emit only a status line (e.g., deepl cache clear).

1 — GeneralError

Unclassified failure: emitted when an error escapes every typed handler and matches none of the message-classification heuristics in src/utils/exit-codes.ts. Any command can surface this. Treat it as "unknown failure — inspect stderr." Typically indicates an unexpected CLI bug or an error from a third-party dependency.

CI branching. Exit 1 now means exactly "unclassified failure." Partial sync failure has its own code — see #12 — PartialFailure. A CI script can safely treat exit 1 as "CLI crashed, investigate" without misreading a partial-locale outcome.

2 — AuthError

Authentication failed or no API key is available. Emitted by:

  • deepl auth set-key when the key cannot be validated
  • Every command that touches the API (translate, write, voice, glossary, usage, sync, tm list, admin, etc.) when DEEPL_API_KEY is unset and no key is in the config file
  • HTTP 401/403 responses from the DeepL API

Remediation: run deepl init or deepl auth set-key <your-api-key>, or export DEEPL_API_KEY.

3 — RateLimitError

Too many requests in too short a window. Emitted when the DeepL API returns HTTP 429 from any endpoint (/v2/translate, /v2/write, /v3/glossaries, /v3/translation_memories, document upload/download, voice session). The CLI honors the Retry-After header when the server sends one, otherwise it falls back to exponential backoff for in-process retries. When all internal retries are exhausted, this code is returned to the caller.

Remediation: wait and retry, or lower concurrency with --concurrency (batch translation, sync).

4 — QuotaError

Monthly character quota has been exhausted (HTTP 456). Emitted by any command that consumes characters: translate, write, voice, and sync. Unlike rate limits, quota is not retryable within the billing window.

Remediation: run deepl usage to see remaining characters, or upgrade the plan at https://www.deepl.com/pro.

5 — NetworkError

Connection-layer failure or transient server outage. Covers TCP errors (ECONNREFUSED, ENOTFOUND, ECONNRESET, ETIMEDOUT, socket hang up), timeouts, proxy misconfigurations, and HTTP 503 responses. Also emitted for malformed or empty API responses thrown from src/api/translation-client.ts and src/api/write-client.ts, and from document/structured-file translation when the polling response is unparseable.

A response that came back without the placeholder tokens the CLI substituted for your variables counts as malformed here too: translate names the variables it lost ({username}, %s) and writes nothing rather than leaving its own internal tokens in your text. A directory run fails only the affected files and reports each one in the summary, so its exit code follows the usual batch mapping (1 when nothing translated, 12 when some did).

A response the endpoint cuts off mid-body is exit 5 as well, even though the status line said 200. The HTTP status is what decides it: a rejection carrying a 2xx response means the exchange was accepted and then failed while the body was read — truncated, or undecodable — so the message names the status and what went wrong (Network error: the API answered HTTP 200 but its response body did not arrive intact: stream has been aborted) rather than reporting an API error your input could fix. The replay policy is unchanged by this. A 200 says the server accepted, and may have billed, the request, so a truncated response to a POST (translate, write, a sync batch) is still sent exactly once; an idempotent GET is still replayed up to --max-retries times.

A document upload response whose document_id is not a document identifier is rejected here as well. That value is interpolated into the path of every follow-up request (POST /v2/document/{id}, POST /v2/document/{id}/result), so an ID containing .. or / would send this client's own requests to a different route on the endpoint. Anything outside [A-Za-z0-9_-]+ is refused before the next request is sent, the ID is quoted back in the message, and no output file is written.

A TMS that cannot be reached is exit 5 too, however it fails to answer. deepl sync push / pull used to report a refused connection or an unresolvable hostname as Error: fetch failed at exit 1 — the unclassified code — because fetch rejects with a bare TypeError and puts the errno on cause. Both now name the request and the code (TMS request failed: PUT https://tms.example.com/api/projects/p/keys/greeting: ECONNREFUSED), matching the client-side timeout, which was already exit 5. The replay policy is unchanged by this: retry eligibility is decided before the error is classified, so a refusal is still retried up to tms.retry.max_attempts and an unresolvable name still fails on the first attempt.

Remediation: check connectivity and HTTPS_PROXY / HTTP_PROXY env vars, then retry.

6 — InvalidInput

User-supplied input was rejected by client-side validation before any API call. This is the most commonly emitted non-zero code. Sites include:

  • translate: empty text, missing --to, unsupported file format, invalid --tm-threshold range, --tm-threshold without --translation-memory, --translation-memory without --from, mutually exclusive flags
  • write: empty text, --style and --tone used together, --fix without a file path, unsupported language for the Write API
  • voice: unsupported target or source language code, unsupported --content-type, more than 5 target languages. A plan that does not include the Voice API is exit 9, not 6 — see 9 — VoiceError
  • glossary: missing name/entries, entry not found on delete
  • sync: --frozen combined with --force, --watch combined with --force, --force without --yes anywhere the confirmation prompt cannot be shown (piped stdin, cron, a git hook, --no-input, CI=true), missing .deepl-sync.yaml (before ConfigError hands off)
  • hooks, watch, detect, admin, init, completion, cache: argument parsing, unknown subcommand, bad path, bad size

Remediation: re-read the command's --help and the relevant section of this API reference.

7 — ConfigError

The configuration file or a configuration value is invalid. Emitted by:

  • deepl config set with a key that is not in the schema, or a value that fails validation (invalid language code, invalid formality, invalid output format, non-positive cache size, non-HTTPS baseUrl, path-traversal attempts)
  • deepl config get with a malformed key
  • Any command that loads the config file when the file fails to parse, is missing a required field, or specifies an unsupported version
  • sync when .deepl-sync.yaml is missing required fields, has invalid locales, or declares an unsupported version
  • sync push / sync pull when the remote TMS returns 401/403 (surfaced as ConfigError with a hint to check TMS_API_KEY / TMS_TOKEN and the relevant YAML fields)
  • sync push / sync pull when .deepl-sync.yaml names a tms.server hostname that is not in tms.allowedServers and the approval prompt is unavailable (--no-input, non-TTY) or declined
  • glossary when a named glossary cannot be resolved

Remediation: run deepl config get to inspect the current config, or edit the file directly and re-run.

8 — CheckFailed

A check-style command ran successfully but found actionable issues. Emitted by:

  • deepl write --check <text|file> and deepl correct --check <text|file> when the Write API would suggest changes (needsImprovement === true). Under --format json the same run also emits the ok: true check result payload on stdout, so the count and the file are readable without parsing prose. Exit is soft here — process.exitCode is set so cleanup still runs
  • deepl sync validate when validation surfaces one or more error-severity issues: a placeholder present in the source and missing from the translation, a mismatched ICU bracket nesting depth (icu-brackets), or a translated ICU argument name, format type or selector keyword (icu-structure) — plus a target file on disk that could not be read, reported under an unusable_target check while every other locale is still validated. Issues the validator raises at warn severity never affect the exit code: missing HTML tags (html-tags), extra placeholders the source does not have, a translation identical to its source (untranslated), and an outlying length ratio (length-ratio); a run with warnings and no errors exits 0. Exit is hard here — process.exit(8) is called as soon as the report is written, so do not rely on later cleanup running

This code is specifically designed for CI use: a check step can block a merge without requiring try/catch wrappers in the calling script. It does not indicate a CLI failure.

9 — VoiceError

Voice API call failed for a reason other than authentication, rate limiting, or generic network trouble. Emitted by:

  • deepl voice when the plan does not include the Voice API (pre-flight check in the voice client)
  • Voice streaming URL validation failures (src/api/voice-client.ts: non-wss:// scheme, unparseable URL, disallowed host)
  • Voice session lifecycle errors (failed to open, unexpected close)
  • deepl voice when the stream ends with the source transcribed but no translation for a requested target language

Remediation: confirm Pro/Enterprise plan, verify the session configuration, and retry.

10 — SyncDrift

deepl sync --frozen (alias --ci) detected that lockfile-tracked translations are out of date with the source strings. Emitted only from src/cli/commands/sync/register-sync-root.ts when the sync run completes and result.driftDetected === true. No other command returns this code.

Use this in CI to fail a pull request when a contributor edits source strings without running deepl sync. Exit is softprocess.exitCode is set so in-flight writes, auto-commit steps, and any --watch event loop drain cleanly before the process exits.

Remediation: run deepl sync locally and commit the updated translations and lockfile.

11 — SyncConflict

deepl sync resolve found git merge conflict markers in .deepl-sync.lock but could not automatically resolve every region. Emitted only by sync resolve when auto-resolution leaves residual conflict markers or produces invalid JSON (typically because the conflict region split a JSON entry in two and neither side parses in isolation).

Distinct from exit code 1 (GeneralError): a pipeline that runs deepl sync resolve in CI can now branch on 11 to route the lockfile to a human for manual merge without masking real CLI crashes under the same code.

Remediation: open .deepl-sync.lock, resolve the remaining <<<<<<< / ======= / >>>>>>> regions by hand, save, and run deepl sync to fill any gaps.

12 — PartialFailure

deepl sync completed, but at least one key failed to translate. This covers both a locale that failed entirely and a locale that translated some of its keys and failed the rest — a failed key is absent from the written target file, so any failure count above zero means the run did not produce a complete result. The successful translations' target files and lockfile entries are written; a locale that failed entirely has its file left untouched. Emitted only by deepl sync (the root command).

A key whose translation failed placeholder/ICU validation counts as failed here too: it is withheld from the target file rather than written corrupt. See validation in the sync configuration reference. With validation.fail_on_error: true the same run raises ValidationError (exit 6) instead.

A key that translated successfully but that the target file's format could not be given a slot for also counts as failed. Every key is read back out of the content just written before the lockfile is updated, so the run reports it rather than recording a translation the file does not contain. See A string added after the first sync.

A locale whose target file is on disk but could not be read or parsed fails here as well, and fails before any translation is requested, so nothing is billed for it and the file is not written. That file is the only copy of its locale's translations, since the lockfile records source hashes rather than translated text. See A target file that cannot be read.

deepl watch uses the same code when the session ends: on Ctrl+C (SIGINT/SIGTERM) it exits 12 rather than 0 if it recorded any failed translation or any failed --auto-commit, so a script driving a watch session can tell a clean run from one that lost work. The counts are printed beside the translation total before the process exits.

Authentication failures (401/403) abort the entire run and surface as exit code 2 (AuthError) instead of 12. Network / rate-limit / quota failures bubble up as 5 / 3 / 4 respectively. Code 12 specifically means "the run proceeded far enough to attempt per-locale work, and the result was mixed."

Retry model. Re-running deepl sync (optionally with --locale <failed,comma,separated>) will retry only the locales whose lockfile entries weren't written. This is the canonical CI recovery path: branch on $? -eq 12, inspect the per-locale error report, and re-run with the failed locales as the filter.

The paired typed error is SyncPartialFailureError in src/utils/errors.ts; the JSON error envelope emits code: "SyncPartialFailure" to match the SyncConflict/SyncDrift naming convention.

Classification heuristics (fallback)

When an error reaches the top-level handler without being a DeepLCLIError, the CLI inspects the error message (lowercased) and maps it to a code. These substring matches live in classifyByMessage in src/utils/exit-codes.ts:

  • 2 — AuthError: authentication failed, invalid api key, api key not set, api key is required
  • 3 — RateLimitError: rate limit exceeded, too many requests, \b429\b
  • 4 — QuotaError: quota exceeded, character limit reached, \b456\b
  • 5 — NetworkError: econnrefused, enotfound, econnreset, etimedout, socket hang up, network error, network timeout, connection refused, connection reset, connection timed out, service temporarily unavailable, \b503\b
  • 9 — VoiceError: voice api, voice session
  • 7 — ConfigError (checked before 6 because config messages may contain "invalid"): config file, config directory, configuration file, configuration error, failed to load config, failed to save config, failed to read config
  • 6 — InvalidInput: cannot be empty, file not found, path not found, directory not found, not found in glossary, unsupported format, unsupported language, not supported for, not supported in, invalid target language, invalid source language, invalid language code, invalid glossary, invalid hook, invalid url, invalid size, is required, cannot specify both
  • 1 — GeneralError: anything not matched above

The list is in the order the classifier actually tests, which is what determines the code when a message matches more than one pattern.

Trace IDs

API error messages include the DeepL X-Trace-ID header when available, which is useful when contacting DeepL support:

deepl translate "Hello" --to es
# Error: Authentication failed: Invalid API key (Trace ID: abc123-def456-ghi789)

The trace ID is also accessible programmatically via DeepLClient.lastTraceId after any API call.

Shell handling examples

Retry only on retryable codes (3 and 5):

#!/bin/bash
deepl translate "Hello" --to es
case $? in
  0) echo "Success" ;;
  3|5) sleep 5 && deepl translate "Hello" --to es ;;
  *) echo "Non-retryable error ($?)"; exit $? ;;
esac

Fail a CI build when translations drift:

deepl sync --frozen || {
  code=$?
  [ $code -eq 10 ] && echo "::error::Translation drift — run 'deepl sync' locally" >&2
  exit $code
}

Block a merge when deepl write --check flags a file:

deepl write --check README.md
[ $? -eq 8 ] && echo "Write suggests improvements; run: deepl write --fix README.md" >&2

See Also


Last Updated: August 9, 2026 DeepL CLI Version: 2.0.0