Releases: AronSoto/verbaly
Release list
Release v0.23.0
The 1.0-readiness release: one <Trans> contract everywhere, live extraction and a coverage view. The three framework adapters now share the exact same <Trans> surface and rendering rules, the CLI learns status (how much is left to translate, per locale) and extract --watch (live extraction for bundlers without the Vite plugin), and the last planned pre-1.0 breaking changes land: @verbaly/svelte moves to Svelte 5 only, and React/Vue <Trans> render whitelisted tags as real elements like the rest of the toolkit always did. Two breaking changes, both called out below.
Highlights
<Trans>is now the same component in React, Vue and Svelte. Same props everywhere (id,values,instance,components,richTags,links) and same rendering rules: your custom component wins, then named links, then the safe tag whitelist, and anything unknown degrades to plain text.- Breaking: React and Vue
<Trans>now render whitelisted tags for real. A message likeThe <em>build</em> gateshows a real<em>element, matching Svelte and the DOM interpreter. Before, React and Vue flattened it to plain text. If you relied on that, passrichTags={[]}. - Breaking:
@verbaly/svelterequires Svelte 5. Svelte 4 support is retired while the project is still 0.x. In exchange, the Svelte<Trans>finally gets thecomponentsprop: map a tag in your message to your own component. verbaly statustells you how much is left to translate. One line per language ("es: 45/48 translated (94%)"), no CI noise.verbaly checkstays the build gate.verbaly extract --watchkeeps catalogs and types in sync as you code. Made for webpack, Rspack and Rollup setups, where the Vite plugin's live extraction isn't available.- Easier to give feedback. The README now says exactly where a bug report or a friction report goes, and the issue forms cover all ten packages.
For the full details of this release, see the repository changelog.
Release v0.22.0
Your catalogs become native mobile resources. verbaly export learns two new formats: android-xml writes drop-in res/values-*/strings.xml folders and ios-strings writes *.lproj/Localizable.strings, so a web app and its mobile companion can share one set of translations. The same release makes catalog reads safer: a corrupt catalog file now stops the CLI with a clear error instead of being silently treated as empty. No breaking changes.
Highlights
- Export your translations for mobile apps.
verbaly export --format android-xmlwrites Android resource folders you can drop intores/, and--format ios-stringswrites.lprojfolders for Xcode. One catalog, web and mobile. - Sensible defaults for the platforms. Your source language becomes the platform default (
values/strings.xml,en.lproj), and untranslated keys are left out so the app falls back to the default language instead of showing empty text. - Safer catalog files. A catalog with broken JSON now stops the command and names the file, instead of being read as empty (which could have ended in lost translations on the next extract). A Windows BOM at the start of the file is simply tolerated.
For the full details of this release, see the repository changelog.
Release v0.21.0
Quality pass across the ten packages, plus typed Nuxt options. An adversarial review swept the whole monorepo for duplicated logic, dead code and readability debt, and every worthwhile finding landed: shared plugin primitives grew, the runtime got slightly smaller, verbaly.d.ts stops rewriting itself when nothing changed, and @verbaly/nuxt options are now typed inside nuxt.config.ts (still with zero dependency on @nuxt/kit). One small breaking change for direct compiler-API consumers: generateDts/writeDts take the plain catalog object instead of a Map.
Highlights
- Typed options for
@verbaly/nuxt. Theverbalykey innuxt.config.tsnow autocompletes and catches typos ("Did you mean 'cookie'?"), without adding any dependency to the module. - A calmer editor in dev.
verbaly.d.tsis only rewritten when its content actually changes, so the TypeScript server stops reloading types on every save. - Slightly smaller runtime. The quality pass trimmed dead work from the core: the tree-shaken runtime is now 3.28 KB min+gzip (was 3.30).
- Cleaner generated types. A param used as both date and text no longer produces a duplicated union in
verbaly.d.ts. - Clearer CLI messages. Every error and hint now reads with plain connectors, and
checkprints the source text right next to the missing key. - Breaking (compiler API only):
generateDts(catalog)andwriteDts(cfg, catalog)now take the catalog object (Record<string, string>) directly instead of aMap. The CLI and all plugins are unaffected.
For the full details of this release, see the repository changelog.
Release v0.20.0
Write your text in .svelte and .vue files too. The write-in-source promise now covers single-file components: t`…` (and Svelte's $t`…`) is extracted, keyed and typed straight from .svelte and .vue files, script blocks and markup alike. Until now the compiler only read .js/.ts/.jsx/.tsx, so Svelte and Vue components had to fall back to hand-written keys. This closes the most visible DX gap on the road to 1.0. No breaking changes.
Highlights
- Natural text in Svelte and Vue components. Write
<h1>{$t`Hello ${name}`}</h1>in a.sveltefile, or{{ t`Hello ${name}` }}in a.vuetemplate, and the compiler does the rest: stable key, typed params, per-locale catalogs. Works in script blocks, markup, template interpolations and attribute bindings. - Svelte's store form is understood. In Svelte components
tis a store, so you use it as$t. The extractor recognizes$t`…`,$t('key')and$t.id('key')`…`everywhere in a.sveltefile. - Live in dev, gated in build. Save a
.svelte/.vuefile with new text and the catalogs plusverbaly.d.tsupdate on the spot (Vite). Missing translations still block the build. - Readable keys and runtime keys work in markup too:
t.id('home.title')`…`extracts under your key, andt('key')calls count as used keys forcheckandextract --prune. verbaly extractscans more by default. The defaultincludenow covers.svelte/.vuefiles and theapp/folder (where Nuxt 4 and the Next.js App Router live), next tosrc/. Your ownincludeconfig still wins.
For the full details of this release, see the repository changelog.
Release v0.19.0
Next.js joins the family — the cycle is complete. The third and last big meta-framework integration: @verbaly/next brings the write→ship cycle to the Next.js App Router — Server Components translate with await getT(), Client Components use the familiar React hooks, and every request negotiates its own locale (cookie → Accept-Language → fallback) with flash-free hydration. Works on Turbopack (the Next 16 default) and webpack. Also fixes a long-standing rich-text limitation: messages can now display literal markup like <html lang> (HTML entities decode in text runs). No breaking changes.
Highlights
- New package
@verbaly/next— Next.js App Router in two steps: wrap your config withwithVerbaly()innext.config.tsand drop<VerbalyProvider>into the root layout. Server Components translate withawait getT(); Client Components keep usinguseT/<Trans>(re-exported from@verbaly/react). - Turbopack and webpack, both first-class — the
t`…`compiler runs as a loader underturbopack.rulesand under webpack; catalogs stay code-split per locale.next devextracts your messages live;next buildblocks on missing translations. - Per-request negotiation, zero locale leaks — cookie →
Accept-Language→ fallback resolved once per request via Reactcache(); concurrent visitors never see each other's language, and hydration renders exactly what the server sent (no flash of untranslated text). useSwitchLocale()— switches the client instantly, persists the cookie the server reads, and re-renders Server Components throughrouter.refresh().- Rich messages can show literal markup —
parseTagsnow decodes<,>and&in text runs, so a message can display "<html lang>" as visible text instead of a broken escape.
For the full details of this release, see the repository changelog.
Release v0.18.0
Nuxt joins the family. The second meta-framework integration: @verbaly/nuxt is a zero-config Nuxt module — one line in nuxt.config wires the Vite plugin, negotiates the locale per request (cookie → Accept-Language → fallback), awaits the catalog before render and keeps <html lang> in sync, so pages arrive translated and hydrate flash-free. Built entirely on the 0.16/0.17 primitives (resolveRequestLocale, createRequestInstance) with zero dependency on nuxt or @nuxt/kit. switchLocale() moves to core so both SSR integrations share it (@verbaly/sveltekit re-exports — nothing breaks). No breaking changes.
Highlights
- New package
@verbaly/nuxt— Nuxt SSR in one line:modules: ['@verbaly/nuxt']. The module wires@verbaly/vite(live extraction +virtual:verbaly), negotiates the visitor's language per request and hydrates the client with the same locale and catalog — no flash of untranslated text, no hydration mismatch, no locale leaking between concurrent users. - Zero framework lock — no
@nuxt/kit, nonuxtpeer: the module is typed structurally (same approach that keeps@verbaly/sveltekitfree of@sveltejs/kit), verified against the realNuxtModuletype and a real Nuxt 4 app. switchLocale()is now a core export — the client-side language switch (catalog first, then locale, then cookie +<html lang>) lives once inverbalyand serves SvelteKit, Nuxt and hand-rolled setups.@verbaly/sveltekitre-exports it: existing imports keep working unchanged.- Client-only apps covered too — with
ssr: falsethe module still resolves the locale in the browser (cookie →navigator.languages→ fallback). - Support Verbaly — GitHub Sponsors is live: github.com/sponsors/AronSoto.
For the full details of this release, see the repository changelog.
Release v0.17.0
Robust inside, for real. The hardening release: a full-code audit of all eight packages fixed every sharp edge it found — a catalog string can no longer inject a javascript: URL through attribute translation, a bad currency code or date style degrades with a warning instead of crashing t(), the CLI rejects flags that belong to another command instead of silently ignoring them, and the SSR story gets its missing piece: createRequestInstance() in virtual:verbaly gives you a per-request instance with the catalog already loaded, in one call. One breaking change: inspect() renames its locale field to from (see Changed).
Highlights
- Attribute translation is now XSS-safe end to end —
data-verbaly-attrruns URL attributes (href,src,action…) through the samesafeHrefguard as rich links and blocksstyle/srcdocentirely. A malicious or compromised catalog can't inject scripts. t()never crashes on bad format arguments — an invalid currency code ({v:currency/US}), date or time style now warns once and renders the plain value, matching howunitandrelativealready behaved.createRequestInstance(locale)invirtual:verbaly— the SSR per-request pattern in one call: fresh instance, catalog awaited, no flash. SvelteKit today, Nuxt next.- The CLI fails loudly on misplaced flags —
verbaly translate --locale esused to silently translate all locales; now it errors with "did you mean--locales?". Plus--dry-runforextract --prune. normalizeLink()andresolveRequestLocale()in core — the link sanitizer and the cookie→Accept-Languagenegotiation now live once in core; every adapter and SSR integration shares them.- Breaking:
instance.inspect(key)now returns{ from, source }instead of{ locale, source }— one name for one concept across the observability API (ResolveInfo.from).
For the full details of this release, see the repository changelog.
Release v0.16.0
Server-side rendering, for real. The first meta-framework integration: @verbaly/sveltekit renders every page in the visitor's language on the server — negotiated per request from their cookie or Accept-Language header — and the client hydrates with the same locale and the same catalog, so there's no flash of untranslated text and no hydration mismatch. Built on two framework-agnostic pieces (core's negotiateLocale and the new createInstance factory in virtual:verbaly) that Nuxt/Next integrations will reuse. No breaking changes, no API removals, no new dependencies.
Highlights
- New package
@verbaly/sveltekit— SvelteKit SSR in three wires:verbalyHandle()in your server hooks, one+layoutload, and%verbaly.lang%inapp.html. Pages arrive translated; hydration is flash-free. - Per-request locale negotiation — cookie →
Accept-Language(with q-values andes-PE→esnarrowing) → fallback. Concurrent requests never leak each other's language. switchLocale()— client-side language switch that loads the catalog first, then persists the choice in the cookie the server reads, so the next SSR request already matches.negotiateLocale()in core — theAccept-Languagematcher is a public, framework-agnostic export; use it in any Node server today.createInstance()invirtual:verbaly— build a fresh, request-scoped instance sharing your compiled catalogs and loaders; the old singleton stays for SPAs. Also new:localesandsourceLocaleexports.
For the full details of this release, see the repository changelog.
Release v0.15.0
Your translators can work now. The write→ship cycle opens up to humans: verbaly export writes translator-ready XLIFF 2.0 or CSV files (source text + current translation per entry) and verbaly import fills the catalogs back — validating every entry the way translate does, so a translator's typo in a {param} or an <em> tag never reaches your UI. Flat JSON stays the native format (most TMS platforms ingest it directly); export/import is the round-trip for everything else. No breaking changes, no API removals, no new dependencies.
Highlights
verbaly export— one translator-ready file per locale, XLIFF 2.0 (TMS standard) or CSV (spreadsheets), with the source text next to each translation.--missingexports only what's untranslated.verbaly import— reads translated XLIFF 2.0/1.2 or CSV back into your catalogs. Every entry is structure-validated: translations that drop a{param}, a plural variant or a tag are rejected and reported instead of breaking your UI.- Existing translations are kept unless you pass
--overwrite;--dry-runpreviews the whole import. - No TMS needed for the simple case: catalogs are plain flat JSON — Crowdin, Lokalise, Phrase and friends ingest them natively.
For the full details of this release, see the repository changelog.
Release v0.14.5
[0.14.5] — 2026-07-10
Revision of 0.14.0 — the review release. A hard audit of what 0.14.0 shipped found three real defects, all fixed here: the devtools overlay froze the page (its own MutationObserver re-triggered on its own panel writes — infinite microtask loop), '' catalog entries were treated inconsistently across the toolchain (the runtime rendered them as blank holes while extract/check/render call them untranslated — and render's '' handling silently did nothing for nested catalogs), and the new hreflang alternates were being voided by cross-locale canonicals (mirrored pages kept the source page's <link rel="canonical">, which search engines prefer over hreflang). Ready to publish. One behavior change ('' now falls back — see Changed), no API removals, no new dependencies.
Fixed
- Devtools freeze (
verbaly/devtools):attachDevtools's MutationObserver re-fired on its ownpanel.innerHTML/tooltip writes — in a real browser the microtask queue never drained and the tab hung the moment the overlay mounted. The observer now ignores mutations originating inside its own panel/tip. Regression test drains the observer microtasks and asserts the scan count settles (the 0.14.0 tests were synchronous and couldn't see the loop). renderHtml''handling with nested catalogs (@verbaly/compiler): the "''counts as untranslated → fall back" cleanup iterated only top-level entries, so with nested catalogs (verbaly-web's shape) it silently kept''and the static output diverged from the runtime. The cleanup is deleted — the core lookup now implements the semantics (see Changed), nested or flat, and render output == post-hydration output is guaranteed again.RenderHtmlOptions.catalogsis officially widened to accept nested trees (Catalogs | Record<string, MessageTree>).- Cross-locale canonical voided hreflang (
@verbaly/compiler): mirrored locale pages carried the source page's<link rel="canonical">(andog:url) — a canonical pointing at another locale tells search engines to fold the page into it, neutralizing the hreflang set 0.14.0 introduced.renderHtmlnow rewritesrel="canonical"hrefs andog:urlcontent to the locale's own URL when they match the source URL (alternates present). A canonical deliberately pointing elsewhere is left untouched; source-locale pass and re-runs are no-ops (idempotent).
Changed
''= untranslated, now everywhere (verbalycore — behavior change, call it out): the lookup chain skips empty-string entries and keeps falling back (es: ''→ensource text;''in every locale of the chain → miss: warn + key). Why:extractscaffolds new keys as''andcheckcounts''as missing — the runtime was the one place that treated''as a valid message and rendered a blank hole in the UI. Standard i18n behavior (source text beats invisible content) and the render/runtime FOUC divergence disappears. If you relied on''to intentionally render nothing, use a single space' 'or restructure the key (verbaly-web does exactly this — see Docs impact).has()/inspect()follow the same rule.packages/core/src/types.tsis a text file again (internal, no API change): the type-level message parser used two raw NUL bytes as its escape sentinel, which made git treat the file as binary since the initial commit (no reviewable diffs — how the audit had to find this the hard way). The raw bytes are now the\u0000escape sequence — identical type semantics, verified by typecheck + the full suite.
Notes
- 359 tests (core 146 · compiler 144 · svelte 22 · vue 12 · react 11 · unplugin 9 · vite 15) — was 353; +3 core (
''fallback ×2, devtools loop regression), +3 compiler (nested'', canonical/og:url rewrite ×2). - Bench re-run (ritual): lookup 31.2×, interpolation 10.6×, plural 4.3×, currency 4.5× vs i18next 26 — in family with 0.14.0 (27.9×/11.0×/5.0×/5.1×; machine variance). The
''check (if (msg)vs!== undefined) costs nothing on the hot path. - Bundle check: tree-shaken
createVerbalyruntime 3.28 KB min+gzip (flat), full core surface 4.61 KB (was 4.63),verbaly/devtools1.62 KB (was 1.58 — the observer guard). - publint All good ×7 + arethetypeswrong core/react/vue node16 CJS/ESM 🟢 (known-OK:
verbaly/devtoolsnode10 view, predates subpath exports). Tarballs verified (core shipsdist/devtools.*;@verbaly/viteworkspace:*→0.14.5, peerverbaly→^0.14.5). - API surface: no additions, no removals.
RenderHtmlOptions.catalogstype widened (accepts what already worked at runtime). No new dependencies.