Skip to content

Latest commit

 

History

History
360 lines (329 loc) · 71 KB

File metadata and controls

360 lines (329 loc) · 71 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[1.2.0] - 2026-04-25

Added

  • write: Japanese (ja), Korean (ko), and Simplified Chinese (zh, zh-Hans) are now accepted target languages for deepl write.
  • write: --tone and --style now apply to Spanish (es), Italian (it), French (fr), and Portuguese (pt, pt-BR, pt-PT) in addition to the previously supported locales. The full value lists on --tone and --style are unchanged — the same 9 styles and 9 tones are accepted, and the mutual-exclusion rule between --style and --tone is unchanged. See docs/API.md for supported target-language / style / tone combinations.
  • write: 4xx responses from the Write API that arrive while --style or --tone is set now carry an explicit recovery hint pointing at docs/API.md for supported target-language / style / tone combinations.
  • style-rules: Full CRUD — deepl style-rules create|show|update|delete alongside the existing list. create requires --name and --language. update accepts --name for a rename and --rules for replacing configured rules (PUT /configured_rules); at least one is required. --rules takes a JSON object of category → settings, e.g. '{"punctuation":{"quotation_mark":"use_guillemets"}}' — matching the DeepL API's two-level rule shape. delete supports -y/--yes, --dry-run, and a TTY confirmation prompt. All new subcommands support --format text|json. Text-format output sanitizes control characters from rule names, configured-rule keys/values, and instruction text.
  • style-rules: Custom instructions management — deepl style-rules instructions <style-rule-id> (list, synthesized from the detailed show response), plus add-instruction <style-id> <label> <prompt>, update-instruction <style-id> <label> <prompt>, and remove-instruction <style-id> <label> subcommands. remove-instruction ships -y/--yes, --dry-run, and a TTY confirmation prompt (custom instructions are user-authored text and deserve confirmation before deletion).
  • style-rules: deepl style-rules list and deepl style-rules instructions <style-rule-id> accept --format table for aligned column output via cli-table3, matching the existing translate, languages, usage, and cache commands. In non-TTY output (pipe, redirect, CI), table falls back to plain text with a WARN line on stderr — same pattern used by deepl translate --format table.
  • examples: examples/35-style-rules-crud.sh — end-to-end style-rules workflow (create → show → update rules → add custom instruction → update instruction → remove instruction → delete rule).
  • examples: examples/36-write-extended-languages.shdeepl write with Japanese, Korean, and Simplified Chinese targets and tone / style applied to Spanish, Italian, French, and Portuguese variants.

Fixed

  • languages / cache stats / usage: --format table now actually renders a cli-table3 table on these commands. Previously the flag was advertised in --help but the action handler only branched on 'json', so --format table silently produced text output. Same non-TTY fallback as the other table commands.
  • sync: deepl sync export --output <path> (and other sync surfaces that call assertPathWithinRoot) no longer reject valid output paths under a project root that contains a symlink in its ancestor chain. The containment check now resolves both sides through fs.realpath before comparing, so a project under macOS /tmp (a symlink to /private/tmp) — or any other symlinked directory — works regardless of which form the user types or the engine captures. Symlink-based escape attempts (a symlink inside the project pointing outside) are now also rejected as a defense-in-depth bonus.

Security

  • api: Server-returned error messages are now passed through sanitizeForTerminal before being interpolated into the user-facing API error: … and Server error (5xx): … strings emitted from src/api/http-client.ts. Defense-in-depth against a buggy or malicious server scribbling ANSI escape codes or other terminal control characters on the user's terminal via the error path. Mirrors the existing TMS-client hardening; no change to error-message wording when the server payload is well-formed.

[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
  • 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 <name-or-uuid> and --tm-threshold <n> — 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 "<name>" -> <uuid>, [verbose] Resolved translation memory "<name>" -> <uuid>) 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 appendix.
  • sync: New sync.max_scan_files config key (default 50,000).
  • errors: SyncConflictError class in src/utils/errors.ts mirroring SyncDriftErrorExitCode.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 <language> 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

  • 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 <dir>, 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 "<text>" (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 <failed,comma,separated>. 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-<timestamp> (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 <x> and deepl sync pull --locale <x> 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-runsyncstatus 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-<tag>-<timestamp> 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 <name> and --translation-memory <name-or-uuid> 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. &amp;lt; (the literal 5-character string "&lt;") was silently collapsed to "<" because the decoder ran &amp;& on the first pass and then &lt;< 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 <source> / <target> 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., <note>) 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 <value> 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

Added

  • Text translation via DeepL's next-generation LLM (deepl translate)
  • Document translation for PDF, DOCX, PPTX, XLSX, HTML, SRT, XLIFF, and images with formatting preservation
  • Structured file translation for JSON and YAML i18n locale files (keys, nesting, comments preserved)
  • Writing enhancement with grammar, style, and tone suggestions (deepl write) via the DeepL Write API
  • Real-time speech translation via WebSocket streaming (deepl voice) with automatic reconnection
  • Watch mode for real-time file monitoring with auto-translation (deepl watch)
  • Batch directory translation with parallel processing, glob filtering, and concurrency control
  • Glossary management with full v3 API support including multilingual glossaries (deepl glossary)
  • Language detection (deepl detect)
  • Git hooks for pre-commit, pre-push, commit-msg, and post-commit translation workflows (deepl hooks)
  • Interactive setup wizard (deepl init)
  • Admin API for key management and organization usage analytics (deepl admin)
  • Shell completion for bash, zsh, and fish (deepl completion)
  • SQLite-based translation cache with LRU eviction and configurable TTL
  • Custom translation instructions (--custom-instruction) and style rules (--style-id)
  • XDG Base Directory Specification support with legacy path migration
  • JSON output format (--format json) across all commands for CI/CD scripting
  • Table output format (--format table) for structured comparison views
  • Semantic exit codes (0–9) for CI/CD integration and scripted error handling
  • HTTP/HTTPS proxy support via standard environment variables
  • Automatic retry with exponential backoff and Retry-After header support
  • Dry-run mode (--dry-run) for previewing destructive and batch operations
  • Cost transparency with --show-billed-characters flag
  • Multi-target translation to multiple languages in a single command
  • Context-aware translation (--context) for disambiguation
  • Model type selection (--model-type) for quality vs. latency trade-offs
  • Advanced XML/HTML tag handling with splitting, non-splitting, and ignore tags

Security

  • HTTPS enforcement for all API communication (localhost exempted for testing)
  • Symlink rejection on all file-reading paths to prevent directory traversal
  • API key masking in logs, config output, and error messages
  • Config file permissions restricted to owner read/write (0o600)
  • Path traversal defense for batch output patterns
  • Atomic writes for translated output and config files to prevent corruption

Changed

  • Requires Node.js >= 20