Skip to content

Releases: AronSoto/verbaly

Release v0.23.0

Choose a tag to compare

@github-actions github-actions released this 15 Jul 01:37
95eff24

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 like The <em>build</em> gate shows a real <em> element, matching Svelte and the DOM interpreter. Before, React and Vue flattened it to plain text. If you relied on that, pass richTags={[]}.
  • Breaking: @verbaly/svelte requires Svelte 5. Svelte 4 support is retired while the project is still 0.x. In exchange, the Svelte <Trans> finally gets the components prop: map a tag in your message to your own component.
  • verbaly status tells you how much is left to translate. One line per language ("es: 45/48 translated (94%)"), no CI noise. verbaly check stays the build gate.
  • verbaly extract --watch keeps 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

Choose a tag to compare

@github-actions github-actions released this 14 Jul 21:54
f15810b

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-xml writes Android resource folders you can drop into res/, and --format ios-strings writes .lproj folders 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

Choose a tag to compare

@github-actions github-actions released this 14 Jul 19:34
852dfda

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. The verbaly key in nuxt.config.ts now autocompletes and catches typos ("Did you mean 'cookie'?"), without adding any dependency to the module.
  • A calmer editor in dev. verbaly.d.ts is 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 check prints the source text right next to the missing key.
  • Breaking (compiler API only): generateDts(catalog) and writeDts(cfg, catalog) now take the catalog object (Record<string, string>) directly instead of a Map. The CLI and all plugins are unaffected.

For the full details of this release, see the repository changelog.

Release v0.20.0

Choose a tag to compare

@github-actions github-actions released this 14 Jul 04:29
d0410d6

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 .svelte file, or {{ t`Hello ${name}` }} in a .vue template, 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 t is a store, so you use it as $t. The extractor recognizes $t`…`, $t('key') and $t.id('key')`…` everywhere in a .svelte file.
  • Live in dev, gated in build. Save a .svelte/.vue file with new text and the catalogs plus verbaly.d.ts update 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, and t('key') calls count as used keys for check and extract --prune.
  • verbaly extract scans more by default. The default include now covers .svelte/.vue files and the app/ folder (where Nuxt 4 and the Next.js App Router live), next to src/. Your own include config still wins.

For the full details of this release, see the repository changelog.

Release v0.19.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 20:42
10b6a2b

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 with withVerbaly() in next.config.ts and drop <VerbalyProvider> into the root layout. Server Components translate with await getT(); Client Components keep using useT/<Trans> (re-exported from @verbaly/react).
  • Turbopack and webpack, both first-class — the t`…` compiler runs as a loader under turbopack.rules and under webpack; catalogs stay code-split per locale. next dev extracts your messages live; next build blocks on missing translations.
  • Per-request negotiation, zero locale leaks — cookie → Accept-Language → fallback resolved once per request via React cache(); 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 through router.refresh().
  • Rich messages can show literal markupparseTags now decodes &lt;, &gt; and &amp; 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

Choose a tag to compare

@github-actions github-actions released this 13 Jul 07:43
99b6541

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, no nuxt peer: the module is typed structurally (same approach that keeps @verbaly/sveltekit free of @sveltejs/kit), verified against the real NuxtModule type 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 in verbaly and serves SvelteKit, Nuxt and hand-rolled setups. @verbaly/sveltekit re-exports it: existing imports keep working unchanged.
  • Client-only apps covered too — with ssr: false the 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

Choose a tag to compare

@github-actions github-actions released this 11 Jul 19:35
59a4151

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 enddata-verbaly-attr runs URL attributes (href, src, action…) through the same safeHref guard as rich links and blocks style/srcdoc entirely. 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 how unit and relative already behaved.
  • createRequestInstance(locale) in virtual: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 flagsverbaly translate --locale es used to silently translate all locales; now it errors with "did you mean --locales?". Plus --dry-run for extract --prune.
  • normalizeLink() and resolveRequestLocale() in core — the link sanitizer and the cookie→Accept-Language negotiation 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

Choose a tag to compare

@github-actions github-actions released this 11 Jul 17:40
b1f000f

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 +layout load, and %verbaly.lang% in app.html. Pages arrive translated; hydration is flash-free.
  • Per-request locale negotiation — cookie → Accept-Language (with q-values and es-PEes narrowing) → 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 — the Accept-Language matcher is a public, framework-agnostic export; use it in any Node server today.
  • createInstance() in virtual:verbaly — build a fresh, request-scoped instance sharing your compiled catalogs and loaders; the old singleton stays for SPAs. Also new: locales and sourceLocale exports.

For the full details of this release, see the repository changelog.

Release v0.15.0

Choose a tag to compare

@github-actions github-actions released this 10 Jul 23:37
ecbba0b

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. --missing exports 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-run previews 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

Choose a tag to compare

@github-actions github-actions released this 10 Jul 13:03
7ff3f97

[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 own panel.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.catalogs is 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"> (and og:url) — a canonical pointing at another locale tells search engines to fold the page into it, neutralizing the hreflang set 0.14.0 introduced. renderHtml now rewrites rel="canonical" hrefs and og:url content 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 (verbaly core — behavior change, call it out): the lookup chain skips empty-string entries and keeps falling back (es: ''en source text; '' in every locale of the chain → miss: warn + key). Why: extract scaffolds new keys as '' and check counts '' 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.ts is 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 \u0000 escape 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 createVerbaly runtime 3.28 KB min+gzip (flat), full core surface 4.61 KB (was 4.63), verbaly/devtools 1.62 KB (was 1.58 — the observer guard).
  • publint All good ×7 + arethetypeswrong core/react/vue node16 CJS/ESM 🟢 (known-OK: verbaly/devtools node10 view, predates subpath exports). Tarballs verified (core ships dist/devtools.*; @verbaly/vite workspace:*0.14.5, peer verbaly^0.14.5).
  • API surface: no additions, no removals. RenderHtmlOptions.catalogs type widened (accepts what already worked at runtime). No new dependencies.