Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2b47c62
docs: fix high-severity errors from the pre-release documentation audit
sjsyrek Jul 29, 2026
1a100d7
fix(cache): seed the in-memory enabled flag from config
sjsyrek Jul 29, 2026
d003dc4
fix(hooks): resolve the effective hooks directory and stop clobbering…
sjsyrek Jul 29, 2026
aceb999
fix(cache): persist cache enable/disable across processes
sjsyrek Jul 29, 2026
cbc8c1f
fix(formats): refuse Android CDATA values that close the section
sjsyrek Jul 29, 2026
48dc090
fix(sync): validate auto-glossary terms and make repeat syncs idempotent
sjsyrek Jul 29, 2026
775626c
fix(formats): bound YAML alias expansion in the parser walkers
sjsyrek Jul 29, 2026
429f60d
fix(glossary): reject separator characters in terms and sniff the dia…
sjsyrek Jul 29, 2026
865c3e7
fix(sync): route --locale and --sync-config to subcommand handlers
sjsyrek Jul 29, 2026
2299bfd
fix(api): retry only idempotent requests and classify transport failu…
sjsyrek Jul 29, 2026
fff3e16
fix(sync): reject a --locale value that is not in target_locales
sjsyrek Jul 29, 2026
c5af78b
Merge branch 'fix/cache-toggle-persist' into fix/release-blockers-batch
sjsyrek Jul 29, 2026
0248b82
Merge branch 'fix/yaml-alias-guard' into fix/release-blockers-batch
sjsyrek Jul 29, 2026
6b50a88
fix(sync): stop using the source hash as a translation identity in sy…
sjsyrek Jul 29, 2026
3ab502d
fix(sync): honor --sync-config in sync init
sjsyrek Jul 29, 2026
f886bb5
fix(api): never retry the document result endpoint and widen transfer…
sjsyrek Jul 29, 2026
a644a07
fix(sync): harden TMS request URLs, timeout classification and pull b…
sjsyrek Jul 29, 2026
99ad341
fix(sync): write .deepl-sync.yaml atomically
sjsyrek Jul 29, 2026
6fd0519
fix(voice): reconnect on transport failures and close the input on gi…
sjsyrek Jul 29, 2026
b8b67ab
fix(formats): scan Android XML and XLIFF elements in linear time
sjsyrek Jul 29, 2026
0f1a647
feat(sync)!: remove deprecated --source-lang/--target-langs from sync…
sjsyrek Jul 29, 2026
c585a19
Merge branch 'fix/git-hooks-glossary' into fix/release-blockers-batch
sjsyrek Jul 29, 2026
0e08eb5
feat(cli): add --timeout and --max-retries global options
sjsyrek Jul 29, 2026
24fec1f
Merge branch 'fix/sync-locale-routing' into fix/release-blockers-batch
sjsyrek Jul 29, 2026
beae0e6
Merge branch 'fix/parser-quadratic' into fix/release-blockers-batch
sjsyrek Jul 29, 2026
5f934dc
chore(changelog): record the release-blocker fix batch under Unreleased
sjsyrek Jul 29, 2026
75b820d
fix(api): keep error classification idempotent
sjsyrek Jul 29, 2026
3100657
style(api): restore the backoff doc comment and reword the socket-err…
sjsyrek Jul 29, 2026
76d5aec
test(api): order the retry-policy suite so replyWithError blocks run …
sjsyrek Jul 29, 2026
edc6a78
test(api): assert a POST on 429 is retried and honours Retry-After
sjsyrek Jul 29, 2026
a9585ea
Merge branch 'fix/http-retry-classify' into fix/release-blockers-batch
sjsyrek Jul 29, 2026
105ef6b
Merge branch 'fix/docs-audit' into fix/release-blockers-batch
sjsyrek Jul 29, 2026
ae34cd7
docs: describe the idempotency-aware retry policy and record the API-…
sjsyrek Jul 29, 2026
c83078f
docs: align retry troubleshooting and changelog with the shipped tran…
sjsyrek Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions CHANGELOG.md

Large diffs are not rendered by default.

29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,9 +650,13 @@ deepl write "Fresh improvement please." --lang en-US --no-cache
- Spanish (`es`)
- French (`fr`)
- Italian (`it`)
- Japanese (`ja`)
- Korean (`ko`)
- Portuguese (`pt`) - generic, defaults to Brazilian Portuguese
- Portuguese - Brazilian (`pt-BR`)
- Portuguese - European (`pt-PT`)
- Chinese (`zh`) - generic, defaults to Simplified Chinese
- Chinese - Simplified (`zh-Hans`)

**Writing Styles:**

Expand Down Expand Up @@ -1043,22 +1047,24 @@ DeepL CLI includes built-in retry logic and timeout handling for robust API comm

**Automatic Retry Logic:**

- Automatically retries failed requests on transient errors (5xx, network failures)
- Default: 3 retries with exponential backoff
- Does not retry on client errors (4xx - bad request, auth failures, etc.)
- Exponential backoff delays: 1s, 2s, 4s, 8s, 10s (capped at 10s)
- Idempotent requests (GET, HEAD, PUT, DELETE) are retried on 5xx responses and transient network failures
- Non-idempotent requests (POST — translations, uploads, glossary and key creation) are replayed only when the error proves the request never reached the server (connection refused, DNS failure), so a slow response can never be submitted and billed twice
- Rate limiting (429) is retried for all methods, honoring the `Retry-After` header when the server sends one
- Other client errors (4xx — bad request, auth failures, etc.) are not retried
- Default: 3 retries with full-jitter exponential backoff (capped at 10s)

**Timeout Configuration:**

- Default timeout: 30 seconds per request
- Default timeout: 30 seconds per request, with an overall time budget across retries
- Applies to all API requests (translate, usage, languages, etc.)
- Override per invocation with the global `--timeout <ms>` and `--max-retries <n>` flags

**Features:**

- ✅ Automatic retry on transient failures
- ✅ Exponential backoff to avoid overwhelming the API
- ✅ Smart error detection (retries 5xx, not 4xx)
- ✅ Configurable via library-consumer options; not exposed as a CLI flag
- ✅ Retry policy aware of request idempotency
- ✅ Configurable via `--timeout` / `--max-retries` (or library-consumer options)
- ✅ Works across all DeepL API endpoints

**Retry Behavior Examples:**
Expand All @@ -1072,11 +1078,12 @@ deepl translate "Hello" --to es
deepl translate "Hello" --to es
# Fails immediately without retries on auth errors

# Rate limiting (429) - does not retry
# You may want to wait before retrying manually
# Rate limiting (429) - retried automatically
# Honors the Retry-After header when the server sends one,
# otherwise falls back to jittered exponential backoff
```

**Note:** Retry and timeout settings use sensible defaults optimized for the DeepL API. These are internal features that work automatically - no configuration required.
**Note:** Retry and timeout settings use sensible defaults optimized for the DeepL API. No configuration is required; `--timeout` and `--max-retries` are available when a specific invocation needs different bounds (e.g. very large documents on a slow uplink).

### Glossaries

Expand Down Expand Up @@ -1331,7 +1338,7 @@ Cache location: `~/.cache/deepl-cli/cache.db` (or `~/.deepl-cli/cache.db` for le

### Prerequisites

- Node.js >= 20.19.0
- Node.js >= 24.0.0
- npm >= 9.0.0
- DeepL API key

Expand Down
25 changes: 18 additions & 7 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DeepL CLI - API Reference

**Version**: 1.2.0
**Last Updated**: April 25, 2026
**Version**: 2.0.0
**Last Updated**: July 29, 2026

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

Expand Down Expand Up @@ -52,8 +52,12 @@ Options that work with all commands:
--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.

**Examples:**

```bash
Expand All @@ -78,6 +82,9 @@ deepl --quiet translate docs/ --to es --output docs-es/
# 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
Expand Down Expand Up @@ -730,9 +737,13 @@ When `--style` or `--tone` is set for a target language that does not support it
- `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

Expand Down Expand Up @@ -1134,7 +1145,7 @@ Interactive setup wizard that creates `.deepl-sync.yaml` by scanning the project
- `--path GLOB` - Source file path or glob pattern
- `--sync-config PATH` - Path to `.deepl-sync.yaml`

`--source-lang` and `--target-langs` are accepted as deprecated aliases for one minor release and emit a stderr warning; they will be removed in the next major release. `deepl translate --target-lang` is unchanged — it operates on strings and stays aligned with the DeepL API's wire name.
`--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:**

Expand Down Expand Up @@ -3071,7 +3082,7 @@ Unclassified failure: emitted when an error escapes every typed handler and matc

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

- `deepl auth set-key`, `deepl auth test` when the key cannot be validated
- `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

Expand Down Expand Up @@ -3113,7 +3124,7 @@ Remediation: re-read the command's `--help` and the relevant section of this API
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/unset` with a malformed key
- `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)
Expand Down Expand Up @@ -3230,5 +3241,5 @@ deepl write --check README.md

---

**Last Updated**: April 20, 2026
**DeepL CLI Version**: 1.1.0
**Last Updated**: July 29, 2026
**DeepL CLI Version**: 2.0.0
6 changes: 3 additions & 3 deletions docs/SYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ deepl sync init [OPTIONS]
| `--path <glob>` | Source file path or glob pattern |
| `--sync-config <path>` | Path to `.deepl-sync.yaml` (default: auto-detect) |

`--source-lang` and `--target-langs` remain accepted as deprecated aliases for one minor release and emit a stderr deprecation warning; they will be removed in the next major release. The `--locale` filter on `sync push` / `pull` / `status` / `export` is unchanged. `deepl translate --target-lang` is unchanged — it operates on strings and stays aligned with the DeepL API's wire name.
`--source-lang` and `--target-langs` were accepted as deprecated aliases during `1.x` and were removed in `2.0.0`; use `--source-locale` and `--target-locales`. The `--locale` filter on `sync push` / `pull` / `status` / `export` is unchanged. `deepl translate --target-lang` is unchanged — it operates on strings and stays aligned with the DeepL API's wire name.

**Examples:**

Expand Down Expand Up @@ -793,7 +793,7 @@ Each target locale's approved dictionary is fetched exactly once per `sync pull`

### Worked example

`deepl sync init` currently accepts `--source-lang` and `--target-langs` as deprecated aliases for `--source-locale` and `--target-locales` (shipped via commit `d65cbb3`). Both aliases emit a stderr deprecation warning pointing at the replacement flag. Under the policy above, these aliases will be removed at the `2.0` cut — this is confirmed, and the removal will not slip into any `1.x` release. See the [`deepl sync init`](#deepl-sync-init) section for the current accepted synopsis.
`deepl sync init` accepted `--source-lang` and `--target-langs` as deprecated aliases for `--source-locale` and `--target-locales` throughout `1.x`; both emitted a stderr deprecation warning pointing at the replacement flag. Per the policy above, the aliases were removed at the `2.0` cut — since `2.0.0` they fail as unknown options. See the [`deepl sync init`](#deepl-sync-init) section for the current accepted synopsis.

## CI/CD Integration

Expand Down Expand Up @@ -861,7 +861,7 @@ The `.deepl-sync.lock` file is only staged when this sync run actually wrote an
```yaml
i18n-check:
stage: test
image: node:20
image: node:24
script:
- npm install -g @deepl/cli
- deepl sync --frozen
Expand Down
6 changes: 3 additions & 3 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Common issues and solutions when using the DeepL CLI.

2. For batch/directory translation, the CLI uses concurrency control internally. Avoid running multiple CLI instances simultaneously on the same API key.

3. The CLI automatically retries with exponential backoff (1s, 2s, 4s, up to 10s, max 3 retries). If errors persist, wait and try again.
3. Retries honor the server's `Retry-After` header when present, falling back to full-jitter exponential backoff (max 3 retries by default; see the global `--max-retries` flag). If errors persist, wait and try again.

---

Expand Down Expand Up @@ -167,7 +167,7 @@ Common issues and solutions when using the DeepL CLI.
export HTTP_PROXY=http://proxy.example.com:8080
```

4. The CLI retries on transient network errors automatically with exponential backoff.
4. The CLI retries idempotent requests on transient network errors automatically. Requests that submit work (translations, document uploads, glossary creation) are replayed only when the error proves the request never reached the server, so they are never double-billed — for those, rerun the command (exit code 5 is safe to retry at the script level).

### "Request failed with status code 503"

Expand All @@ -183,7 +183,7 @@ Common issues and solutions when using the DeepL CLI.
deepl translate "Hello" --to es --no-cache
```

3. The CLI automatically retries on 503 errors with exponential backoff. If the error persists, the API may be experiencing an extended outage.
3. Read-only requests are retried automatically on 503. Requests that submit work (translate, document upload) surface the error immediately so they are never double-billed; retry them at the script level (exit code 5). If the error persists, the API may be experiencing an extended outage.

---

Expand Down
19 changes: 17 additions & 2 deletions src/api/document-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ interface DeepLDocumentStatusResponse {
error_message?: string;
}

/** Uploads and downloads move whole files, so they need far more headroom
* than the interactive request timeout. */
const TRANSFER_TIMEOUT_MS = 300_000;

export class DocumentClient extends HttpClient {
constructor(apiKey: string, options: DeepLClientOptions = {}) {
super(apiKey, options);
}

private get transferTimeout(): number {
return Math.max(this.requestTimeout, TRANSFER_TIMEOUT_MS);
}

async uploadDocument(
file: Buffer,
options: DocumentTranslationOptions
Expand Down Expand Up @@ -69,7 +77,8 @@ export class DocumentClient extends HttpClient {
...formData.getHeaders(),
},
};
}
},
{ timeout: this.transferTimeout }
);

return {
Expand Down Expand Up @@ -101,6 +110,11 @@ export class DocumentClient extends HttpClient {
}
}

/**
* The result endpoint is effectively single-use: a replayed download can
* consume the result of an already-billed translation and lose it
* permanently, so this request is never retried.
*/
async downloadDocument(handle: DocumentHandle): Promise<Buffer> {
try {
const response = await this.makeRawRequest<Buffer>(
Expand All @@ -116,7 +130,8 @@ export class DocumentClient extends HttpClient {
},
responseType: 'arraybuffer',
};
}
},
{ maxRetries: 0, timeout: this.transferTimeout }
);

return Buffer.from(response);
Expand Down
Loading