Release v0.14.0
[0.14.0] — 2026-07-10
Ships translated, and now you can see why. Three fronts, one theme — closing dead points on the road to 1.0: the SSG renderer grows real multi-locale SEO (hreflang alternates + i18n sitemap + stale cleanup) and is dogfooded into verbaly-web's own build (the FOUC is dead for real, measured on the live site); the runtime gains observability — an onResolve hook and an opt-in verbaly/devtools inspector that answers "what key is this text?" in the browser; and server-side use in Node is hardened and locked with tests. Ready to publish. One behavior change (locale auto-detection now requires a DOM — see Changed), no API removals.
Added
- hreflang + i18n SEO in
verbaly render(@verbaly/compiler):renderSitenow emits reciprocal<link rel="alternate" hreflang>(incl.x-default) into every page's<head>for the whole locale set, and optionally a locale-aware sitemap (sitemap-i18n.xml, one<url>per locale with<xhtml:link>alternates). URLs derive from the built path (directory-style:index.html→/). New configrender.{baseUrl, hreflang, sitemap, clean, site, attribute}and matchingRenderSiteOptions;hreflangdefaults on whenbaseUrlis set. Injection is idempotent (marker-delimited, re-runs are byte-stable). New exported typeAlternate. - Stale cleanup (
@verbaly/compiler):render --clean(orrender.clean) removes existingdist/<locale>/dirs before mirroring, so pages deleted from the source no longer linger in locale mirrors. - Wider
renderCLI:--attribute,--base-url,--sitemap,--clean(previously only--site/--locales;attribute/baseUrlwere programmatic-only). - Runtime observability —
onResolvehook (verbalycore):createVerbaly({ onResolve })fires once pert(key)call with{ key, locale, value, status, from? }wherestatusis'hit' | 'fallback' | 'miss'andfromis the locale that actually provided the message. Zero cost when unset (optional-chained; the hot path is unchanged — bench flat). New exported typesResolveInfo,ResolveStatus. instance.inspect(key)(verbalycore): returns{ locale, source }(the resolved locale + raw source message) for a key, orundefinedif missing — the read-side primitive devtools uses without going throught().verbaly/devtools— opt-in in-browser inspector (new subpath export, core's first):attachDevtools(instance, { root?, attribute?, hotkey?, catalogDir? })mounts a vanilla-DOM overlay — a floating panel with ok/fallback/missing counts and a missing-keys list that names the exact catalog file to fix (doctor's actionable-errors ethos, now at runtime), plus hold-Alt+hover to see any bound element's key, resolve status, source-locale and text. Zero deps,sideEffects: false, tree-shaken out of the core runtime — a separate 1.58 KB gzip chunk paid only when imported. Exported typeDevtoolsOptions.
Changed
- Locale auto-detection now requires a DOM (
verbalycore — behavior change, call it out):detectLocale(used bycreateVerbalywith nolocale) andresolveLocale's navigator step now gate ontypeof document !== 'undefined', not justnavigator. Why: Node 21+ exposes a globalnavigatorwhoselanguageis the server's OS locale — under the old guard,createVerbaly()on the server silently adopted the machine's locale (a real SSR footgun found while hardening server-side use). Now non-DOM contexts (Node/SSR) deterministically fall back to'en'/ the configuredfallback; browsers are unaffected (both globals present). Server code should passlocaleexplicitly per request (documented in the new server-side guide). - Comparison table re-sealed (pillar 5, 2026-07-10): competitor versions re-verified — i18next 26.3.6, react-i18next 17.0.9, Lingui 6.5.0, typesafe-i18n 5.27.1 (still one release since 2023 — sporadic), Paraglide 2.21.0, next-intl 4.13.1. No figures changed since 0.13.0's seal (one day prior).
Notes
- 353 tests (core 143 · compiler 141 · svelte 22 · vue 12 · react 11 · unplugin 9 · vite 15) — was 335; +8 core (observability + devtools + server-side), +5 compiler (hreflang/sitemap/clean).
- Bench re-run (ritual): lookup 27.9×, interpolation 11.0×, plural 5.0×, currency 5.1× vs i18next 26 — flat vs 0.13.0 (the
onResolveguard adds nothing to the hot path when unset). - Bundle check: tree-shaken
createVerbalyruntime 3.28 KB min+gzip (was 3.26), full core surface 4.63 KB (flat),verbaly/devtools1.58 KB as its own chunk (not in the runtime path). - publint All good ×7 + arethetypeswrong no problems (core/react/vue node16 CJS/ESM 🟢). Known-OK:
verbaly/devtoolsfails attw's node10 view — node10 predates subpath exports (same category as@verbaly/svelte/Trans.svelte); it resolves 🟢 under node16/bundler. Tarballs verified (core shipsdist/devtools.*;@verbaly/viteworkspace:*→0.14.0, peerverbaly→^0.14.0). - New public API — core:
VerbalyOptions.onResolve,ResolveInfo,ResolveStatus,Verbaly.inspect; subpathverbaly/devtools:attachDevtools,DevtoolsOptions. compiler:RenderConfig.{site,attribute,baseUrl,hreflang,sitemap,clean},RenderSiteOptions.{baseUrl,hreflang,sitemap,clean},Alternate. All additive. No new dependencies anywhere. - Dogfood (dead point #3, closed):
verbaly renderrun against verbaly-web's realdist— 12 pages × 3 locales, zero missing keys,dist/es/…/dist/pt/…ship pre-translated with<html lang>, reciprocal hreflang and a 36-URLsitemap-i18n.xml;data-verbalyattributes remain so client switching still works; re-run idempotent. This is what the web build wires in post-publish (Docs impact).