Skip to content

Releases: mariokreitz/verbatra

@verbatra/studio@0.2.2

Choose a tag to compare

@github-actions github-actions released this 23 Jul 10:23
cc23bc7

Patch Changes

  • Updated dependencies [a6767a6]
  • Updated dependencies [62dbc7e]
  • Updated dependencies [72bacc3]
  • Updated dependencies [b98d7f2]
  • Updated dependencies [ca2d99a]
    • @verbatra/sdk@0.6.2

@verbatra/sdk@0.6.2

Choose a tag to compare

@github-actions github-actions released this 23 Jul 10:23
cc23bc7

Patch Changes

  • a6767a6: Harden the human-translator Excel round trip so a returned workbook is never silently misread. Import now reports the changed rows a translator left blank as pending (unfilled) work instead of quietly counting the locale as done, and a translation cell that holds only whitespace is treated exactly like an empty cell. A single malformed row no longer discards the rest of its sheet: the good rows still import and each bad row is reported by sheet, row number, and column (never any cell content). A duplicate key within a sheet is reported as a conflict, with the first occurrence winning deterministically. A translator can now deliberately unset a value by typing [[CLEAR]] in the Translation cell, which writes an empty value while keeping the key, still honoring the source-drift check. Exported workbooks lock their structure so the language tabs cannot be trivially renamed, deleted, or reordered, and on import a configured locale whose tab is missing or renamed is reported as a named, structured failure rather than silently dropped. The instructions sheet documents the new behavior, and the CLI prints the new counts and key lists.

  • 62dbc7e: Add optional locale-level concurrency to the translate flow. translate() and watch() now accept an optional concurrency (a positive integer, surfaced on the CLI as the --concurrency <n> flag on translate and watch), running up to that many target locales at once through a bounded worker pool. The default is 1, which stays strictly serial and byte-identical to before: same written files, same RunSummary.locales order, same lock-file content. Regardless of completion order, results are always collected back into source-locale order. Because a token budget's stop guarantee is order-dependent, a live run that sets concurrency greater than 1 while maxTokens is configured is refused up front with a CONCURRENCY_BUDGET_CONFLICT error (a dry run is exempt); an invalid value is rejected with CONCURRENCY_INVALID. No new locking is added: the per-locale write locks already isolate concurrent locales on disk.

  • 72bacc3: Add support for the Java/Spring .properties format. Files with the .properties
    extension are now detected and translated: keys are read flat (never split into a
    tree), the standard escapes and \uXXXX are decoded on read, and output is written
    canonically with = separators and ASCII-safe \uXXXX escapes for every non-ASCII
    character, so it loads under a legacy Properties.load. Comments, blank lines, and
    key order in an existing target file are preserved on write.

    Placeholder integrity understands the java.text.MessageFormat argument syntax these
    files are consumed through, including the typed and styled forms ({0,number,integer},
    {0,date,short}) and the sub-message forms ({count,plural, ...}), so a translation
    that drops or alters an argument is caught. MessageFormat single-quote quoting is not
    interpreted: a quoted literal such as '{0}' is still treated as an argument. This is
    deliberate, so that an ordinary apostrophe in translated text never swallows a
    following placeholder.

  • b98d7f2: Report progress during translate and watch. translate() and watch() now accept an optional onProgress listener that fires as a run advances: once per locale before it starts and after it finishes, once per provider sub-batch within a locale, and once when the whole run ends. As with the existing lock-wait signal, the SDK writes nothing itself; the CLI renders these events to stderr in both human and --json mode, so stdout stays a clean summary or NDJSON stream. A dry-run makes no provider call and so emits no sub-batch events.

  • ca2d99a: Add a content-addressed translation-memory (TM) cache so a translation whose source content is unchanged is reused for free instead of being re-sent to the provider. A translation is reused even when its key was renamed, and identical source text shared across two keys is paid for once. The cache lives in a local, gitignored, regenerable verbatra.cache.json sibling to the lock file (scaffolded into .gitignore by init); it is never a field on the lock file and never committed.

    Each entry is keyed by (sourceContentHash, targetLocale, fingerprint), nested by fingerprint under a top-level version. The fingerprint is a stable hash over the provider id, model, tone, and sorted glossary; format is deliberately excluded because every reused value is re-checked by the placeholder/ICU integrity gate against the current source before it is applied, so a hit that no longer matches the target format is discarded and its key falls through to the provider. Reused hits apply silently (never flagged for review). A changed fingerprint (for example a different tone) never serves a stale value.

    The cache is resilient by design: a missing, corrupt, oversized, or unrecognized-version file degrades to an empty cache and never fails a run (unlike the fatal lock-file). It is read once as an immutable snapshot at run start and written once at the end (best-effort, dry-run-skipped), which keeps it safe under locale concurrency. Values accepted by importWorkbook, editEntry, and retranslateEntry are also fed into the cache so a later run reuses them.

    The cache is on by default. translate() and watch() accept an optional cache input (surfaced on the CLI as --no-cache) that bypasses both the read and the write for a run, making it behave exactly as if no cache existed and leaving any existing cache file untouched. To rebuild or discard the cache, delete verbatra.cache.json; it is regenerated naturally on the next run. LocaleSummary gains a cacheHits bucket (rendered as "from cache" in the CLI) reporting the keys served from cache as avoided provider usage.

    Within a single run, byte-identical source text shared across two or more keys is translated once per target locale: the provider misses are deduplicated by source content hash, one representative is sent, and its accepted value is fanned out to every key that shares the content (and cached and lock-advanced identically). This holds even when the keys would otherwise fall into separate provider batches.

    Known limitation: generated plural forms are out of v1 TM scope. A synthesized CLDR plural form is neither served from nor written to the cache; only main-path diff candidates participate.

@verbatra/cli@0.6.2

Choose a tag to compare

@github-actions github-actions released this 23 Jul 10:23
cc23bc7

Patch Changes

  • 62dbc7e: Add optional locale-level concurrency to the translate flow. translate() and watch() now accept an optional concurrency (a positive integer, surfaced on the CLI as the --concurrency <n> flag on translate and watch), running up to that many target locales at once through a bounded worker pool. The default is 1, which stays strictly serial and byte-identical to before: same written files, same RunSummary.locales order, same lock-file content. Regardless of completion order, results are always collected back into source-locale order. Because a token budget's stop guarantee is order-dependent, a live run that sets concurrency greater than 1 while maxTokens is configured is refused up front with a CONCURRENCY_BUDGET_CONFLICT error (a dry run is exempt); an invalid value is rejected with CONCURRENCY_INVALID. No new locking is added: the per-locale write locks already isolate concurrent locales on disk.

  • ca2d99a: Add a content-addressed translation-memory (TM) cache so a translation whose source content is unchanged is reused for free instead of being re-sent to the provider. A translation is reused even when its key was renamed, and identical source text shared across two keys is paid for once. The cache lives in a local, gitignored, regenerable verbatra.cache.json sibling to the lock file (scaffolded into .gitignore by init); it is never a field on the lock file and never committed.

    Each entry is keyed by (sourceContentHash, targetLocale, fingerprint), nested by fingerprint under a top-level version. The fingerprint is a stable hash over the provider id, model, tone, and sorted glossary; format is deliberately excluded because every reused value is re-checked by the placeholder/ICU integrity gate against the current source before it is applied, so a hit that no longer matches the target format is discarded and its key falls through to the provider. Reused hits apply silently (never flagged for review). A changed fingerprint (for example a different tone) never serves a stale value.

    The cache is resilient by design: a missing, corrupt, oversized, or unrecognized-version file degrades to an empty cache and never fails a run (unlike the fatal lock-file). It is read once as an immutable snapshot at run start and written once at the end (best-effort, dry-run-skipped), which keeps it safe under locale concurrency. Values accepted by importWorkbook, editEntry, and retranslateEntry are also fed into the cache so a later run reuses them.

    The cache is on by default. translate() and watch() accept an optional cache input (surfaced on the CLI as --no-cache) that bypasses both the read and the write for a run, making it behave exactly as if no cache existed and leaving any existing cache file untouched. To rebuild or discard the cache, delete verbatra.cache.json; it is regenerated naturally on the next run. LocaleSummary gains a cacheHits bucket (rendered as "from cache" in the CLI) reporting the keys served from cache as avoided provider usage.

    Within a single run, byte-identical source text shared across two or more keys is translated once per target locale: the provider misses are deduplicated by source content hash, one representative is sent, and its accepted value is fanned out to every key that shares the content (and cached and lock-advanced identically). This holds even when the keys would otherwise fall into separate provider batches.

    Known limitation: generated plural forms are out of v1 TM scope. A synthesized CLDR plural form is neither served from nor written to the cache; only main-path diff candidates participate.

  • Updated dependencies [a6767a6]

  • Updated dependencies [62dbc7e]

  • Updated dependencies [72bacc3]

  • Updated dependencies [b98d7f2]

  • Updated dependencies [ca2d99a]

    • @verbatra/sdk@0.6.2

@verbatra/studio@0.2.1

Choose a tag to compare

@github-actions github-actions released this 18 Jul 15:12
57b1ec5

Patch Changes

  • 67f1768: Withhold degenerate machine translations at the write-path integrity gate. Output that is structurally corrupt (a repetition loop or runaway-length text) but carries no placeholders previously passed the placeholder and ICU checks and was written to disk. Such values are now detected and withheld as an integrity mismatch, so they are retried on the next run and never overwrite an existing good value. Studio surfaces the new rejection reason in its review actions.
  • 002248b: Show withheld keys after a translate-pending run instead of a blanket success. When a run keeps some translations but withholds others, Studio now reports how many keys were withheld and for which locales, rather than displaying "Translated".
  • Updated dependencies [67f1768]
  • Updated dependencies [a90bc7e]
  • Updated dependencies [720716c]
  • Updated dependencies [adc9536]
    • @verbatra/sdk@0.6.1

@verbatra/sdk@0.6.1

Choose a tag to compare

@github-actions github-actions released this 18 Jul 15:12
57b1ec5

Patch Changes

  • 67f1768: Withhold degenerate machine translations at the write-path integrity gate. Output that is structurally corrupt (a repetition loop or runaway-length text) but carries no placeholders previously passed the placeholder and ICU checks and was written to disk. Such values are now detected and withheld as an integrity mismatch, so they are retried on the next run and never overwrite an existing good value. Studio surfaces the new rejection reason in its review actions.

  • a90bc7e: Report a locale honestly when its keys are withheld, and retry truncated batches. A run that withheld every key for a locale previously reported it as succeeded and exited 0, so a run that produced nothing looked like a clean success in CI. Such a locale is now reported as failed and the command exits non-zero. A locale that translated some keys but withheld others is reported as partial and still exits 0, because withheld keys keep their prior state and are retried next run. The run summary gains a partial list alongside succeeded and failed. This exit-code change applies to both translate and import.

    On an OUTPUT_TRUNCATED provider error (common with reasoning models whose reasoning tokens consume the output budget), the failing sub-batch is now automatically re-split toward a single entry and retried before any key is withheld.

  • 720716c: Make a contended write-lock wait visible instead of silent. When a locale's write lock is held by another run, or was left behind by a killed process, translate and watch now report that they are waiting, naming the lock file and, when it can be read, the holding process id and how long it has been held, so a blocked run no longer looks hung. A new --lock-timeout flag adjusts how long to wait before giving up. Lock acquisition is otherwise unchanged.

  • adc9536: Bound every provider request with an abortable timeout so a hung-but-alive endpoint can no longer stall a run indefinitely. A stuck request previously held the per-locale write lock forever, blocking every later run for that locale. Requests now abort after a default of two minutes and surface a retriable timeout error, releasing the lock. Each provider accepts an optional requestTimeoutMs to tune the bound.

@verbatra/cli@0.6.1

Choose a tag to compare

@github-actions github-actions released this 18 Jul 15:12
57b1ec5

Patch Changes

  • Updated dependencies [67f1768]
  • Updated dependencies [a90bc7e]
  • Updated dependencies [720716c]
  • Updated dependencies [adc9536]
    • @verbatra/sdk@0.6.1

@verbatra/studio@0.2.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 21:28
25437cb

Minor Changes

  • 28667da: Add an opt-in WebMCP agent-tools surface to Studio, off by default.

    When enabled, the prebuilt dashboard registers its existing RPC methods as WebMCP tools on a
    supporting browser's document.modelContext, so an agent on the open, authenticated tab can drive
    the same read, edit, and (with --allow-spend) provider actions the dashboard already exposes.
    Each tool is a 1:1 wrapper over the same authenticated server call, validation, and capability gate;
    registration grants no authority the tab did not already hold. Enable it with the new
    verbatra studio --expose-agent-tools flag or the VERBATRA_STUDIO_AGENT_TOOLS environment
    variable; both default to off. The two spend tools require both flags: --expose-agent-tools to
    expose the surface and --allow-spend to enable them.

Patch Changes

  • @verbatra/sdk@0.6.0

@verbatra/studio@0.1.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 05:51
7e94532

Minor Changes

  • 9e43dc1: Verbatra Studio is a redesigned application, from the information architecture down. It is
    organized as four pages in two sidebar zones: Translations (the daily workspace: a status banner
    with the last run's token figures, the key-by-locale explorer, and per-locale coverage with the
    lock file's state), Review (the flagged-entry queue with locale and key filters), Activity (the
    commit feed beside the last run's token and budget breakdown), and Settings (the session's
    capabilities plus the resolved configuration and glossary). The current page lives in the URL
    hash, so a reload lands back on the same page and browser back/forward work.

    The dashboard is now fully live: every page re-fetches on the file-watcher's refresh signal
    (coverage, the key diff, the lock state, the review queue, usage, and history), and the top bar
    carries a live indicator that turns amber while the stream reconnects. The key detail drawer is
    richer, showing the key's current source value and each locale's current translation alongside
    status and integrity, all updating live.

    Local editing needs no flag: the needs-review queue's edit, approve, and reject actions are
    available from the start, behind the loopback session and the same placeholder and ICU
    integrity gate as every write. Provider-calling actions (retranslate, translate pending) are
    opt-in via --allow-spend.

    The interface is rebuilt on Tailwind CSS with a reusable design system and a restrained, minimal
    look: overhauled dot-style badges, neutral elevation, and no page transitions. It ships a full
    light theme beside the dark one (System/Light/Dark switcher, persisted, following live OS
    changes on System), both contrast-checked against WCAG AA during development. Navigation is the
    flat four-page sidebar; every page is reachable in one click.

  • 55c6af2: The Translations page's missing, changed, and orphaned key lists get their own distinct color
    vocabulary, separate from the status badges, since "what changed" and "is this correct" are
    different signals. When a project has nothing missing, changed, or orphaned in any locale, the
    page shows a single designed all-clear state instead of a wall of empty per-locale sections.

    Key and glossary translations render with right-to-left text direction for right-to-left locales
    such as Arabic, Hebrew, Persian, and Urdu.

    New: clicking a key opens a detail drawer showing that key's status per locale and the project's
    commit history for its locale files. The drawer supports a focus trap, closing with Escape, and
    returns focus to whatever was focused before it opened.

  • 3f5fa4b: Two small, additive dashboard usability improvements.

    The Translations page gets a "Copy as review report" button that renders the full, currently
    loaded diff data (never the on-screen filtered or capped view) as a Markdown summary, per locale,
    the missing, changed, and orphaned key counts and key names, and copies it to the clipboard, with
    a brief "Copied" confirmation.

    RPC errors now render specific, actionable copy for known error codes (transport-level errors, sdk
    errors reachable through the read-only check, diff, and lock endpoints, and adapter parse errors on
    a target locale file), falling back to the existing generic message for any other code. Nothing
    about how errors are produced, redacted, or transported changes; this is a client-rendering
    improvement only.

  • 674beb0: Add a provider-calling Studio action: an inline "Retranslate" button in the key detail drawer,
    for a locale row whose key currently fails placeholder or ICU integrity. Off by default;
    reachable only when the verbatra studio command is started with --allow-spend (or its
    environment variable equivalent). With the flag off, the action is absent from the dashboard,
    not merely disabled, and the underlying RPC method is absent from the server's own dispatch
    registry.

    Provider spend is the one gated capability: the server reaches a translation provider only when
    an operator opts in at process start, never through any request the dashboard itself can send.
    Local editing of the project's own locale files needs no flag and is always available.

    project.snapshot's result gains a read-only capabilities field ({ spend, writeToDisk });
    spend reflects the resolved flag and writeToDisk is always true, since local editing is
    always on. The dashboard uses it only to hide the retranslate affordance the server would refuse
    anyway, never as an authorization check.

    Internally, the RPC handler registry is now built per server instance from its resolved
    capabilities (createRpcHandlers) rather than a fixed module-level constant, and the
    retranslate method is gated by a dedicated, process-scoped rate limit at the dispatch layer.

    key.integrity's per-locale result gains a new icuValid: boolean field (a boolean only, never a
    message string), and the key detail drawer's integrity pill gains a new danger state, "Invalid
    message syntax", for a target value that is placeholder-valid but fails ICU message-syntax
    validation, checked before the existing neutral "no placeholders" state. This is exactly the kind
    of failure the new Retranslate action exists to fix, so it was already covered by
    canRetranslate's existing tone === "danger" gate once the pill itself learned to render it.

  • 4515726: Add a new sdk function, keyIntegrity, that reports per changed key
    and target locale whether the format's placeholders or ICU structure
    still match between source and target: a boolean match result plus,
    on a mismatch, the specific placeholder tokens that are missing or
    extra. It reuses core's checkPlaceholders and an adapter's own
    comparePlaceholders exactly as they exist today; only "changed" keys
    are checked, since a missing or orphaned key has no value on one side
    to compare.

    Studio exposes this through a new read-only RPC method, key.integrity,
    scoped to exactly the one key currently open in the detail drawer,
    mirroring the existing history.list pattern of supplementary data
    fetched lazily on open rather than growing the already-uncapped
    status.diff payload. KeyDetailDrawer now renders an Integrity
    column with a pill: green for a match, red with the mismatched tokens
    for a mismatch, and neutral (never a false red) for a format with no
    placeholders at all. The pill reuses the existing Badge component
    and its success, neutral, and danger tones; no new styling is added.

    No RPC response carries a full source or target string value at any
    point, only the boolean result and, on a mismatch, the specific
    placeholder tokens involved.

    @verbatra/cli is version-locked with @verbatra/sdk and picks up the
    same bump; its own behavior is unchanged.

  • 45a7774: Commit history rows (in the Activity feed and in the commit history section of a key's detail
    drawer) now show which locale files each commit touched, as a row of file chips under the
    summary line. This data was already present in every response from the underlying commit-history
    API; it just was not rendered before.

  • ea054a2: Studio's live-refresh SSE channel now reports a real, still-content-free key delta instead of a
    blank "something changed" signal. RefreshEvent gains two optional fields, locale and delta
    (added/changed/removed counts), populated for "source" and "targets" refresh events; a
    "lock" event is unchanged. The targets watch category is now split into one chokidar watcher and
    one debounce per configured target locale, so a change to one target locale's file is distinguishable
    from a change to another, and each locale reports its own delta.

    The delta is a plain content diff of one locale file against its own last observed snapshot (taken at
    Studio startup and after every settled change), independent of source drift or the lock baseline.
    This is a deliberate semantics choice: it is the only reading under which a translator hand-editing an
    existing translation's wording, with the key itself untouched, is ever detected as a change. Two rapid
    changes to the same locale file, close enough together that the second's debounce window opens while
    the first's snapshot read is still in flight, are serialized so the second's reported delta is always
    correct against the first's settled state, never a stale or out-of-order baseline.

    @verbatra/sdk gains a new small read-only module, readLocaleFileSnapshot and
    diffLocaleSnapshots, exported for this purpose: reading one locale file through the configured
    adapter into a per-key content hash, and comparing two such snapshots into added/changed/removed
    counts. No translation string, key name, or file content ever crosses the SSE wire, only locale codes
    and counts.

    @verbatra/cli is version-locked with @verbatra/sdk and picks up the same bump with no behavior
    change of its own.

  • 68b3ee8: The Translations page's key explorer gets a grid view: rows are keys, columns are target
    locales, and each cell
    shows that key's status (missing, changed, orphaned, or in sync) with the same color and badge
    vocabulary as the rest of the dashboard. Each locale column header shows its completeness
    percentage. Grid is the default view; the previous flat per-locale key lists stay reachable as a
    "List" view through a toggle above the table.

    The grid supports keyboard-first navigation: arrow keys move between cells and wrap at the grid's
    edges, Enter or Space opens the key detail drawer for the focused row's key, and Escape closes it.
    Only the currently focused cell is in the Tab order, so tabbing into the grid and back out stays a
    single stop either way.

  • d99347a: Add a live needs-review queue to Verbatra Studio: a new Review page listing every (locale, key)
    pair the most recent CLI run flagged for human review, backed by a new unco...

Read more

@verbatra/sdk@0.6.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 21:28
25437cb
@verbatra/sdk@0.6.0

@verbatra/sdk@0.5.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 05:51
7e94532

Minor Changes

  • 81dd225: A JSON, YAML, or ARB locale file that contains a stray non-string leaf, such as "count": 5,
    "enabled": true, or "active": null, no longer fails translate, watch, check, diff,
    import, or export for the whole file. The non-string leaf is accepted as valid file structure,
    excluded from the translatable set (never sent to a provider, hashed, diffed, or checked for
    placeholder or ICU integrity), and every sibling string key in that file is read and translated
    normally. This is a strict widening of what was previously rejected outright with
    INVALID_STRUCTURE. A non-string leaf is not preserved if the same file is later rewritten by
    verbatra: its path is silently absent from the output the next time that target file is written,
    since the write path rebuilds the file purely from the translatable entries. This applies to every
    JSON-family format (i18next, vue-i18n, next-intl, ngx-translate), YAML, and Flutter ARB.

    Two smaller, unrelated correctness fixes ship on the same branch. check and diff findings from
    validate() now sort by plain code-unit order instead of locale-sensitive localeCompare, so their
    order no longer depends on the host's locale and always agrees with diff()'s own ordering for the
    same key set. Writes to locale files are now crash-durable: the temp file is fsynced before the
    rename that makes it visible, and the containing directory is fsynced (best-effort) after, closing a
    window where a crash between the rename and disk flush could leave a target file renamed but empty
    or corrupt.

    @verbatra/cli is version-locked with @verbatra/sdk and picks up the same bump.

  • 35fe0f6: Fix the DeepL provider silently mishandling two boundary cases it never checked. First, a locale code DeepL's API does not accept (a regional source code like en-US, when only the base code is valid as a DeepL source; or a deprecated bare target code like en or pt that DeepL requires disambiguated) now fails fast with a structured INVALID_REQUEST error naming the rejected code, instead of reaching DeepL and surfacing as an opaque generic provider failure. A locale code DeepL does accept, including a title-case script subtag like zh-Hans, passes through unchanged.

    Second, the DeepL provider now chunks its own outgoing requests to stay within DeepL's documented per-request caps (50 texts, 128 KiB of payload), independent of and in addition to the existing maxBatchSize config. Previously a maxBatchSize above DeepL's real cap (its default of 50 happened to match, but any larger configured value did not) reached translateText unchunked and failed only at the provider. A sub-batch that already fits in one request is sent exactly as before; only an over-cap sub-batch is now split into multiple sequential requests and merged back transparently.

    @verbatra/cli is version-locked with @verbatra/sdk and picks up the same bump; its own behavior is unchanged.

  • 565eb89: Move ProviderNotice and ProviderNoticeCode onto the shared TranslateResult type instead of
    DeepL's own extended result shape, and add an optional notices field to TranslateResult itself.
    Every provider now populates it as a present array: DeepL reports its real graceful-degradation
    notices (FORMALITY_DOWNGRADED, GLOSSARY_IGNORED, PLACEHOLDER_UNSUPPORTED), and every LLM
    provider (Anthropic, OpenAI, Gemini, openai-compatible) returns an empty array rather than omitting
    the field. The SDK's internal notice reader is now a plain, typed accessor over this field instead of
    a duck-typed structural cast, so a provider-side rename or shape change is now caught by the
    compiler instead of silently returning no notices.

    Also fixes DeepL's supportsGlossary flag, which is a behavior change worth calling out explicitly:
    it previously reported true unconditionally, even though DeepL only ever applies a pre-created
    native glossary id, never the SDK's generic source-term to target-term map. Supplying a term map
    without a configured native glossary id already produced a GLOSSARY_IGNORED notice; the flag was
    simply lying about it. supportsGlossary now reports true only when a native glossaryId is
    configured, and false for the generic term-map-only case. This is not a regression: DeepL's actual
    glossary behavior is unchanged, and nothing in the SDK gates glossary data on this flag, so a
    supplied term map still flows through to DeepL (and is still ignored with the same notice) exactly
    as before.

    @verbatra/cli is version-locked with @verbatra/sdk and picks up the same bump; its own behavior
    is unchanged.

  • 4c6fd52: The shared LLM layer (runLlmTranslation) no longer discards an entire sub-batch when the model's
    response is only partially well-formed. Previously, a response missing, duplicating, or adding a single
    key relative to what was requested failed the whole batch with INVALID_RESPONSE, so a 50-key sub-batch
    that came back with 49 good keys and one bad one withheld and re-paid for all 50 on the next run.

    Reconciliation now partitions a response into the well-formed keys (accepted immediately) and the keys
    missing or duplicated (neither is safe to guess at). The well-formed remainder is kept, and exactly one
    bounded repair round re-requests only the still-missing keys through the same schema-bound boundary.
    Placeholder and ICU integrity still runs on every accepted value, including one recovered in the repair
    round. A key still missing after the repair round is withheld and reported under the existing
    providerFailures category (nothing was translated for it), never counted as a placeholder-integrity
    mismatch, and the lock baseline advances only for keys actually accepted this run so a withheld key
    retries next time.

    An unrequested (hallucinated) key is unaffected by this change: it still fails the whole batch
    immediately, in the first response or the repair round, exactly as before. This is a reliability
    improvement, not a breaking change: @verbatra/sdk's and @verbatra/cli's (version-locked) public
    behavior is unchanged except that fewer whole-batch failures are observable when a provider response is
    mostly, but not perfectly, well-formed.

  • 54a641a: Add a new provider id openai-compatible for pointing verbatra at a local or self-hosted OpenAI-compatible inference server (LM Studio, Ollama, vLLM). Configure it with { baseUrl, model, maxOutputTokens, apiKeyEnvVar? }; baseUrl is validated as an absolute http or https URL at config-parse time, and lives in config rather than the environment since it is a network address, not a secret. It must include your server's API path segment (typically /v1, the same convention the underlying client already uses for the hosted openai provider).

    The API key still never lives in config. It resolves in three tiers: an explicitly named apiKeyEnvVar (throws a clear error if that variable is unset), then the new convention variable OPENAI_COMPATIBLE_API_KEY, then the non-secret placeholder "local" for servers that need no key at all. apiKeyEnvVar cannot name any of the four hosted providers' environment variables, and the new provider's client never reads OPENAI_API_KEY or shares any code path with the hosted openai provider, so a hosted key can never reach a custom baseUrl.

    The request body uses the same strict, schema-constrained response format as the hosted openai provider (verified against a live LM Studio server); the one difference is that this provider tolerantly extracts the first brace-balanced JSON object anywhere in the response before parsing, since a local or smaller model can still wrap an otherwise-correct answer in prose or a ```json block despite the constraint. The extraction is string-aware, so prose or Markdown fence characters before, after, or even embedded inside a translated string value never defeat it. Its output still runs through the exact same canonical schema validation and placeholder and ICU integrity checks as every other provider.

    @verbatra/cli is version-locked with @verbatra/sdk and picks up the same bump; its own behavior is unchanged, and verbatra init does not yet offer openai-compatible as a scaffold option (it has no single required environment variable, unlike every other provider).

  • 7d50d22: translate and watch now persist each non-dry-run's review-flag and token/usage
    data to a new gitignored file, .verbatra-local/run-status.json, written once after
    the per-locale loop completes. A new SDK function, runStatus, reads it back:
    { available: false } when no file exists yet or it cannot be parsed, or
    { available: true, version, generatedAt, usage?, budget?, locales } when it does.
    The write is best-effort (any failure is caught and swallowed, never failing the run
    or reaching RunSummary) and is skipped on dry-run, mirroring the existing lock-file
    write discipline. verbatra.lock.json itself is unchanged.

    verbatra init now also scaffolds .verbatra-local/ into a project's .gitignore,
    alongside the existing .env/.env.local entries.

  • 400e044: Providers now classify a failed translation call by HTTP status code or SDK error class instead of collapsing every failure into one opaque error: a 429 or an equivalent rate-limit error class surfaces as RATE_LIMITED, a network or request timeout as TIMEOUT, and a 401 or 403 as AUTH_FAILED, with the prior generic code kept as the fallback for anything unclassified. Classification never inspects error message text, so nothing provider-specific or key-shaped can leak through it. A caller-initiated cancellation (via AbortSignal) is now re-thrown as an abort instead of being wrapped as a provider error, so it can be told apart from a real failure; abort detection correlates the caught error's own identity with the signal instead of trusting the signal's `abort...

Read more