Skip to content

Commit 59c4e4a

Browse files
authored
fix: land the v2.0.0 release-blocker batch (cache, formats, sync CLI, api/voice, hooks/glossary, docs)
Fixes the seven P1 release blockers under epic sync-8mqh: cache toggle persistence, YAML alias-expansion guard, linear-time Android XML/XLIFF parsing with CDATA-breakout refusal, sync --locale/--sync-config routing with target_locales validation, idempotency-aware HTTP retry policy with truthful exit codes and voice reconnect, git-hooks path resolution and glossary/TSV hardening, and the pre-release documentation corrections. BREAKING CHANGE: deepl sync init --source-lang/--target-langs (deprecated 1.x aliases) are removed at the 2.0 cut; use --source-locale/--target-locales. Non-idempotent POSTs are no longer retried on 5xx or client-side timeout.
1 parent 2398c76 commit 59c4e4a

69 files changed

Lines changed: 4023 additions & 880 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 31 additions & 14 deletions
Large diffs are not rendered by default.

‎README.md‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -650,9 +650,13 @@ deepl write "Fresh improvement please." --lang en-US --no-cache
650650
- Spanish (`es`)
651651
- French (`fr`)
652652
- Italian (`it`)
653+
- Japanese (`ja`)
654+
- Korean (`ko`)
653655
- Portuguese (`pt`) - generic, defaults to Brazilian Portuguese
654656
- Portuguese - Brazilian (`pt-BR`)
655657
- Portuguese - European (`pt-PT`)
658+
- Chinese (`zh`) - generic, defaults to Simplified Chinese
659+
- Chinese - Simplified (`zh-Hans`)
656660

657661
**Writing Styles:**
658662

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

10441048
**Automatic Retry Logic:**
10451049

1046-
- Automatically retries failed requests on transient errors (5xx, network failures)
1047-
- Default: 3 retries with exponential backoff
1048-
- Does not retry on client errors (4xx - bad request, auth failures, etc.)
1049-
- Exponential backoff delays: 1s, 2s, 4s, 8s, 10s (capped at 10s)
1050+
- Idempotent requests (GET, HEAD, PUT, DELETE) are retried on 5xx responses and transient network failures
1051+
- 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
1052+
- Rate limiting (429) is retried for all methods, honoring the `Retry-After` header when the server sends one
1053+
- Other client errors (4xx — bad request, auth failures, etc.) are not retried
1054+
- Default: 3 retries with full-jitter exponential backoff (capped at 10s)
10501055

10511056
**Timeout Configuration:**
10521057

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

10561062
**Features:**
10571063

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

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

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

1079-
**Note:** Retry and timeout settings use sensible defaults optimized for the DeepL API. These are internal features that work automatically - no configuration required.
1086+
**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).
10801087

10811088
### Glossaries
10821089

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

13321339
### Prerequisites
13331340

1334-
- Node.js >= 20.19.0
1341+
- Node.js >= 24.0.0
13351342
- npm >= 9.0.0
13361343
- DeepL API key
13371344

‎docs/API.md‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# DeepL CLI - API Reference
22

3-
**Version**: 1.2.0
4-
**Last Updated**: April 25, 2026
3+
**Version**: 2.0.0
4+
**Last Updated**: July 29, 2026
55

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

@@ -52,8 +52,12 @@ Options that work with all commands:
5252
--verbose, -v Show extra information (source language, timing, cache status)
5353
--config, -c FILE Use alternate configuration file
5454
--no-input Disable all interactive prompts (abort instead of prompting)
55+
--timeout MS HTTP request timeout in milliseconds (default: 30000)
56+
--max-retries N Maximum automatic retries for retryable requests (default: 3)
5557
```
5658

59+
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.
60+
5761
**Examples:**
5862

5963
```bash
@@ -78,6 +82,9 @@ deepl --quiet translate docs/ --to es --output docs-es/
7882
# Use custom config file
7983
deepl --config ~/.deepl-work.json translate "Hello" --to es
8084

85+
# Give a large document more time, and disable automatic retries
86+
deepl --timeout 120000 --max-retries 0 translate report.pdf --to de
87+
8188
# Use custom config directory (via environment variable)
8289
export DEEPL_CONFIG_DIR=/path/to/config
8390
deepl translate "Hello" --to es
@@ -730,9 +737,13 @@ When `--style` or `--tone` is set for a target language that does not support it
730737
- `es` - Spanish
731738
- `fr` - French
732739
- `it` - Italian
740+
- `ja` - Japanese
741+
- `ko` - Korean
733742
- `pt` - Portuguese (generic, defaults to Brazilian Portuguese)
734743
- `pt-BR` - Brazilian Portuguese
735744
- `pt-PT` - European Portuguese
745+
- `zh` - Chinese (generic, defaults to Simplified Chinese)
746+
- `zh-Hans` - Simplified Chinese
736747

737748
#### Examples
738749

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

1137-
`--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.
1148+
`--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.
11381149

11391150
**Examples:**
11401151

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

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

3074-
- `deepl auth set-key`, `deepl auth test` when the key cannot be validated
3085+
- `deepl auth set-key` when the key cannot be validated
30753086
- 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
30763087
- HTTP 401/403 responses from the DeepL API
30773088

@@ -3113,7 +3124,7 @@ Remediation: re-read the command's `--help` and the relevant section of this API
31133124
The configuration file or a configuration value is invalid. Emitted by:
31143125

31153126
- `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)
3116-
- `deepl config get/unset` with a malformed key
3127+
- `deepl config get` with a malformed key
31173128
- Any command that loads the config file when the file fails to parse, is missing a required field, or specifies an unsupported version
31183129
- `sync` when `.deepl-sync.yaml` is missing required fields, has invalid locales, or declares an unsupported version
31193130
- `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)
@@ -3230,5 +3241,5 @@ deepl write --check README.md
32303241

32313242
---
32323243

3233-
**Last Updated**: April 20, 2026
3234-
**DeepL CLI Version**: 1.1.0
3244+
**Last Updated**: July 29, 2026
3245+
**DeepL CLI Version**: 2.0.0

‎docs/SYNC.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ deepl sync init [OPTIONS]
476476
| `--path <glob>` | Source file path or glob pattern |
477477
| `--sync-config <path>` | Path to `.deepl-sync.yaml` (default: auto-detect) |
478478

479-
`--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.
479+
`--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.
480480

481481
**Examples:**
482482

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

794794
### Worked example
795795

796-
`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.
796+
`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.
797797

798798
## CI/CD Integration
799799

@@ -861,7 +861,7 @@ The `.deepl-sync.lock` file is only staged when this sync run actually wrote an
861861
```yaml
862862
i18n-check:
863863
stage: test
864-
image: node:20
864+
image: node:24
865865
script:
866866
- npm install -g @deepl/cli
867867
- deepl sync --frozen

‎docs/TROUBLESHOOTING.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ Common issues and solutions when using the DeepL CLI.
139139

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

142-
3. The CLI automatically retries with exponential backoff (1s, 2s, 4s, up to 10s, max 3 retries). If errors persist, wait and try again.
142+
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.
143143

144144
---
145145

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

170-
4. The CLI retries on transient network errors automatically with exponential backoff.
170+
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).
171171

172172
### "Request failed with status code 503"
173173

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

186-
3. The CLI automatically retries on 503 errors with exponential backoff. If the error persists, the API may be experiencing an extended outage.
186+
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.
187187

188188
---
189189

‎src/api/document-client.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ interface DeepLDocumentStatusResponse {
1616
error_message?: string;
1717
}
1818

19+
/** Uploads and downloads move whole files, so they need far more headroom
20+
* than the interactive request timeout. */
21+
const TRANSFER_TIMEOUT_MS = 300_000;
22+
1923
export class DocumentClient extends HttpClient {
2024
constructor(apiKey: string, options: DeepLClientOptions = {}) {
2125
super(apiKey, options);
2226
}
2327

28+
private get transferTimeout(): number {
29+
return Math.max(this.requestTimeout, TRANSFER_TIMEOUT_MS);
30+
}
31+
2432
async uploadDocument(
2533
file: Buffer,
2634
options: DocumentTranslationOptions
@@ -69,7 +77,8 @@ export class DocumentClient extends HttpClient {
6977
...formData.getHeaders(),
7078
},
7179
};
72-
}
80+
},
81+
{ timeout: this.transferTimeout }
7382
);
7483

7584
return {
@@ -101,6 +110,11 @@ export class DocumentClient extends HttpClient {
101110
}
102111
}
103112

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

122137
return Buffer.from(response);

0 commit comments

Comments
 (0)