Repository navigation
Commit e08225a
release: v2.0.0 — publish as @deepl/cli, migrate the cache to node:sqlite (#115)
* feat(cli): apply --glossary to document translation
Document translation accepted --glossary and then discarded it: the handler
left glossary out of its supported set, so warnIgnoredOptions reported it as
unsupported and no glossary ever reached the API, even though POST /v2/document
supports them. Resolve the glossary in the document path and drop the warning.
Repeating the flag works here too, with the same last-one-wins precedence.
DocumentTranslationOptions and the document client already carried glossaryIds,
so no client change was needed.
--from is now required with a document glossary, because the API rejects the
request otherwise: "source_lang has to be specified in order to use a
glossary." Translation memories stay unsupported for documents, so the glossary
half of applySharedTmAndGlossary is extracted into applyGlossarySelection and
shared rather than pulling TM resolution into the document path.
The pre-existing test asserting that document mode warns about --glossary
encoded the old behavior and is replaced by one asserting it does not.
* docs: document glossary support for document translation
Record the repeatable flag, the mandatory --from, and that translation
memories remain unsupported for documents.
Also note that glossary matching is context-dependent for documents exactly as
it is for text — a term applied in one sentence may be left alone in another,
and a bare word list often gets few terms applied. This was mistaken for a
document-specific defect while investigating, so it is written down.
* fix(docs): add the required --from to every --glossary example
A glossary has always required --from on every path, but the flag was missing
from six documented invocations, so each would have exited 6 if a reader had
copied it. Two predate this work (the API.md glossary-usage block and the
inline help example) and the rest came in with the repeatable flag.
The Commander help for --glossary now states the --from requirement, following
the existing convention of --tm-threshold noting its own, and records that the
flag applies to documents as well.
Add the document glossary examples and feature note to the README, which the
earlier commits left covering only text translation.
Fold the glossary into the document handler's existing "only supported options
produce no warning" test instead of asserting it separately: glossary is now a
supported document option, so it belongs in that list, and a separate test
duplicated the handler suite's own coverage.
* docs(watch): note that --glossary needs --from
/v2/translate rejects a glossary without a source language ("Use of a glossary
requires the source_lang parameter to be specified"), and watch passes --from
straight through without checking it, so --glossary alone fails on every file
change. Say so in the watch option docs and its inline help.
sync needs no equivalent note: it has no --from, taking the source language
from the required source_locale field in .deepl-sync.yaml, so its requests
always carry one.
* fix(voice): fail when a session ends without a translation
A voice session that ended with the audio transcribed but no translation
for a requested target language resolved successfully, printing an empty
translation line and exiting 0. A script consuming the output saw success
with the translation missing.
Such a session now rejects with a VoiceError (exit 9) naming the
untranslated languages. Audio containing no speech concludes no source
text and is translated to nothing, which is legitimate, so the check
applies only when a source transcript exists.
The two frame shapes that reach the same place — a whitespace-only
translation and one that stayed tentative without ever concluding — count
as missing, because neither reaches the printed output.
Unit tests that emitted source frames with no target frame have gained one:
under this rule that shape is a failed session, not a valid one.
* fix(watch): require --from when --glossary is used
deepl watch --glossary X without --from started a watch session happily and
then failed on every single file change, surfacing a raw server message each
time. The API rejects any translation naming a glossary without a source
language ("Use of a glossary requires the source_lang parameter to be
specified"), which translate already guards against up front for text, files,
and documents.
watch now rejects the combination before the watcher starts, with the same
message the translate handlers use and a watch-shaped example, exiting 6 as
they do. The check runs before the glossary name is resolved, so it costs no
API call.
The option docs and inline help warned the reader that the check was missing;
they now describe the requirement instead.
sync needs no equivalent guard: it has no --from at all, taking the source
language from the required source_locale field in .deepl-sync.yaml, so its
requests always carry one.
* refactor(cli): display language codes in lowercase everywhere
Output mixed three casings: `deepl languages` printed lowercase from the
registry, `glossary show` and `tm list` uppercased at display time,
`translate`'s table uppercased the target language, and `write`/`correct` used
BCP-47 (en-GB, zh-Hans). Lowercase matches the CLI's own normalized form, the
registry, what `deepl languages` teaches users to type, and the wire format
/v3/languages moved to. Uppercase followed a v2-era docs convention that v3
abandons.
Display-time .toUpperCase() is removed from glossary.ts, tm.ts, and
formatters.ts. The v3 glossary and translation-memory endpoints already return
lowercase, so removal is sufficient — verified against the live API.
WRITE_LANGUAGES is lowercased, which changes what write/correct send as
target_lang, not just what they display. The Write API accepts any casing and
canonicalizes server-side: /v2/write/rephrase and /v2/write/correct return 200
for en-gb, zh-hans, and zh-HANS alike and echo back en-GB / zh-Hans. Since the
echoed value is the API's casing rather than ours, WriteImprovement's
targetLanguage is typed as the string it is instead of claiming to be one of
our codes.
Input remains case-insensitive everywhere, so no command line has to change.
Wire parameters that are not display are untouched: translate and the glossary
create endpoint still send uppercase as those endpoints document.
Parseable output shifts casing for scripts: `glossary show` reports
"Source language: en" and "en → es", `tm list` renders
"brand-terms (en → de, fr)", and `write --format json` reports
"language": "en-us".
* test(watch): recover the dropped fsevents add in the filesWatched test
"should track files added and removed while watching" timed out under load at
roughly 1 run in 20. The cause is not slowness: instrumenting the wait showed
every failure was the add phase with the counter still at 1, while successful
adds land in ~20ms and never approach the 5s budget. fsevents drops the
notification for a file created in the moment after 'ready' fires, while the
stream is still arming, and `ignoreInitial: true` means no rescan recovers it —
so the event never arrives at all.
The wait now re-touches the file every 500ms, which produces a fresh
notification once the stream is delivering. Measured over 200 iterations under
CPU contention: 0 failures, with 10 (5%) recovered by a nudge, matching the
4.5% failure rate seen before. Detection power is unchanged, since a watcher
that fails to count adds never reaches the expected value however often the
file is touched.
The timeout message also reports the count it last saw. "Timed out waiting for
condition" said nothing about whether the counter had stalled or overshot,
which is what made the diagnosis need a separate harness.
* fix(languages): source formality from the v3 features matrix
GET /v3/languages does report formality support -- as features.formality,
which is what v2's supports_formality became. The static supportsFormality
snapshot added alongside the v3 migration was therefore unnecessary, and it
had already drifted: live v3 reports formality for pt, the registry did not,
so `deepl languages --target` omitted [F] for Portuguese.
Formality now derives from features.formality presence, and the features
matrix is carried through on LanguageInfo for both roles. The 11-entry
snapshot and the supportsFormality field are removed from the registry;
category and targetOnly are untouched.
Verified live: `deepl languages --target` output is byte-identical to before
except pt, which now correctly shows [F].
* feat(languages): add --features to show per-language feature support
`deepl languages --features` surfaces the features matrix from
GET /v3/languages, so glossary, style-rules and translation-memory support
no longer has to be discovered by trial and error.
Which columns appear is derived from the response rather than a fixed list: a
feature gets a column when its value differs across the listed languages, and
is otherwise reported once as "All listed languages also support: ...". A
uniform column discriminates nothing, and deriving this from the data means a
newly reported feature appears without a code change. In practice tag_handling
suppresses in both listings and auto_detection only in the source listing,
where every language has it.
Support is key presence; `status` is maturity, so anything other than stable
renders verbatim (`glossary (beta)`) instead of collapsing to yes. --features
subsumes the [F] shorthand, so the marker and its legend are dropped when the
matrix is shown.
Default output is unchanged: text and table rendering are untouched without
the flag, and the matrix is stripped from --format json unless asked for.
Table output gains a seam (formatDisplayEntriesTable) matching the text path
so rendering can be tested without the registry merge.
* test(languages): cover --features end to end against the mock API
The languages e2e suite ran offline only, so nothing exercised the command
against a real /v3/languages response. It now spawns the mock server, whose
fixture gains a features object, and asserts the per-language feature list,
that --features replaces the [F] shorthand, and that the matrix reaches
--format json only when requested.
Column suppression stays a unit-level assertion: the row set is the whole
registry, so languages the mock does not return report no features and
nothing comes out uniform.
* docs(languages): document --features and the formality fix
Covers the flag in API.md, README and the languages example, including the
data-driven column rule, the per-listing difference it produces, and that the
matrix is finer-grained than the core/regional/extended tiers. The example
gains a scriptable preflight snippet using --format json.
CHANGELOG records the feature under Added and the pt formality gap under
Fixed. Extends examples/24-languages.sh rather than adding a script, since
that example already covers the command.
* refactor(languages): generate the language list from the API
The list of supported languages was hand-maintained, so it could fall behind
the API silently -- and had: de-CH, de-DE, fr-CA and fr-FR were being served
by GET /v3/languages and accepted by the translate endpoint while absent here.
It was verified as an exact match on 2026-08-01 and was four codes short by
2026-08-03.
The data now lives in src/data/language-entries.ts, generated by
scripts/generate-language-registry.mjs from GET /v3/languages;
language-registry.ts keeps the lookup helpers. --check reports drift without
writing, wired up as npm run generate:languages / check:languages.
Tiers are derived rather than assigned: glossary support separates extended
from the rest, source usability separates core from regional. Against the live
response this reproduces the previously hand-assigned tiers exactly -- same
32/82 split, extended set byte-identical -- so the tiers can no longer disagree
with the API either. deriveLanguageEntry is exported because the display path
needs the same rule for codes the snapshot predates.
Regenerating brings the list to 125 languages (32 core, 11 regional, 82
extended) with API names verbatim, which is why de-de is "German" like de and
fr-fr is "French" like fr; the API reports them that way and the snapshot
mirrors it instead of inventing distinct names.
* fix(languages): let the API decide which language codes exist
Regenerating the bundled list fixes today's four missing languages but not the
failure mode: a snapshot can always lag the API, and rejecting against it made
languages the API accepts unusable. `deepl translate --to de-CH` failed locally
with "Invalid target language code" while POST /v2/translate answered DE-CH
with 200 and a translation.
A well-formed language code the snapshot does not list is now sent to the API,
which accepts or rejects it authoritatively. Input that is not shaped like a
language tag is still rejected locally with the pointer to `deepl languages`,
so `--to grman` still fails fast without a request; the API answers those with
a plain 400 and no suggestion.
Applies to validateLanguageCodes, covering translate and sync, and to
ConfigService.validateLanguage, which would otherwise still reject de-ch in
config files.
* fix(languages): list languages the API offers but the snapshot predates
mergeWithRegistry iterated the bundled list and only took names from the API,
so a language the API served but the snapshot lacked was dropped from the
output entirely. That is why `deepl languages` could not show de-DE, and why
the "Run: deepl languages" suggestion on the rejection was a dead end -- the
command could not list the language it was telling the user to look for.
The row set is now the union of both, the API winning on name and features,
the snapshot supplying the tier. Snapshot entries the API omits are kept, so a
partial response never makes languages vanish. For codes the snapshot does not
know, the tier comes from the shared derivation; source usability is inferred
from the presence of a subtag rather than the role being listed, so the same
code is not tiered differently in the source and target listings.
* docs(languages): document API-as-authority and the 125-language list
Counts move from 121 to 125 and regional from 7 to 11 across README, API.md
and the languages example. API.md and README gain a section on where the list
comes from: the API is authoritative, the bundled list is a generated snapshot
for offline use, well-formed unknown codes are passed to the API, and the
listing is the union of both. Also notes that the API gives a bare code and its
explicit-region variant the same name, so "German" appearing twice is expected.
CHANGELOG records the four unusable languages under Fixed and the shift of
authority under Changed. The release checklist gains the regeneration step,
since a stale snapshot no longer breaks translation but does go stale for
offline listing and the derived tiers.
* test(languages): stop the regional-variant assertions depending on colour
The row assertions anchored on leading whitespace, but the code is chalk.cyan'd,
so wherever colour is forced on the line reads " \e[36mde-ch \e[39m German
(Swiss)" and the anchor lands on the escape sequence instead of the code. The
test passed in a NO_COLOR environment and failed in one with FORCE_COLOR set.
Passes noColor so the assertion sees the text it is written against, and
excludeApiKey so the graceful-degradation case reads the snapshot rather than
quietly calling the live API when a key happens to be configured. The rows now
assert code and name together, which also pins de-de and fr-fr carrying the
API's duplicate names.
* docs(changelog): note the ten display names the API differs on
Generating the language list replaced ten hand-written names with the API's own
(Norwegian Bokmål -> Norwegian (bokmål), Chinese (Simplified) -> Chinese
(simplified), Central Kurdish -> Kurdish (Sorani), and seven more). Worth
recording because this changelog flags output that scripts may scrape, though
the effect is limited to the no-API-key path: with a key the API name already
won at display time, so keyed output is unchanged and offline output now agrees
with it.
* feat(translate): preflight a named glossary against the requested pair
Translation memories already refused a name that did not cover the requested
language pair, locally and before any request. Glossaries did not: the request
went out and the API answered "No dictionary found for language pair EN-DE in
glossary <uuid>", naming a UUID the user never typed.
resolveGlossaryId now takes the same optional `expected` pair that
resolveTranslationMemoryId does and reports what the glossary actually covers.
It costs no extra request, because resolving a name already fetches the
glossary list. Matching is per dictionary, so a glossary holding en→es and
de→fr is not read as covering en→fr, and every target of a multi-target
translation must be covered.
Two paths deliberately skip the check: a UUID is trusted and left to the API,
matching translation-memory behaviour and leaving an escape hatch if the check
is ever wrong; and a glossary the API reports with no dictionaries says nothing
about coverage, so it is not rejected on no evidence.
The pair is threaded from the text/file path via applySharedTmAndGlossary and
from the document path, both of which already require --from with --glossary.
Existing call-site assertions now pin the pair rather than just the name.
* refactor(write): generate the Write target languages from the API
The Write API's 14 target languages were hand-maintained in two places -- the
WRITE_LANGUAGES array and a separate WriteLanguage union in types/api.ts --
either of which could fall behind GET /v3/languages?resource=write the way the
translation list silently went four codes stale.
The generator now emits both language lists, and check:languages names which
one drifted rather than reporting a misleading translate_text count. The type
is derived from the generated list via `as const`, so a language added upstream
widens it on regenerate instead of needing a second hand edit.
No behaviour change: the generated list is byte-identical to the hand-written
one, and write/correct still reject an unknown code locally while enumerating
every valid option. That strictness is deliberate and not what translate does
-- at 14 of 125 languages, naming the options beats a round trip.
The style/tone support table in API.md stays hand-maintained on purpose: it
records what the API accepts, and resource=write omits writing_style for en
even though --style --lang en works. Noted there so it is not "corrected"
against the metadata later.
* feat(translate): pin tag_handling_version to v2 instead of the server default
The version was sent only when --tag-handling-version was passed, so every
other tag-handling request inherited the API's default. That default is
documented as moving from v1 to v2, which would have shifted output with no
CLI change to attribute it to — and would have gone undetected by the cache,
since a request omitting the version hashes identically either side of the
flip.
One resolver feeds both the wire parameter and cache key field 10, so they
cannot disagree. An explicitly named version always wins, including v1;
requests without tag handling send nothing and keep their keys.
* fix(glossary): treat a base-language glossary as covering regional targets
The language-pair preflight compared the requested target verbatim against the
glossary's dictionaries, but dictionaries only ever name base languages while
--to accepts en-us, en-gb, pt-br and the rest. A de->en glossary therefore
failed locally for --to en-us, which the API accepts, making glossaries
unusable for the regional variants DeepL steers users towards.
Both sides now compare on their base language, so the check stays a nicer
error for pairs a glossary genuinely lacks without blocking valid work.
* fix(translate): honour --tag-handling-version outside text mode
buildTranslationOptions mapped tagHandling but not tagHandlingVersion, so only
the text handler carried the flag. That was harmless while no version was sent,
but since the CLI pins v2 whenever tag handling is on, a file or directory
translation asked for v1 silently got v2 -- with no warning, because single-file
mode does not run warnIgnoredOptions.
The flag is now mapped in the shared base mapping every handler uses, and its
validation moves with it so both paths reject the same input.
* fix(glossary): resolve the source language from config before requiring --from
TranslationService merges defaults.sourceLang, so a glossary request supplies
source_lang whether or not --from was typed. Rejecting on a missing flag alone
broke watch sessions and translations that had been working from config.
The guard now resolves the effective source language onto `from`, which also
gives the document path -- which merges no defaults of its own -- and the
glossary preflight the pair they need. Two related holes close with it:
- `--glossary` was tested for bare truthiness, and `[]` is truthy, so a caller
passing an empty selection got a spurious error
- watch --dry-run returned before the guard, reporting a command as runnable
that fails on the first file change
Watch also passes the pair to resolveGlossaryId now, so a glossary that does
not cover it fails at launch rather than once per file change. The text
handler's duplicate tag-handling-version block goes with the shared mapping.
* fix(services): key the translation cache on every parameter that changes the text
The cache key carried tagHandling and the glossary selection but not the
translation memory, the XML tag parameters, or preserveFormatting -- so a plain
request and the same request with --translation-memory, a different
--ignore-tags, or --preserve-formatting collided and the cache served the wrong
translation while reporting cached: true.
preserveFormatting was excluded on the grounds that it does not affect output,
but preserve_formatting suppresses the sentence-boundary punctuation and case
correction, which shows up in the text.
* refactor(types)!: derive the Language union from the generated snapshot
The union was a fourth hand-maintained copy of the language list and had already
fallen four codes behind the snapshot it is supposed to describe: de-ch, de-de,
fr-ca and fr-fr -- the very codes the /v3/languages migration was written to
add. Neither generate:languages nor check:languages touched it, and it is
published, so `deepl config set defaults.targetLangs de-de` succeeded at runtime
while DeepLConfig could not type the config the CLI had just written.
ENTRIES is now generated `as const satisfies readonly LanguageEntry[]` and
Language derives from its codes, the same way WriteLanguage already derived from
WRITE_TARGET_LANGUAGES. Regenerating the snapshot widens both.
The registry's own lookups keep the interface rather than 125 literal types, and
the suite's tier counts are compared against the snapshot instead of written
out, so the documented release step no longer turns the suite red.
* fix(scripts): guard the language generator against silent bad output
Three ways the generator could write a snapshot that breaks the CLI without
saying anything:
- an empty write list collapses the WriteLanguage union to never, so every
--lang is rejected while the error names no valid option at all
- tiers come solely from features.glossary, so a matrix that stopped reporting
it would retier all 125 languages as extended and make --formality and
--glossary unusable everywhere
- --check compared quoted lowercase codes only, so the ten renamed display
names of c955ca2 would have been reported as "formatting only"
Also fetches both resources together instead of failing fast on the first, so a
key that cannot read resource=write no longer blocks regenerating the
translation list it can read, and reports both failures at once. The rendering
is exported so the snapshot can be re-rendered from the data it already holds
when only the template changes.
* fix(languages): stop --features claiming knowledge it does not have
The listing keeps snapshot entries the API response omitted, so those rows had
no feature data -- and every one of them was rendered as the positive claim
"none". With a response covering a handful of languages, --features reported
over a hundred languages as supporting nothing. Those rows also made every
feature look non-uniform, so features shared by all the described languages
became columns of repeated values instead of the single footer note the feature
was built around.
A language the response did not describe now reads as unknown, only described
languages decide what varies, and the shared-feature note says "languages with
reported features" when some rows have none, so it does not speak for them.
Two smaller lies with it:
- a feature reported without a `status` rendered as literal "undefined"; the
enum is open and an absent status still means the feature is there
- supportsFormality was asserted false for a language whose features the
response never mentioned, turning on the "[F] = supports formality" legend
with no [F] anywhere to explain it
Also fetches /v3/languages once per client: both roles are filtered out of the
same payload, so `deepl languages` was making the identical full-list request
twice.
* fix(voice): keep partial transcripts and match echoed languages case-insensitively
Failing a session that ends with a target untranslated was the right call, but
two things made it worse than the silence it replaced.
Target updates were matched against the exact requested spelling, and the
requested set uses casings like zh-HANS and en-GB. A server echoing another
canonicalization had its translation dropped by the existing `if (target)`
guard, and the new check then reported that target as missing -- turning a
cosmetic mismatch into a failed session. Lookups are keyed lowercase now.
A single missing target also discarded the source transcript and every
successful translation, after the audio had been transcribed and billed. The
failure now carries what did arrive, and the command prints it to stderr before
exiting non-zero, so nothing has to be re-streamed to see it.
* fix(usage): stop reporting API-key usage as the account total
Duration-billed products fell back to `apiKeyUnitCount` for the account column
because live responses omit `unit_count` for them -- so `deepl usage` printed
the same number twice, as though the key accounted for all voice usage on the
account. The account-wide figure the response does carry, `account_unit_count`,
was neither typed nor parsed.
It is parsed now, and when no account-wide figure is present the row says
"(API key)" rather than inventing a total.
* fix(translate): stop repeating a doomed request and say when a code is deferred
Since validation defers unknown-but-well-formed codes to the API, a two-letter
typo now reaches it -- and a directory translation asked the same rejected
question once per batch, spending a round trip each time to be told the same
thing. An unsupported target_lang or source_lang is a property of the request,
not of one batch, so the remaining batches fail without being sent. Errors
specific to a batch still let the run continue.
Local validation also used to be the thing that pointed at `deepl languages`,
and deferring left the user with a bare "target_lang not supported" from the
server. A code the bundled snapshot does not list now says so up front, before
anything is sent or billed.
* fix(write): defer an unknown Write language to the API instead of rejecting it
The Write list is checked locally because it is small enough for the error to
name every option -- but it is still a generated snapshot, and nothing in CI
regenerates it. A language DeepL added was therefore unreachable until someone
ran generate:languages, with no flag or env var to get past it.
A code shaped like a language tag now goes to the API with a warning that still
names the bundled set, so the user gets more information than the old hard
failure gave, not less. Malformed input is still rejected locally.
The generated file is also added to .prettierignore: check:languages compares
raw text, so `npm run format` would have reported permanent drift.
* fix(glossary): dedupe IDs, check coverage in sync, and validate before dry-run
Six smaller defects around the multi-glossary work:
- naming one glossary twice (or a name plus its own UUID) sent glossary_ids with
a duplicate: it flipped the wire parameter away from glossary_id, minted a
third cache key for an identical request, and spent two of the five slots
- sync resolved its glossary without the language pair, so a glossary that does
not cover it failed once per file instead of at startup, as the sibling
translation-memory resolution already avoided
- translate --dry-run reported a glossary command as runnable that the real run
rejects for a missing --from
- the document path never ran the extended-tier constraint check, so a glossary
with an extended target uploaded the file and let the API refuse it, while the
text path said so locally
- `glossary info` printed raw dictionary languages under a normalized summary,
so one glossary could show "EN → DE" beneath "Source language: en"
- a coverage error listed every dictionary of a multilingual glossary on one line
The glossary pair matrix also compares languages after normalization, so casing
differing between the two role lists can no longer emit a self-pair.
* fix(config): normalize language codes and warn about ones the snapshot lacks
`config set defaults.sourceLang DE` was rejected while `translate --from DE`
works, because the translate paths lowercase the flag first and the fallback
pattern is lowercase-only -- so the one casing that is certainly valid was the
one class of input still refused. Values are lowercased before validating and
stored normalized.
A code the bundled snapshot does not list is still accepted, since the snapshot
can lag the API, but it now warns at the point of entry: a typo written to config
otherwise failed on every later command with nothing pointing back at the config
value that caused it.
Also documents the repeatable --glossary in the flag table, adds the required
--from to the last example that omitted it, and gives the api.ts import of
./common the extension its neighbour already had.
* fix(api): compare glossary pair languages after normalization
The v3 cross-product skipped the identity pair on raw casing, so a language
spelled differently across the two role lists would emit a self-pair the API
does not offer.
* docs(changelog): record the review fixes and correct claims they invalidated
Adds Fixed entries for the cache-key omissions, the per-batch retry of a
rejected language, the voice usage figure, the glossary minors, the config-only
source language, the --features overclaiming, config code normalization, the
tag-handling version dropped outside text mode, and the generator guards; plus a
Changed entry for the derived Language union.
Five existing Unreleased claims were no longer true and are corrected in place
rather than contradicted further down:
- write no longer rejects every unknown code locally
- the multipart glossary_ids encoding was described as live-verified; the live
document endpoint answers any unresolvable glossary the same way regardless of
encoding, so only parameter recognition was confirmed there
- the glossary preflight compares base languages, so regional targets work
- the shared-feature note is scoped when some languages have no data
- formality comes from the v3 features matrix, not the registry
* fix(languages): list the bundled snapshot in --format json without an API key
The JSON path answered {"source":[],"target":[]} while text and table printed all
125 languages from the same snapshot, so the documented offline listing worked in
two formats out of three. JSON falls back to the snapshot as the others do.
* docs(comments): describe current behaviour instead of how it got there
Comments across the language, glossary, voice, cache, usage and generator code
narrated the development process -- what the code used to do, which bug a check
exists because of, what an earlier version got wrong. They now state the
behaviour and the constraint behind it.
Two corrections came with the pass: the generated Write-language doc still said
`write`/`correct` reject an unknown code locally, which is no longer what the
code does, and the same text lives in the generator template, so the snapshot
was re-rendered from it and verified byte-identical against the live API.
* fix(package)!: give the ESM entry point resolvable specifiers
`import '@deepl/cli'` failed with ERR_UNSUPPORTED_DIR_IMPORT: the package is
`type: module`, but src/index.ts re-exported the directory './types' and
src/types/config.ts imported './common' without an extension. Node resolves
neither, so the programmatic API could not be loaded at all, and `nodenext`
consumers saw no exported types -- `import type { Language }` was an error
whatever the union held.
The CLI itself was unaffected, which is why nothing caught it: `bin` points at
dist/cli/index.js, whose module graph uses full specifiers throughout.
The manifest suite now imports the built entry in a real Node ESM process (jest's
CJS transform cannot) and fails on any extensionless relative specifier in the
emitted entry chain. Verified by reverting the fix: 3 of the new assertions fail.
* fix(scripts)!: validate the API response before writing it into TypeScript
The generator interpolated `lang` and `name` from GET /v3/languages straight into
a single-quoted TypeScript literal. `lang` was not escaped at all and `name`
escaped only quotes, so a response field ending in a backslash -- or containing
`' }] as const;` -- could close the literal and append arbitrary code to
src/data/language-entries.ts, which the next build compiles and the test suite
imports. The existing guards check the shape of the response, never the content
of a field.
Codes must now match the language-tag pattern, display names a conservative
letter/mark/digit/punctuation set, and categories one of the three tiers;
everything is quoted with escaping as well. Validation runs before grouping,
because grouping filters by category and would otherwise drop an entry with an
unrecognized one before it was ever checked.
The main guard also compares argv[1] through realpathSync: Node resolves the ESM
entry to its real path, so under a symlinked checkout both npm scripts exited 0
without doing anything -- including the release step that exists to stop the
Write list going stale.
* fix(cache): retire the translation entries the new key derivation orphans
Adding preserveFormatting to the cache key changes every translation key, not
only the keys of requests using the newly-hashed parameters: the service merges
`defaults.preserveFormatting`, which defaults to true, so the field is always
materialized. The design note and the CHANGELOG both claimed otherwise.
Rather than walk the correctness fix back, the schema version is bumped so those
rows are dropped on first open instead of lingering unreachable until their
30-day TTL, counting towards the size budget and `cache stats` in the meantime.
The purge is scoped to the `translation:` namespace -- write and correct entries
key the same way they always did and are still read in place, as the v2 notes
promise.
* fix(translate): extend the doomed-request abort past plain-text batches
The short-circuit only covered .txt/.md, the files that were already grouped into
batches. Everything else -- .json, .yaml, .html, .srt, .xliff -- goes through the
per-file path, which still spent one round trip per file to collect the same
rejection, so `translate ./locales --to xx` over 200 JSON files made 200 failing
requests. That path now stops the same way, and the files it never sent are
reported as skipped rather than as failures carrying another batch's error text.
The classifier also gates on the error class before matching the message. A 5xx
interpolates the upstream body into a NetworkError, so a gateway error quoting
`target_lang` would have aborted a run that was otherwise succeeding. A refused
key and an exhausted quota now abort too: they are as identical across items as a
bad language code.
* fix(voice): keep the salvaged transcripts visible, and match casing in the display
Three holes in the partial-result path:
- the salvage went through Logger.warn, which --quiet suppresses, while the TTY
display was erased regardless -- so `voice … -q` on a TTY wiped the rendered
transcripts and printed nothing in their place, for audio already billed. It
goes through Logger.error now, which quiet mode does not touch, and the display
is only cleared once there is something to print instead
- --format json was ignored, so a consumer capturing stderr got prose
- the live display still keyed target state by the requested spelling, the exact
defect the session-level fix addressed: with `--to zh-HANS`, a `zh-Hans` echo
left the row blank for the whole session
Also corrects the voice `--glossary` row in docs/API.md, which had picked up
translate's semantics: voice takes a single glossary, is not repeatable, and has
no --from requirement.
* fix(languages): stop --features contradicting its own footer
A language the response described but which supports none of the discriminating
features rendered as `none`, two lines above a footer crediting it with the
features every described language shares. Only a language credited with nothing
at all now reads `none`; the rest defer to the footer.
Two more display defects with it: `--features` disabled the Formality column
without always replacing it, so the flag could show strictly less than the plain
listing, and the `?` cell for an undescribed language had no legend even though
the text mode and the `[F]` marker both explain themselves. The `[F]` legend also
keyed on `supportsFormality !== undefined`, so a language whose features were
described without formality turned the legend on with nothing under it.
* fix: five defects the second review pass turned up
- **glossary**: deduplication kept a repeat's first position, which inverted the
documented last-wins precedence -- `--glossary base --glossary override
--glossary base` handed conflicts to `override` although the user put `base`
last. A repeat now keeps its last position.
- **sync**: the new startup coverage check required the top-level glossary to
cover every target locale, including ones with their own
`locale_overrides.<locale>.glossary`. That glossary never translates those
locales, so a documented configuration failed before any file was touched.
- **config**: normalization was wired into `set()` only, so a file written or
edited with uppercase codes kept them, and `TranslationService` merges
`defaults.sourceLang` verbatim -- `DE` in config and `--from de` keyed two
cache entries for one request. The load path normalizes too. The
unknown-code note is also scoped to the write path; shared with the loader it
fired on every invocation, including `deepl --version`.
- **translate**: document mode warned that `--model-type` and `--tag-handling*`
would be ignored and then rejected the command over them, because the shared
validation still saw them. They are dropped after the warning, so the mode
ignores what it says it ignores.
- **usage**: a duration-billed product could still render a character count as
hours by scaling it, and a `null` count was reported as a real zero. Character
counts are read only for `milliseconds` billing, where they carry the duration;
non-finite values are treated as absent.
* fix: report a failed directory run, and accept voice codes in any casing
A directory translation where every file failed printed its failure list and
exited 0, so a script or CI job read it as success. Since language validation
defers unknown codes to the API, a plain --to typo takes that path: on 1.x it
exited 6 locally. A run with no successes now exits 1 and a partial failure
exits 12, matching sync.
`voice` also demanded the exact mixed-case spelling of a regional code, so
`--to en-gb` and `--to zh-hans` exited 6 while `--to en-GB` worked -- the
lowercase form being the one `deepl languages` prints and every other command
accepts. Codes are matched case-insensitively and canonicalized to what the
Voice API expects.
* test: restore the guards the relational counts removed
Comparing the registry against the ENTRIES array it is built from proves the
plumbing and nothing about the data: deleting 71 of 125 languages left the
registry suite green. Floors well under today's counts, plus named codes from
every tier, fail on a snapshot that lost most of its languages while still
surviving any plausible upstream change -- which was the point of de-pinning the
literal counts.
Three more assertions that could not fail:
- the batch-abort classifier had no direct test at all; three of its four
patterns were unverified, and the error-class gate untested
- the tiering heuristic for a language the snapshot does not list was asserted
nowhere, so a regional variant could silently land in the wrong tier
- two assertions passed on substrings that occur in unrelated words ('de' inside
"code", 'glossary' inside the extended-languages header)
The shared /v3/languages nock fixture also carried no `features` matrix, so
through the real code every language in it tiered as extended with formality
unreported -- the opposite of what those four languages support.
* docs(changelog): repair the Unreleased structure and the claims that were false
The Unreleased section had two `### Changed` and two `### Removed` headings, and
eight entries under the first `### Removed` described features that exist and
work: NO_PROXY support, `auth set-key --no-verify`, `check-deps`, the `t`/`w`
aliases, the GitHub Release workflow, `--timeout`/`--max-retries`, `cached` in
JSON output, and the Node<24 fast-fail. release.yml publishes this section
verbatim as the release notes, so v2 would have announced eight live features as
deleted. Categories are merged, one per release, in Keep a Changelog order.
Three claims corrected rather than left to ship:
- the cache entry scoped invalidation to "requests using any of these options";
it is every translation entry, because a preserveFormatting default is always
merged. The schema bump that retires them is described instead
- the tag-handling entry repeated the same scoping
- "Input remains case-insensitive everywhere" was false for `voice`, which
rejected the lowercase regional codes the CLI itself prints
Also fixes three stale passages in docs/API.md: the `voice --glossary` row had
picked up translate's repeatable/`--from` semantics, the `write` reference still
said an unknown code is rejected locally, and the `usage` reference still
documented the removed Speech-to-Text section with output showing the duplicated
API-key figure.
* fix(usage): render a null count as unreported instead of throwing
`unit_count: null` reached formatNumber on the non-duration path and threw a
TypeError, so one unexpected null in the response took down `deepl usage`
entirely rather than leaving one field blank.
Also records in the changelog that write/correct entries for the five recased
target codes are unreachable. They are left to their TTL rather than dropped: the
key is a hash, so those rows cannot be distinguished from the ones for de/en/es/fr,
which still serve, and purging the namespace would discard far more than it
reclaimed.
* fix(translate): validate languages in one place for every input mode
Language validation was spread over eight call sites in five files and
only three paired the target-code check with the extended-tier check, so
file, directory and --dry-run runs accepted constrained options that text
runs rejected locally: --to af --formality more exited 6 for text and went
to the network for a file, and a dry run reported --to 'not!!a!!lang' as
runnable.
validateTranslationLanguages() is now the single gate. Each mode passes
only the flags it honours, so nothing is refused over a flag its mode
discards -- a document run still accepts --model-type, which it strips
after warning, and a directory run still accepts --glossary, which it
ignores.
Also folds in the two rough edges around the same code path: --from is
validated, with its own wording, and the deferral note is said once per
code per run rather than once per call site. The extended-tier glossary
arm reads hasGlossarySelection(), so an empty repeatable --glossary list
no longer counts as selecting one.
Closes cli-01on.2, cli-01on.1, cli-01on.7
* fix(voice): validate options before resolving --glossary, and check coverage
Resolution costs a glossary-list round trip, and it was spent before the
target language, source language and content type were checked -- so
'voice a.ogg --to bogus --glossary g' paid for a request and then failed
locally. The local checks move into an exported validateVoiceOptions()
that the registrar calls first and buildOptions reuses.
Resolution now also passes the requested pair, so a glossary that does not
cover it fails locally the way it already did for translate, watch and
sync. The pair is the canonical spelling voice sends; without --from there
is no pair and the API keeps the judgement.
Closes cli-01on.3
* fix(sync): check override glossaries at startup, and validate during --dry-run
locale_overrides.<locale>.glossary was resolved with no expected pair, so
it skipped the coverage preflight the top-level glossary and the TM
override already had. It was also resolved inside the bucket loop, once
per file, which means a reference that does not cover its locale failed
only after earlier files had been translated and written.
Both kinds of override now resolve once in the orchestrator, against their
own locale, and the resolved maps are threaded into processBucket -- so
that function no longer needs the glossary service, the translation
service or the TM cache.
sync --dry-run also drops its resolution guards. It already requires an
API key to get that far, and a glossary or TM the config names but the
account cannot use for the pair is what a preview exists to report. Zero
/v2/translate calls is unchanged and still asserted.
Closes cli-01on.4
* test(languages): pin the supportsFormality JSON contract
The field moved from v2's supports_formality boolean to the v3 features
matrix, and is now omitted for a target whose features the response does
not describe rather than reported as false. /v3/languages carries a matrix
for all 125 languages it lists, so the serialized shape is the same one
1.2.0 produced -- present and boolean for every target, absent for every
source. Nothing was asserting that, so nothing would have caught it
changing.
Also states the omission case in the CHANGELOG next to the "existing JSON
consumers are unaffected" claim, which spoke only for the features matrix.
Closes cli-01on.5
* docs: correct example output, the language endpoint, and the glossary rule
Display output went lowercase in d15bc89 but several examples still showed
`[ES]` and `| ES |` table cells, so copied output did not match reality.
Two file-translation examples were wrong beyond casing: a single-target run
prints `Translated x -> y`, not a `to 1 language(s):` list. Verified each
against a real run.
TROUBLESHOOTING's reachability check called /v2/languages, which this
release migrates off; it now uses /v3/languages?resource=translate_text and
names both hosts, since the free host answers a Pro key with 403 -- a
reachable server, which is what the check is asking about.
--glossary was documented as requiring --from in four places, which stopped
being true when the requirement started accepting defaults.sourceLang, and
the translate flag reference omitted it entirely. All five now state the
implemented rule.
Adds two mechanical guards to the docs suite, which previously checked only
that documented commands and flags exist: no documented line may show an
uppercase language code (matched against the registry, so markdown badges
are left alone), and no doc may reference a retired v2 language endpoint.
Closes cli-01on.9
* fix(watch): settle the glossary source language in the handler, not the registrar
register-watch filled options.from from defaults.sourceLang before calling
watch(), while the handler kept a flag-only guard -- so a library consumer
calling WatchCommand.watch() directly with a glossary and a configured
source language got a rejection the CLI does not make.
The handler now calls the shared applyGlossarySourceLang, as every other
command does, and WatchCommand takes the config service to read the default
from. The parameter is optional, so a caller with no configuration still
gets the requirement, just without a default to satisfy it from.
Closes cli-01on.6
* fix(languages,voice): sanitize API-provided strings before printing them
languages printed the name, feature keys and feature statuses from
/v3/languages raw, and voice did the same with transcript text and language
labels, while the glossary and style-rule listings hardened in this release
already wrapped API strings in sanitizeForTerminal. Logger's own sanitizer
only redacts secrets; it does not strip ANSI or bidi.
It matters most for voice, whose live display clears a fixed number of
lines: a newline inside a transcript deranges the whole rendering rather
than one line of it. --format json keeps the text verbatim, since
JSON.stringify escapes control characters and that path feeds machines.
Test strings spell the hostile characters as JS unicode escapes rather than
embedding literal control bytes, which do not survive editing reliably.
Closes cli-01on.8
* fix(translate): report a directory run's rejection class in the exit code
A directory run where the API refused the request itself exited 1, the
generic "nothing succeeded" code, even though the run aborted for a reason
the caller can act on. The batch services already tracked that rejection to
stop sending doomed batches; it is now surfaced on the result, and the
handler reports its exit code -- 6 for a refused target_lang, 4 for an
exhausted quota, 2 for a refused key. A run whose files failed individually
still exits 1, or 12 with some successes.
Text and file modes already exited 6 here, since the rejection reaches the
top-level handler as a typed ValidationError; only the directory path
substituted its own code.
Also resets process.exitCode after each test in the directory handler
suite. The handler writes to the jest process's own exit code, which left
set made the suite exit non-zero with every test passing.
* fix(glossary): relax the coverage check on the requested side only
The preflight compared both sides on their base language, so a pt-br
dictionary satisfied a request for pt-pt -- a different language pair. The
relaxation exists because dictionaries name base languages while --to
accepts regional variants, which needs it in one direction: a dictionary
language now matches the requested one exactly, or matches the base it
reduces to.
de->en still covers --to en-us, and a dictionary naming pt-br still matches
--to pt-br. Not reachable through the current API, whose glossary language
set is entirely base codes, but GlossaryClient types targets as Language,
which includes the variants.
* fix(languages): stop inferring a tier, and a role, from a silent response
Three related conflations of "unknown" with "no", all in how a
/v3/languages entry is read:
deriveLanguageEntry filed a language with no features matrix as extended,
inferring absent glossary support from silence. The extended tier is what
refuses formality and glossary before a request is sent, so that claims
knowledge the response did not give -- the same fault already fixed for the
--features table. Such a language is now tiered by source usability and the
API keeps the judgement. An empty matrix is still evidence, and still
extended. The bundled snapshot is unchanged, since every language the API
lists today carries a matrix.
The role flags had two conventions: the registry reads
`usable_as_source !== false` while the translation and glossary clients
filtered on truthiness, so an entry with the flag absent was dropped from
`deepl languages` but recorded as core by the generator. Both clients now
read it the registry's way.
LanguageEntry's fields are readonly, because the accessors hand out the
registry's own objects: mutating one changed the registry for the rest of
the process, which the generated `as const` snapshot forbids but the widened
element type allowed.
* fix(scripts): report a thrown fetch from the language generator as an error
The generator had no top-level catch, so a rejected fetch or an error body
that does not parse surfaced as an unhandled rejection with a stack trace
rather than the script's own "error:" line and exit 1. Every other failure
in the script already reported itself that way.
Covered by a test that preloads a throwing fetch, which fails without the
catch.
* docs(glossary): record the verified multipart glossary_ids semantics
The multi-glossary work could not establish how the multipart
POST /v2/document parses glossary_ids, because that endpoint answers any
unresolvable glossary with `glossary_ids is not valid` whatever the
encoding, so probing with fake UUIDs cannot separate a parse failure from
a non-existent glossary. Two real glossaries settle it.
A comma-joined glossary_ids applies the terms from every glossary in the
list. The same two IDs sent as repeated multipart fields apply only the
first, and the API reports no error for the ones it drops, so the
comma-joining encodeGlossaryIdsForMultipart() performs is required rather
than merely sufficient and its absence would fail silently.
Whitespace after the commas turns out to be tolerated, contrary to what
the comment claimed; the encoder emits none either way, so this is a
comment correction only.
* fix(glossary): stop documenting flag order as resolving a conflicting term
--glossary was documented as last-one-wins: with a term defined in
several glossaries, the last flag on the command line was said to decide
the translation. Two real glossaries defining the same source term show
that it does not. The winner did not track list order — the same glossary
won under both orderings in five of six trials, and the first-listed one
won in the sixth. Differing verb choices between those two runs confirm
they were separate translations rather than cache hits, so conflict
resolution belongs to the API and is context-dependent, exactly as
glossary matching itself already is.
Nothing about the request changes. The list is still sent in the caller's
order and never sorted, which stays correct for the reason the cache
always needed it: a reordered list is a different request, so it gets its
own cache entry. Only the rationale was wrong, and four tests carried
that rationale in their names.
The --help text now says the API picks the winner regardless of flag
order, so users have no reason to reorder flags expecting an override.
* docs(test): correct the nock leak comment's attribution and audit rule
The block added in dd7591c pinned the leak on six `replyWithError` sites
in deepl-client.test.ts plus three integration files. Tracing the handles
says otherwise: they arise in the translate() and error handling blocks,
through ordinary `.reply(...)` interceptions. That file has 122 `.reply()`
calls against 2 `replyWithError`, only one of which even sits in a leaking
block, and the replyWithError inventory has since grown to 14 call sites
across 6 files.
That stale baseline made the audit instruction unusable: it asked for "any
NEW leak source beyond the six known replyWithError sites", which names the
wrong construct and drifts every time a test moves. The rule is now
type-based — every reported handle should be HTTPINCOMINGMESSAGE from
@mswjs/interceptors, and anything else is a real leak.
The mechanism and both mitigations were re-verified and kept: forceExit
does not suppress the warning, and --runInBand avoids it at ~5x wall clock.
Whether a newer @mswjs/interceptors helps is now recorded as untested
rather than assumed, since 0.42 is ESM-only and fails the CJS transform.
* docs: correct inaccuracies found in the pre-2.0 audit
`docs/API.md` and `README.md` said the last `--glossary` wins a conflicting
term, contradicting the same files, the flag's own help text, and the verified
behaviour: which mapping wins is the API's choice and does not follow flag
order.
`docs/API.md`'s command-group table omitted `correct` while claiming to match
`deepl --help`, and the README's table of contents omitted its Spelling and
Grammar Correction section. A new check in the documented-surface suite fails
when the table and the CLI's top-level commands disagree in either direction.
`docs/API.md`'s environment-variable reference omitted `NO_PROXY`, which the
HTTP client honours and the README already documented.
`docs/TROUBLESHOOTING.md` attributed "Translation cache backend failed to load"
to running Node.js < 24, which the startup version check rejects earlier with
its own message, so the stated cause was unreachable. That entry now describes
the reachable case — a v24+ runtime with no usable `node:sqlite` — and the
version error is documented under exit code 6, where it appeared nowhere.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(watch): honour the documented 500ms debounce default and watch.debounceMs
`deepl watch` debounced at 300 ms while `--help`, the docs and the config
schema all said 500. The command forwarded a debounce only when `--debounce`
was passed, so an omitted flag fell through to `WatchService`'s own 300 ms
fallback — a third copy of a value the schema and the help text both put at
500.
`watch.debounceMs` was also dead: `deepl config set` accepted it and the
documented schema listed it, but nothing read it for `watch`.
Resolution is now the flag, then `watch.debounceMs`, then the documented
default, which is a single exported constant shared with the config schema so
the two cannot drift again. `deepl sync --watch` already resolved to 500 and is
unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(exit-codes): consolidate on exitCodeForError
`getExitCodeFromError` was a same-body alias documented as kept "for backwards
compatibility with callers that import the old name", but `exit-codes.ts` is
not exported from the package entry, so no such caller could exist — the only
`src/` use was the CLI's own top-level handler.
Dropping it also removes a redundant guard at that call site: the alias
narrowed its parameter to `Error`, so the handler tested `instanceof Error` and
supplied `GeneralError` itself. `exitCodeForError` takes `unknown` and already
returns `GeneralError` for a non-`Error`, so the ternary is now a single call
with identical behaviour.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: apply prettier to src and tests
`npm run format:check` failed on 409 of the repo's TypeScript files, so the
gate could not be used to catch anything. This applies the existing
`.prettierrc` with no configuration changes. `src/data/language-entries.ts`
stays untouched, as `.prettierignore` requires: `check:languages` compares its
raw text, so reformatting it would report permanent drift.
Reformatting relocated four `eslint-disable-next-line` directives away from the
lines they suppressed, because rewrapping to the 80-column width split those
lines in two. That turned one into a `require-yield` error and left
`prefer-nullish-coalescing` reported 15 times against deliberate `||` uses.
Each directive is restored over the code it covers, using the block
disable/enable form where a statement now spans several lines and a
`-next-line` directive cannot reach it. `paths.ts` and `run-cli.ts` keep `||`
on purpose — an empty string is a value `??` would pass through, but which the
XDG spec and the exit-status handling both need to treat as unset.
Lint, type-check, `format:check` and all 284 suites pass; lint is back to the
zero-warning baseline it had before the reformat.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: prune dead .gitignore entries and un-ignore db fixtures
Remove patterns nothing in the repo produces: build/ and lib/ (output only
goes to dist/), .nyc_output/ (coverage is Jest), .npm, the yarn/pnpm/npm
debug logs and logs/ (redundant with *.log), *.pid/*.seed/*.pid.lock,
cert and credential files, Windows-only OS files, the .cache/.temp/.tmp
family, and .deepl-cli/ (the legacy config dir only resolves under $HOME).
Add !tests/fixtures/*.db so committed fixture databases are not silently
ignored by *.db; the transient sqlite sidecars stay ignored.
Local per-clone tooling (.beads/, issues.jsonl, qa/, .playwright-mcp/)
moves to .git/info/exclude. The set of ignored files is unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: prune redundant .prettierignore entries
node_modules is ignored by prettier natively, and *.log and .DS_Store are
skipped during directory traversal because no parser can be inferred, so
none of the three changed what prettier touched. dist and coverage stay:
prettier does descend into them when pointed at the repo root.
Verified src/data/language-entries.ts is still ignored, and that prettier
would rewrite it without that entry.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: gate formatting and fail lint on warnings
`format:check` and `lint` both existed but neither was enforced as written:
nothing ran `format:check`, and `lint` exited 0 on warnings. The prettier
adoption in 01ec9a3 showed why both matter — rewrapping moved four
`eslint-disable-next-line` directives off the lines they suppressed, and the 15
resulting warnings would have gone green.
- `lint` now runs with `--max-warnings 0`, matching the zero-warning baseline
the repo already held.
- CI runs `format:check` before `lint`, so formatting drift is reported as
formatting rather than as the lint failures it goes on to cause.
- `format:staged` formats staged `src/` and `tests/` TypeScript and re-stages
it, for use from a pre-commit hook, so formatting is applied rather than
remembered. `.git/hooks` is not version-controlled, so CONTRIBUTING.md
documents enabling it per clone.
Prettier runs twice over the staged set. It preserves whether an object literal
was written multi-line, which interacts with member-chain breaking: an
over-long single-line object ending a chain formats to "chain broken, object
expanded", then to "chain collapsed, object hugged", which is the fixed point.
A single pass would leave content that `format:check` then rejects.
A partially staged file is reported and skipped, since reformatting and
re-adding it would stage hunks the author left out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: guard test fixtures in .prettierignore
A root-level prettier run reached test fixtures, which are byte-exact
inputs. en-reserved-words.yaml quotes "yes"/"no" so YAML 1.1 cannot read
them as booleans, and prettier rewrites those to single quotes; the sync
context fixtures are parsed for surrounding source, so rewriting them
changes what the extractor sees.
No change to the format scripts' scope, which stays src/ and tests/;
format:check still passes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(test): restore coverage source maps in ts-jest transform
Coverage was measured against the emitted JavaScript without source maps,
so istanbul attributed statements and functions to arbitrary positions --
import lines and JSDoc blocks were reported as uncovered, and downlevel
helpers appeared as never-executed anonymous functions. Reported totals
sat 4-8 points below the real ones and every configured threshold failed
while all tests passed.
tsconfig.json sets sourceMap:false so maps stay out of the published
tarball, and ts-jest merges the transform's inline options over that file
rather than replacing it, so the setting has to be re-enabled here.
Measured against the same 6536 tests, no test changes:
statements 90.47% -> 94.84% (threshold 93)
branches 82.36% -> 88.64% (threshold 86)
lines 92.15% -> 95.30% (threshold 93)
functions 86.50% -> 94.71% (threshold 94)
npm run test:coverage now exits 0.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test: cover parsePositiveIntOption…1 parent 3559742 commit e08225a
586 files changed
Lines changed: 73262 additions & 18281 deletions
File tree
- .github
- ISSUE_TEMPLATE
- workflows
- docs
- examples
- scripts
- src
- api
- cli
- commands
- sync
- translate
- data
- formats
- util
- services
- storage
- sync
- types
- utils
- tests
- e2e
- fixtures
- helpers
- integration
- property
- unit
- cli
- docs
- formats
- services
- sync
- types
- utils
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
20 | 23 | | |
21 | 24 | | |
22 | 25 | | |
| |||
27 | 30 | | |
28 | 31 | | |
29 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
30 | 37 | | |
31 | 38 | | |
32 | 39 | | |
33 | 40 | | |
34 | | - | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | 3 | | |
7 | 4 | | |
8 | 5 | | |
| |||
18 | 15 | | |
19 | 16 | | |
20 | 17 | | |
21 | | - | |
22 | | - | |
23 | 18 | | |
24 | 19 | | |
25 | 20 | | |
| |||
29 | 24 | | |
30 | 25 | | |
31 | 26 | | |
32 | | - | |
33 | | - | |
34 | 27 | | |
35 | 28 | | |
36 | 29 | | |
| |||
44 | 37 | | |
45 | 38 | | |
46 | 39 | | |
47 | | - | |
48 | 40 | | |
49 | 41 | | |
50 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | 3 | | |
5 | 4 | | |
6 | 5 | | |
7 | | - | |
8 | | - | |
9 | 6 | | |
10 | 7 | | |
11 | 8 | | |
12 | 9 | | |
13 | | - | |
14 | 10 | | |
15 | 11 | | |
16 | 12 | | |
| |||
19 | 15 | | |
20 | 16 | | |
21 | 17 | | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | 18 | | |
28 | 19 | | |
29 | 20 | | |
| |||
34 | 25 | | |
35 | 26 | | |
36 | 27 | | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | 28 | | |
51 | 29 | | |
52 | | - | |
53 | 30 | | |
54 | 31 | | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
| 32 | + | |
60 | 33 | | |
61 | 34 | | |
62 | | - | |
63 | | - | |
| 35 | + | |
| 36 | + | |
64 | 37 | | |
| 38 | + | |
65 | 39 | | |
66 | 40 | | |
67 | 41 | | |
68 | 42 | | |
69 | 43 | | |
70 | 44 | | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
2 | 5 | | |
3 | 6 | | |
4 | | - | |
5 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
0 commit comments