Releases: mariokreitz/verbatra
Release list
@verbatra/studio@0.2.2
@verbatra/sdk@0.6.2
Patch Changes
-
a6767a6: Harden the human-translator Excel round trip so a returned workbook is never silently misread. Import now reports the
changedrows 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()andwatch()now accept an optionalconcurrency(a positive integer, surfaced on the CLI as the--concurrency <n>flag ontranslateandwatch), 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, sameRunSummary.localesorder, 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 setsconcurrencygreater than 1 whilemaxTokensis configured is refused up front with aCONCURRENCY_BUDGET_CONFLICTerror (a dry run is exempt); an invalid value is rejected withCONCURRENCY_INVALID. No new locking is added: the per-locale write locks already isolate concurrent locales on disk. -
72bacc3: Add support for the Java/Spring
.propertiesformat. Files with the.properties
extension are now detected and translated: keys are read flat (never split into a
tree), the standard escapes and\uXXXXare decoded on read, and output is written
canonically with=separators and ASCII-safe\uXXXXescapes for every non-ASCII
character, so it loads under a legacyProperties.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()andwatch()now accept an optionalonProgresslistener 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--jsonmode, 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.jsonsibling to the lock file (scaffolded into.gitignorebyinit); 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-levelversion. 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, andretranslateEntryare also fed into the cache so a later run reuses them.The cache is on by default.
translate()andwatch()accept an optionalcacheinput (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, deleteverbatra.cache.json; it is regenerated naturally on the next run.LocaleSummarygains acacheHitsbucket (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
Patch Changes
-
62dbc7e: Add optional locale-level concurrency to the translate flow.
translate()andwatch()now accept an optionalconcurrency(a positive integer, surfaced on the CLI as the--concurrency <n>flag ontranslateandwatch), 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, sameRunSummary.localesorder, 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 setsconcurrencygreater than 1 whilemaxTokensis configured is refused up front with aCONCURRENCY_BUDGET_CONFLICTerror (a dry run is exempt); an invalid value is rejected withCONCURRENCY_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.jsonsibling to the lock file (scaffolded into.gitignorebyinit); 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-levelversion. 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, andretranslateEntryare also fed into the cache so a later run reuses them.The cache is on by default.
translate()andwatch()accept an optionalcacheinput (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, deleteverbatra.cache.json; it is regenerated naturally on the next run.LocaleSummarygains acacheHitsbucket (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
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
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-timeoutflag 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
requestTimeoutMsto tune the bound.
@verbatra/cli@0.6.1
@verbatra/studio@0.2.0
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'sdocument.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-toolsflag or theVERBATRA_STUDIO_AGENT_TOOLSenvironment
variable; both default to off. The two spend tools require both flags:--expose-agent-toolsto
expose the surface and--allow-spendto enable them.
Patch Changes
- @verbatra/sdk@0.6.0
@verbatra/studio@0.1.0
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 theverbatra studiocommand 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-onlycapabilitiesfield ({ spend, writeToDisk });
spendreflects the resolved flag andwriteToDiskis 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 newicuValid: booleanfield (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 existingtone === "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'scheckPlaceholdersand an adapter's own
comparePlaceholdersexactly 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 existinghistory.listpattern of supplementary data
fetched lazily on open rather than growing the already-uncapped
status.diffpayload.KeyDetailDrawernow 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 existingBadgecomponent
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/cliis version-locked with@verbatra/sdkand 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.RefreshEventgains two optional fields,localeanddelta
(added/changed/removedcounts), populated for"source"and"targets"refresh events; a
"lock"event is unchanged. Thetargetswatch 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/sdkgains a new small read-only module,readLocaleFileSnapshotand
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/cliis version-locked with@verbatra/sdkand 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...
@verbatra/sdk@0.6.0
@verbatra/sdk@0.6.0
@verbatra/sdk@0.5.0
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 failstranslate,watch,check,diff,
import, orexportfor 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.
checkanddifffindings from
validate()now sort by plain code-unit order instead of locale-sensitivelocaleCompare, so their
order no longer depends on the host's locale and always agrees withdiff()'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/cliis version-locked with@verbatra/sdkand 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 likeenorptthat DeepL requires disambiguated) now fails fast with a structuredINVALID_REQUESTerror 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 likezh-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
maxBatchSizeconfig. Previously amaxBatchSizeabove DeepL's real cap (its default of 50 happened to match, but any larger configured value did not) reachedtranslateTextunchunked 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/cliis version-locked with@verbatra/sdkand picks up the same bump; its own behavior is unchanged. -
565eb89: Move
ProviderNoticeandProviderNoticeCodeonto the sharedTranslateResulttype instead of
DeepL's own extended result shape, and add an optionalnoticesfield toTranslateResultitself.
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
supportsGlossaryflag, which is a behavior change worth calling out explicitly:
it previously reportedtrueunconditionally, 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 aGLOSSARY_IGNOREDnotice; the flag was
simply lying about it.supportsGlossarynow reportstrueonly when a nativeglossaryIdis
configured, andfalsefor 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/cliis version-locked with@verbatra/sdkand 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 withINVALID_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
providerFailurescategory (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-compatiblefor pointing verbatra at a local or self-hosted OpenAI-compatible inference server (LM Studio, Ollama, vLLM). Configure it with{ baseUrl, model, maxOutputTokens, apiKeyEnvVar? };baseUrlis 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 hostedopenaiprovider).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 variableOPENAI_COMPATIBLE_API_KEY, then the non-secret placeholder"local"for servers that need no key at all.apiKeyEnvVarcannot name any of the four hosted providers' environment variables, and the new provider's client never readsOPENAI_API_KEYor shares any code path with the hostedopenaiprovider, so a hosted key can never reach a custombaseUrl.The request body uses the same strict, schema-constrained response format as the hosted
openaiprovider (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/cliis version-locked with@verbatra/sdkand picks up the same bump; its own behavior is unchanged, andverbatra initdoes not yet offeropenai-compatibleas a scaffold option (it has no single required environment variable, unlike every other provider). -
7d50d22:
translateandwatchnow 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 reachingRunSummary) and is skipped on dry-run, mirroring the existing lock-file
write discipline.verbatra.lock.jsonitself is unchanged.verbatra initnow also scaffolds.verbatra-local/into a project's.gitignore,
alongside the existing.env/.env.localentries. -
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 asTIMEOUT, and a 401 or 403 asAUTH_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 (viaAbortSignal) 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...