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.
- write: Japanese (
ja), Korean (ko), and Simplified Chinese (zh,zh-Hans) are now accepted target languages fordeepl write. - write:
--toneand--stylenow 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--toneand--styleare unchanged — the same 9 styles and 9 tones are accepted, and the mutual-exclusion rule between--styleand--toneis unchanged. Seedocs/API.mdfor supported target-language / style / tone combinations. - write: 4xx responses from the Write API that arrive while
--styleor--toneis set now carry an explicit recovery hint pointing atdocs/API.mdfor supported target-language / style / tone combinations. - style-rules: Full CRUD —
deepl style-rules create|show|update|deletealongside the existinglist.createrequires--nameand--language.updateaccepts--namefor a rename and--rulesfor replacing configured rules (PUT/configured_rules); at least one is required.--rulestakes a JSON object of category → settings, e.g.'{"punctuation":{"quotation_mark":"use_guillemets"}}'— matching the DeepL API's two-level rule shape.deletesupports-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 detailedshowresponse), plusadd-instruction <style-id> <label> <prompt>,update-instruction <style-id> <label> <prompt>, andremove-instruction <style-id> <label>subcommands.remove-instructionships-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 listanddeepl style-rules instructions <style-rule-id>accept--format tablefor aligned column output viacli-table3, matching the existingtranslate,languages,usage, andcachecommands. In non-TTY output (pipe, redirect, CI), table falls back to plain text with aWARNline on stderr — same pattern used bydeepl 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.sh—deepl writewith Japanese, Korean, and Simplified Chinese targets and tone / style applied to Spanish, Italian, French, and Portuguese variants.
- languages / cache stats / usage:
--format tablenow actually renders acli-table3table on these commands. Previously the flag was advertised in--helpbut the action handler only branched on'json', so--format tablesilently produced text output. Same non-TTY fallback as the other table commands. - sync:
deepl sync export --output <path>(and other sync surfaces that callassertPathWithinRoot) 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 throughfs.realpathbefore 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.
- api: Server-returned error messages are now passed through
sanitizeForTerminalbefore being interpolated into the user-facingAPI error: …andServer error (5xx): …strings emitted fromsrc/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.
- 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 withglayzzle/php-parser— AST allowlist over string-literal return-array entries; double-quoted interpolation ("Hello $name"), heredoc, nowdoc, and string concatenation are rejected with aValidationError. 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 indeepl sync statusvia a newskippedKeyscount.php-parseris lazy-loaded only when alaravel_phpbucket is configured. - sync:
deepl sync initauto-detects Laravel projects —composer.jsonat the repo root plus.phpfiles underlang/en/(Laravel 9+) orresources/lang/en/(Laravel ≤8 / Lumen) triggers alaravel_phpbucket 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
requiresfield on each detection pattern. Markers are plainfs.existsSyncchecks — never parsed — matching the filesystem-only stance of the sibling detectors. Laravel'scomposer.jsonis the first required marker; the ARB (Flutter) detector was retroactively tightened withpubspec.yamlto eliminate false positives for the very rare non-Flutter ARB use. - sync:
deepl sync initnow 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 — alocales/en.ymlmatch used to emit alocales/en.yamlbucket pattern that wouldn't match at sync time..ymland.yamlare now handled as separate detection entries so the extension round-trips faithfully. - sync:
deepl sync initauto-detects go-i18n's root-levelactive.en.tomllayout as a dedicated detection entry, emitting theactive.{locale}.tomlfilename template. Previously onlylocales/en.toml/i18n/en.tomldirectory layouts were covered; root-level users had to fall through to the four-flag non-interactive path. - sync:
deepl sync initauto-detects Rails namespaced layouts underconfig/locales/**/en.yml(and.yaml) — engines, concerns, and per-namespace splits are now recognized alongside the canonicalconfig/locales/en.yml. The namespace directory is preserved in the generated bucketinclude:pattern. - sync:
deepl sync initauto-detects Symfony'stranslations/messages.en.xlflayout as a dedicated XLIFF detection entry — distinct from Angular'ssrc/locale/messages.xlfconvention. Target locales are emitted astranslations/messages.{locale}.xlf. - sync:
sync.limitsconfig block — per-file parser capsmax_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 withConfigError(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.lockcontent 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 exportcommand — 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_instructionsfor 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_templatesconfig — user-customizable instruction templates per HTML element type, overriding built-in defaultstranslation.length_limitsconfig — 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_templatesis set but context scanning is disabled or no element types are detected --batch/--no-batchCLI flags —--batchforces plain batch (fastest, no context);--no-batchforces true per-key context (slowest, max quality); default uses section-batched contextPushResult/PullResulttypes forsync 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
ConfigErrorwith a remediation hint that namesTMS_API_KEY/TMS_TOKENand the relevant.deepl-sync.yamlfields. context_sentfield in lockfile translation entries — records whether source code context was included in the API requestcharacter_countfield in lockfile translation entries — records characters billed per key per locale- Live progress output during
deepl sync— per-keykey-translatedevents during translation and per-localelocale-completeevents when each locale finishes, in both text and JSON formats context.overridesconfig — 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
--frozenmode and exit code 10 for translation drift detection validation.fail_on_missingandvalidation.fail_on_staleconfig options for granular--frozendrift 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) estimatedCharactersandtargetLocaleCountfields in JSON output- Dollar cost estimates in sync output and JSON (at DeepL Pro rates, $25/1M chars)
sync.max_charactersconfig option — cost cap that aborts sync before translation if estimated characters exceed limit (override with--force)sync.backupconfig option — pre-overwrite backup of target files (defaulttrue);.bakfiles cleaned up after successful sync--watchmode — monitors source i18n files for changes and auto-syncs with debouncing (configurable via--debounce)--flag-for-reviewmarks MT translations withreview_status: machine_translatedin the lock file for human review workflows- Free API key (
:fxsuffix) support with automatic endpoint resolution toapi-free.deepl.com - Custom/regional endpoint support (e.g.
api-jp.deepl.com) that takes priority over auto-detection sync export --overwriteflag — required to overwrite an existing--outputfile; protects against accidental clobberingdeepl sync status --format jsonerror-mode output: failures now emit{error, code}JSON to stderr with the error class name (ConfigError,ValidationError, etc.) as thecode- Translation memory support in
deepl translatevia--translation-memory <name-or-uuid>and--tm-threshold <n>— forcesquality_optimizedmodel, requires--from(pair-pinned), threshold is an integer 0–100 (default 75) - Translation memory support in
deepl syncviatranslation.translation_memoryandtranslation.translation_memory_thresholdconfig keys, with per-locale overrides undertranslation.locale_overrides - Translation memory name-to-ID resolution is cached per run to avoid redundant
GET /v3/translation_memoriescalls; TM files are authored and uploaded via the DeepL web UI - Verbose-mode logs at the glossary and translation memory resolution boundary:
--verbosenow 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 listsubcommand — lists all translation memories on the account, mirroringdeepl 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 jsonemits the rawTranslationMemory[]as returned byGET /v3/translation_memories. Help text ondeepl translate --translation-memorynow cross-references the new commandsrc/utils/uuid.ts— shared strict UUID regex (UUID_RE) +validateUuid/validateTranslationMemoryIdhelpers.validateTranslationMemoryIdis dormant today (TM IDs only appear in/v2/translatePOST 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 resolvenow prints a per-entry decision report (kept ours/kept theirs/length-heuristic/unresolved) plus a summary, and accepts--dry-runto preview decisions without writing the lockfile. - sync docs:
docs/SYNC.mdExit Codes table anddocs/API.mdsync Behavior bullet now cross-link to the canonical Exit Codes appendix. - sync: New
sync.max_scan_filesconfig key (default 50,000). - errors:
SyncConflictErrorclass insrc/utils/errors.tsmirroringSyncDriftError—ExitCode.SyncConflict(11) is now throwable as a typed error so library consumers caninstanceof-match the conflict case. - SECURITY.md:
1.1.xrow 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.mdandfeature_request.mdtemplates 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--touniformly acrossdeepl translateanddeepl write— the single most common vocabulary split flagged in cross-command usage.--lang/-lremain fully supported; nothing deprecated. The short form-tis intentionally not bound onwrite(it would collide withdeepl translate -t, --to). Passing both--toand--langwith different values exits with aValidationError; passing the same value works fine. - docs:
docs/API.mdgained a one-paragraph callout distinguishingdeepl sync --locale(filter over locales already configured in.deepl-sync.yaml#target_locales) fromdeepl 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_filesconfig field. Caps how many source files a single bucket'sincludeglob may match before the bucket is skipped with a warning. Default10000, hard ceiling1000000. Guards against a misconfigured**/*.jsonthat accidentally picks up a vendored subtree. Sibling fieldsmax_entries_per_file/max_file_bytes/max_depthgate 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.
deepl synccost 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 jsonoutput carriesrateAssumption: "pro".docs/SYNC.mdnow 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 jsonoutput contract stabilized: the success JSON payload is now a curatedSyncJsonOutputshape (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 initno-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 jsonmode 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 statusdocumentation: 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 aTranslation Status:header that the code never emits. The per-localeoutdatedfield is now documented in the JSON field legend.- sync:
deepl sync initnow 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'sdetected[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 inDETECTION_PATTERNSfor enumeration; only the first-pick order changed. - translate: Centralize
TranslateOptionsconstruction fordeepl translate,deepl translate file.txt,deepl translate <dir>, and the document path in a newsrc/cli/commands/translate/translation-options-factory.ts. All four handlers now callbuildBaseTranslationOptions()+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-targettargetLangstripping) stays in the handler.deepl syncis intentionally untouched — itsTranslationOptionsare built from resolved config with per-locale overrides andcontext_sentwiring, a different construction domain that lives insrc/sync/sync-locale-translator.ts. - sync: Format-name knowledge consolidated under
src/formats/registry.ts;--file-formatCLI choices now derive from the registry. Prevents silent divergence between parser, CLI help, and registration. - sync: Removed per-parser
sortcalls (consumers sort once); extracteddetectIndentto a sharedsrc/formats/util/detect-indent.tsused by JSON, ARB, and xcstrings. Pure refactor, no behavior change. - sync:
scan_pathsfile walk is now bounded (default 50,000 files; configurable viasync.max_scan_filesin.deepl-sync.yaml) — exceeding the cap throws ValidationError with a suggestion, preventing CI wedges on misconfigured patterns. - sync:
deepl sync push --helpanddeepl sync pull --helpnow include a TMS onboarding hint — the requiredtms:YAML block, theTMS_API_KEY/TMS_TOKENenv vars, and a pointer todocs/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.mdpush/pull sections get the same hint and cross-link. - sync:
deepl sync --forcehelp text now warns that the flag bypasses thesync.max_characterscost-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.mdanddocs/SYNC.mdupdated to match. - sync: Extract CLI exit-code enum to
src/utils/exit-codes.ts(next to the errors module); addsSyncConflict(11) forsync resolveunresolvable-conflict exits. No runtime behavior change from the extraction alone; enables the envelope contract wiring. - sync:
deepl sync initflag vocabulary aligned with the rest of sync:--source-localeand--target-localesare now the primary names, matching--localeinsync push/pull/status/export.deepl translate --target-langis unchanged (operates on strings, distinct from locale-file semantics). - sync: Rename
deepl sync --context/--no-contextboolean to--scan-context/--no-scan-contextto disambiguate fromdeepl translate --context "<text>"(string-valued). Bare--context/--no-contexton sync now errors with a did-you-mean pointing to the new flag.deepl synchad 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 singleapplyCliOverrideshelper insync-config.ts. The TM-requires-quality_optimizedguard now also fires at the CLI-override boundary, so--model-type latency_optimizedis rejected with an actionableConfigErrorwhen the loaded YAML hastranslation_memoryset (previously the override silently bypassed the check). - sync:
deepl sync glossary-reportis renamed todeepl 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 aValidationError(exit 6) and a did-you-mean hint pointing toaudit. No deprecation alias — this is a pre-release rename;glossary-reportnever shipped in a tagged release.audithere means translation-consistency audit (term divergence across locales), not security audit in thenpm auditsense. - 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 initinteractive 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-batchto restore per-key behavior. - sync:
deepl sync status --format jsonoutput shape declared stable across 1.x —{sourceLocale, totalKeys, locales[]}withcoverageas 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.yamlIS 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 toVERSION/package.jsonandnpm testoutput. - 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 initsection cross-links todeepl sync initfor continuous-localization setup. - sync exit codes:
deepl syncpartial-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 aliasedExitCode.PartialFailuretoGeneralError(both1), which prevented CI scripts from telling a partial sync outcome from a CLI crash. With this change, CI can safely branch on$? -eq 12and retry only the failed locales viadeepl sync --locale <failed,comma,separated>. The paired typed error classSyncPartialFailureError(exit 12, envelopecode: "SyncPartialFailure") is added tosrc/utils/errors.ts, mirroringSyncDriftError(10) andSyncConflictError(11). Migration: any CI script that branched on$? -eq 1to detect partial sync failure should switch to$? -eq 12; a generic$? -ne 0check continues to work unchanged. - sync drift exit:
deepl sync --frozennow exits soft (setsprocess.exitCode = 10and returns from the action handler) instead of callingprocess.exit(10)directly. Observable exit code is unchanged at 10; the internal change lets in-flight writes, auto-commit steps, and any--watchevent loop drain cleanly before the process exits.docs/API.mdhas promised this shape since 1.1.0 but the implementation drifted to a hard exit — now aligned. - tests: The shared
tests/setup.tsafterEachhook now asserts that everynockinterceptor 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 callingnock.cleanAll()from their ownafterEachbefore 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 aConfigErrorrather than risking data loss. - cache: Corrupted cache databases are now backed up aside as
cache.db.corrupt-<timestamp>(plus any-wal/-shmsidecars) 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.warnnames the backup path. - http: Retry backoff now uses full jitter (AWS-recommended variant): the delay for attempt
nis a uniform random value in[0, min(INIT * 2^n, MAX)]rather than the fixedmin(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. TheRetry-Afterheader path is unchanged — server-specified delays are honored verbatim. - http: Retries now emit a
Logger.verboseline 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.
- sync:
deepl sync init --source-langand--target-langsare deprecated in favor of--source-localeand--target-locales. The old flags continue to work but emit a stderr deprecation warning; they will be removed in the next major release.
- sync: Dead
onProgresscallback andSyncProgressEventinterface fromSyncOptions(never wired up). - sync: Remove silently-ignored
--batch-sizeflag - sync: Remove 5 unimplemented config fields from types and docs
- package.json: Drop
exports["./cli"]subpath. It pointed atdist/cli/index.js, which runsprogram.parseAsync+process.exitat module load — any consumer who importeddeepl-cli/cliwould have had their own process terminated mid-import. The CLI remains available as a binary via thebinfield.
- sync cost cap: When a brand-new target locale is added to an existing project,
sync.max_charactersnow correctly includes the character cost of translating all current keys into the new locale in its preflight estimate. Previously,toTranslatewas 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-runand the live run always report the same estimated character count for the same workload. - sync perf: Stale-lock entry cleanup now issues a single
fgcall 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
.baksweep (sweepStaleBackups) is now scoped to the directories implied by each bucket'sincludeglobs 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, anddeepl sync resolve --format jsonnow 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)
resolveTemplatePatternsloop over duplicate template-pattern entries. The accumulator inextractAllKeyContextspushed oneTemplatePatternMatchper 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). ASet-based dedup before the resolve loop collapses all per-file duplicates to at most one entry per distinct pattern string;MAX_LOCATIONS=3downstream is unaffected since the first-seenfilePath/lineis sufficient context. - sync: Eliminated O(N²)
Array.includesscan in the per-locale plural-slot hot path (sync-locale-translator.ts). Three call sites that testedbatchIndices.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 aSetbefore thepluralSlotsloop and useSet.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 pushanddeepl sync pullCLI 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. Afterpipe_pluralizationwas added as a thirdSkipReason, 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 sharedformatSkippedSummary(skipped)helper insync-tms.ts; the programmaticPushResult.skipped/PullResult.skippedshape is unchanged. - sync:
deepl sync pushanddeepl sync pullnow enforce the walker's skip-metadata partition at every inlineparser.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 toTmsClient.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 exportedpartitionEntrieshelper insync-bucket-walker.tsis 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, andPushResult/PullResultnow surface aSkippedRecordwithreason: 'pipe_pluralization'andkeyper leaked entry so silent-partition regressions are detectable. - sync:
deepl sync initJSON detector now emits a glob bucket pattern for the directory-per-locale i18next layout (locales/en/*.json) instead of fabricating a nonexistentlocales/en/en.jsonsingle-file path. Flat (locales/en.json) and dir-per-locale layouts are now separate detection entries. - sync:
deepl sync initiOS detector no longer claims bare-root*.stringsfiles — Apple's bundle model mandates.lproj, and the root-level glob was a relocation magnet that emitted{locale}.lproj/Localizable.stringstarget 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 initXLIFF detector no longer claims bare-root*.xlf/*.xlifffiles — CAT-tool dumps (Trados/memoQ/Xcode.xclocextracts) are a false-positive magnet and the detector used to relocate them undersrc/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. Previouslyreconstruct()ransmol-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-tomlis retained forextract(). - sync:
.deepl-sync.yamlnow 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 insrc/cannot leave orphaned.js/.d.tsfiles that would ship vianpm publish. - voice: Voice API no longer hardcodes the Pro endpoint; it follows the same endpoint resolution as all other commands.
- auth:
auth set-keyandinitnow 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>anddeepl sync pull --locale <x>now narrow the fan-out to the named locale instead of silently over-fetching every configured target. Commander was routing--localeto whichever scope declared it first, so the subcommand handlers receivedundefinedand treated the filter as absent. The subcommands now resolve--localevia a sharedresolveLocale(opts, command)helper that prefers the subcommand's value and falls back to the parentsync --locale, matching the existingresolveFormatpattern. - sync: Every sync subcommand now cleans up in-flight
.tmpand.baksibling files on SIGINT/SIGTERM (previously onlysync --watchhad this discipline), and sweeps stale.bakfiles older thansync.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 --watchnow caches the validated sync config across debounced change events instead of reloading + revalidating it every tick. The cache invalidates onSIGHUP(explicit reload) or when.deepl-sync.yamlitself 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 astderrwarning at config-load time on everydeepl sync …subcommand, including non-TTY contexts like CI. Previously the warning was only emitted on thesync push/sync pullcode path, so a user runningsync statusor 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 initnow 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 to0for 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 syncruns. Every completed (file, locale) pair was being printed twice — once live via thelocale-completeprogress event and again in a post-sync aggregated summary built fromfileResults. 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.translateForLocaleis 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:
resolveTemplatePatternsnow compiles each distinct pattern regex once per sync run instead of once perTemplatePatternMatchoccurrence. Duplicate pattern strings (same template literal appearing in many source files) reuse the sameRegExp. - sync: Template-pattern prep no longer reads every source file twice during
deepl syncruns 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, andinitnow emit a machine-parseable JSON error envelope on stderr when--format jsonis 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 initalso gains a--format jsonsuccess envelope ({ok: true, created: {configPath, sourceLocale, targetLocales, keys}}) for project-bootstrap scripts. Envelope shape is guarded by an AJV schema and a sharedassertErrorEnvelopetest helper. - sync:
deepl sync resolvenow 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.lockmanually and re-rundeepl sync. - sync:
deepl sync --watch --auto-commitnow 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 --watchno longer leaks SIGINT/SIGTERM listeners across invocations and no longer serves a staletmCacheentry 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 pushnow issues push requests with bounded concurrency (default 10, configurable viatms.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 resolvenow emits a loud warning whenJSON.parseon 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 initnon-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 nextdeepl syncrun with a cryptic error. - sync:
deepl sync --watchnow 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 --watchnow cleans up.bakfiles on SIGINT/SIGTERM even when a translation is in flight, and sweeps stale.baksiblings 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 --helpnow groups examples under First-time setup and Everyday use, showing theinit→--dry-run→sync→statusonboarding flow, and adds a pointer todeepl tm listfor translation-memory discoverability. - sync: Acquires an exclusive advisory lock (
.deepl-sync.lock.pidfile) at sync start to prevent two concurrentdeepl syncinvocations 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-commitnow 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 fromconfig.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 hungdeepl sync push/pullindefinitely 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.yamlnow exits 7 (ConfigError) instead of 6 (ValidationError), matching the documented exit-code contract in docs/SYNC.md and docs/TROUBLESHOOTING.md. - sync:
deepl sync initnow 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/promptsand either threwExitPromptErroror blocked indefinitely in CI. - sync:
deepl sync --frozen --watchnow exits with ValidationError (code 6). Previously the combination was documented as invalid but entered an infinite drift-check watch loop. - sync: Every
ConfigErrorthrown fromvalidateSyncConfig(.deepl-sync.yamlvalidation) now includes a remediationsuggestionstring pointing the user at the exact YAML field to fix. Previously ~15 of 18 throw sites provided only a title, defeating the advertisedDeepLCLIError.suggestionconsumer contract. - sync:
deepl sync pullnow 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:
listTranslationMemoriesnow paginates theGET /v3/translation_memoriesresponse using the documentedpage/page_sizequery parameters (max 25 per page, bounded at 20 pages). Accounts with more than 25 translation memories previously received a silently truncated list, which causeddeepl tm listand 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'stotal_countindicates more pages are available. - sync:
deepl sync --format jsonnow emits{error, code}JSON to stderr on failure (matching thesync status --format jsonerror 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(andstatus,validate,audit) now emit the success JSON payload on stdout, not stderr. Previouslydeepl sync --format json > out.jsonproduced 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:--fromis required, TM rejects non-quality_optimizedmodel types, glossary and TM are resolved once per invocation, andmodelTypedefaults toquality_optimizedwhen 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-leveltranslation_memorybutlocale_overridessharing a TM name across locales with mismatched pair support could silently reuse an incompatible TM UUID on the second locale. - translate:
warnIgnoredOptionsnow actually fires for--translation-memoryand--tm-thresholdin modes that do not support them (e.g.directory,document). The keys were present in the handler supported-sets but missing fromoptionLabels, so the warning was inert. - translate: Harden TM name resolution against API-returned name pollution.
resolveTranslationMemoryIdnow filters entries whose names contain ASCII control chars or zero-width codepoints before matching, and throwsConfigErrorwhen 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 —
resolveGlossaryIdnow filters API-returned glossary entries whose names contain ASCII control chars or zero-width codepoints before name matching, and throwsConfigErrorwith a UUID-disambiguation hint when two surviving entries share the same name. Mirrors the TM resolver defenses. - examples:
examples/31-sync-ci.shpasses--file-format jsontodeepl sync init(was--format json, which is not a registered flag oninitand would fall through to the interactive-prompt branch in non-TTY environments). - api:
listGlossariesandlistTranslationMemorieserrors now carry their method name as a[listGlossaries]/[listTranslationMemories]suffix onerror.message. Suffix (not prefix) preservesdeepl sync --format jsonstderr-shim consumer greps on canonical phrases likeAuthentication failed: Invalid API key. - sync: Reject
translation_memorypaired with a non-quality_optimizedmodel_typeat 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 keyslines for up-to-date locales. - sync: New-locale translations now correctly count in progress output.
- sync:
sync resolveconflict marker detection now works mid-file (added multiline flag to regex) - sync:
sync validate,sync status,sync export,sync push, andsync pullnow handle multi-locale formats (.xcstrings) correctly - sync:
sync initauto-detection now generates valid glob patterns instead of{locale}placeholders that fast-glob cannot match - sync:
resolveTargetPathsupportstarget_path_patternfor 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, andignoreYAML blocks - sync: CLI overrides (
--formality,--glossary,--model-type,--context) now merge into config - sync:
--forcemode 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:
--frozenmode 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 --outputnow rejects paths that escape the project root and creates missing intermediate directories before writing - sync:
deepl sync auditnow 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
--localeand--formatoptions on the baredeepl synccommand (previously dropped during an earlier commander option-shadowing fix) and wire--sync-configend-to-end — commander camelCases the flag tosyncConfig, but the handler was readingconfig, 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
assertPathWithinRootguard in sync validate - sync: Fix
resolveTargetPath$nlocale injection via function callbacks - sync: Skip deleted diffs in sync-status coverage counts
- sync: Validate HTTPS scheme in TmsClient
- sync: Replace blocking
readFileSyncwith 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-preservationString.replacecalls - 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, andsync 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:
--frozennow detects drift when a new target locale is added - sync:
--dry-runreports pending new-locale translation in key counts - sync: Clean stale lock entries for files no longer matched by any bucket glob
- sync: Merge
config.ignorepatterns into fast-glob for status, validate, push, pull - sync: Guard
source_locale==target_localein 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:
--frozenguards stale lock entry cleanup and lock file write - sync: Preserve translateBatch index alignment by returning sparse array on partial failure
- sync:
restorePlaceholdersreplaces all occurrences (not just first) - sync: Fix
context_linesdefault to 3 (matching documentation) - android-xml: Escape
<,>,&in translations to prevent XML injection - json: Guard against 0-byte source files
- translate: Invalid
--toerror is now concise — the 100+ language-code dump is removed; the message points atdeepl languagesfor the full list. - examples:
examples/30-sync-basic.shandexamples/31-sync-ci.shnow clean up/tmp/deepl-sync-demo/and/tmp/deepl-sync-ci-demo/on mid-script failure viatrap cleanup EXIT(matching the pattern already in examples 32 and 34). - docs:
docs/API.mdanddocs/SYNC.mdnow document the--format FORMAToption ondeepl sync export(previously undocumented even though the flag was registered insrc/cli/commands/sync/register-sync-export.ts). Clarified that onsync exportthe format choice affects only the error envelope on stderr; the success output is always XLIFF 1.2. - docs:
docs/API.mdcorrected the note on theauditsubcommand rename — the previous wording said "Prior to the 1.0.0 release, this subcommand was namedglossary-report", which implied 1.0.0 users had access to it. The prototype nameglossary-reportnever shipped in any tagged release; now worded consistently with the 1.1.0 CHANGELOG entry. - write:
deepl write --interactivenow fails fast with aValidationErrorwhen stdin is not a TTY (e.g., a CI job that passes--interactivewithout--no-input). Previously the process would hang indefinitely on an@inquirer/promptsselectcall that a non-TTY stream can never answer. - translate:
deepl translate --format tablenow falls back to plain[lang] textoutput with aWARNline on stderr when stdout is not a TTY. Screen readers and log scrapers no longer have to parsecli-table3's Unicode box-drawing characters; pipe--format table > out.txtproduces parseable plain text instead of Unicode noise. - output: Spinners (
ora) are now gated onprocess.stderr.isTTYat theLogger.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_COLORis now explicitly honored in the CLI bootstrap by settingchalk.level = 0when the env var is present.chalkalready auto-detectsNO_COLOR, but the explicit hook keeps the two color-detection paths in the codebase (chalkandisColorEnabled()inutils/formatters.ts) unambiguously in sync ifchalk's auto-detection ever changes or is mocked in tests. - sync init: a bare
process.exit(7)literal inregister-sync-init.ts's JSON-output path now goes throughExitCode.ConfigErrorso 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
<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 aValidationErrorat 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.
deepl sync --forcebilling defense:--watch --forceis now rejected at startup withValidationError(exit 6) — the combination would silently retranslate every key on every file change, creating an unbounded billing loop. Additionally,--forcenow 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),--forcerequires--yesexplicitly — the process exits 6 with an actionable hint rather than proceeding silently.- Updated
minimatchfrom^9.0.5to^10.2.1to fix ReDoS vulnerability (GHSA-3ppc-4f35-3m26) - sync:
deepl sync pullnow acquires the pidfile process lock (acquireSyncProcessLock) before writing any target files. Previously, a concurrentdeepl sync(which holds the lock while writing target files) anddeepl sync pullcould race across multiple files —atomicWriteFileprevents torn individual writes but the multi-file read-modify-write cycle was unguarded.deepl sync pushis read-only toward local files and does not need the guard. - sync:
sanitizePullKeysResponsenow enforces a hard cap of 50,000 keys (MAX_PULL_KEY_COUNT) on TMS pull responses. A response exceeding this limit is rejected with aValidationErrorbefore 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
sanitizeForTerminalbefore appearing in thrownErrormessages. Both the response body (capped at 1024 bytes) andresponse.statusTextare 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 pullnow 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_pathsagainst project root with symlink protection - security: Use URL hostname check for
tms.server(preventslocalhost.evil.combypass) - security: Encode
tms.project_idin URL path - sync:
sync push,sync pull,sync export, andsync validatenow refuse to follow symbolic links when scanning source files, matching the policy already enforced bysyncitself andsync-context. Previously a symlink inside a bucket'sincludepattern 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_TOKENenv values, andAuthorization: ApiKey/Bearer <value>headers). Previously onlyDEEPL_API_KEYandDeepL-Auth-Keywere covered, so TMS credentials could leak into logs via error messages, Headers dumps, or stringified fetch error bodies. - sync: Harden
sync resolveconflict-fragment merge against prototype pollution. JSON-parsed fragments can carry__proto__/constructor/prototypeas own properties; the merge now skips those keys and usesObject.create(null)accumulators sodeepl sync resolveon a hostile.deepl-sync.lockcannot mutateObject.prototype. - sync:
deepl sync exportnow rejects source-side paths that resolve outside the project root with a ValidationError, matching the--outputdestination guard. Previously a.deepl-sync.yamlwith absolute source paths or symlinks pre-dating the fast-glob hardening could read files outside the configured scan root during export. - sync:
scan_pathsconfig 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 priorassertPathWithinRootguard. No change to valid configurations; rejected configurations now produce a ConfigError with the offending pattern shown. - deps:
npm audit fix— resolvesaxiosGHSA-3p68-rc4w-qgx5 (SSRF via NO_PROXY normalization),axiosGHSA-fvcv-3m26-pcqx (cloud-metadata exfil via header injection), andfollow-redirectsGHSA-r4q5-vmmm-2653 (auth-header leak on redirect). Not reachable from the CLI (baseUrl hardcoded toapi.deepl.com; TMS uses nativefetch); transitive advisories are now quiet. - sync:
.deepl-sync.yamland auto-detect-path reads (package.json, the first-match i18n file for key counting) now route throughsafeReadFileSync, which rejects symbolic links with aValidationError. A hostile repo could previously ship a.deepl-sync.yamlsymlinked 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 duringdeepl syncitself are unchanged — the bucket walker already refuses symlinks viafast-glob'sfollowSymbolicLinks: false. - http: When
HTTP_PROXY/HTTPS_PROXYis configured with anhttp://proxy and the target endpoint ishttps://, 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 redactsX-Api-KeyandX-Auth-Tokenheaders, plus?api_key=/?apikey=query parameters. Previously onlyDeepL-Auth-Key,Authorization: ApiKey|Bearer,?token=/&token=, and theDEEPL_API_KEY/TMS_API_KEY/TMS_TOKENenv-var exact values were covered. axios error dumps that includeconfig.headerson TMS-style third-party backends (e.g., Phrase, Lokalise, custom REST endpoints) no longer leak these credentials via verbose logs.
- 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-Afterheader support - Dry-run mode (
--dry-run) for previewing destructive and batch operations - Cost transparency with
--show-billed-charactersflag - 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
- 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
- Requires Node.js >= 20