diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2e8b450 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug report +about: Report a bug in DeepL CLI +title: 'bug: ' +labels: bug +assignees: '' +--- + +## Summary + +A clear, concise description of the bug. + +## Environment + +- **DeepL CLI version**: (run `deepl --version`) +- **Node.js version**: (run `node --version`) +- **Operating system**: (e.g., macOS 14.5, Ubuntu 22.04, Windows 11) +- **Install method**: (source, npm link, other) + +## Reproduction Steps + +1. +2. +3. + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. Include the full error output (redact any API keys): + +``` + +``` + +## Additional Context + +Anything else that might help — config snippets (redacted), example files, +related issues, workarounds you've tried. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c2cfa5d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Suggest a new feature or enhancement for DeepL CLI +title: 'feat: ' +labels: enhancement +assignees: '' +--- + +## Problem + +What problem does this feature solve? Who is affected? What workflow is +currently painful or impossible? + +## Proposed Solution + +A clear, concise description of what you'd like to see. + +## Alternatives Considered + +Other approaches you've thought about and why you believe the proposed +solution is the best fit. + +## Additional Context + +Use cases, mockups, related tools, or links to similar features in other +CLIs. diff --git a/.gitignore b/.gitignore index 471e185..af0927d 100644 --- a/.gitignore +++ b/.gitignore @@ -66,8 +66,15 @@ logs/ *.db-wal *.db-shm -# Claude Code local settings -.claude/settings.local.json +# Claude Code local tooling (settings, hooks, agent worktrees, session state) +.claude/ + +# Playwright MCP local state +.playwright-mcp/ # Beads issue tracking (local) .beads/ +issues.jsonl + +# QA harness +qa/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f1559f2..8bd411c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,23 +5,290 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.1.0] - 2026-04-23 ### Added +- **Exit Codes appendix** in docs/API.md enumerating all CLI exit codes with emitting commands. +- Continuous localization sync engine (`deepl sync`) for scanning, diffing, and translating i18n resource files +- 11 i18n file format parsers: JSON, YAML, Gettext PO, Android XML, iOS Strings, ARB, XLIFF, TOML, Java Properties, Xcode String Catalog, Laravel PHP arrays +- Xcode String Catalog (`.xcstrings`) format parser for iOS/macOS projects — multi-locale, comment preservation +- **sync**: Laravel PHP arrays (`.php`) format parser with `glayzzle/php-parser` — AST allowlist over string-literal return-array entries; double-quoted interpolation (`"Hello $name"`), heredoc, nowdoc, and string concatenation are rejected with a `ValidationError`. Reconstruct is span-surgical (AST offsets only; every byte outside a replaced string literal is preserved verbatim — comments, PHPDoc, trailing commas, irregular whitespace, and quote style all round-trip unchanged). Laravel pipe-pluralization values (`|{n}`, `|[n,m]`, `|[n,*]`) are detected at extract, excluded from the translation batch, and surfaced in `deepl sync status` via a new `skippedKeys` count. `php-parser` is lazy-loaded only when a `laravel_php` bucket is configured. +- **sync**: `deepl sync init` auto-detects Laravel projects — `composer.json` at the repo root plus `.php` files under `lang/en/` (Laravel 9+) or `resources/lang/en/` (Laravel ≤8 / Lumen) triggers a `laravel_php` bucket suggestion. Filesystem-only (no manifest parsing), consistent with the Rails / Django / Flutter / Angular detectors. +- **sync**: The auto-detect engine now supports optional root-marker files via a `requires` field on each detection pattern. Markers are plain `fs.existsSync` checks — never parsed — matching the filesystem-only stance of the sibling detectors. Laravel's `composer.json` is the first required marker; the ARB (Flutter) detector was retroactively tightened with `pubspec.yaml` to eliminate false positives for the very rare non-Flutter ARB use. +- **sync**: `deepl sync init` now auto-detects four additional ecosystems that the docs previously promised but the detector never actually covered: Rails (`config/locales/en.yml` / `.yaml`), Xcode String Catalog (`Localizable.xcstrings` / `Resources/Localizable.xcstrings` / `*.xcstrings`, multi-locale), go-i18n TOML (`locales/en.toml`, `i18n/en.toml`), and Java / Spring properties (`src/main/resources/messages_en.properties`). Also fixes a pre-existing extension-preservation bug in the YAML detector — a `locales/en.yml` match used to emit a `locales/en.yaml` bucket pattern that wouldn't match at sync time. `.yml` and `.yaml` are now handled as separate detection entries so the extension round-trips faithfully. +- **sync**: `deepl sync init` auto-detects go-i18n's root-level `active.en.toml` layout as a dedicated detection entry, emitting the `active.{locale}.toml` filename template. Previously only `locales/en.toml` / `i18n/en.toml` directory layouts were covered; root-level users had to fall through to the four-flag non-interactive path. +- **sync**: `deepl sync init` auto-detects Rails namespaced layouts under `config/locales/**/en.yml` (and `.yaml`) — engines, concerns, and per-namespace splits are now recognized alongside the canonical `config/locales/en.yml`. The namespace directory is preserved in the generated bucket `include:` pattern. +- **sync**: `deepl sync init` auto-detects Symfony's `translations/messages.en.xlf` layout as a dedicated XLIFF detection entry — distinct from Angular's `src/locale/messages.xlf` convention. Target locales are emitted as `translations/messages.{locale}.xlf`. +- **sync**: `sync.limits` config block — per-file parser caps `max_entries_per_file` (default 25 000, hard max 100 000), `max_file_bytes` (default 4 MiB, hard max 10 MiB), `max_depth` (default 32, hard max 64). Default-exceed = file-skip + warn; setting a value above the hard ceiling fails at config load with `ConfigError` (exit 7). +- Multi-locale format support in sync engine (`FormatParser.multiLocale`) for single-file formats like `.xcstrings` +- Incremental sync with change detection via `.deepl-sync.lock` content hashing +- Interactive setup wizard (`deepl sync init`) with framework auto-detection (i18next, Rails, Django, Flutter, Angular, etc.) +- Translation coverage reporting (`deepl sync status`) with per-locale progress bars +- Translation validation (`deepl sync validate`) for placeholder, format string, and HTML tag integrity +- `sync export` command — export source strings to XLIFF 1.2 for CAT tool handoff +- Auto-context extraction from source code for improved translation quality, including template literal calls (e.g., `` t(`features.${key}.title`) ``) +- Key path context synthesis — i18n key hierarchy (e.g., `pricing.free.cta`) is parsed into natural-language context descriptions sent to the DeepL API +- Element type detection — HTML/JSX element types (button, h2, th, etc.) are extracted from surrounding source code during context scanning +- Element-aware custom instructions — auto-generated `custom_instructions` for 16 element types (button, a, h1-h6, th, label, option, input, title, summary, legend, caption), batched by element type for efficient API usage. Only for the 8 locales supporting custom instructions (DE, EN, ES, FR, IT, JA, KO, ZH) +- `translation.instruction_templates` config — user-customizable instruction templates per HTML element type, overriding built-in defaults +- `translation.length_limits` config — opt-in length-aware translation instructions using per-locale expansion factors based on industry-standard approximations (IBM, W3C); user-overridable +- Section-batched context translation — keys sharing the same i18n section (e.g., `nav.*`) are batched with shared section context, ~3.4x faster than per-key while preserving disambiguation quality +- Translation strategy summary in sync output — shows how many keys used context, instructions (by element type), or plain batch translation +- Config warnings when `instruction_templates` is set but context scanning is disabled or no element types are detected +- `--batch` / `--no-batch` CLI flags — `--batch` forces plain batch (fastest, no context); `--no-batch` forces true per-key context (slowest, max quality); default uses section-batched context +- `PushResult` / `PullResult` types for `sync push` / `sync pull` — return `{pushed|pulled, skipped[]}` so callers can distinguish truly-nothing-to-do from silently-dropped cases. CLI output now appends `(N skipped: ...)` when appropriate. +- Actionable TMS authentication errors — 401/403 responses from the TMS server now surface as `ConfigError` with a remediation hint that names `TMS_API_KEY` / `TMS_TOKEN` and the relevant `.deepl-sync.yaml` fields. +- `context_sent` field in lockfile translation entries — records whether source code context was included in the API request +- `character_count` field in lockfile translation entries — records characters billed per key per locale +- Live progress output during `deepl sync` — per-key `key-translated` events during translation and per-locale `locale-complete` events when each locale finishes, in both text and JSON formats +- `context.overrides` config — manual context strings per key, preferred over auto-extracted context +- Auto-glossary management from translation history +- Optional TMS integration (`deepl sync push`/`pull`) for collaborative editing and human review workflows; documented REST contract lets any compatible TMS be wired up +- CI/CD integration with `--frozen` mode and exit code 10 for translation drift detection +- `validation.fail_on_missing` and `validation.fail_on_stale` config options for granular `--frozen` drift detection +- Dry-run mode (`deepl sync --dry-run`) with character and cost estimates from source string lengths +- Per-locale progress display after sync (`✓ de: 10/10 ✓ fr: 10/10`) +- `estimatedCharacters` and `targetLocaleCount` fields in JSON output +- Dollar cost estimates in sync output and JSON (at DeepL Pro rates, $25/1M chars) +- `sync.max_characters` config option — cost cap that aborts sync before translation if estimated characters exceed limit (override with `--force`) +- `sync.backup` config option — pre-overwrite backup of target files (default `true`); `.bak` files cleaned up after successful sync +- `--watch` mode — monitors source i18n files for changes and auto-syncs with debouncing (configurable via `--debounce`) +- `--flag-for-review` marks MT translations with `review_status: machine_translated` in the lock file for human review workflows - Free API key (`:fx` suffix) support with automatic endpoint resolution to `api-free.deepl.com` -- Shared endpoint resolver used by all commands including voice, auth, and init - Custom/regional endpoint support (e.g. `api-jp.deepl.com`) that takes priority over auto-detection +- `sync export --overwrite` flag — required to overwrite an existing `--output` file; protects against accidental clobbering +- `deepl sync status --format json` error-mode output: failures now emit `{error, code}` JSON to stderr with the error class name (`ConfigError`, `ValidationError`, etc.) as the `code` +- Translation memory support in `deepl translate` via `--translation-memory ` and `--tm-threshold ` — forces `quality_optimized` model, requires `--from` (pair-pinned), threshold is an integer 0–100 (default 75) +- Translation memory support in `deepl sync` via `translation.translation_memory` and `translation.translation_memory_threshold` config keys, with per-locale overrides under `translation.locale_overrides` +- Translation memory name-to-ID resolution is cached per run to avoid redundant `GET /v3/translation_memories` calls; TM files are authored and uploaded via the DeepL web UI +- Verbose-mode logs at the glossary and translation memory resolution boundary: `--verbose` now shows the resolved UUID for each glossary or TM name (`[verbose] Resolved glossary "" -> `, `[verbose] Resolved translation memory "" -> `) and a cache-hit line when the same TM name + pair is reused within a session +- `deepl tm list` subcommand — lists all translation memories on the account, mirroring `deepl glossary list`. Text output filters control chars and zero-width codepoints from TM names so a malicious API-returned name cannot corrupt the terminal; `--format json` emits the raw `TranslationMemory[]` as returned by `GET /v3/translation_memories`. Help text on `deepl translate --translation-memory` now cross-references the new command +- `src/utils/uuid.ts` — shared strict UUID regex (`UUID_RE`) + `validateUuid` / `validateTranslationMemoryId` helpers. `validateTranslationMemoryId` is dormant today (TM IDs only appear in `/v2/translate` POST bodies, which are JSON-escaped) but guards the path-injection surface the moment any future per-TM endpoint interpolates a user-supplied UUID into a URL segment +- **sync**: `deepl sync resolve` now prints a per-entry decision report (`kept ours` / `kept theirs` / `length-heuristic` / `unresolved`) plus a summary, and accepts `--dry-run` to preview decisions without writing the lockfile. +- **sync docs**: `docs/SYNC.md` Exit Codes table and `docs/API.md` sync Behavior bullet now cross-link to the canonical [Exit Codes](API.md#exit-codes) appendix. +- **sync**: New `sync.max_scan_files` config key (default 50,000). +- **errors**: `SyncConflictError` class in `src/utils/errors.ts` mirroring `SyncDriftError` — `ExitCode.SyncConflict` (11) is now throwable as a typed error so library consumers can `instanceof`-match the conflict case. +- **SECURITY.md**: `1.1.x` row added to the Supported Versions table. +- **CONTRIBUTING.md**: PR checklist reminds contributors to register new example scripts in `examples/run-all.sh`. +- **.github/ISSUE_TEMPLATE/**: `bug_report.md` and `feature_request.md` templates for structured issue intake. +- **write**: `deepl write --to ` is now accepted as a long-only alias of `--lang`. The alias exists so users can reach for `--to` uniformly across `deepl translate` and `deepl write` — the single most common vocabulary split flagged in cross-command usage. `--lang` / `-l` remain fully supported; nothing deprecated. The short form `-t` is intentionally **not** bound on `write` (it would collide with `deepl translate -t, --to`). Passing both `--to` and `--lang` with different values exits with a `ValidationError`; passing the same value works fine. +- **docs**: `docs/API.md` gained a one-paragraph callout distinguishing `deepl sync --locale` (filter over locales already configured in `.deepl-sync.yaml#target_locales`) from `deepl translate --to` (invocation-time target-language specifier). The split is semantic — sync owns its locale mapping via config; translate does not — and documenting the distinction is the right fix rather than forcing one to compromise for surface symmetry. +- **sync**: new `sync.limits.max_source_files` config field. Caps how many source files a single bucket's `include` glob may match before the bucket is skipped with a warning. Default `10000`, hard ceiling `1000000`. Guards against a misconfigured `**/*.json` that accidentally picks up a vendored subtree. Sibling fields `max_entries_per_file` / `max_file_bytes` / `max_depth` gate individual files; this one gates the whole bucket because processing the first N of an oversized glob would silently drop the rest. Narrow the pattern or raise the cap in `.deepl-sync.yaml`. + +**Note:** `deepl sync` intentionally exposes no `--translation-memory` / `--tm-threshold` CLI override in this release; configure translation memory via `.deepl-sync.yaml`. ### Changed -- Voice API no longer hardcodes the Pro endpoint; it follows the same endpoint resolution as all other commands -- `auth set-key` and `init` now validate entered keys against the correct endpoint based on key suffix -- Standard DeepL URLs (`api.deepl.com`, `api-free.deepl.com`) in saved config no longer override key-based auto-detection +- **`deepl sync` cost estimates now labeled as Pro tier**: text-mode output appends `(Pro tier estimate)` to all cost lines (both dry-run and post-sync). The `--format json` output carries `rateAssumption: "pro"`. `docs/SYNC.md` now documents the Pro-rate assumption ($25/1M chars) and points users to their account page to determine the applicable rate for their tier. +- **`deepl sync --format json` output contract stabilized**: the success JSON payload is now a curated `SyncJsonOutput` shape (`ok`, `totalKeys`, `translated`, `skipped`, `failed`, `targetLocaleCount`, `estimatedCharacters`, `estimatedCost?`, `rateAssumption: "pro"`, `dryRun`, `perLocale[]`) instead of a raw internal spread. The public shape is documented in docs/API.md and guaranteed stable across 1.x. +- **`deepl sync init` no-detection exit**: when the auto-detector finds no recognized i18n files, the command now exits 7 (`ConfigError`) instead of 0, and prints an actionable remediation hint listing all four required flags (`--source-locale`, `--target-locales`, `--file-format`, `--path`). In `--format json` mode the canonical error envelope (`{ok:false, error:{code:"ConfigError",...}, exitCode:7}`) is emitted to stderr. Scripts that previously relied on exit 0 in empty projects must be updated to handle exit 7. +- **`deepl sync status` documentation**: the docs/SYNC.md example output now matches the actual CLI output — ASCII progress bar (`[####....]`), integer coverage percentage, and per-locale `(N missing, N outdated)` parenthetical. The previous example showed Unicode block characters, decimal percentages, and a `Translation Status:` header that the code never emits. The per-locale `outdated` field is now documented in the JSON field legend. +- **sync**: `deepl sync init` now prefers the dir-per-locale JSON layout (`locales/en/*.json`) over the flat layout (`locales/en.json`) when both coexist in the same repo. The init wizard's `detected[0]` selection was silently picking the flat entry, which is usually legacy / sample content while the nested layout is the real source — i18next, react-i18next, and next-i18next all default to nested. Both entries remain in `DETECTION_PATTERNS` for enumeration; only the first-pick order changed. +- **translate**: Centralize `TranslateOptions` construction for `deepl translate`, `deepl translate file.txt`, `deepl translate `, and the document path in a new `src/cli/commands/translate/translation-options-factory.ts`. All four handlers now call `buildBaseTranslationOptions()` + `applySharedTmAndGlossary()` instead of each maintaining its own copy of the base mapping plus a near-identical TM/glossary resolution block. Behavior-preserving for the shared flags (`--formality`, `--glossary`, `--model-type`, `--translation-memory`, `--tm-threshold`, `--preserve-formatting`); fixes latent drift risk where one handler could silently diverge from another. Handler-specific shaping (custom instructions, style id, XML tag handling, multi-target `targetLang` stripping) stays in the handler. `deepl sync` is intentionally untouched — its `TranslationOptions` are built from resolved config with per-locale overrides and `context_sent` wiring, a different construction domain that lives in `src/sync/sync-locale-translator.ts`. +- **sync**: Format-name knowledge consolidated under `src/formats/registry.ts`; `--file-format` CLI choices now derive from the registry. Prevents silent divergence between parser, CLI help, and registration. +- **sync**: Removed per-parser `sort` calls (consumers sort once); extracted `detectIndent` to a shared `src/formats/util/detect-indent.ts` used by JSON, ARB, and xcstrings. Pure refactor, no behavior change. +- **sync**: `scan_paths` file walk is now bounded (default 50,000 files; configurable via `sync.max_scan_files` in `.deepl-sync.yaml`) — exceeding the cap throws ValidationError with a suggestion, preventing CI wedges on misconfigured patterns. +- **sync**: `deepl sync push --help` and `deepl sync pull --help` now include a TMS onboarding hint — the required `tms:` YAML block, the `TMS_API_KEY` / `TMS_TOKEN` env vars, and a pointer to `docs/SYNC.md#tms-rest-contract`. Previously the help surface listed only `--locale` / `--sync-config`, so users had to run the subcommand once and read a runtime ConfigError to discover the integration requirements. `docs/API.md` push/pull sections get the same hint and cross-link. +- **sync**: `deepl sync --force` help text now warns that the flag bypasses the `sync.max_characters` cost-cap preflight and can incur unexpected API costs by rebilling every translated key. Previous wording ("Retranslate all strings, ignoring lock file") described the lockfile effect but was silent on the billing surprise. `docs/API.md` and `docs/SYNC.md` updated to match. +- **sync**: Extract CLI exit-code enum to `src/utils/exit-codes.ts` (next to the errors module); adds `SyncConflict` (11) for `sync resolve` unresolvable-conflict exits. No runtime behavior change from the extraction alone; enables the envelope contract wiring. +- **sync**: `deepl sync init` flag vocabulary aligned with the rest of sync: `--source-locale` and `--target-locales` are now the primary names, matching `--locale` in `sync push`/`pull`/`status`/`export`. `deepl translate --target-lang` is unchanged (operates on strings, distinct from locale-file semantics). +- **sync**: Rename `deepl sync --context` / `--no-context` boolean to `--scan-context` / `--no-scan-context` to disambiguate from `deepl translate --context ""` (string-valued). Bare `--context` / `--no-context` on sync now errors with a did-you-mean pointing to the new flag. `deepl sync` had not shipped in a tagged release prior to this change, so no deprecation cycle is needed. +- **sync**: CLI override layering (`--formality`, `--glossary`, `--model-type`, `--scan-context`, `--batch`/`--no-batch`) is now centralized in a single `applyCliOverrides` helper in `sync-config.ts`. The TM-requires-`quality_optimized` guard now also fires at the CLI-override boundary, so `--model-type latency_optimized` is rejected with an actionable `ConfigError` when the loaded YAML has `translation_memory` set (previously the override silently bypassed the check). +- **sync**: `deepl sync glossary-report` is renamed to `deepl sync audit`. Every other sync subcommand is a single action verb (`init`, `status`, `validate`, `export`, `resolve`, `push`, `pull`); the hyphenated noun-phrase was an outlier and a name mismatch (the command detects terminology inconsistency whether or not a glossary is configured). The old form is rejected with a `ValidationError` (exit 6) and a did-you-mean hint pointing to `audit`. No deprecation alias — this is a pre-release rename; `glossary-report` never shipped in a tagged release. `audit` here means translation-consistency audit (term divergence across locales), not security audit in the `npm audit` sense. +- **sync**: Lockfile writes now serialize in-place without deep-cloning; a 10K-key × 10-locale lockfile peaks at ~2× rather than ~3× its serialized size. Watch-mode sync runs that write on every tick see the same reduction. +- **sync**: `deepl sync init` interactive wizard now offers the full DeepL target-locale set (~25 locales) in the checkbox prompt, with 8 common locales pre-checked. Previously the wizard exposed only de/es/fr/ja/zh. +- **sync**: Default context translation mode: keys with auto-extracted context are now section-batched instead of per-key. Use `--no-batch` to restore per-key behavior. +- **sync**: `deepl sync status --format json` output shape declared stable across 1.x — `{sourceLocale, totalKeys, locales[]}` with `coverage` as an integer 0-100. CLI JSON uses camelCase; on-disk lockfile/config use snake_case. +- **endpoint**: Shared endpoint resolver now used by all commands including voice, auth, and init. +- **docs**: Corrected Watch Mode section of `docs/SYNC.md` — `.deepl-sync.yaml` IS watched and triggers a config hot-reload on change (was documented as not watched); CLI flags (`--locale`, `--dry-run`, `--formality`, `--glossary`, etc.) are baked at invocation and do NOT reload between cycles (was documented as re-read each cycle); added SIGHUP force-reload behavior (previously undocumented). +- **sync**: TMS credential-hygiene warnings now route through `Logger.warn` (respects `--quiet`, consistent with the rest of the CLI and flowing through the Logger sanitizer). +- **CLAUDE.md**: Architecture block refreshed to include `sync/`, `formats/`, `data/` layers; drift-prone version/test-count metadata replaced with references to `VERSION`/`package.json` and `npm test` output. +- **README**: Featured `deepl sync` (Continuous Localization) prominently in Key Features. +- **README**: Voice Translation Key Features bullet now labeled `(Pro/Enterprise)`. +- **README**: Quick Start version-output example replaced with schematic `deepl-cli 1.x.x` (no longer drifts per release). +- **README**: Configurable timeout/retry copy reworded — now described as library-consumer options, not exposed as a CLI flag. +- **README**: "GDPR compliant" softened to "GDPR-aligned with DeepL's DPA" for legal precision. +- **README**: DeepL® trademark attribution appended to the License section. +- **README**: `deepl init` section cross-links to `deepl sync init` for continuous-localization setup. +- **sync exit codes**: `deepl sync` partial-failure (one or more locales failed while others succeeded) now exits **12** instead of 1. Exit 1 now means strictly "unclassified CLI failure." A prior version aliased `ExitCode.PartialFailure` to `GeneralError` (both `1`), which prevented CI scripts from telling a partial sync outcome from a CLI crash. With this change, CI can safely branch on `$? -eq 12` and retry only the failed locales via `deepl sync --locale `. The paired typed error class `SyncPartialFailureError` (exit 12, envelope `code: "SyncPartialFailure"`) is added to `src/utils/errors.ts`, mirroring `SyncDriftError` (10) and `SyncConflictError` (11). Migration: any CI script that branched on `$? -eq 1` to detect partial sync failure should switch to `$? -eq 12`; a generic `$? -ne 0` check continues to work unchanged. +- **sync drift exit**: `deepl sync --frozen` now exits *soft* (sets `process.exitCode = 10` and returns from the action handler) instead of calling `process.exit(10)` directly. Observable exit code is unchanged at 10; the internal change lets in-flight writes, auto-commit steps, and any `--watch` event loop drain cleanly before the process exits. `docs/API.md` has promised this shape since 1.1.0 but the implementation drifted to a hard exit — now aligned. +- **tests**: The shared `tests/setup.ts` `afterEach` hook now asserts that every `nock` interceptor registered during a test actually fired. An unasserted mock (registered scope with no matching request) now throws with the pending interceptor list, surfacing silent test gaps where the SUT never exercised the mocked network call. No test changes were required — the existing suite (49 integration files, 766 tests, plus unit + E2E = 4501 tests) already had clean hygiene. Negative-path tests that intentionally register non-firing interceptors can opt out by calling `nock.cleanAll()` from their own `afterEach` before the shared hook runs. +- **cache**: SQLite cache DB now carries a schema version via `PRAGMA user_version`. Fresh DBs are stamped at version 1; pre-versioned DBs (created before this field existed) report 0 and are upgrade-stamped in place — no data migration, no user-visible change. Opening a DB whose version is **newer** than the CLI supports now fails with a `ConfigError` rather than risking data loss. +- **cache**: Corrupted cache databases are now backed up aside as `cache.db.corrupt-` (plus any `-wal` / `-shm` sidecars) instead of being unlinked. Users keep their 30-day cache contents and a forensic artifact for post-mortem; the CLI creates a fresh DB alongside and continues. `Logger.warn` names the backup path. +- **http**: Retry backoff now uses **full jitter** (AWS-recommended variant): the delay for attempt `n` is a uniform random value in `[0, min(INIT * 2^n, MAX)]` rather than the fixed `min(INIT * 2^n, MAX)`. Concurrent clients (e.g., parallel sync buckets) that all hit 429 at the same moment no longer form a thundering herd on the retry. The `Retry-After` header path is unchanged — server-specified delays are honored verbatim. +- **http**: Retries now emit a `Logger.verbose` line per retry decision naming the attempt number, delay, and reason (429 with Retry-After, 429 with jitter backoff, or generic network error). Previously retries were silent; a user seeing elevated latency had no visibility into whether the CLI was backing off or stuck. + +### Deprecated + +- **sync**: `deepl sync init --source-lang` and `--target-langs` are deprecated in favor of `--source-locale` and `--target-locales`. The old flags continue to work but emit a stderr deprecation warning; they will be removed in the next major release. + +### Removed + +- **sync**: Dead `onProgress` callback and `SyncProgressEvent` interface from `SyncOptions` (never wired up). +- **sync**: Remove silently-ignored `--batch-size` flag +- **sync**: Remove 5 unimplemented config fields from types and docs +- **package.json**: Drop `exports["./cli"]` subpath. It pointed at `dist/cli/index.js`, which runs `program.parseAsync` + `process.exit` at module load — any consumer who imported `deepl-cli/cli` would have had their own process terminated mid-import. The CLI remains available as a binary via the `bin` field. + +### Fixed + +- **sync cost cap**: When a brand-new target locale is added to an existing project, `sync.max_characters` now correctly includes the character cost of translating all current keys into the new locale in its preflight estimate. Previously, `toTranslate` was empty (no new/stale diffs) so the cap check passed with 0 estimated characters while the actual sync translated the entire key set — a silent cost surprise. The live-path preflight now mirrors the dry-run math (`currentChars × newLocaleCount`) so `--dry-run` and the live run always report the same estimated character count for the same workload. +- **sync perf**: Stale-lock entry cleanup now issues a single `fg` call with all stale-basename patterns instead of one call per stale entry. A reorg renaming 50 files previously triggered 50 sequential full-tree scans before sync completed; it now completes in one pass regardless of stale-entry count. +- **sync perf**: Startup `.bak` sweep (`sweepStaleBackups`) is now scoped to the directories implied by each bucket's `include` globs instead of walking the entire project tree. On large monorepos the sweep cost is now proportional to the number of bucket-matched directories rather than total project size. Callers without bucket config fall back to the previous full-tree walk with a one-time warning. +- `deepl sync push --format json`, `deepl sync pull --format json`, and `deepl sync resolve --format json` now emit a JSON success envelope to stdout on the happy path (`{ok:true, pushed/pulled/resolved: N, skipped/decisions: [...]}`) instead of silently writing nothing; scripts piping output to a file no longer receive an empty result. +- **sync**: Eliminated O(F×K) `resolveTemplatePatterns` loop over duplicate template-pattern entries. The accumulator in `extractAllKeyContexts` pushed one `TemplatePatternMatch` per template-literal match per source file with no dedup; a 2K-file repo with 20 template literals per file produced 40K entries × 10K keys = 400M `.test()` calls (~8s/sync). A `Set`-based dedup before the resolve loop collapses all per-file duplicates to at most one entry per distinct pattern string; `MAX_LOCATIONS=3` downstream is unaffected since the first-seen `filePath`/`line` is sufficient context. +- **sync**: Eliminated O(N²) `Array.includes` scan in the per-locale plural-slot hot path (`sync-locale-translator.ts`). Three call sites that tested `batchIndices.includes(slot.diffIndex)` — one in Path A (plain batch), one in Path C (element-instruction batch), one in Path B1 (section-batched context) — now precompute a `Set` before the `pluralSlots` loop and use `Set.has`. With 5K plural entries, 50 locales, and a 200-file repo the old code added ~40 min of pure array-scan overhead per sync run. +- **sync**: `deepl sync push` and `deepl sync pull` CLI summary lines now render a per-reason breakdown when entries are skipped (e.g., `(4 skipped: 1 target file not yet present, 2 pipe-pluralization (never sent to TMS), 1 no matching keys)`) instead of a single stale message. After `pipe_pluralization` was added as a third `SkipReason`, the previous "target file not yet present" / "no matching keys" strings were incorrect for Laravel users hitting the pipe-plural skip. Logic extracted to a shared `formatSkippedSummary(skipped)` helper in `sync-tms.ts`; the programmatic `PushResult.skipped` / `PullResult.skipped` shape is unchanged. +- **sync**: `deepl sync push` and `deepl sync pull` now enforce the walker's skip-metadata partition at every inline `parser.extract(...)` site (multi-locale source, non-multi-locale target file, pull-merge template). Laravel pipe-pluralization values (`|{n}`, `|[n,m]`, `|[n,*]`) were leaking past the partition on push (sent verbatim to `TmsClient.pushKey`, where the TMS would store them as a single malformed string) and on pull merge (overwriting the preserved pipe-plural target value with the single-string TMS payload, corrupting Laravel's pluralization syntax). A new exported `partitionEntries` helper in `sync-bucket-walker.ts` is applied at the three callsites, `TmsClient.pushEntry()` now rejects skip-tagged entries at the client boundary so pipe-plural values cannot reach the TMS even if a caller forgets to partition, and `PushResult`/`PullResult` now surface a `SkippedRecord` with `reason: 'pipe_pluralization'` and `key` per leaked entry so silent-partition regressions are detectable. +- **sync**: `deepl sync init` JSON detector now emits a glob bucket pattern for the directory-per-locale i18next layout (`locales/en/*.json`) instead of fabricating a nonexistent `locales/en/en.json` single-file path. Flat (`locales/en.json`) and dir-per-locale layouts are now separate detection entries. +- **sync**: `deepl sync init` iOS detector no longer claims bare-root `*.strings` files — Apple's bundle model mandates `.lproj`, and the root-level glob was a relocation magnet that emitted `{locale}.lproj/Localizable.strings` target patterns pointing at paths the source never lived in. Projects with that layout now fall through to the four-flag non-interactive init path. +- **sync**: `deepl sync init` XLIFF detector no longer claims bare-root `*.xlf` / `*.xliff` files — CAT-tool dumps (Trados/memoQ/Xcode `.xcloc` extracts) are a false-positive magnet and the detector used to relocate them under `src/locale/`. Canonical Angular layouts (`src/locale/messages.xlf`) are unchanged. +- **sync**: TOML parser reconstruct is now span-surgical — comments, blank lines between sections, per-value quote style (double vs literal), key order within a section, and irregular whitespace around `=` all round-trip byte-identically. Previously `reconstruct()` ran `smol-toml.stringify(data)` on a mutated parse tree, silently discarding every `# translator: …` comment and collapsing blank lines on first sync — a content-loss regression users saw as noisy first-sync diffs. Multi-line triple-quoted strings remain pass-through (out of scope). `smol-toml` is retained for `extract()`. +- **sync**: `.deepl-sync.yaml` now rejects unknown fields at every nesting level (top-level, buckets, translation, context, validation, sync, tms, locale_overrides) with a ConfigError (exit 7) and a did-you-mean hint pointing at the closest known field. Previously typos were silently discarded — for example, `target_locale: en` (singular) produced a "missing target_locales" error with no pointer to the offending key. +- **build**: Build pipeline now wipes `dist/` before compilation (`npm run clean && tsc`) so file renames in `src/` cannot leave orphaned `.js`/`.d.ts` files that would ship via `npm publish`. +- **voice**: Voice API no longer hardcodes the Pro endpoint; it follows the same endpoint resolution as all other commands. +- **auth**: `auth set-key` and `init` now validate entered keys against the correct endpoint based on key suffix. +- **endpoint**: Standard DeepL URLs (`api.deepl.com`, `api-free.deepl.com`) in saved config no longer override key-based auto-detection. +- **sync**: `deepl sync push --locale ` and `deepl sync pull --locale ` now narrow the fan-out to the named locale instead of silently over-fetching every configured target. Commander was routing `--locale` to whichever scope declared it first, so the subcommand handlers received `undefined` and treated the filter as absent. The subcommands now resolve `--locale` via a shared `resolveLocale(opts, command)` helper that prefers the subcommand's value and falls back to the parent `sync --locale`, matching the existing `resolveFormat` pattern. +- **sync**: Every sync subcommand now cleans up in-flight `.tmp` and `.bak` sibling files on SIGINT/SIGTERM (previously only `sync --watch` had this discipline), and sweeps stale `.bak` files older than `sync.bak_sweep_max_age_seconds` (default 300) at the start of each non-watch run. Reduces accumulation of orphaned artifacts in locale directories after crashes. +- **sync**: `deepl sync --watch` now caches the validated sync config across debounced change events instead of reloading + revalidating it every tick. The cache invalidates on `SIGHUP` (explicit reload) or when `.deepl-sync.yaml` itself is one of the changed files. The watcher also tracks the config file itself so in-session edits are picked up automatically. Previously every file-change event paid for a YAML parse and full config validation even though config rarely changes during a watch session. +- **sync**: Inline TMS credentials in `.deepl-sync.yaml` (`tms.api_key`, `tms.token`) now produce a `stderr` warning at config-load time on every `deepl sync …` subcommand, including non-TTY contexts like CI. Previously the warning was only emitted on the `sync push` / `sync pull` code path, so a user running `sync status` or piping output through another tool would never see that their config held a secret. +- **sync**: Section-batched context translation now honors the key-path separator the source format emitted. YAML keys (flattened with NUL) are now batched by section alongside JSON keys, and a literal dot in a flat YAML key (e.g., `version.major: "1"`) is no longer mis-split into two sections by the section-batcher. +- **sync**: `deepl sync init` now reports an accurate key count for every supported format, not just JSON and YAML. The detection step used to hard-code JSON/YAML parsing and silently fell back to `0` for Android XML, iOS Strings, PO, ARB, XLIFF, TOML, xcstrings, and Java Properties, so the wizard printed "Found 0 keys" for correctly-configured projects. Detection now routes through the FormatRegistry so key counts match what sync itself will extract. +- **sync**: Remove duplicate per-locale tick output in default `deepl sync` runs. Every completed (file, locale) pair was being printed twice — once live via the `locale-complete` progress event and again in a post-sync aggregated summary built from `fileResults`. The aggregated summary is removed; the live tick is now the sole emission site, so the console reflects progress as it happens without a redundant end-of-run block. +- **sync**: Per-key new-locale lookup in `LocaleTranslator.translateForLocale` is now O(1) (Map-indexed) instead of O(N) linear-scan. No user-visible behavior change; reduces cost on projects with large current-diff sets. +- **sync**: `resolveTemplatePatterns` now compiles each distinct pattern regex once per sync run instead of once per `TemplatePatternMatch` occurrence. Duplicate pattern strings (same template literal appearing in many source files) reuse the same `RegExp`. +- **sync**: Template-pattern prep no longer reads every source file twice during `deepl sync` runs that use template-literal patterns. Source content is cached once at the pattern-resolution step and reused in the main translation loop. +- **sync**: `push`, `pull`, `resolve`, `export`, `validate`, `audit`, and `init` now emit a machine-parseable JSON error envelope on stderr when `--format json` is set and an error occurs: `{ok: false, error: {code, message, suggestion?}, exitCode}`. Previously these subcommands wrote free-form text to stderr on failure, breaking script consumers that parse the output. `sync init` also gains a `--format json` success envelope (`{ok: true, created: {configPath, sourceLocale, targetLocales, keys}}`) for project-bootstrap scripts. Envelope shape is guarded by an AJV schema and a shared `assertErrorEnvelope` test helper. +- **sync**: `deepl sync resolve` now exits 11 (SyncConflict) when auto-resolution leaves unresolved conflicts, instead of exit 1 (GeneralError). CI pipelines can now distinguish "lockfile needs human merge" from "CLI crashed". Error message includes an actionable hint to edit `.deepl-sync.lock` manually and re-run `deepl sync`. +- **sync**: `deepl sync --watch --auto-commit` now commits on every successful sync cycle, not only on the initial sync before the watcher attaches. Matches the expected "commit on save" semantics. Gated by the same conditions as the pre-watch auto-commit (clean tree, not dry-run, files written). +- **sync**: `deepl sync --watch` no longer leaks SIGINT/SIGTERM listeners across invocations and no longer serves a stale `tmCache` entry after the TM has been rotated or deleted. The cache now enforces a 5-minute TTL and signal handlers are detached on watcher shutdown; the debounce timer is cleared so "Change detected" cannot print after "Stopping watch". +- **sync**: Stale-lock GC no longer silently deletes lockfile entries when a glob miss is potentially a moved-source rather than a truly-absent file. A broader projectRoot scan by base name guards the deletion; entries that would be GC'd now log a "glob change suspected" warning and are preserved. +- **sync**: Error messages now sanitize control chars and zero-width codepoints from user-supplied content (YAML keys, key paths, translation text) before rendering, so a malicious config or TMS-returned string cannot corrupt the terminal when shown in a ConfigError or ValidationError. +- **sync**: `deepl sync push` now issues push requests with bounded concurrency (default 10, configurable via `tms.push_concurrency`). Previously pushes ran serially per-key-per-locale, so a 5000-key × 10-locale project took ~hours at typical RTT; the new behavior completes in minutes. Aborts on first failure (unchanged semantic). +- **sync**: `deepl sync resolve` now emits a loud warning when `JSON.parse` on a conflict fragment fails and the resolver falls back to a length-heuristic. Previously the heuristic ran silently; users could not audit which entries needed manual review. +- **sync**: `deepl sync init` non-interactive path now validates inputs before writing `.deepl-sync.yaml`: rejects source locale appearing in target-langs, duplicate targets, empty target-langs, malformed locale codes, and path-traversal. Previously the wizard could write a self-invalidating config that failed at the next `deepl sync` run with a cryptic error. +- **sync**: `deepl sync --watch` now coalesces file-change events that fire during an in-flight sync; the watcher re-runs once after the current sync completes instead of silently dropping events. Previously rapid edits could leave final changes unsynced until a manual trigger. +- **sync**: `deepl sync --watch` now cleans up `.bak` files on SIGINT/SIGTERM even when a translation is in flight, and sweeps stale `.bak` siblings at watcher startup (older than 5 minutes). In-flight syncs terminate gracefully after the current locale completes. +- **sync**: Auto-glossary sync now issues a single dictionary-mutation request per locale (previously one per added/removed term) and caches glossary list responses across same-run lookups. Large glossary updates (e.g., 100 term changes) go from 200+ round-trips to ~2 per locale. +- **sync**: `deepl sync --help` now groups examples under *First-time setup* and *Everyday use*, showing the `init` → `--dry-run` → `sync` → `status` onboarding flow, and adds a pointer to `deepl tm list` for translation-memory discoverability. +- **sync**: Acquires an exclusive advisory lock (`.deepl-sync.lock.pidfile`) at sync start to prevent two concurrent `deepl sync` invocations from racing the lockfile and losing keys. Stale locks from crashed processes are detected via PID-liveness check and reclaimed with a warning. +- **sync**: `deepl sync --auto-commit` now refuses to commit when the working tree has unrelated modifications, is mid-rebase/mid-merge/mid-cherry-pick, or HEAD is detached. Also runs git commands from `config.projectRoot` (not the CLI's cwd) and stages only files actually written by the sync run. Previously, auto-commit could bundle a user's in-progress edits into the chore(i18n) commit or fail ambiguously mid-rebase. +- **sync**: TmsClient push/pull now uses a 30s default request timeout (configurable via `tms.timeout_ms`), retries 429 and 503 responses with jittered exponential backoff (max 3 attempts), and includes the response body in error messages when available. Previously a stalled TMS server hung `deepl sync push`/`pull` indefinitely and 500-class errors surfaced with no diagnostic context. +- **sync**: Lockfile version-mismatch and JSON-parse recovery now backs up the prior lockfile to `.deepl-sync.lock.bak--` before resetting in-memory state. Previously a corrupt or wrong-version lockfile was silently discarded, forcing full retranslation with no recovery path. +- **sync**: Invalid `.deepl-sync.yaml` now exits 7 (ConfigError) instead of 6 (ValidationError), matching the documented exit-code contract in docs/SYNC.md and docs/TROUBLESHOOTING.md. +- **sync**: `deepl sync init` now exits 6 (ValidationError) immediately when stdin is not a TTY and fewer than all four init flags are supplied. Previously the partial-flag path fell through to `@inquirer/prompts` and either threw `ExitPromptError` or blocked indefinitely in CI. +- **sync**: `deepl sync --frozen --watch` now exits with ValidationError (code 6). Previously the combination was documented as invalid but entered an infinite drift-check watch loop. +- **sync**: Every `ConfigError` thrown from `validateSyncConfig` (`.deepl-sync.yaml` validation) now includes a remediation `suggestion` string pointing the user at the exact YAML field to fix. Previously ~15 of 18 throw sites provided only a title, defeating the advertised `DeepLCLIError.suggestion` consumer contract. +- **sync**: `deepl sync pull` now fetches each target locale's dictionary once per sync instead of once per (source file x locale) pair. Previously a repo with N source files and L target locales issued N x L identical GETs to the TMS; the new behavior issues L. Affects push/pull throughput on multi-bucket or multi-file projects. +- **api**: `listTranslationMemories` now paginates the `GET /v3/translation_memories` response using the documented `page` / `page_size` query parameters (max 25 per page, bounded at 20 pages). Accounts with more than 25 translation memories previously received a silently truncated list, which caused `deepl tm list` and the TM name → UUID resolver to miss entries. The first call is still issued without query params for backward compatibility and only continues when the server's `total_count` indicates more pages are available. +- **sync**: `deepl sync --format json` now emits `{error, code}` JSON to stderr on failure (matching the `sync status --format json` error contract) and exits with the correct granular exit code. Previously the top-level command fell through to free-form stderr regardless of `--format`. +- **sync**: `deepl sync --format json` (and `status`, `validate`, `audit`) now emit the success JSON payload on stdout, not stderr. Previously `deepl sync --format json > out.json` produced an empty file because the payload was interleaved with progress logs on stderr. +- **translate**: `deepl translate file.txt --to en,fr,es --glossary ` and `--translation-memory ` were silently dropped on the multi-target code path, so terminology and TM were not enforced when translating to more than one language. The multi-target branch now mirrors the single-target precondition and resolution shape: `--from` is required, TM rejects non-`quality_optimized` model types, glossary and TM are resolved once per invocation, and `modelType` defaults to `quality_optimized` when TM is set. +- **translate**: Translation memory resolver cache now keys entries by `name|from|targets`, so the pair-check runs every time a different pair is requested under the same TM name within a session. Previously, sync configs with no top-level `translation_memory` but `locale_overrides` sharing a TM name across locales with mismatched pair support could silently reuse an incompatible TM UUID on the second locale. +- **translate**: `warnIgnoredOptions` now actually fires for `--translation-memory` and `--tm-threshold` in modes that do not support them (e.g. `directory`, `document`). The keys were present in the handler supported-sets but missing from `optionLabels`, so the warning was inert. +- **translate**: Harden TM name resolution against API-returned name pollution. `resolveTranslationMemoryId` now filters entries whose names contain ASCII control chars or zero-width codepoints before matching, and throws `ConfigError` when two entries share the exact name a caller is resolving (asks for UUID disambiguation instead of first-create-wins). Closes a theoretical collision vector against server-side tenancy. +- **glossary**: Glossary resolver hardening — `resolveGlossaryId` now filters API-returned glossary entries whose names contain ASCII control chars or zero-width codepoints before name matching, and throws `ConfigError` with a UUID-disambiguation hint when two surviving entries share the same name. Mirrors the TM resolver defenses. +- **examples**: `examples/31-sync-ci.sh` passes `--file-format json` to `deepl sync init` (was `--format json`, which is not a registered flag on `init` and would fall through to the interactive-prompt branch in non-TTY environments). +- **api**: `listGlossaries` and `listTranslationMemories` errors now carry their method name as a `[listGlossaries]` / `[listTranslationMemories]` suffix on `error.message`. Suffix (not prefix) preserves `deepl sync --format json` stderr-shim consumer greps on canonical phrases like `Authentication failed: Invalid API key`. +- **sync**: Reject `translation_memory` paired with a non-`quality_optimized` `model_type` at config load (ConfigError, exit 7) instead of letting the DeepL API reject each translate request. Applies at top-level and per-locale override. +- **sync**: ICU MessageFormat preservation — plural, select, and selectordinal structures are now preserved during translation. Only leaf text is sent to the API; structural keywords (`plural`, `one`, `other`, etc.) and variable names are kept intact. Handles nested ICU (e.g., select inside plural). +- **sync**: Progress output no longer shows `0/0 keys` lines for up-to-date locales. +- **sync**: New-locale translations now correctly count in progress output. +- **sync**: `sync resolve` conflict marker detection now works mid-file (added multiline flag to regex) +- **sync**: `sync validate`, `sync status`, `sync export`, `sync push`, and `sync pull` now handle multi-locale formats (`.xcstrings`) correctly +- **sync**: `sync init` auto-detection now generates valid glob patterns instead of `{locale}` placeholders that fast-glob cannot match +- **sync**: `resolveTargetPath` supports `target_path_pattern` for Android XML and XLIFF where source locale is absent from source path +- **json**: Warn when JSON files contain duplicate keys (last value used per RFC 8259) +- **sync**: Validation now detects untranslated content (translation identical to source) and excessive length ratio (>150%) +- **sync**: Config validator now passes through `translation`, `validation`, `sync`, `tms`, and `ignore` YAML blocks +- **sync**: CLI overrides (`--formality`, `--glossary`, `--model-type`, `--context`) now merge into config +- **sync**: `--force` mode no longer causes index misalignment when lock has deleted keys +- **sync**: Failed translations now recorded as 'failed' (not 'translated') in lock file per locale +- **sync**: `--frozen` mode now detects drift for deletion-only changes +- **sync**: Lock file structural validation prevents crash on malformed lock files +- **sync**: Batch translation context correctly scoped — per-key requests when context is available, batch without context otherwise +- **sync**: Reject path traversal in target locale at config validation +- **sync**: `sync export --output` now rejects paths that escape the project root and creates missing intermediate directories before writing +- **sync**: `deepl sync audit` now surfaces real translated text read from target files instead of SHA hashes, so terminology-inconsistency output is readable. Missing target files fall back to the hash so divergence is still detected. +- **sync**: Restore `--locale` and `--format` options on the bare `deepl sync` command (previously dropped during an earlier commander option-shadowing fix) and wire `--sync-config` end-to-end — commander camelCases the flag to `syncConfig`, but the handler was reading `config`, so the flag was silently ignored. +- **sync**: Protect placeholders from translation via preserveVariables +- **sync**: PO format reconstruct preserves translations on re-sync +- **sync**: Validate source_locale for path traversal characters +- **sync**: Add `assertPathWithinRoot` guard in sync validate +- **sync**: Fix `resolveTargetPath` `$n` locale injection via function callbacks +- **sync**: Skip deleted diffs in sync-status coverage counts +- **sync**: Validate HTTPS scheme in TmsClient +- **sync**: Replace blocking `readFileSync` with async read in context extraction +- **po**: Correct escape sequence order in `unquote()` -- backslash processed first +- **po**: Multi-line PO header no longer deleted during reconstruct +- **po**: Use ASCII EOT separator for msgctxt keys (fixes `#` in msgid collision) +- **formats**: Prevent `$`-pattern corruption in XLIFF, iOS Strings, and text-preservation `String.replace` calls +- **android-xml**: Add backslash escaping and preserve extra attributes on plurals/string-arrays +- **json**: Handle UTF-8 BOM in JSON files +- **yaml**: Handle empty content in YAML reconstruct +- **utils**: Use unique temp filenames in atomicWriteFile to prevent concurrent corruption +- **sync**: Pre-initialize localeSuccessMap to prevent concurrent locale overwrite race +- **sync**: Retry failed lock entries — computeDiff checks translation status +- **sync**: Resolve per-locale glossary override by name instead of passing raw string +- **sync**: Guard force+frozen combination in sync() API +- **sync**: Only write pull lock file when entries were actually processed +- **android-xml**: Correct unescapeAndroid escape order using single-pass regex. +- **formats**: Reconstructed output for Android XML, YAML, iOS Strings, XLIFF, and ARB parsers now omits keys that were deleted from the source. ARB also inserts newly-added keys at reconstruct time. +- **sync**: Unified placeholder regex with frequency-based comparison +- **sync**: Support Unicode placeholder names and positional printf specifiers (%1$s) +- **po**: Remove fuzzy flag when providing fresh translation +- **sync**: Validate optional config block types (translation, validation, sync, tms) before casting +- **sync**: Normalize documented bucket keys across `sync`, `status`, `validate`, `push`, `pull`, and `sync init` +- **sync**: New locale detection — translate existing keys when a target locale is added +- **sync**: Write lock entries for new-locale translations to prevent re-translation +- **sync**: `--frozen` now detects drift when a new target locale is added +- **sync**: `--dry-run` reports pending new-locale translation in key counts +- **sync**: Clean stale lock entries for files no longer matched by any bucket glob +- **sync**: Merge `config.ignore` patterns into fast-glob for status, validate, push, pull +- **sync**: Guard `source_locale` == `target_locale` in config validation +- **sync**: Deep-clone diff metadata per locale to prevent concurrent mutation +- **sync**: Wrap locale worker in try/catch for graceful per-locale error handling +- **sync**: New-locale translation path applies `preserveVariables`/`restorePlaceholders` +- **sync**: PO plural forms for 3+ form languages (Russian, Arabic) fill higher msgstr indices +- **sync**: `--frozen` guards stale lock entry cleanup and lock file write +- **sync**: Preserve translateBatch index alignment by returning sparse array on partial failure +- **sync**: `restorePlaceholders` replaces all occurrences (not just first) +- **sync**: Fix `context_lines` default to 3 (matching documentation) +- **android-xml**: Escape `<`, `>`, `&` in translations to prevent XML injection +- **json**: Guard against 0-byte source files +- **translate**: Invalid `--to` error is now concise — the 100+ language-code dump is removed; the message points at `deepl languages` for the full list. +- **examples**: `examples/30-sync-basic.sh` and `examples/31-sync-ci.sh` now clean up `/tmp/deepl-sync-demo/` and `/tmp/deepl-sync-ci-demo/` on mid-script failure via `trap cleanup EXIT` (matching the pattern already in examples 32 and 34). +- **docs**: `docs/API.md` and `docs/SYNC.md` now document the `--format FORMAT` option on `deepl sync export` (previously undocumented even though the flag was registered in `src/cli/commands/sync/register-sync-export.ts`). Clarified that on `sync export` the format choice affects only the error envelope on stderr; the success output is always XLIFF 1.2. +- **docs**: `docs/API.md` corrected the note on the `audit` subcommand rename — the previous wording said "Prior to the 1.0.0 release, this subcommand was named `glossary-report`", which implied 1.0.0 users had access to it. The prototype name `glossary-report` never shipped in any tagged release; now worded consistently with the 1.1.0 CHANGELOG entry. +- **write**: `deepl write --interactive` now fails fast with a `ValidationError` when stdin is not a TTY (e.g., a CI job that passes `--interactive` without `--no-input`). Previously the process would hang indefinitely on an `@inquirer/prompts` `select` call that a non-TTY stream can never answer. +- **translate**: `deepl translate --format table` now falls back to plain `[lang] text` output with a `WARN` line on stderr when stdout is not a TTY. Screen readers and log scrapers no longer have to parse `cli-table3`'s Unicode box-drawing characters; pipe `--format table > out.txt` produces parseable plain text instead of Unicode noise. +- **output**: Spinners (`ora`) are now gated on `process.stderr.isTTY` at the `Logger.shouldShowSpinner()` chokepoint. CI and piped-stderr contexts no longer risk ANSI escape leaks into log files regardless of which callsite instantiates the spinner. +- **color**: `NO_COLOR` is now explicitly honored in the CLI bootstrap by setting `chalk.level = 0` when the env var is present. `chalk` already auto-detects `NO_COLOR`, but the explicit hook keeps the two color-detection paths in the codebase (`chalk` and `isColorEnabled()` in `utils/formatters.ts`) unambiguously in sync if `chalk`'s auto-detection ever changes or is mocked in tests. +- **sync init**: a bare `process.exit(7)` literal in `register-sync-init.ts`'s JSON-output path now goes through `ExitCode.ConfigError` so future exit-code renumbering can't desync the hard-coded value from the rest of the codebase. +- **xliff**: the chained-`.replace()` XML entity decoder double-decoded entities. `&lt;` (the literal 5-character string "`<`") was silently collapsed to "`<`" because the decoder ran `&` → `&` on the first pass and then `<` → `<` on the second. Replaced with a single-pass regex that handles the five named entities plus decimal (`&#NN;`) and hex (`&#xNN;`) numeric character references, retiring both bugs in one stroke. Round-tripping literal entities through translation now preserves them byte-for-byte. +- **xliff**: CDATA sections inside `` / `` elements were silently malformed on round-trip — `<` / `>` bytes inside a CDATA body round-tripped asymmetrically through the escape pass. The parser now rejects CDATA inside translatable elements with a `ValidationError` at extract time, matching the allowlist posture of the Laravel PHP parser's heredoc / interpolation rejection. CDATA in non-translatable positions (e.g., ``) is still accepted. ### Security +- **`deepl sync --force` billing defense**: `--watch --force` is now rejected at startup with `ValidationError` (exit 6) — the combination would silently retranslate every key on every file change, creating an unbounded billing loop. Additionally, `--force` now requires confirmation in interactive mode ("Retranslate all keys and bypass cost cap? [y/N]"); add `--yes` (`-y`) to skip the prompt in scripts. In CI environments (`CI=true`), `--force` requires `--yes` explicitly — the process exits 6 with an actionable hint rather than proceeding silently. - Updated `minimatch` from `^9.0.5` to `^10.2.1` to fix ReDoS vulnerability (GHSA-3ppc-4f35-3m26) +- **sync**: `deepl sync pull` now acquires the pidfile process lock (`acquireSyncProcessLock`) before writing any target files. Previously, a concurrent `deepl sync` (which holds the lock while writing target files) and `deepl sync pull` could race across multiple files — `atomicWriteFile` prevents torn individual writes but the multi-file read-modify-write cycle was unguarded. `deepl sync push` is read-only toward local files and does not need the guard. +- **sync**: `sanitizePullKeysResponse` now enforces a hard cap of 50,000 keys (`MAX_PULL_KEY_COUNT`) on TMS pull responses. A response exceeding this limit is rejected with a `ValidationError` before any data is written to disk, preventing OOM conditions on large TMS inventories (e.g. 100K keys × 20 locales). Remediation: partition the TMS export by locale or paginate the pull. +- **sync**: TMS error responses are now sanitized through `sanitizeForTerminal` before appearing in thrown `Error` messages. Both the response body (capped at 1024 bytes) and `response.statusText` are stripped of ANSI escape sequences, bidi override codepoints (U+202A–U+202F), and other terminal-unsafe control characters. Previously a malicious or compromised TMS server could inject sequences such as `\x1b[2J` (screen clear) or U+202E (right-to-left override) into the operator's terminal via a crafted 4xx/5xx HTTP response. +- **sync**: TMS `sync pull` now validates response payloads at the TmsClient boundary before writing to source trees. Non-string values, keys with path separators or control chars, and values >64KiB are rejected with ValidationError; control chars are stripped from accepted values. Previously a compromised or misconfigured TMS could write arbitrary bytes — including format-breaking XML or terminal-corrupting control sequences — into the user's working tree. +- **security**: Validate `context.scan_paths` against project root with symlink protection +- **security**: Use URL hostname check for `tms.server` (prevents `localhost.evil.com` bypass) +- **security**: Encode `tms.project_id` in URL path +- **sync**: `sync push`, `sync pull`, `sync export`, and `sync validate` now refuse to follow symbolic links when scanning source files, matching the policy already enforced by `sync` itself and `sync-context`. Previously a symlink inside a bucket's `include` pattern would be silently followed, potentially reading and transmitting files outside the project root (e.g., `/etc/passwd`, SSH keys). +- **sync**: `Logger.sanitize()` now redacts TMS credentials (`TMS_API_KEY`, `TMS_TOKEN` env values, and `Authorization: ApiKey/Bearer ` headers). Previously only `DEEPL_API_KEY` and `DeepL-Auth-Key` were covered, so TMS credentials could leak into logs via error messages, Headers dumps, or stringified fetch error bodies. +- **sync**: Harden `sync resolve` conflict-fragment merge against prototype pollution. JSON-parsed fragments can carry `__proto__`/`constructor`/`prototype` as own properties; the merge now skips those keys and uses `Object.create(null)` accumulators so `deepl sync resolve` on a hostile `.deepl-sync.lock` cannot mutate `Object.prototype`. +- **sync**: `deepl sync export` now rejects source-side paths that resolve outside the project root with a ValidationError, matching the `--output` destination guard. Previously a `.deepl-sync.yaml` with absolute source paths or symlinks pre-dating the fast-glob hardening could read files outside the configured scan root during export. +- **sync**: `scan_paths` config entries are validated against path traversal using a proper glob-literal prefix extractor rather than a regex strip. Previously crafted patterns using brace-expansion (`{..,src}/**`), extglobs (`@(..)/**`), or escaped wildcards could bypass the prior `assertPathWithinRoot` guard. No change to valid configurations; rejected configurations now produce a ConfigError with the offending pattern shown. +- **deps**: `npm audit fix` — resolves `axios` GHSA-3p68-rc4w-qgx5 (SSRF via NO_PROXY normalization), `axios` GHSA-fvcv-3m26-pcqx (cloud-metadata exfil via header injection), and `follow-redirects` GHSA-r4q5-vmmm-2653 (auth-header leak on redirect). Not reachable from the CLI (baseUrl hardcoded to `api.deepl.com`; TMS uses native `fetch`); transitive advisories are now quiet. +- **sync**: `.deepl-sync.yaml` and auto-detect-path reads (`package.json`, the first-match i18n file for key counting) now route through `safeReadFileSync`, which rejects symbolic links with a `ValidationError`. A hostile repo could previously ship a `.deepl-sync.yaml` symlinked to `~/.ssh/id_rsa` (or another dotfile outside the project root) and surface the target's contents in YAML parser errors on stderr. Affected sites: `src/sync/sync-config.ts:566`, `src/sync/sync-init.ts:239`, `src/sync/sync-init.ts:271`. Runtime file reads during `deepl sync` itself are unchanged — the bucket walker already refuses symlinks via `fast-glob`'s `followSymbolicLinks: false`. +- **http**: When `HTTP_PROXY` / `HTTPS_PROXY` is configured with an `http://` proxy and the target endpoint is `https://`, the CLI now emits a startup warning naming the proxy host and noting the TLS-termination MITM risk. TLS is still tunneled end-to-end via CONNECT, so this is a visibility fix rather than a behavior change — a malicious proxy that terminates TLS with a trusted cert would see the Authorization header, and the user should be aware. Users with legitimate corporate http-only proxies see the warning but the connection proceeds (no forced abort; the CLI can't tell corporate infra apart from attacker infra). +- **log redaction**: `Logger`'s sanitizer now redacts `X-Api-Key` and `X-Auth-Token` headers, plus `?api_key=` / `?apikey=` query parameters. Previously only `DeepL-Auth-Key`, `Authorization: ApiKey|Bearer`, `?token=` / `&token=`, and the `DEEPL_API_KEY` / `TMS_API_KEY` / `TMS_TOKEN` env-var exact values were covered. axios error dumps that include `config.headers` on TMS-style third-party backends (e.g., Phrase, Lokalise, custom REST endpoints) no longer leak these credentials via verbose logs. ## [1.0.0] - 2026-02-17 diff --git a/CLAUDE.md b/CLAUDE.md index 0930573..f2b032c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,21 +6,25 @@ DeepL CLI is a command-line interface for the DeepL API that integrates translat ### Current Status -- **Version**: 1.0.0 -- **Tests**: 2757 tests passing (100% pass rate), ~91% coverage +- **Version**: see `VERSION` file / `package.json` +- **Tests**: see `npm test` output (target: all green; coverage thresholds enforced by jest config) - **Test mix**: ~70-75% unit, ~25-30% integration/e2e -- **Next Milestone**: 1.0.0 (stable public API) ### Architecture ``` -CLI Commands (translate, write, watch, glossary, etc.) +CLI Commands (translate, write, voice, sync, watch, glossary, tm, …) ↓ -Service Layer (Translation, Write, Batch, Watch, GitHooks, Cache, Glossary) +Service Layer (Translation, Write, Voice, Batch, Watch, Glossary, + TranslationMemory, StyleRules, Admin, Document, + GitHooks, Usage, Detect, Languages) + ↓ ↓ +Sync Engine (src/sync) Format Parsers (src/formats — 11 i18n formats) + ↓ ↓ +API Client (Translate, Write, Glossary, Document, Voice, + StyleRules, Admin, TMS) ↓ -API Client (DeepL API: /v2/translate, /v2/write, /v3/glossaries) - ↓ -Storage (SQLite Cache, Config Management) +Storage (SQLite Cache, Config) + Static Data (src/data — language registry) ``` ### Configuration @@ -98,8 +102,11 @@ Use **Semantic Versioning** with **Conventional Commits**: src/ ├── cli/ # CLI interface and commands ├── services/ # Business logic +├── sync/ # Continuous localization engine (scan, diff, translate, write, lock) +├── formats/ # i18n file format parsers (JSON, YAML, PO, XLIFF, Android XML, etc.) ├── api/ # DeepL API client -├── storage/ # Data persistence +├── storage/ # Data persistence (SQLite cache, config) +├── data/ # Static data (language registry) ├── utils/ # Utility functions └── types/ # Type definitions ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fd5810..bc99a5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,6 +195,7 @@ Contributions must be licensed under the same license as the project: - [ ] `README.md` updated if user-facing feature changed - [ ] `docs/API.md` updated if command/flag added or changed - [ ] Example script added/updated in `examples/` for new features +- [ ] Added new example script to `examples/run-all.sh` EXAMPLES array (for new CLI commands) ## Adding a New CLI Command @@ -227,4 +228,4 @@ When filing a bug report, include: - [docs/API.md](./docs/API.md) -- Complete CLI command reference - [DeepL API Docs](https://www.deepl.com/docs-api) -- Official API documentation -[issues]: https://github.com/kwey/deepl-cli/issues +[issues]: https://github.com/DeepLcom/deepl-cli/issues diff --git a/LICENSE b/LICENSE index 6c3eca3..d0df5e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 DeepL +Copyright (c) 2026 DeepL SE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3fc3220..020582f 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ ## 🌟 Key Features - **🌍 Translation** - High-quality translation using DeepL's next-gen LLM +- **🔄 Continuous Localization (`deepl sync`)** - Incremental i18n file sync with 11 format parsers - **📄 Document Translation** - Translate PDF, DOCX, PPTX, XLSX with formatting preservation -- **🎙️ Voice Translation** - Real-time speech translation via WebSocket streaming (Voice API) +- **🎙️ Voice Translation (Pro/Enterprise)** - Real-time speech translation via WebSocket streaming (Voice API) - **👀 Watch Mode** - Real-time file watching with auto-translation - **✍️ Writing Enhancement** - Grammar, style, and tone suggestions (DeepL Write API) - **💾 Smart Caching** - Local SQLite cache with LRU eviction @@ -31,26 +32,27 @@ For security policy and vulnerability reporting, see [SECURITY.md](SECURITY.md). - [Verbose Mode](#verbose-mode) - [Quiet Mode](#quiet-mode) - [Custom Configuration Files](#custom-configuration-files) + - [Configuration Paths](#configuration-paths) + - [Proxy Configuration](#proxy-configuration) + - [Retry and Timeout Configuration](#retry-and-timeout-configuration) - [Usage](#-usage) - **Core Commands:** [Translation](#translation) | [Writing Enhancement](#writing-enhancement) | [Voice Translation](#voice-translation) - - **Resources:** [Glossaries](#glossaries) + - **Resources:** [Glossaries](#glossaries) | [Translation Memories](#translation-memories) - **Workflow:** [Watch Mode](#watch-mode) | [Git Hooks](#git-hooks) - **Configuration:** [Setup Wizard](#setup-wizard) | [Authentication](#authentication) | [Configure Defaults](#configure-defaults) | [Cache Management](#cache-management) | [Style Rules](#style-rules) - - **Information:** [Usage Statistics](#api-usage-statistics) | [Language Detection](#language-detection) | [Languages](#supported-languages) | [Shell Completion](#shell-completion) + - **Information:** [Usage Statistics](#api-usage-statistics) | [Language Detection](#language-detection) | [Languages](#supported-languages) | [Shell Completion](#shell-completion) | [Command Suggestions](#command-suggestions) - **Administration:** [Admin API](#admin-api) - [Development](#-development) - [Architecture](#-architecture) +- [Testing](#-testing) +- [Documentation](#-documentation) +- [Environment Variables](#-environment-variables) +- [Security & Privacy](#-security--privacy) - [Contributing](#-contributing) - [License](#-license) ## 📦 Installation -### From npm (Coming Soon) - -```bash -npm install -g deepl-cli -``` - ### From Source ```bash @@ -69,7 +71,7 @@ npm link # Verify installation deepl --version -# Output: 1.0.0 +# Output: deepl-cli 1.x.x ``` > **Note:** This project uses [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) for local caching, which requires native compilation. If `npm install` fails with build errors, ensure you have: @@ -78,6 +80,12 @@ deepl --version > - **Linux**: `python3`, `make`, and `gcc` (`apt install python3 make gcc g++`) > - **Windows**: Visual Studio Build Tools or `windows-build-tools` (`npm install -g windows-build-tools`) +### From npm (not yet published) + +An npm package is not yet published; install from source until then. + +> **CI examples below** (and generated hook output) assume a published npm package; source-installed users should substitute the source-install path. + ## 🚀 Quick Start ### 1. Get Your DeepL API Key @@ -288,13 +296,13 @@ deepl translate locales/en.json --to es,fr,de --output locales/ **Smart Caching for Text Files:** -Small text-based files (under 100 KB) automatically use the cached text translation API for faster performance and reduced API calls. Larger files automatically fall back to the document translation API (not cached). +Small text-based files (under 100 KiB) automatically use the cached text translation API for faster performance and reduced API calls. Larger files automatically fall back to the document translation API (not cached). -- **Cached formats:** `.txt`, `.md`, `.html`, `.htm`, `.srt`, `.xlf`, `.xliff` (files under 100 KB only) +- **Cached formats:** `.txt`, `.md`, `.html`, `.htm`, `.srt`, `.xlf`, `.xliff` (files under 100 KiB only) - **Structured formats:** `.json`, `.yaml`, `.yml` — parsed and translated via batch text API (no size limit) -- **Large file fallback:** Files ≥100 KB use document API (not cached, always makes API calls) +- **Large file fallback:** Files ≥100 KiB use document API (not cached, always makes API calls) - **Binary formats:** `.pdf`, `.docx`, `.pptx`, `.xlsx` always use document API (not cached) -- **Performance:** Only small text files (<100 KB) benefit from instant cached translations +- **Performance:** Only small text files (<100 KiB) benefit from instant cached translations - **Cost savings:** Only small text files avoid repeated API calls ```bash @@ -840,6 +848,8 @@ deepl init # - Basic configuration ``` +> To configure continuous localization for an existing project, see also [`deepl sync init`](#sync-init) or run the wizard directly. + #### Authentication ```bash @@ -1040,7 +1050,7 @@ DeepL CLI includes built-in retry logic and timeout handling for robust API comm - ✅ Automatic retry on transient failures - ✅ Exponential backoff to avoid overwhelming the API - ✅ Smart error detection (retries 5xx, not 4xx) -- ✅ Configurable timeout and retry limits (programmatic API only) +- ✅ Configurable via library-consumer options; not exposed as a CLI flag - ✅ Works across all DeepL API endpoints **Retry Behavior Examples:** @@ -1313,7 +1323,7 @@ Cache location: `~/.cache/deepl-cli/cache.db` (or `~/.deepl-cli/cache.db` for le ### Prerequisites -- Node.js >= 20.0.0 +- Node.js >= 20.19.0 - npm >= 9.0.0 - DeepL API key @@ -1387,25 +1397,33 @@ deepl translate "Hello" --to es DeepL CLI follows a layered architecture: ``` -CLI Interface (Commands, Parsing, Help) - ↓ -Core Application (Command Handlers, Interactive Shell) +CLI Commands (translate, write, voice, sync, watch, glossary, tm, …) ↓ -Service Layer (Translation, Write, Cache, Watch, Glossary) +Service Layer (Translation, Write, Voice, Batch, Watch, Glossary, + TranslationMemory, StyleRules, Admin, Document, + GitHooks, Usage, Detect, Languages) + ↓ ↓ +Sync Engine (src/sync) Format Parsers (src/formats — 11 i18n formats) + ↓ ↓ +API Client (Translate, Write, Glossary, Document, Voice, + StyleRules, Admin, TMS) ↓ -API Client (DeepL Translate, Write, Glossary APIs) - ↓ -Storage (SQLite Cache, Config, Translation Memory) +Storage (SQLite Cache, Config) + Static Data (src/data — language registry) ``` ### Key Components -- **Translation Service** - Core translation logic with caching and preservation -- **Write Service** - Grammar and style enhancement -- **Cache Service** - SQLite-based cache with LRU eviction -- **Preservation Service** - Preserves code blocks, variables, formatting -- **Watch Service** - File watching with debouncing -- **Glossary Service** - Glossary management and application +- **Translation Service** — core translation with caching and text preservation +- **Write Service** — grammar, style, and tone suggestions +- **Voice Service** — real-time speech translation over WebSocket +- **Sync Engine** — continuous localization: scan, diff, translate, write, lock +- **Format Parsers** — 11 i18n parsers (JSON, YAML, TOML, PO, Android XML, iOS Strings, xcstrings, ARB, XLIFF, Properties, Laravel PHP) with format-preserving reconstruct +- **Batch Service** — parallel multi-file translation +- **Watch Service** — file watching with debouncing +- **Glossary Service** — glossary management and application +- **Translation Memory Service** — reuse approved translations (`--translation-memory`) +- **Cache** — SQLite cache with LRU eviction (`src/storage/cache.ts`) +- **Preservation utilities** — `src/utils/` helpers for code blocks, variables, and ICU MessageFormat ## 🧪 Testing @@ -1452,6 +1470,10 @@ npm run examples:fast | `DEEPL_CONFIG_DIR` | Override config and cache directory | | `XDG_CONFIG_HOME` | Override XDG config base (default: `~/.config`) | | `XDG_CACHE_HOME` | Override XDG cache base (default: `~/.cache`) | +| `HTTP_PROXY` | HTTP proxy URL for outbound DeepL API traffic | +| `HTTPS_PROXY` | HTTPS proxy URL (takes precedence over `HTTP_PROXY`) | +| `TMS_API_KEY` | API key for the configured TMS server (`sync push`/`pull`) | +| `TMS_TOKEN` | Alternative auth token for the configured TMS server | | `NO_COLOR` | Disable colored output | | `FORCE_COLOR` | Force colored output even when terminal doesn't support it. Useful in CI. `NO_COLOR` takes priority if both are set. | | `TERM=dumb` | Disables colored output and progress spinners. Automatically set by some CI environments and editors. | @@ -1464,12 +1486,14 @@ See [docs/API.md#environment-variables](./docs/API.md#environment-variables) for - **Local caching** - All cached data stored locally in SQLite, never shared - **No telemetry** - Zero usage tracking or data collection - **Environment variable support** - Use `DEEPL_API_KEY` environment variable for CI/CD -- **GDPR compliant** - Follows DeepL's GDPR compliance guidelines +- **GDPR-aligned with DeepL's DPA** - Follows DeepL's Data Processing Agreement terms ## 📄 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +DeepL® is a registered trademark of DeepL SE. + --- _Powered by DeepL's next-generation language model_ diff --git a/SECURITY.md b/SECURITY.md index 2f1286c..ee58f23 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,7 +1,10 @@ +# Security Policy + ## Supported Versions | Version | Supported | |---------|--------------------| +| 1.1.x | :white_check_mark: | | 1.0.x | :white_check_mark: | | < 1.0 | :x: | diff --git a/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/docs/API.md b/docs/API.md index bafcff9..8d8860e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,7 +1,7 @@ # DeepL CLI - API Reference -**Version**: 1.0.0 -**Last Updated**: February 17, 2026 +**Version**: 1.1.0 +**Last Updated**: April 23, 2026 Complete reference for all DeepL CLI commands, options, and configuration. @@ -17,8 +17,10 @@ Complete reference for all DeepL CLI commands, options, and configuration. - [voice](#voice) - Resources - [glossary](#glossary) + - [tm](#tm) - Workflow - [watch](#watch) + - [sync](#sync) - [hooks](#hooks) - Configuration - [init](#init) @@ -34,8 +36,8 @@ Complete reference for all DeepL CLI commands, options, and configuration. - Administration - [admin](#admin) - [Configuration](#configuration) -- [Exit Codes](#exit-codes) - [Environment Variables](#environment-variables) +- [Exit Codes](#exit-codes) --- @@ -174,8 +176,8 @@ Commands are organized into six groups, matching the `deepl --help` output: | Group | Commands | Description | | ------------------ | ------------------------------------------------ | ------------------------------------------------------------------------- | | **Core Commands** | `translate`, `write`, `voice` | Translation, writing enhancement, and speech translation | -| **Resources** | `glossary` | Manage translation glossaries | -| **Workflow** | `watch`, `hooks` | File watching and git hook automation | +| **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 | @@ -237,6 +239,8 @@ Translate text directly, from stdin, from files, or entire directories. Supports - `--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`) - `--glossary NAME-OR-ID` - Use glossary by name or ID for consistent terminology +- `--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. - `--enable-beta-languages` - Include beta languages that are not yet stable (forward-compatibility with new DeepL languages) @@ -448,8 +452,6 @@ deepl translate "Bank" --to es --context "Financial institution" deepl translate app.json --to es --context "E-commerce checkout flow" ``` -**Note:** The `--context` feature may not be supported by all DeepL API tiers. Check your API plan for context support availability. - **Formality levels:** ```bash @@ -528,6 +530,37 @@ deepl translate "API documentation" --to es --glossary tech-terms deepl translate README.md --to fr --glossary abc-123-def-456 --output README.fr.md ``` +**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. + +```bash +# 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. Glossary name resolution works transparently across all target languages. Translation memory name resolution, however, requires a single TM that covers every requested target language pair — because each TM in DeepL is scoped to one source→target pair, using a TM name with differing multi-targets surfaces a `ConfigError` (exit 7). For multi-target TM use, pass the TM UUID directly. + +```bash +# 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:** ```bash @@ -640,6 +673,7 @@ Enhance text quality with AI-powered grammar checking, style improvement, and to **Language:** - `--lang, -l LANG` - Target language: `de`, `en`, `en-GB`, `en-US`, `es`, `fr`, `it`, `pt`, `pt-BR`, `pt-PT`. 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):** @@ -986,6 +1020,402 @@ deepl watch docs/ --to de,ja --git-staged --auto-commit --- +### sync + +Continuous localization engine for i18n file translation. + +#### Synopsis + +```bash +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](#exit-codes) 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](SYNC.md#sync). + +#### 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. + +**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 ""` 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](./SYNC.md#deepl-sync-init) 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 +- `--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. + +**Examples:** + +```bash +# 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 across 1.x):** + +```json +{ + "sourceLocale": "en", + "totalKeys": 142, + "skippedKeys": 1, + "locales": [ + { "locale": "de", "complete": 140, "missing": 2, "outdated": 0, "coverage": 98 } + ] +} +``` + +`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 success JSON payload is written to **stdout**, so `deepl sync status --format json > status.json` produces a parseable file. Diagnostic/progress logs stay on **stderr**. 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 `sync` subcommand):** On failure, `--format json` emits the following JSON envelope to **stderr** and exits with the typed exit code: + +```json +{ + "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. The envelope shape is identical for `deepl sync`, `sync push`, `sync pull`, `sync resolve`, `sync export`, `sync validate`, `sync audit`, `sync init`, and `sync status`. + +**`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: + +```json +{ + "ok": true, + "created": { + "configPath": "/absolute/path/.deepl-sync.yaml", + "sourceLocale": "en", + "targetLocales": ["de", "fr"], + "keys": 128 + } +} +``` + +**Casing convention:** CLI JSON output uses `camelCase`; 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. + +**Examples:** + +```bash +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) +``` + +##### `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:** + +```bash +deepl sync validate + +# Validate only German +deepl sync validate --locale de +``` + +**Sample output:** + +``` +Validation Results: + + de: + ✓ 138/140 strings valid + ✗ 2 issues found: + - messages.welcome: placeholder {name} missing in translation + - errors.count: format specifier %d replaced with %s + + fr: + ✓ 142/142 strings valid +``` + +##### `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:** + +```json +{ + "totalTerms": 1, + "inconsistencies": [ + { + "sourceText": "Dashboard", + "locale": "de", + "translations": ["Armaturenbrett", "Dashboard"], + "files": ["locales/en/common.json", "locales/en/admin.json"] + } + ] +} +``` + +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 stderr (matching other sync subcommands) so script consumers can parse failure shape uniformly +- `--sync-config PATH` - Path to `.deepl-sync.yaml` + +**Examples:** + +```bash +# 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` + +**JSON success envelope (stable across 1.x):** `{ "ok": true, "resolved": , "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](./SYNC.md#tms-rest-contract) for the full field reference and REST contract. + +**JSON success envelope (stable across 1.x):** `{ "ok": true, "pushed": , "skipped": [...] }` + +##### `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` + +**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](./SYNC.md#tms-rest-contract) for the full field reference and REST contract. + +**JSON success envelope (stable across 1.x):** `{ "ok": true, "pulled": , "skipped": [...] }` + +#### Examples + +**Basic sync:** + +```bash +# Sync all configured locales +deepl sync + +# Preview what would be translated +deepl sync --dry-run +``` + +**CI/CD (frozen mode):** + +```bash +# Fail if translations are out of date (exit code 10) +deepl sync --frozen +``` + +**Locale filtering:** + +```bash +# Sync only German and French +deepl sync --locale de,fr +``` + +**Force re-translation:** + +```bash +# Re-translate everything, ignoring the lockfile +deepl sync --force +``` + +**JSON output for scripting:** + +```bash +deepl sync --format json +deepl sync status --format json +``` + +**`deepl sync --format json` output contract (stable across 1.x):** + +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 in any 1.x release: + +| 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 exits with code 10 if any translations are missing or outdated. This is the recommended mode for CI/CD pipelines. +- 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](./SYNC.md#tms-rest-contract). 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](./SYNC.md) for the complete sync guide including configuration schema, CI/CD recipes, and troubleshooting. + +--- + ### hooks Manage git hooks for translation workflow automation. @@ -1505,6 +1935,53 @@ deepl glossary delete-dictionary abc-123-def-456 fr --- +### 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 + +```bash +deepl tm list [options] +``` + +#### Subcommands + +##### `list` + +List all translation memories on the account. + +**Options:** + +- `--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:** + +```bash +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 ` — use a listed TM on a single translate call. +- `.deepl-sync.yaml` `translation.translation_memory` — configure a TM for a sync run (see [sync](#sync)). + +--- + ### cache Manage translation cache. @@ -2267,96 +2744,6 @@ Existing `~/.deepl-cli/` installations continue to work with no changes needed. --- -## Exit Codes - -The CLI uses semantic exit codes to enable intelligent error handling in scripts and CI/CD pipelines. - -| Code | Meaning | Description | Retryable | -| ---- | -------------------- | -------------------------------------------------------------- | --------- | -| 0 | Success | Operation completed successfully | N/A | -| 1 | General Error | Unclassified error | No | -| 2 | Authentication Error | Invalid or missing API key | No | -| 3 | Rate Limit Error | Too many requests (HTTP 429) | Yes | -| 4 | Quota Exceeded | Character limit reached (HTTP 456) | No | -| 5 | Network Error | Connection timeout, refused, or service unavailable (HTTP 503) | Yes | -| 6 | Invalid Input | Missing arguments, unsupported format, or validation error | No | -| 7 | Configuration Error | Invalid configuration file or settings | No | -| 8 | Check Failed | Text needs improvement (`deepl write --check`) | No | -| 9 | Voice Error | Voice API error (unsupported plan or session failure) | No | - -**Special Cases:** - -- `deepl write --check`: Exits with 0 if no changes needed, 8 (CheckFailed) if improvements suggested - -**Exit Code Classification:** - -The CLI automatically classifies errors based on error messages and HTTP status codes: - -- **Authentication (2)**: "authentication failed", "invalid api key", "api key not set" -- **Rate Limit (3)**: "rate limit exceeded", "too many requests", HTTP 429. The CLI respects the `Retry-After` header when present, falling back to exponential backoff when absent -- **Quota (4)**: "quota exceeded", "character limit reached", HTTP 456 -- **Network (5)**: "econnrefused", "enotfound", "econnreset", "etimedout", "socket hang up", "network error", "network timeout", "connection refused", "connection reset", "connection timed out", "service temporarily unavailable", HTTP 503 -- **Invalid Input (6)**: "cannot be empty", "not found", "unsupported", "not supported", "invalid", "is required", "expected", "cannot specify both" -- **Configuration (7)**: "config file", "config directory", "configuration file", "configuration error", "failed to load config", "failed to save config", "failed to read config" - -**Trace IDs for Debugging:** - -API error messages include the DeepL `X-Trace-ID` header when available. This trace ID is useful for debugging and when contacting DeepL support: - -```bash -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. - -**CI/CD Integration:** - -Use exit codes to implement intelligent retry logic in scripts: - -```bash -#!/bin/bash -# Retry on rate limit or network errors only - -deepl translate "Hello" --to es -EXIT_CODE=$? - -case $EXIT_CODE in - 0) - echo "Success" - ;; - 3|5) - echo "Retryable error (code $EXIT_CODE), retrying in 5 seconds..." - sleep 5 - deepl translate "Hello" --to es - ;; - *) - echo "Non-retryable error (code $EXIT_CODE)" - exit $EXIT_CODE - ;; -esac -``` - -**Checking Exit Codes:** - -```bash -# Check if translation succeeded -if deepl translate "Hello" --to es; then - echo "Translation succeeded" -else - EXIT_CODE=$? - echo "Translation failed with exit code: $EXIT_CODE" -fi - -# Handle specific errors -deepl translate "Hello" --to invalid -if [ $? -eq 6 ]; then - echo "Invalid input provided" -fi -``` - ---- - ## Environment Variables ### `DEEPL_API_KEY` @@ -2416,6 +2803,236 @@ When set to `dumb`, disables colored output and progress spinners. This is autom 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`. + +```bash +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`. + +```bash +export HTTPS_PROXY="http://proxy.example.com:3128" +``` + +### `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](SYNC.md) for setup details. + +```bash +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](SYNC.md) for setup details. + +```bash +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, 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 locale | 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](#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`, `deepl auth test` 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 `, 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 . + +#### 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. + +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`: missing target languages, unsupported plan (pre-flight check), invalid session parameters +- `glossary`: missing name/entries, entry not found on delete +- `sync`: `--frozen` combined with `--force`, 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/unset` 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) +- `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. Exit is *soft* — `process.exitCode` is set so cleanup still runs. Emitted by: + +- `deepl write --check ` when the Write API would suggest changes (`needsImprovement === true`) +- `deepl sync validate` when validation surfaces one or more `error`-severity issues (missing placeholders, format-string mismatches, unbalanced HTML tags) + +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) + +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 **soft** — `process.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 locale failed while at least one other locale succeeded. The successful locales' target files and lockfile entries are written; the failed locales' files are not touched. Emitted only by `deepl sync` (the root command). + +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 `) 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` +- **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` +- **9 — VoiceError**: `voice api`, `voice session` +- **1 — GeneralError**: anything not matched above + +### Trace IDs + +API error messages include the DeepL `X-Trace-ID` header when available, which is useful when contacting DeepL support: + +```bash +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`): + +```bash +#!/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: + +```bash +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: + +```bash +deepl write --check README.md +[ $? -eq 8 ] && echo "Write suggests improvements; run: deepl write --fix README.md" >&2 +``` + --- ## See Also @@ -2425,5 +3042,5 @@ export TERM=dumb --- -**Last Updated**: February 17, 2026 -**DeepL CLI Version**: 1.0.0 +**Last Updated**: April 20, 2026 +**DeepL CLI Version**: 1.1.0 diff --git a/docs/SYNC.md b/docs/SYNC.md new file mode 100644 index 0000000..624ec78 --- /dev/null +++ b/docs/SYNC.md @@ -0,0 +1,976 @@ +# DeepL Sync -- Continuous Localization Engine + +> Scan, translate, and sync i18n files from the command line. + +## Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Supported File Formats](#supported-file-formats) +- [Configuration](#configuration) +- [Commands](#commands) +- [Stability & deprecation](#stability--deprecation) +- [CI/CD Integration](#cicd-integration) +- [Troubleshooting](#troubleshooting) +- [Exit Codes](#exit-codes) +- [Further Reading](#further-reading) + +## Overview + +`deepl sync` is a continuous localization engine that keeps your project's translation files in sync with your source strings. It scans your project for i18n resource files, detects new and changed strings using a lockfile, sends only the delta to the DeepL API, and writes back properly formatted target files -- preserving indentation, comments, and format-specific conventions. This replaces the manual export/translate/import cycle with a single command that fits into both local development and CI/CD pipelines. + +## Quick Start + +### Prerequisites + +- DeepL API key (`deepl auth set-key YOUR_KEY` or `DEEPL_API_KEY` env var) +- Project with i18n resource files (JSON, YAML, TOML, PO, Android XML, iOS Strings, ARB, XLIFF, Java Properties, Xcode String Catalog, or Laravel PHP arrays) + +### First Sync in 30 Seconds + +```bash +# 1. Initialize (auto-detects your project's i18n framework) +deepl sync init --source-locale en --target-locales de,fr --file-format json --path "locales/en.json" + +# 2. Preview what would be translated +deepl sync --dry-run + +# 3. Translate +deepl sync +``` + +## How It Works + +1. **Scan** -- finds i18n files matching bucket patterns in `.deepl-sync.yaml` +2. **Diff** -- compares source strings against `.deepl-sync.lock` to find new/changed/deleted keys +3. **Translate** -- sends only new and changed strings to the DeepL API +4. **Write** -- reconstructs target files preserving format, indentation, and comments +5. **Lock** -- updates `.deepl-sync.lock` with translation hashes for incremental sync + +The lockfile tracks content hashes for every source string. On subsequent runs, only strings whose hash has changed (or that are newly added) are sent to DeepL. Deleted keys are removed from target files. This makes sync fast and cost-efficient -- you only pay for what actually changed. + +All sync commands (`sync`, `sync push`, `sync pull`, `sync export`, `sync validate`) refuse to follow symbolic links when scanning `include` globs. A symlink matching a bucket pattern is silently skipped, preventing a hostile symlink (e.g., `locales/en.json` -> `/etc/passwd`) from exfiltrating files outside the project root to the TMS server or into an exported XLIFF. + +### Concurrent sync + +Only one `deepl sync` run is supported at a time per project directory. At startup, sync writes a `.deepl-sync.lock.pidfile` containing its PID; a second invocation that sees an existing pidfile whose PID is still alive exits with `ConfigError` (exit code 7). If the PID is dead (e.g., a previous run crashed), sync removes the stale pidfile with a warning and proceeds. The pidfile is deleted automatically on normal completion and on SIGINT/SIGTERM. + +## Supported File Formats + +| Format | Extensions | Used By | +|--------|-----------|---------| +| JSON (i18n) | `.json` | i18next, react-intl, vue-i18n, next-intl | +| YAML | `.yaml`, `.yml` | Rails, Hugo, Symfony | +| TOML | `.toml` | Go go-i18n | +| Gettext PO | `.po`, `.pot` | Django, WordPress, LinguiJS | +| Android XML | `.xml` | Android (`strings.xml`) | +| iOS Strings | `.strings` | iOS, macOS (`Localizable.strings`) | +| Xcode String Catalog | `.xcstrings` | iOS, macOS (`Localizable.xcstrings`) — multi-locale | +| ARB | `.arb` | Flutter, Dart | +| XLIFF | `.xlf`, `.xliff` | Angular, Xcode, enterprise CAT tools | +| Java Properties | `.properties` | Java, Spring (`ResourceBundle`) | +| Laravel PHP arrays | `.php` | Laravel (`lang/**/*.php`, `resources/lang/**/*.php`) | + +All parsers preserve format-specific metadata: + +- **JSON**: nested key structure, indentation style, trailing newlines +- **YAML**: comments, anchors, flow/block style +- **PO**: translator comments, flags, plural forms, msgctxt +- **Android XML**: `translatable="false"` attributes, comments, string-arrays, plurals +- **iOS Strings**: comments, ordering, escape sequences +- **Xcode String Catalog**: per-locale `stringUnit` state, comments, multi-locale structure +- **ARB**: `@key` metadata (description, placeholders, type) +- **XLIFF**: `` elements, state attributes, translation units +- **TOML**: `#` comments, blank lines between sections, key order within a section, per-value quote style (double-quoted vs literal single-quoted), irregular whitespace around `=`, and every byte outside a replaced string literal round-trip verbatim via span-surgical reconstruct. Multi-line triple-quoted strings are passed through as-is (not translated). +- **Properties**: comments, Unicode escapes (`\uXXXX`), line continuations, separator style +- **Laravel PHP arrays**: PHPDoc/line/block comments, quote style (single vs double), trailing commas, irregular whitespace, and every byte outside a replaced string literal round-trip verbatim. Span-surgical reconstruct — the AST is used for string-literal offsets only, never reprinted. Allowlist rejects double-quoted interpolation (`"Hello $name"`), heredoc, nowdoc, and string concatenation; Laravel pipe-pluralization values (`|{n}` / `|[n,m]` / `|[n,*]`) are excluded from the translation batch and counted separately in `deepl sync status`. + +The sync engine also supports **multi-locale formats** where all locales are stored in a single file (e.g., Apple `.xcstrings`). For these formats, the engine automatically serializes locale writes to prevent race conditions and passes the locale to the parser so it can scope extract/reconstruct operations to the correct locale section. + +## Configuration + +### `.deepl-sync.yaml` + +This file defines what `deepl sync` translates. It lives in your project root and should be committed to version control. + +```yaml +version: 1 +source_locale: en +target_locales: + - de + - fr + - es + - ja + +buckets: + json: + include: + - "locales/en.json" + - "locales/en/*.json" + exclude: + - "locales/en/generated.json" + +translation: + formality: default + model_type: prefer_quality_optimized + glossary: my-project-terms + translation_memory: my-tm + translation_memory_threshold: 80 + instruction_templates: + button: "Keep translation concise, maximum 3 words." + th: "Table column header. Maximum 2 words." + +context: + enabled: true + scan_paths: + - "src/**/*.{ts,tsx}" + overrides: + save: "Save button in document editor toolbar" + close: "Close button in modal dialog" +``` + +### Schema Reference + +**Validation.** Unknown fields are rejected at every nesting level (top-level, buckets, translation, context, validation, sync, tms, locale_overrides) with a `ConfigError` (exit 7). Typos produce a did-you-mean hint pointing at the closest known field — for example, `target_locale: en` (singular) reports `Unknown field "target_locale" in .deepl-sync.yaml top level` with the hint `Did you mean "target_locales"?`. + +#### Top-level fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `version` | `number` | Yes | -- | Config schema version (currently `1`) | +| `source_locale` | `string` | Yes | -- | BCP-47 source language code (e.g., `en`, `de`, `ja`) | +| `target_locales` | `string[]` | Yes | -- | List of target language codes | + +#### `buckets` + +Each bucket maps a format name to a set of file patterns. The format name must be one of: `json`, `yaml`, `toml`, `po`, `android_xml`, `ios_strings`, `xcstrings`, `arb`, `xliff`, `properties`, `laravel_php`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `include` | `string[]` | Yes | Glob patterns for source files to include | +| `exclude` | `string[]` | No | Glob patterns for source files to exclude | +| `target_path_pattern` | `string` | No | Template for target file paths. Use `{locale}` for the target locale and `{basename}` for the source filename. Required for formats where the source locale is not in the source file path (e.g., Android XML, XLIFF). | +| `key_style` | `string` | No | Key format: `nested` (dot-separated keys become nested objects) or `flat` (keys preserved as-is). | + +#### `translation` + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `formality` | `string` | No | `default` | Formality level: `default`, `more`, `less`, `prefer_more`, `prefer_less`, `formal`, `informal` | +| `model_type` | `string` | No | `prefer_quality_optimized` | Model type: `quality_optimized`, `latency_optimized`, `prefer_quality_optimized` | +| `glossary` | `string` | No | -- | Glossary name or ID, or `auto` for automatic glossary management | +| `translation_memory` | `string` | No | -- | Translation memory name or UUID. Requires `model_type: quality_optimized`. Invalid pairing rejected at config load (ConfigError, exit 7). See [Translation memory](#translation-memory). | +| `translation_memory_threshold` | `number` | No | `75` | Minimum match score 0–100 (requires `translation_memory`). Non-integer or out-of-range values exit 7 (ConfigError). | +| `custom_instructions` | `string[]` | No | -- | Custom instructions passed to the DeepL API | +| `style_id` | `string` | No | -- | Style ID for consistent translation style | +| `locale_overrides` | `object` | No | -- | Per-locale overrides for `formality`, `glossary`, `translation_memory`, `translation_memory_threshold`, `custom_instructions`, `style_id` | +| `instruction_templates` | `object` | No | -- | Per-element-type instruction templates. Built-in defaults cover 16 element types: `button`, `a`, `h1`-`h6`, `th`, `label`, `option`, `input`, `title`, `summary`, `legend`, `caption`. User-provided templates override defaults. Only effective for locales supporting custom instructions: DE, EN, ES, FR, IT, JA, KO, ZH. See [Translation Strategies](#translation-strategies). | +| `length_limits.enabled` | `boolean` | No | `false` | Enable length-aware translation instructions. Adds "Keep under N characters" per key based on source text length and locale expansion factors. Only applies to length-constrained element types (button, th, label, option, input, title) for keys sent via per-key API calls. | +| `length_limits.expansion_factors` | `object` | No | built-in defaults | Per-locale expansion factors relative to English source. Built-in defaults: DE 1.3, FR 1.3, ES 1.25, JA 0.5, KO 0.7, ZH 0.5, etc. Based on industry-standard approximations (IBM, W3C). User-overridable. | + +##### `glossary: auto` + +Setting `translation.glossary: auto` enables automatic project glossaries. Each time `deepl sync` runs, the engine scans completed translations for source terms that appear in at least three distinct keys with a consistent translation across all of them, and creates (or updates) a DeepL glossary per target locale named `deepl-sync-{source}-{target}`. On the first run the glossary is created; on subsequent runs the existing glossary is found by name and its entries are replaced with the freshly computed set in a single `PATCH /v3/glossaries/{id}` call per locale — rather than one API call per added or removed term. The glossary list response is also cached for the duration of a sync run, so multi-locale projects issue one `GET /v3/glossaries` lookup total instead of one per locale. The resulting `glossary_id` is stored in the lockfile under `glossary_ids` (keyed by `{source}-{target}` pair) so you can see which glossary the engine is tracking. Only source terms of 50 characters or fewer are considered. + +##### Translation memory + +Set `translation.translation_memory` to a translation memory name or UUID to reuse approved translations across a sync run. Translation memories are authored and uploaded through the DeepL web UI; the CLI never creates or edits them. Names are resolved to UUIDs once via `GET /v3/translation_memories` and cached for the remainder of the invocation, so a multi-locale sync issues at most one list call per unique name. TM composes with glossary — both `glossary_id` and `translation_memory_id` are sent on the same translate call when both are configured. + +Translation memories require `model_type: quality_optimized`. Set `model_type: quality_optimized` at the same scope as `translation_memory` (top-level `translation.model_type`, or the matching per-locale override). Other values are rejected at config load with `ConfigError` (exit 7), before any API call is made. Threshold propagates from YAML into each translate request (default 75, range 0–100); `translation_memory_threshold` without `translation_memory` is inert. Per-locale `locale_overrides..translation_memory` takes precedence over the top-level `translation.translation_memory`; `locale_overrides..translation_memory_threshold` falls back to the top-level threshold when unset. See [Is translation memory actually being applied?](#is-translation-memory-actually-being-applied) for verification steps. + +#### `context` + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | `boolean` | No | `false` | Enable auto-context extraction from source code | +| `scan_paths` | `string[]` | No | `['src/**/*.{ts,tsx,js,jsx}']` | Glob patterns for source files to scan for context | +| `function_names` | `string[]` | No | `['t', 'i18n.t', '$t', 'intl.formatMessage']` | Function names to search for key usage. Both string literal calls (`t('key')`) and template literal calls (`` t(`prefix.${var}`) ``) are matched. | +| `context_lines` | `number` | No | `3` | Number of surrounding code lines to include as context | +| `overrides` | `object` | No | -- | Manual context strings per key (e.g., `save: "Save button in toolbar"`). Overrides auto-extracted context. | + +Template literal calls like `` t(`features.${key}.title`) `` are resolved against the known keys in your source locale files. The interpolation is treated as a wildcard, so the pattern `features.*.title` matches `features.incremental.title`, `features.multiformat.title`, etc. Each matched key inherits the surrounding source code as context. This is useful for idiomatic React/Vue i18n patterns that iterate over keys dynamically. + +When context scanning is enabled, two additional signals are automatically extracted: + +- **Key path context**: The i18n key hierarchy (e.g., `pricing.free.cta`) is parsed into a natural-language description (`"Call-to-action in the pricing > free section."`) and prepended to the context string sent to the API. This helps the API disambiguate short strings like "Save" (verb vs noun). + +- **Element type detection**: The HTML/JSX element type surrounding each `t()` call (e.g., `