Skip to content

Changelog

0.72.0

Latest release

Installing with pnpm works the first time

Verbaly stops bringing esbuild into your project, which made pnpm add fail on a build script nobody asked for. verbaly.lock merges itself after two branches translated in parallel, <br> works however you write it, and a visitor gets the language they read even when your list has only a close variant of it. Breaking: Verbaly now needs Node 22.12 or newer.

Highlights
  • Installing with pnpm works the first time. Verbaly no longer brings esbuild with it, so pnpm add @verbaly/vite stops failing on a build script you never asked for, and the install is about 12 MB lighter. A verbaly.config.ts keeps working, and so do its imports written without an extension.
  • A merge no longer breaks verbaly.lock. When two branches translated in parallel, Verbaly reads both sides of the conflict, keeps every draft either side had, and the next extract writes the file whole. Add it to the commit that resolves the merge.
  • <br> works however you write it. <br>, <br/> and <br /> are all line breaks in rich messages, in the files you send to translators and in verbaly check. Before, <br> vanished and <br /> showed up as text.
  • Visitors get the language they read. A browser or a request in zh-HK now gets your Traditional Chinese, and pt or pt-PT gets your pt-BR, instead of falling back to English.
  • Faster builds. The Vite plugin and @verbaly/unplugin read each file once instead of twice.
  • Breaking: Node 22.12 or newer. Node 20 reached its end of life in April 2026, and the frameworks Verbaly supports already ask for Node 22.
Full release notesEvery bullet, per package, in the repo changelog.

0.71.0

Release

The state of your translations gets a lockfile

What Verbaly remembers about your translations (which source text each one was made for, and which ones are drafts) leaves your catalog folder for verbaly.lock, next to your config, and moves there on its own. A key written with two different texts now fails verbaly check, because one of its places shows the other one's text. Breaking: a project with such a key sees check and doctor fail, and failOnMissing: true stops the build on it.

Highlights
  • Your catalog folder holds only your catalogs. What Verbaly remembers about your translations now lives in verbaly.lock, next to verbaly.config.ts. Treat it like a package lockfile: Verbaly writes it, you commit it, and nobody edits it.
  • The old file moves on its own. The next extract or dev server run moves .verbaly-state.json into verbaly.lock and says so once. There is nothing to configure.
  • Breaking: one key with two texts fails verbaly check. When t.id('title') is written twice with different words, one place shows the other's text. Every build now lists it with the broken translations, and check stops on it, naming both places.
  • Small things: the first example in the README shows the key the compiler really writes, and the repository has an AGENTS.md for contributors and their coding agents.
Full release notesEvery bullet, per package, in the repo changelog.

0.70.0

Release

A text never stops your build

A missing or broken translation, or a key no catalog has, is now listed at the end of every build, and the build finishes: verbaly check in your CI is what stops a release. To test the way in, we moved Memos, an open source app with 46 languages, from react-i18next to Verbaly and fixed everything it ran into. Breaking: the build no longer stops on its own, and a key written in backticks that no catalog has now fails check.

Highlights
  • A text never stops your build. Every build lists what is missing or broken, broken translations first, and finishes. npx verbaly check is the gate, in your CI. Breaking: to keep a build that fails, set failOnMissing: true in the plugin options.
  • Tested on a real migration. We moved Memos (46 languages, 879 keys) from react-i18next to Verbaly: 20 files changed by hand, the same build and the same tests passing, and about 11 KB less for every visitor. init now keeps the catalogs you already have, and migrate leaves text like {timestamp} as text and never converts a language twice.
  • Faster builds with Vite. The Vite plugin was slowing large builds down: Memos took 37 seconds to build, and now takes 17 (13 without Verbaly).
  • No provider needed in the browser. In a single-page app and in component tests, useT() works without <VerbalyProvider>, the Vue plugin or provideVerbaly, as it does in react-i18next.
  • Fewer type errors when you migrate. A key kept in a variable typed as a set of keys passes with its params, and the generated types no longer trip Biome or Prettier.
  • Small things: doctor says "setup works, with 1 warning to read" instead of "healthy" after a warning, and the MCP server rereads a CommonJS config. Breaking: a key written in backticks, or as {'…'} in JSX, is checked like a quoted one.
Full release notesEvery bullet, per package, in the repo changelog.

0.69.1

Release

Keys from data compile again

0.69.0 typed every t, and with it turned away any key held in a plain string, so code that reads its keys from data, and every move from another library, started full of errors. Now a key you write by hand is still checked against your catalog, with its params, and a key that comes from data compiles as it is. Nothing to change in your code.

Highlights
  • Keys that come from data compile again. t(item.key) and t(`status.${s}`) work in every t with no cast, while a key you write by hand is still checked against your catalog, params included, and your editor still completes it.
  • The same rule for an instance you build yourself. On createVerbaly({ messages }), a typo now names the key it got wrong, and a key from data passes there too.
Full release notesEvery bullet, per package, in the repo changelog.

0.69.0

Release

Every t knows your keys

Every t in your project now checks its keys against your catalog, and the generated types land where your framework already reads them. This release fixes what an audit of 0.68.0 found in the parts you meet first: the types, the Next.js server, the install lines and the language switch. Next.js also gets translations without a request and rich text in Server Components. Breaking, in types only: a key held in a plain string no longer compiles.

Highlights
  • Typed keys in every t. useT() in React, Vue and Svelte, getT() in Next.js and the t of any instance check each key and its params against your catalog. A typo fails tsc, and the error names the key. Breaking: a key held in a plain string no longer compiles: declare it with defineKeys, or mark it as VerbalyKey.
  • The types are written where your framework looks. src/verbaly.d.ts in Vite and SvelteKit apps, .verbaly/types.d.ts in Next.js (Verbaly adds it to your tsconfig.json), and the folders Astro and Nuxt keep for this. The CLI and the dev server write the same file, the old one at the root goes away on its own, and npx verbaly typegen writes them in CI.
  • Next.js: translations without a request, and rich text on the server. getT({ locale: 'es' }) works in an OG image, a sitemap or an email and keeps the route static, and <Trans> renders in Server Components from @verbaly/next/server.
  • Text Verbaly cannot see is no longer silent. When a build or a dev server starts, Verbaly names each file outside your include that writes t`…`, like components/ in a Next.js app without src/, with the pattern to add. doctor lists them too.
  • Switching languages keeps the rest of the address. switchLocale keeps the ?q= and the #anchor.
  • Small things that used to bite: init and doctor print the README's install line in your own package manager, a catalog with Windows line endings is no longer rewritten when nothing changed, and the MCP server sees an edited verbaly.config.mjs without a restart.
Full release notesEvery bullet, per package, in the repo changelog.

0.68.0

Release

The promises hold everywhere

0.67.0 promised that nothing changes behind your back. An audit found the places where that still failed, and this release closes them at the root. An outdated translation is caught however it was written, every command reads the text that actually ships, one key always shows the same words, nothing in the review state is lost in silence, and the dev servers say what matters once. Nothing new fails your build.

Highlights
  • An outdated translation is caught however it was written. translate, import, Studio and an agent's verbaly_write_drafts record which source text a translation was written for the moment they write it, and import takes it from the translator's file. A source that changes before the next extract, or a file made for an older text, is flagged instead of quietly accepted.
  • Every command reads the text that ships. check --outdated, status, review, translate, export, import, Studio and the MCP server read the text in your code wherever the code owns it, so CI no longer passes a translation the build ships next to a newer text.
  • One key, the same words everywhere. When one key has two texts, the one written first by file path wins, in extract, every build and both dev servers. It used to depend on the order files were read, which changed from run to run.
  • Nothing in the review state is lost in silence. extract --locales es leaves the other languages alone, a teammate's old drafts file is merged instead of dropped, and a state file nobody can read stops the commands that write drafts before they write anything.
  • --prune keeps the translations a t under another name reads, in .vue, .svelte and .astro files too, and the warning about it now appears there.
  • The dev servers say what matters, once. Vite dev never writes an old text back over a checkout, a catalog saved half-typed is a one-line pause in both dev servers, and a warning comes back if its problem does.
Full release notesEvery bullet, per package, in the repo changelog.

0.67.0

Release

Nothing changes behind your back

Verbaly stops hiding things from you. A translation written for a text that has since changed is flagged, two texts under one key are reported with both places, and dev and build always show the same words. In Next.js, next typegen no longer stops on missing translations, and agents can save their own translations as drafts. Nothing new fails your build.

Highlights
  • Outdated translations are flagged. Change a source text and the translations written for the old one show up in status, check and doctor until someone updates them or keeps them with verbaly review --approve. check --outdated can make that fail your CI.
  • Dev and build always show the same text. A text written in your code wins over a hand edit of it in the catalog, in dev and in the build alike, and Verbaly tells you where that text lives instead of replacing your edit without a word.
  • Two texts under one key are reported, with both places. The second one used to disappear quietly.
  • next typegen works with missing translations. The Next.js gate now runs once the build has compiled, not when the config loads, and next experimental-analyze no longer needs a next dev first.
  • extract --prune cleans up drafts too, and agents save their own translations as drafts with the new MCP tool verbaly_write_drafts, so nobody edits the drafts file by hand. That file is now .verbaly-state.json, and the old one moves over on its own.
  • In React, useT() gives a new t when the language changes, so useMemo(…, [t]) follows it.
Full release notesEvery bullet, per package, in the repo changelog.

0.66.0

Release

No translation lost in silence

A file with a typo can no longer let extract --prune delete translations, keys named like toString or constructor no longer crash the gate, and machine translation works with every current Claude model, with Sonnet 5.5 as the default. Next.js stops downloading a catalog twice, and your build now says in one line when check has warnings. Nothing new fails your build.

Highlights
  • extract --prune never deletes a translation because a file has a typo. While any source file does not parse, prune waits and names the file. Before, every key only that file used lost its translation in every language.
  • Keys named like toString or constructor can no longer crash check, doctor or your build. A translation carrying one used to stop the gate with an error about matchAll. Now it is a key like any other.
  • Machine translation uses Claude Sonnet 5.5 by default, and every current Claude model works. Choosing Sonnet 5.5 or Opus 5.5 with --model used to fail every batch. A batch the model declines now goes to a fallback model instead of coming back untranslated.
  • Next.js downloads each catalog once. The page no longer fetches the catalog the server already sent, and the build only rewrites the files verbaly extract reads.
  • Your build now says when check has warnings, in one line, and still passes.
  • Arabic or Hebrew written as ar_EG or he_IL reads right to left, and a full browser storage no longer blocks a language switch.
Full release notesEvery bullet, per package, in the repo changelog.

0.65.0

Release

Safer work, sturdier pages

Machine translation no longer overwrites a message you fixed while it ran, names that JavaScript objects already carry can no longer break a page, and a language written like pt_BR stops crashing numbers and dates. Three problems that used to go unnoticed now come with a warning, and none of them fails your build. If you use ICU messages with TypeScript, their parameters are now typed, so a call that left them out shows up as a type error.

Highlights
  • Machine translation never overwrites your work. If you fix a message while verbaly translate, Studio or an agent is translating, your text stays, and the run tells you how many it left alone.
  • Names that JavaScript objects already have can no longer break a page. A language, a format, a rich tag or a catalog group called constructor, toString or __proto__ could crash a render or turn into an empty link. Now it is just a name like any other.
  • A language written like pt_BR no longer crashes numbers, dates and plurals. They read it as pt-BR and say so once. doctor points it out, init writes it correctly, and import fills your pt-BR from a gettext pt_BR file.
  • Three silent problems now come with a warning, and none of them fails your build: a key that only one translation has (Studio shows it on its overview too), a language name the browser cannot read, and a Next.js app whose URLs carry the language while its config says they do not.
  • Next.js language switching follows your URLs. With routing: 'prefix-all' in your config, useSwitchLocale() goes to the same page in the new language, with no options to pass.
  • Watching for changes on Linux got much lighter. next dev and verbaly extract --watch stop walking node_modules, which could use up the system's limit on watched files before reaching your code.
Full release notesEvery bullet, per package, in the repo changelog.

0.64.0

Release

A bad parameter stops crashing the render

A message parameter that JavaScript refuses to turn into text used to make Verbaly throw, and the path meant to absorb it threw as well. Now the message renders without it and Verbaly says so once.

Highlights
  • A bad parameter can no longer crash a render. If a value cannot be turned into text, the message renders without it and Verbaly says so once, the same way it handles every other bad value.
  • JSON hands you this shape for free. A payload with a field called toString holding an object is valid JSON, so a CMS or a catalog fetched at runtime could carry one all the way into your page.
  • Nothing changes for a project that was fine. The check only runs where a parameter was already unusable.
Full release notesEvery bullet, per package, in the repo changelog.

0.63.0

Release

A menu on every row

Every message row in Studio now has a menu. Copy the key, copy the source text, edit in place, see which files use a message, and see the commit that last changed it, which is the one thing the panel could never answer before.

Highlights
  • Every message row has a menu. Copy its key, copy the source text, or start editing, without leaving the table.
  • See the commit that last changed a message. Studio reads the history of your catalogs and names the commit, who wrote it and what it said. Clicking copies the short sha, so a git show in your terminal is one paste away.
  • See which files use a message, straight from the row. When more than one does, it says how many, and it opens into the full list.
  • When there is no answer, the menu says which kind of no it is. A message you have not committed yet, one older than the history Studio read, and a project without git are three different things, and they read as three different things.
  • Nothing in the menu is a button that does nothing. The AI entry the design calls for is not there, because the assistant it would call does not exist yet.
Full release notesEvery bullet, per package, in the repo changelog.

0.62.0

Release

The language you just translated

Tick one language and Studio stops comparing. It tells you the work already shipped and that nothing is broken, then puts the handful worth reading at the top with the reason written next to each one.

Highlights
  • Tick one language and you get that language. A bar over the table says how many messages it holds, that they are already on your site, and that the check your build runs passes. The reassurance comes first because it is the true part.
  • Every row worth a look says why, in words, right next to it. Not that a link changed, but which link. Not that the numbers moved, but that the source says 20 and this one says 22.
  • Mark a whole language as read in one click, and undo it. There is no confirmation dialog, because you keep editing in the row and the undo waits in the rail.
  • A language in the rail shows everything that is pending, not just the worst of it. It used to read one breaks your site for a language that also had one missing, so the second one was invisible.
Full release notesEvery bullet, per package, in the repo changelog.

0.61.0

Release

Three checks that were not as tight as they looked

A review of Verbaly's own safety net found three things it promised and did not fully do: a URL check a browser walks straight past, a message lookup that could answer with something that is not a message, and number formats that print NaN without a word. All three are closed.

Highlights
  • A link that comes from your catalog can no longer carry a javascript: URL into your page. The check read the text as written, and a browser deletes tabs and line breaks from a URL before it reads one, so a link could slip past. It is now checked the way a browser reads it.
  • A message named toString or constructor is missing, not empty. Those names belong to every JavaScript object, so the lookup was finding the object's own instead of yours, and you got an empty string with nothing said about it.
  • A number format that is handed something that is not a number says so. A price with a text value used to render NaN, and a price with no value at all rendered a confident 0.00. Both now show the plain value and name the message they came from.
  • Nothing changes in how you write messages. All of this only runs when something is already wrong, so a project that was fine stays exactly as it was.
Full release notesEvery bullet, per package, in the repo changelog.

0.60.0

Release

A catalog you can move around in

Search only looked at the languages you had ticked, so a Spanish word with Spanish unticked found nothing and never said why. And the groups your keys already declare were nowhere on screen. Both are fixed, and they work together.

Highlights
  • Search finds a word in any language, not only the ones you ticked. Before, the box filtered the table, so it could only see what the table was showing. It reads your whole catalog now.
  • Every result says which language it matched in. That is the thing a filtered table cannot tell you: the same word usually only exists in one of your languages.
  • Command K opens it, the arrows move, enter takes you to the message. It scrolls to the row wherever it is, even far past what the screen had loaded.
  • Your keys already had groups, and now the panel shows them. Pick one in the rail and the table, its counts and the commands all narrow to it. A search result says which group it lives in.
Full release notesEvery bullet, per package, in the repo changelog.

0.59.0

Release

Studio opens on what needs you

It used to open on the table, with no idea which of your messages were waiting or whether your project was healthy. Now the first screen tells you, and the last thing it shows is real: the commits that touched your catalogs.

Highlights
  • Studio opens on an overview instead of a table. What is waiting for you, one line each with its own button, and whether the project is healthy. Clicking one takes you to exactly those messages.
  • The last changes to your catalogs, from git. Real commits over your catalog directory, with who and when. A project with no git simply does not show the card, rather than showing an empty one.
  • A key your code calls that no catalog defines is finally visible. It already failed your build, and until now the only place you could see it was the command line.
Full release notesEvery bullet, per package, in the repo changelog.

0.58.0

Release

Your catalog keeps the order you wrote it in

Two places where Verbaly quietly did something other than what it said. It rewrote your catalog in alphabetical order without mentioning it, and it told you a translation was checked without ever looking inside an ICU message. Both are fixed at the source.

Highlights
  • Your catalog keeps the order you wrote it in. Anything that wrote a catalog re-sorted the whole file alphabetically, so the first save turned a hand-ordered file into a diff of every line. On this site's own catalog that was 1817 of 1823 lines, and it is now zero.
  • The checks finally look inside an ICU message. A translation that flattened an ICU plural into plain text, or dropped a parameter from it, came back clean. It is caught now, in verbaly check, in what a machine translation may return, and in what an imported file may contain.
  • Studio's live preview says the same thing the build says, ICU included. It used to go green on a message the server would then refuse.
Full release notesEvery bullet, per package, in the repo changelog.

0.57.0

Release

Studio is a command again

Three releases ago Studio was one thing you run. Then it grew three entry points, an optional peer and a chapter of its README about rendering the panel by hand, and all of that existed so one page on this site could import it. It is gone, and what you install is a command again.

Highlights
  • You install @verbaly/studio and there is nothing to import. One command, no peers to add, and no .svelte for your bundler to compile. The package has one entry again.
  • Breaking: three entry points are removed, and svelte is no longer a peer dependency. They shipped in 0.56.0 and had one consumer in the world, the demo page on this site.
  • Nothing about running Studio changed. npx verbaly-studio opens the same screen, over the same files, with the same commands.
Full release notesEvery bullet, per package, in the repo changelog.

0.56.1

Release

The screen when there is nothing on it

Studio opened on a project that was not set up yet and told you to tick a language that was not there. Every empty state and every disabled button now names what is actually true, which is the part of standing it up that a person actually reads.

Highlights
  • The empty table stopped asking for the impossible. With one language in your config there is nothing to tick, so it says you have one language and where to add the next one.
  • Four empty states, one sentence each: no catalogs at all, one language, none ticked, and a filter that matched nothing. Each names a fact rather than a button.
  • A command that cannot run is off, and says why. With source scanning turned off in your config, Find new text no longer waits for you to click it and get an error back.
Full release notesEvery bullet, per package, in the repo changelog.

0.56.0

Release

The panel stopped fetching its own data

Studio is handed the thing it talks to instead of reaching for it, which is what turns the panel from a page the command serves into something you can put in your own. And it finally opens dark on a dark machine.

Highlights
  • The panel opens in your machine's theme. It had a dark palette and no way to reach it, so npx verbaly-studio opened light on a dark desktop.
  • Nothing in the panel knows about the network any more. It is handed what it should call, so the same components can run over a real server or over anything else that answers the same way.
  • The panel can be rendered in your own page, if your page compiles Svelte 5: the real components, over anything that answers the way the server does.
  • Everything it paints lives on one class. Its colours and its box model cannot reach your page any more, which is what made putting it inside another site possible at all.
  • Nothing changed about installing it. npx verbaly-studio pulls no Svelte and asks for nothing new: the panel it serves is already built.
Full release notesEvery bullet, per package, in the repo changelog.

0.55.0

Release

The panel

Studio shipped its server two releases ago and said, in its own README, that the screen was missing. This is the screen. It opens on your messages, it lets you fix one where you are reading it, and it runs the two commands that change your catalogs, with the bill in front of the expensive one.

Highlights
  • The screen the last Studio release promised. npx verbaly-studio now opens a page with every message in your project, in as many languages as you tick, and the state of each one.
  • You fix a translation in the row it lives in. Nothing opens: the line you are reading becomes the field you type in, and it tells you what is wrong while you type.
  • The states are named after what they mean to you: ready, missing, written by a machine and nobody has read it, or breaks your site. Each one has its own shape as well as its own colour.
  • Two commands run from the page. One reads your code and adds the text your catalogs do not have. The other translates what is missing, and because that one spends money it shows you the bill before anything is called.
  • A translation run is something you can walk away from. Reload the page and the progress bar comes back, because it asks the run how it is going instead of listening to it.
  • What a machine writes is still a draft. A finished run hands you a list to read, not a job marked done.
Full release notesEvery bullet, per package, in the repo changelog.

0.54.0

Release

Bring your catalogs as they are

Someone asked how much you have to learn before Verbaly is useful, and the answer turned out to be a documentation problem rather than a product one. Three of the four things our migration page asked people to do were never required: a catalog straight out of i18next already passes, groups and all. This release deletes that work from the page and hands the one real change to a command.

Highlights
  • A new command: npx verbaly migrate. Point it at a project that uses another i18n library and it ports the catalogs you already have. It shows you what it would do first, and --write applies it.
  • Your files keep their shape. Groups stay grouped, and the command writes back exactly the layout your file already had. Nothing gets reformatted.
  • One thing genuinely has to change, and you no longer do it by hand: {{name}} becomes {name}. A doubled brace is how Verbaly writes a literal one, so both spellings cannot mean the same thing.
  • What it will not guess, it tells you. A format inside the braces, an unescaped value, or a merge that would overwrite a key you use are listed with the reason, and those messages are left exactly as they were.
  • Merging your plural keys is optional. --plurals joins _one and _other into one message, which is what turns on automatic plural selection, and the count then prints itself already localized. Leave it off and those keys keep working.
  • Our migration page stopped asking for work that was never needed. It used to tell you to flatten your groups, merge your plural keys and rename your namespaces. None of the three was ever required.
Full release notesEvery bullet, per package, in the repo changelog.

0.53.0

Release

Your catalogs, on your own machine

You can now see your whole project on one address on your own machine. npx verbaly-studio reads every catalog in one go and lets you write a translation back, checked the same way your build checks it. Your JSON files are the database and git is the history, so it cannot drift from your code: it does not own anything. This first version ships the part underneath, not the screen on top, and it says so itself.

Highlights
  • A new package: @verbaly/studio. Run npx verbaly-studio inside your project and it serves everything Verbaly knows about it on your own machine: every catalog, what is missing, what is broken, what a machine wrote, and where each message is used in your code.
  • This release is the server, not the screen. There is no panel yet. What works today is the address it serves and --json, which prints the same thing the screen will read, so you can already pipe it into a script. The screen is next.
  • It never edits your source text. That text lives in your code and the key comes from it, so writing over it here would be undone the next time you extract. Saving into your source language is refused, and so is a key your source does not have.
  • A translation you save is checked before it lands. It runs the same two checks your CI runs, so a lost {name}, a lost tag, or a plural missing its catch-all case is refused with the reason, instead of passing here and failing your build later.
  • Nothing it reads can take it down. A catalog it cannot read, a drafts file it cannot read, a source file that will not compile, or pointing it at the wrong folder are each reported by name, and the rest still loads. It is what you open to fix those files, so it has to open.
  • It tells you which machine translations are worth reading first. Adding a language writes hundreds at once, and reading them one by one is a wall. Six signals point at the few that deserve your eyes: on our own site that is 18 messages out of 1425 per language.
Full release notesEvery bullet, per package, in the repo changelog.

0.52.0

Release

A link inside a sentence keeps you in your language

On a site published in several languages, every link you wrote by hand already carried the reader’s language, and every link written inside a translated sentence did not. Clicking one dropped a Spanish reader onto the English pages, which is why it felt so strange: the rest of the page was right. It had been that way since links inside messages existed, hidden behind two tests that were asserting it.

Highlights
  • Click a link inside a translated sentence and you stay in your language. Until now those links pointed at your source language, so someone clicking "see the guide" in the middle of a Spanish paragraph landed on the English page.
  • It was hiding behind two tests that asserted it. One of them follows a visitor through the whole translated site, so the suite stayed green while the problem shipped. Both now expect the right address.
  • A translated page carries its own links. It no longer needs the link list from your config once it reaches the browser, so nothing can rebuild a link without its language.
  • Forms go with them. Submitting is navigating, so a form that posts to a page you publish in every language now stays inside that language.
Full release notesEvery bullet, per package, in the repo changelog.

0.51.0

Release

A translated page stays translated when you navigate

The previous release made each page carry its own messages, and got the second page wrong: on a site with client-side routing, clicking a link showed the text in your source language until the visitor reloaded. It was three separate faults rather than one, which is why it looked so total. All three are fixed, and on Astro the fix needs no code from you.

Highlights
  • The regression in 0.50.0 is gone. Reading a site in Spanish and clicking through it stays in Spanish: page to page, section to home, and across a language switch.
  • It was three faults, not one. The messages a page carried were read once and never again, the runtime could not tell "this page did not carry the word" from "this language has no word for it", and the text binder treated a page the router swapped in as text it had written itself.
  • In Astro it fixes itself. Turning on inlineCatalog now wires the client router too, so you write nothing. Every other setup gets one call, adoptPage, from its navigation hook.
  • Switching language got cheaper on the way. Moving from Spanish to Portuguese used to download the whole Portuguese catalog; now the page you land on already carries what it shows, so the switch costs nothing extra.
Full release notesEvery bullet, per package, in the repo changelog.

0.50.0

Release

A translated page stops downloading the whole dictionary

Until now, opening one page in Spanish downloaded every Spanish message your site has, most of which that page never shows. The mirror now writes into each page exactly the messages it renders, so the page needs nothing else. On this site a first visit to the home page stops downloading about 50 KB, which is roughly nine times what the whole Verbaly runtime costs.

Highlights
  • A first visit gets much lighter. A visitor was downloading 53 KB of Spanish messages to read a page that uses 2.8 KB of them. Now they get the 2.8 KB, and it arrives with the page instead of as a second request.
  • The saving is per page, and it is most of the file every time. Measured across this site: 50 KB less on the home, 49 on a docs page, 32 on the changelog, which is the heaviest page here.
  • Nothing goes missing when someone interacts. If your page later asks for a message it did not show, Verbaly fetches the rest once and updates, instead of quietly showing your source language.
  • It is one line and it is off by default. Add inlineCatalog to your render settings. A project that does not turn it on builds exactly as before.
Full release notesEvery bullet, per package, in the repo changelog.

0.49.0

Release

Your agent can read your messages

The MCP server could count your messages, tell you which ones were missing and translate them, and it could not show you a single sentence. That made one thing impossible: reviewing a machine translation through an agent, because you were handed a key and asked to approve it. Now an agent can read your language files and your settings directly, and each translation waiting for you comes with the sentence it came from.

Highlights
  • An agent can now read what a message actually says. Before, it could tell you fourteen translations were missing and never show you one of them. Your language files and your settings are readable directly, and looking at them does not use up one of its actions.
  • Reviewing a machine translation finally works through an agent. You get each one waiting for you next to the original sentence, so you can read both and decide. Accepting is still yours alone, on purpose: no tool here can approve a translation, and none ever will.
  • An agent can set up Verbaly in a project from nothing. It creates your settings file and your language files, works out which bundler or framework you use, and tells you the one line you still have to add yourself.
  • Nothing you already use changed. Every tool answers exactly as it did before.
Full release notesEvery bullet, per package, in the repo changelog.

0.48.0

Release

The address decides the language

When your pages live at addresses like /es/dashboard, that address is what says which language the page is in. Until now Verbaly’s server integrations never looked at it: they asked the visitor’s saved choice and their browser, so the same link could arrive in English for one person and Spanish for another. Now the address wins, because an address gets shared, linked and cached while a saved choice belongs to one person.

Highlights
  • A page at /es/dashboard now arrives in Spanish, always. Before, Verbaly asked the saved choice and the browser instead of reading the address, so the same link could show two different languages to two people.
  • Your English pages stop being translated by accident. Someone who once picked Portuguese used to get Portuguese on your unprefixed pages too. A plain address now means your source language, for everyone.
  • Next.js pages can be built ahead of time again. Verbaly used to read request headers on every page, which quietly turned every translated page into one that rebuilds on each visit. Hand it the language from your folder name and the page goes back to being built once.
  • Your translated pages can tell search engines about each other on a server too. The language links and the canonical address existed only for statically built sites, and the same set is now available wherever you render.
  • SvelteKit routes stay simple. One line and /es/docs is served by your /docs route, so you do not duplicate your route folders per language.
  • Nothing changes if your addresses do not carry a language. All of this switches on with the URL setting you already have.
Full release notesEvery bullet, per package, in the repo changelog.

0.47.0

Release

Keys you write down, and a sitemap that tells the truth

Verbaly’s default is that you write text and never see a key, and that is not changing. This version covers the other real case: keys that are a contract with something outside your app, a mobile client or a translation memory, written once in a module and read from everywhere. Until now Verbaly could not see those keys at all, so cleaning unused messages deleted them and nothing checked they existed. The second half is the sitemap, which was publishing redirects and error pages as if they were results.

Highlights
  • You can keep your keys in a module and Verbaly will still check them. A file that maps names to keys is a shape many projects already write. Before, Verbaly could not see those keys: cleaning unused messages deleted them, and nothing verified they existed.
  • A key no catalog has is an error where you declare it, not a blank space you find in the interface later. Your editor autocompletes from your own catalog while you write the module.
  • Your sitemap stops listing pages that ask not to be indexed. Redirects and error pages were being published as if they were results. On this site that was 21 addresses out of 81, and the list is now only the 60 real pages.
  • Those pages are still translated and published exactly as before. Only the list of addresses changed, so a visitor who lands on one still gets it in their language.
  • Nothing changes if you use neither. Both are additions, and a project that writes neither behaves exactly as before.
Full release notesEvery bullet, per package, in the repo changelog.

0.46.0

Release

The first ten minutes

Everything fixed here sits on the path you walk once, when you add Verbaly to a project that already exists: install it, scaffold it, run the health check, wrap your text, extract it. None of it sits on the path you walk every day, which is why more than a thousand tests never saw it. The scaffolding command ignored the config you had already written and created a second set of catalogs. A stray file in your catalogs folder became a language you could not remove. The command that wraps your text could leave a project unable to compile. All of that is fixed, and a new check runs a real install under two package managers so it stays fixed.

Highlights
  • verbaly init now reads the config you already wrote. It used to ignore it and create a second set of catalogs in the default folder, under the default language. If your config said Spanish in locale/, you got English in locales/.
  • Breaking: the languages you list in your config are now the whole list. A leftover JSON file in your catalogs folder used to become a language you could never remove, and it blocked builds asking for translations you never wanted. If you do not list them, the files in the folder still decide, exactly as before.
  • verbaly wrap --write no longer leaves your project unable to compile. It used to wrap text in files where t was not available. It now skips those files, tells you which ones and how many texts wait there, and says whether each one is a client component so you know which line to add.
  • Wrapping stops picking up things that are not interface text: the contents of code blocks, single lowercase words like demo logins and file names, and sentences that start with a value instead of words.
  • When the verbaly command is not installed, Verbaly says so instead of telling you to run it. The build names the package to add, and the health check has a line for it.
Full release notesEvery bullet, per package, in the repo changelog.

0.45.0

Release

A catalog only grows, and every page was paying for all of it

Text that lives on one page, a list of releases, a blog archive, a long list of questions, was downloaded by every visitor on every page. This version lets you name those groups of messages and keep them in the build. The pages that use them still come out translated, because that text is already written into the HTML, and the browser stops downloading it. Looking for the proof also uncovered a real defect underneath. After its first pass, Verbaly could replace correct translated text on the page with the message’s internal name whenever a message was missing. That is fixed here, and it matters to anyone who pre-renders their site.

Highlights
  • Text that only one page needs stops travelling to all of them. Name that group of messages in your config and the browser no longer downloads it. The pages that use it still come out translated, because that text is already in the HTML.
  • Pre-rendered text is never destroyed any more. If a message is missing, Verbaly keeps the text that is already on the page instead of replacing it with the message’s internal name. Before, that could happen a moment after a page loaded.
  • Measured on this site: the file every page loads went from 46.3 KB to 33.5 KB compressed, and the Spanish and Portuguese trees dropped about 14 KB each. Nothing was lost on screen.
  • Nothing changes if you do not switch it on. It is a new option, and a project that does not write it behaves exactly as before.
  • The build tells you when it looks wrong, naming a group that matches nothing in your catalog, or one your code reads directly.
Full release notesEvery bullet, per package, in the repo changelog.

0.44.0

Release

Our own checks were measuring the machine, not the code

The job that checks our test coverage was failing at random, and the cause was not a slow test. A test that runs in 22 milliseconds was being failed by a 5 second limit, because that limit counts real time and our test command starts twelve processes on the same processor cores. Twice, that blocked a release. This version fixes it at the root, and spends the rest of its time on the same idea applied to our supply chain: what our build is allowed to do, which tools it runs exactly, and code that could never run at all. Nothing about the package changes.

Highlights
  • Nothing you install changes. The runtime is identical to 0.43.0 and every published API behaves the same. This release is about the machinery that checks us.
  • Our build stopped failing at random. Our test suite could fail a perfectly healthy test just because the machine was busy, and twice that blocked a release.
  • Every tool our build runs is pinned to an exact commit. A tag can be moved by whoever owns it, but a commit cannot, so nothing can quietly change what builds your package.
  • The job that publishes releases asks for write access only where it needs it, instead of granting it to the whole workflow.
  • Static analysis runs on every change, and its configuration now lives in our repository, pinned and readable, instead of being a setting only we could see.
Full release notesEvery bullet, per package, in the repo changelog.

0.43.0

Release

Everything we say about ourselves is true again

The badge on our README has said ~3KB since the beginning, while the runtime was actually 3.68 KB. Rather than weaken the claim, this release removes the last piece heavy enough to be worth removing: relative time formatting is 318 bytes that most apps never write, and it now travels only when one of your messages asks for it. The rest of the release is the same idea pointed at everything else the project says about itself.

Highlights
  • The runtime is 3.00 KB. Relative time formatting joins ICU as something that ships only when one of your messages actually uses it, and there is nothing for you to configure.
  • A formatter you write yourself now knows which message it was called for. It receives the parameter name and the message key, so its own errors can say where they happened, which was not possible before.
  • The security policy covers all twelve packages, not the seven that existed when it was written, and it now explains how to verify that a release really came from our CI.
  • Our security posture is something you can check instead of something we claim: a score from the OpenSSF runs on every change and the badge on the README reads it.
Full release notesEvery bullet, per package, in the repo changelog.

0.42.0

Release

A smaller runtime for everyone who never needed ICU

Verbaly’s own syntax covers plurals, selects and formats, and ICU message syntax is the way out for what it does not. That escape hatch was always described as something you opt into, and it never was: the parser went into every app whether or not a single message used it. Our own documentation site has no ICU messages at all and shipped it on every page. Verbaly already reads your catalogs, so now it only sends the parser where a message actually needs one.

Highlights
  • The runtime is 11% smaller for anyone who does not use ICU syntax, which is most projects. The parser only ships when one of your messages actually needs it, and there is nothing for you to switch on.
  • Nothing changes if you do use ICU. Verbaly sees it in your catalogs and brings the parser along, exactly as before.
  • A message that reaches your app after the build and uses ICU now shows its own text and says why, instead of half-rendering into something that looks almost right. If your text comes from a CMS, one setting ships the parser anyway.
Full release notesEvery bullet, per package, in the repo changelog.

0.41.0

Release

One line to switch, and your choice finally sticks

Verbaly shipped a complete language switcher for one way of doing addresses and a spare part for the other, so every project wrote the second one by hand, ours included. Rewriting our own site on the library’s own helper found a bug that had been there since the switcher moved into the runtime: someone who picked a language was sent back to their browser’s language on the next page load. The other half is that Verbaly had started assuming you wanted the language in your addresses; now it reads your setup instead of guessing.

Highlights
  • Switching language is one line now, and it is the same line whichever way your addresses work. It goes to the other address when the language lives there, and changes the text where it stands when it does not.
  • Your choice of language survives the next page load. Picking English on a Spanish page could send you straight back to Spanish, because the switch remembered your choice in one place and the page that greets you read another.
  • Verbaly stops assuming you want the language in your addresses. It reads your setup: build a version of your site per language and you get addresses per language, otherwise one address serves all of them. You only write it down to disagree.
  • verbaly doctor tells you whether it read your choice or worked it out, so a setting nobody wrote down never looks like a decision somebody made.
  • The size check now measures what a real app actually ships, instead of only the smallest possible import and the largest impossible one.
Full release notesEvery bullet, per package, in the repo changelog.

0.40.0

Release

Say where the language lives, and translate the head too

Verbaly has had two ways to carry a language for a long time and never named either of them, so which one you got was decided by whether you remembered to pass an optional field. Naming them turned up the thing that made the naming matter: a page could be translated everywhere a visitor looks and still tell a search engine it is in English, because the title and the description live in the head where nobody reads them. Our own documentation shipped 27 of 27 pages that way.

Highlights
  • You can now say where the language lives in your addresses, once, in the config: /docs and /es/docs, a prefix on every language, or one address for all of them. Before this the answer depended on which helper you called and what you passed it.
  • The language switcher is one line and cannot be written wrong. Verbaly hands you a helper that already knows your languages, your source and your mode, and the same line keeps working if you change your mind about the mode.
  • Your page title and description get translated too. They live in the head, nobody reads them on the page, and they are what a search result shows, so a page could look fully translated and still be listed in English.
  • verbaly render counts the pages whose title still never changes and tells you once, with the fix. It never fails a build: a product name is allowed not to translate.
  • verbaly doctor tells you which mode your addresses are in, and says so when your settings ask for two things that cannot both be true.
  • Breaking: asking for a path without naming your source language no longer compiles. It used to put a prefix on every language including your own, quietly, which was almost never what anyone wanted.
Full release notesEvery bullet, per package, in the repo changelog.

0.39.0

Release

Translating survives a bad connection, and agents get the whole cycle

Verbaly has let a coding agent run it for a while, but only four of its twelve commands were ever handed over, and the two missing ones are the first two an agent needs: finding out what is wrong with a project, and wrapping the text already written in it. Looking at that turned up something worse next door. verbaly translate is the one command that costs money and minutes, and a single bad answer threw the whole run away: batches that had already been paid for and translated were never written to disk.

Highlights
  • verbaly translate no longer loses work it already paid for. A request that does not come back is tried again, and if it still fails the run keeps going and saves everything else. Running it again asks only for what is left, instead of paying for all of it a second time.
  • Translating is about four times faster. Requests now go out together instead of one after another, and a long run tells you what has landed while it runs rather than staying silent for minutes.
  • You can say what must never be translated. Put your product names and any wording you have settled on in translate.glossary, and how you address the reader in translate.instructions, instead of fixing the same word by hand after every run.
  • Your coding agent gets the two tools it was missing: one to work out why a project is not working, and one to find text written straight into your components and wrap it. Bringing an existing app to Verbaly no longer needs a terminal.
  • Agents read numbers now, not sentences. Every tool answers with plain data as well as text, so an agent stops guessing at wording that can change between releases.
  • Accepting a machine translation is still something only a person can do, and that has not changed.
Full release notesEvery bullet, per package, in the repo changelog.

0.38.0

Release

Translations written as groups are safe everywhere

You can keep your translations as nested groups instead of one long flat list, and Verbaly has read that shape for a while. Only half of it did: the commands that write your catalogs read the group names as if they were messages. So extract added a second copy of every message, --prune removed whole groups with every translation inside them, exports reached your translator broken, and translate said there was nothing to do about an app that was half untranslated. Nothing changes for a flat catalog.

Highlights
  • Translations kept in groups are safe from end to end. Every command now reads and writes them the same way, and your file keeps the shape you gave it instead of being flattened behind your back.
  • verbaly extract --prune no longer deletes translations. On a catalog written as groups it read each group name as one message, did not recognise it, and removed the whole group with every translation inside it.
  • Your translator files stop arriving broken. Exporting wrote [object Object] where the text should have been, and verbaly translate reported nothing to do while verbaly status said half the app was untranslated.
  • A language you add later matches the rest. A new catalog takes the shape of the ones you already have, so a project never ends up half grouped and half flat.
  • The tool counts in real English. It used to say 1 messages, 1 locales and 1 pages in nine places.
  • A repeated warning says itself once. A key collision was announced three times in a single verbaly extract, one of them after the summary had already printed.
Full release notesEvery bullet, per package, in the repo changelog.

0.37.0

Release

Your saved language survives a refresh

The last version named the two ways a site can carry its language. Writing the page that explains them turned up the reason one of them had never really worked: if your app keeps one address per page and loads languages on demand, Verbaly knew which language the visitor wanted and simply never fetched it. One line, and the example in our own docs had been showing it all along.

Highlights
  • A saved language now survives a refresh on a site that keeps one address. A returning visitor used to get the source language back every time, with nothing in the console to explain it.
  • Nothing changes if your language is in the address, which is what the last two versions were about. This was only ever broken where the language comes from storage or the browser.
  • A warning that used to give impossible advice now gives the right one. When your page says one language and Verbaly is in another, it told you to read the language from the address. On a site that keeps one address there is nothing to read.
  • React, Vue and Svelte were each checked against a language that arrives after the page is already on screen. That used to be a rare path and is now the normal one.
Full release notesEvery bullet, per package, in the repo changelog.

0.36.0

Release

Your visitors reach their language on their own

Verbaly publishes your site in every language, each at its own address. The last version made those pages right, and this one is about getting people to them, and about asking from your own code which language a page belongs to. Both were things every project had to write by hand, this site twice in one day. It also fixes a way a page name could be mistaken for a language.

Highlights
  • Asking which language a page is in no longer needs a helper of your own. Verbaly answers with the language in the address, or with nothing when the address does not carry one. That is a different question from what a visitor prefers, and mixing the two is what let a saved preference paint the wrong language over a page.
  • Verbaly can send a visitor to their language now. Turn it on and someone landing on your home page goes to your site in their language before anything is drawn. Only the home page, so a search engine still reaches every page of your site directly.
  • A site served from a subfolder works. Tell Verbaly the folder and the links inside each language point where they should, instead of quietly pointing nowhere.
  • A page called /es-la-guia is a page, not Spanish. Verbaly read the start of an address a little too eagerly, so a page whose name began like a language code could be translated into it. Now only a real language code counts.
  • What this version does not change: an app with one address per page works exactly as before. Everything here is for sites where each language has its own address.
Full release notesEvery bullet, per package, in the repo changelog.

0.35.0

Release

Your pre-translated pages stop flickering

Verbaly publishes your static site in every language, under /es/ and /pt/. This version is about what happened to someone who reached one of those pages: it arrived in Spanish and then flashed to English before settling back, nothing told the runtime it was already on a Spanish page, and every link pointed back out. The page was right and everything between it and the visitor was not.

Highlights
  • A pre-translated page stops flashing into English. Verbaly served the Spanish page and then painted the English text over it until the Spanish catalog finished loading, on exactly the pages this feature exists to keep still.
  • Landing on /es/ is now enough. Verbaly reads the prefix it wrote itself before it looks at storage or at the browser, because the page you are looking at is already in that language.
  • Each language stops being an island. Its links pointed at the site root, so arriving in Spanish and clicking once took you back to English. Links to pages that language also has now stay inside it.
  • Your language switcher sends someone to your site in that language. That is the address they can share and the one search engines index, and the page arrives already translated so nothing flickers. In an app with one address per page, changing the language still just changes the text, exactly as before.
  • What this version does not do: it does not route anyone anywhere. Someone who lands on your root is being served the source language, and sending them to their language is one rule in your host settings.
Full release notesEvery bullet, per package, in the repo changelog.

0.34.0

Release

What the build gate cannot see

The build gate refuses to build when a translation cannot render what your source renders, but it only ever sees what is in your project at build time. This version goes after everything that arrives later: a catalog that comes from a content system, a source file Verbaly could not read, and a line break inside a rich message that the pre-rendered page and the browser disagreed about. Four real bugs, each one a way to ship something wrong with every command green.

Highlights
  • A line break inside a rich message no longer doubles in a pre-rendered page. Verbaly wrote it in a way that browsers read as two line breaks, and the page then replaced them with one as it loaded. A visible jump, on exactly the pages that feature exists to keep still.
  • A translation that arrives as null no longer takes your app down when it starts. A field nobody filled in a content system used to stop everything before the first render. Now that one key is skipped, the app runs, and the console tells you which one it was.
  • The commands survive a file they cannot read. One file with a syntax error used to end the whole run with a position and no file name. Now the file is named, its texts are skipped, and every other file is still read.
  • Text inside JSX written in a .js file is found like anywhere else. A React app with components in .js could not run the extract command at all, because the first tag ended it.
  • A warning no longer repeats itself because a number changed. Three of them printed the value that caused them, so a counter going up could fill memory with copies of the same message.
Full release notesEvery bullet, per package, in the repo changelog.

0.33.0

Release

The public API becomes a decision

The compiler package let you import 132 different things, and not one of them was written down anywhere. They had piled up as whatever the framework packages happened to need, and at version 1.0 every single one would have become a promise nobody had ever reviewed. This version reads the whole list against a written rule, publishes the 60 that survive it, documents them, and adds a test that fails if the list ever grows by accident.

Highlights
  • If you use the command line or any of the framework packages, this release changes nothing for you. No config, no imports, nothing to edit. Everything here happens inside a package that only custom integrations reach into.
  • The compiler package now documents what you are allowed to import, in two groups: the types your own config file and your own translation provider need, and the toolkit for building an integration we do not ship. Everything else is internal and now says so.
  • The list went from 132 names to 60. Each one stayed because a real integration uses it, because it is a documented way to extend Verbaly, or because another one needs it. The 72 that matched none of those are gone.
  • A build plugin no longer receives the internal parse tree Verbaly reads your code with. It used to get the whole thing just to read two fields off it, and now it gets the messages it found. One less thing that can break when the parser changes.
  • Both packages fail their own test if the list grows, so adding something public is now a deliberate choice someone reviews, instead of something that happens on the way to fixing something else.
Full release notesEvery bullet, per package, in the repo changelog.

0.32.0

Release

Nothing breaks in silence

Verbaly already refused to build when a translation could not render what your source renders. This version closes the same story from the other end: every remaining place where Verbaly quietly did the safe thing now tells you, at the moment you can act on it. A plural that renders an empty line, a price with no currency, a block written where it cannot work, and a name imported from somewhere it does not exist: four ways to ship the wrong text with everything green.

Highlights
  • A plural without a catch-all case now warns in the browser, not only in the build. The build check already refused it, but a catalog that arrives from a lazy load or from a content system never goes through the build, so the empty line could still reach a real page. Now it names the message and tells you what to add.
  • A placeholder missing its argument stops rendering as a bare number. A price with no currency code, a measure with no unit, a date with no time unit: each one used to print the plain value and say nothing. Each one now says what it needs and shows the shape of the fix.
  • Writing a plural inside a t template no longer ships with the braces visible. In that template the values come from your code, so a plural or a format written there becomes text your users see. verbaly extract and verbaly doctor now point at the file and the exact text.
  • Importing t from the package finally gets an answer that mentions Verbaly. t comes from your instance or from the generated module, never from the package, and until now only the bundler complained, in a message that gave no hint where t should come from.
  • Every error from the command line prints its name once. Every failure used to read [verbaly] [verbaly].
Full release notesEvery bullet, per package, in the repo changelog.

0.31.0

Release

The tooling stops contradicting itself

One command called a project healthy while another blocked its build over the same files. The doctor rejected catalogs written as nested groups, even though the runtime has always read them, and told an Astro or Nuxt app to install the package the setup command had just told it not to. The build gate read any word in angle brackets as markup and failed correct translations over it. All of that is now one story: what a command tells you is what your project is.

Highlights
  • verbaly doctor stops disagreeing with verbaly check. A translation that is there but broken is now an error in the doctor too, so the command you run when something looks wrong can no longer answer "setup looks healthy" about a project whose build fails.
  • Catalogs written as nested groups work everywhere. The doctor used to reject them as invalid, and the gate only compared the group names, so a group with one untranslated line inside counted as fully translated. Now every line is checked on its own, the way the runtime already read it.
  • The doctor points at the package your project actually needs: Astro, Nuxt, Next.js, SvelteKit, Vite or the bundler plugin. It is the same answer verbaly init gives, because both now read one table.
  • The build stops failing over text that only looks like markup. "Press <Enter> to continue" translated as "Pulsa <Intro> para continuar" was reported as two broken tags. A tag counts as structure only when the message closes it, so your prose stays prose and a real emphasis tag or a link is still protected.
  • Each failure names the step that repairs it, instead of one "run extract" that only ever fixed missing translations. And turning off the missing check now means what it says: you can build with untranslated text, and Verbaly still refuses to ship a translation that renders wrong.
  • A missing value says which message it came from. The warning used to print the placeholder name alone, once per name, so the second message with the same gap stayed silent.
Full release notesEvery bullet, per package, in the repo changelog.

0.30.0

Release

The build gate stops trusting a filled-in translation

Until now the check asked one question: is there a value for every key? A translation could drop your name placeholder, lose an emphasis tag or flatten a plural block, and the build stayed green while production quietly showed the wrong text, or nothing at all. Now the gate reads every translation against its source and fails on anything that cannot render it, with the reason in plain words and the source line to click. The runtime stops being silent about the same kind of bug, and the React package is verified against Preact.

Highlights
  • The build now catches translations that are there but broken. A missing placeholder, a lost tag, a plural block turned into plain text, or a plural set with no catch-all case: each one stops the build with a sentence saying what happened and why it matters. Before this, a catalog you edited by hand had no check at all.
  • The empty-text bug is gone. A plural block that lost its catch-all case rendered nothing at all for every count it did not list. In Polish, a message with only "one" and "few" showed an empty string for 5 items. That fails the build now instead of shipping.
  • Verbaly tells a translator which plural forms their language actually needs. A Polish or Russian catalog carrying only two forms gets a warning naming the missing ones. It is advice, not a failure: the text still renders. Forms no real counter ever reaches are never asked for, so Spanish, French, Portuguese and Italian stay quiet.
  • verbaly status shows a broken count per language, so you can see the state of a catalog without running the gate.
  • A missing value now warns in the console. Rendering a raw placeholder because nobody passed the value was the one bad-data case Verbaly did not report. Every other one already did.
  • Preact works, and now it is proven. The React package runs its whole suite a second time with React resolved to preact/compat: hooks, language switching, rich text and server rendering all pass. No separate package, no changes to your code.
  • VS Code can show your translations on hover. The repository README carries the i18n-ally setup that turns hashed keys into readable text while you code.
Full release notesEvery bullet, per package, in the repo changelog.

0.29.0

Release

Your coding agent can now run the whole translation cycle

A new MCP server gives Claude Code, Cursor and any MCP client four tools: see the coverage, list what is missing, extract new text and machine-translate the gaps. Everything the agent translates still waits for your review. A public Agent Skill and an llms.txt on the docs complete the channel, Nuxt projects keep their root clean, and displayed code snippets can no longer become phantom keys.

Highlights
  • The translation cycle as agent tools. One command (claude mcp add verbaly -- npx -y @verbaly/mcp) and your coding agent can check coverage, list missing translations, extract new text and fill the gaps with machine translation. Anything it translates is saved as a draft, so nothing ships without a human saying yes.
  • A public Agent Skill teaches your agent Verbaly. Install skills/verbaly from the repository into your project and the agent knows the write, extract, check, translate cycle and the rules that keep it safe.
  • The docs now speak LLM. This site publishes an llms.txt index, so agents that read documentation find their way around Verbaly without scraping HTML.
  • Nuxt projects keep their root clean. The generated types now live inside .nuxt/ and register themselves, exactly like the Astro integration already did. No file at the project root and no tsconfig edits.
  • Displayed code can no longer become a translation key. Pages that show t examples as visible text in Vue, Astro or Svelte files used to risk those snippets being extracted as real keys. Now only code inside real expressions counts.
Full release notesEvery bullet, per package, in the repo changelog.

0.28.0

Release

Gettext PO, protected placeholders and a sturdier runtime

The translator toolkit is now complete: export and import gettext PO files, and XLIFF now protects your placeholders and tags so a translator cannot break them by accident. A custom sitemap name in render finally works, a rare crash with invalid dates is gone, and the runtime cannot silently grow past its size budget.

Highlights
  • Export and import gettext PO files. verbaly export --format po writes one .po file per language, ready for Poedit, Weblate or any PO editor. verbaly import reads them back with the same safety net as every other format: a translation that breaks a placeholder is rejected, not shipped. Entries a tool marks as fuzzy count as untranslated.
  • Translators can no longer break your placeholders in XLIFF. Placeholders and tags travel as protected codes with meaningful names, so translation tools show them as untouchable chips instead of editable text. Plural and select words stay editable on purpose: those need translating.
  • Custom sitemap names work now. Setting a custom filename for the sitemap in render used to be ignored: it always wrote sitemap-i18n.xml. The name you configure is the name you get.
  • A rare crash is gone. Passing an invalid date to a message could throw instead of degrading gracefully. It was found by a new randomized test suite that hammers the parser with garbage on every run, so this class of bug stays fixed.
  • The runtime cannot silently grow. Every push now measures the core bundle and fails if it exceeds its size budget. The ~3 KB promise is enforced by a machine, not a habit.
Full release notesEvery bullet, per package, in the repo changelog.

0.27.0

Release

Machine translations you review before trusting

Machine translations now arrive as drafts you review, not as finished work you trust blindly. A new review command approves them, the CI gate can wait for a human, and the model translates with context from your code.

Highlights
  • Machine output is a draft until you say otherwise. verbaly translate still fills the gaps, but every text it writes is now flagged as an unreviewed draft. Your catalogs stay exactly as clean as before: the review state lives in a separate .verbaly-drafts.json file next to them.
  • Approve translations when you have read them. verbaly review lists everything waiting for a human; verbaly review --approve accepts it, all of it or one locale at a time. Importing a translator's file counts as review too, so the draft flag clears on its own.
  • Block merges on unreviewed machine text. verbaly check --drafts fails CI while any machine translation is still unreviewed. Plain verbaly check is unchanged, so nothing breaks until you opt in.
  • Better translations, because the model gets context. verbaly translate now tells the provider which source files each text appears in, so tone and length fit where the text is used.
  • See what needs review at a glance. verbaly status (and --json) now report the count of unreviewed drafts per language.
Full release notesEvery bullet, per package, in the repo changelog.

0.26.0

Release

Astro support, a wrap codemod and CI annotations

Astro joins the family: one line wires everything and the per-locale rendering runs by itself. A new wrap command wraps your hardcoded JSX text for you, and CI failures become annotations right on your pull requests.

Highlights
  • Astro support, one line. Add verbaly() to the integrations in astro.config and everything is wired: live extraction while you code, type-safe keys and the build gate. The typed keys live inside Astro's own .astro folder, so no extra file lands in your project. In the mirror flow, verbaly render runs by itself after every build.
  • Write your text right in .astro files. The same t`…` you already use in .js, .tsx, .svelte and .vue now works in the frontmatter and the markup of Astro components.
  • Migrating an existing app? Let verbaly wrap do the boring part. It scans your JSX, finds the hardcoded user-visible text and wraps it in t`…` for you. It reports first; nothing is touched until you pass --write.
  • CI failures now point at your code. verbaly check --reporter github turns every missing translation into an annotation on the exact file and line, right on the pull request.
  • verbaly status --json gives you the coverage numbers machine-readable, ready for badges and dashboards.
Full release notesEvery bullet, per package, in the repo changelog.

0.25.0

Release

Automatic RTL, real language names and context for translators

Adding Arabic or Hebrew now just works: the page direction follows the language everywhere. Language switchers get real names from the platform, and translator files say where each text lives.

Highlights
  • Right-to-left languages just work now. Add Arabic, Hebrew or Persian and the page direction follows the language on its own: when the user switches, when the server renders, and in pre-rendered static pages. No dir attribute to manage, ever.
  • Language switchers without hardcoded names. localeName('es') returns 'español' and localeName('de', 'en') returns 'German'. The names come from the platform, so any locale works without maintaining a table.
  • Need the direction yourself? localeDirection('ar') returns rtl. It is the same helper the rest of the toolkit uses, exported for your own layouts.
  • Translators now see where each text lives. verbaly export marks every message with the source files it came from: XLIFF gets standard location notes and CSV gets a location column, so context stops being guesswork.
Full release notesEvery bullet, per package, in the repo changelog.

0.24.0

Release

Literal braces and safer pre-rendered pages

Rich messages can finally show a literal brace, and pages pre-rendered with verbaly render now apply the same security rules as the browser.

Highlights
  • Messages can now show literal curly braces. Write &#123; and &#125; in a rich message and they render as { and }. Hex forms like &#x7B; work too, in the browser, in every <Trans> and in pre-rendered pages.
  • Pre-rendered pages are now as safe as the browser. Translated attributes go through the same guards, so unsafe links and dangerous attributes from a catalog never reach the static HTML.
Full release notesEvery bullet, per package, in the repo changelog.

0.23.0

Release

One <Trans> everywhere, live extraction and a coverage view

The React, Vue and Svelte <Trans> components now share the same props and the same rendering rules, the CLI learns two everyday commands, and the last planned breaking changes land before 1.0.

Highlights
  • <Trans> is now the same component in React, Vue and Svelte: same props, same rendering rules. Your custom component wins, then named links, then the safe tag whitelist; anything unknown degrades to plain text.
  • Breaking: React and Vue <Trans> now render whitelisted tags like em and code as real elements, matching Svelte and the DOM interpreter. Pass an empty richTags list to keep the old flatten-to-text behavior.
  • Breaking: @verbaly/svelte now requires Svelte 5. In exchange <Trans> gains the components prop: map a tag in a message to your own component. On Svelte 4, stay on 0.22.0.
  • verbaly status shows how much is left to translate, one line per language, without failing anything. 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 is not available.
  • Easier to give feedback: the README points bug reports and friction reports to the right place, and the issue forms cover all ten packages.
Full release notesEvery bullet, per package, in the repo changelog.

0.22.0

Release

Your catalogs become native mobile resources

The export command learns two mobile formats: Android resource folders and iOS string files. A web app and its mobile companion can now share one set of translations.

Highlights
  • Export your translations for mobile apps: verbaly export --format android-xml writes folders you can drop into res/, and --format ios-strings writes .lproj folders for Xcode.
  • Sensible defaults for each platform: your source language becomes the platform default, and untranslated keys are left out so the app falls back to it instead of showing empty text.
  • Safer catalog files: a catalog with broken JSON now stops the command and names the file, instead of being silently read as empty. A Windows BOM at the start of the file is simply tolerated.
Full release notesEvery bullet, per package, in the repo changelog.

0.21.0

Release

A quality pass, and typed Nuxt config

A cleanup sweep across all ten packages, plus fully typed options in your Nuxt config. The internals got tidier and a little smaller, with no change to how you write Verbaly.

Highlights
  • The verbaly key in nuxt.config.ts is now fully typed: you get autocomplete and typo checking, and the module still adds no extra dependency.
  • A calmer editor in dev. verbaly.d.ts is only rewritten when its contents actually change, so your TypeScript server stops reloading types on every save.
  • A slightly smaller runtime: the tree-shaken core is now 3.28 KB gzipped, down from 3.30.
  • Cleaner generated types: a value used as both a date and plain text no longer shows up twice in verbaly.d.ts.
  • Clearer command-line messages. Every error and hint reads in plain words, and check now prints the source text right next to the missing key.
Full release notesEvery bullet, per package, in the repo changelog.

0.20.0

Release

Write your text in .svelte and .vue files too

Natural text now works inside Svelte and Vue components: write it in the file, script or markup, and the compiler extracts it with a stable key and typed params.

Highlights
  • Write t`Hello ${name}` directly in a .vue template or {$t`Hello ${name}`} in .svelte markup. The compiler picks it up like it always did in .ts files: stable key, typed params, per-locale catalogs.
  • Svelte's store form is understood: $t`…`, $t('key') and $t.id('key')`…` all work anywhere in a .svelte file.
  • Live in dev, gated in build: save a component with new text and the catalogs plus verbaly.d.ts update on the spot. Missing translations still stop 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 prune.
  • verbaly extract scans more by default: .svelte and .vue files, plus the app/ folder next to src/ (where Nuxt 4 and the Next.js App Router live). Your own include config still wins.
Full release notesEvery bullet, per package, in the repo changelog.

0.19.0

Release

Next.js joins the family: the cycle is complete

The last big meta-framework integration. Next.js App Router pages render already translated, hydrate without a flash, and work on both Turbopack and webpack.

Highlights
  • New package @verbaly/next. 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 and <Trans> as in any React app.
  • Works on Turbopack (the Next 16 default) and on webpack. next dev extracts your messages as you type, and next build stops if a translation is missing.
  • Every visitor gets their own language, resolved once per request from their cookie or browser preferences. Pages arrive already translated, nothing flashes, and concurrent visitors never see each other’s language.
  • useSwitchLocale() changes the language instantly, remembers the choice for the next visit, and updates Server Components too.
  • Rich messages can now display literal markup, so a message can show "<html lang>" as visible text instead of a broken escape.
Full release notesEvery bullet, per package, in the repo changelog.

0.18.0

Release

Nuxt joins the family

The second meta-framework integration: a zero-config Nuxt module renders every page in the visitor’s language and hydrates without a flash.

Highlights
  • New package @verbaly/nuxt. One line in your config (modules: ['@verbaly/nuxt']) wires everything: live extraction, per-visitor language, and hydration with the same locale and catalog the server used. No flash, no mismatch.
  • No framework lock: the module depends on neither nuxt nor @nuxt/kit, and it is verified against a real Nuxt 4 app.
  • switchLocale() is now a core export shared by SvelteKit, Nuxt and hand-rolled setups. It loads the catalog first and switches after, so nothing flashes. Existing imports keep working.
  • Client-only apps are covered too: with ssr: false the module still picks the right language in the browser.
  • Support Verbaly: GitHub Sponsors is live at github.com/sponsors/AronSoto.
Full release notesEvery bullet, per package, in the repo changelog.

0.17.0

Release

Robust inside, for real

The hardening release: a full audit of all eight packages fixed every sharp edge it found, and SSR gets its missing piece.

Highlights
  • Attribute translation is now XSS-safe end to end: data-verbaly-attr sanitizes URL attributes and blocks style and srcdoc entirely, so a compromised catalog can’t inject scripts.
  • t() never crashes on bad format arguments: an invalid currency code, date or time style warns once and renders the plain value.
  • createRequestInstance(locale) in virtual:verbaly: the SSR per-request pattern in one call, with the catalog already loaded and no flash.
  • The CLI now rejects misplaced flags. verbaly translate --locale es used to silently translate all locales; now it errors and suggests --locales. Plus --dry-run for extract --prune.
  • normalizeLink() and resolveRequestLocale() now live once in core, shared by every adapter and SSR integration.
  • Breaking: instance.inspect(key) now returns { from, source } instead of { locale, source }: one name for one concept across the observability API.
Full release notesEvery bullet, per package, in the repo changelog.

0.16.0

Release

Server-side rendering, for real

The first meta-framework integration: SvelteKit pages render in the visitor’s language on the server and hydrate without a flash.

Highlights
  • New package @verbaly/sveltekit. SSR in three wires: verbalyHandle() in your server hooks, one +layout load, and %verbaly.lang% in app.html. Pages arrive translated.
  • The language is decided per request: cookie first, then the Accept-Language header (es-PE matches es), then your fallback. Concurrent requests never leak each other’s language.
  • switchLocale(): a client-side switch that loads the catalog first, then saves the choice in the cookie the server reads.
  • negotiateLocale() in core: the Accept-Language matcher is a public export any Node server can use.
  • createInstance() in virtual:verbaly: fresh request-scoped instances over your compiled catalogs, plus locales and sourceLocale exports.
Full release notesEvery bullet, per package, in the repo changelog.

0.15.0

Release

Your translators can work now

The write→ship cycle opens up to humans: export translator-ready files, import them back validated.

Highlights
  • verbaly export writes one translator-ready file per locale: XLIFF 2.0 for translation platforms, CSV for spreadsheets. --missing exports only what’s left to translate.
  • verbaly import validates every entry: a translation that drops a placeholder, a variant or a tag is rejected and reported.
  • Existing translations are kept unless you pass --overwrite, and --dry-run previews the whole import.
  • No translation platform needed: catalogs are plain JSON, and Crowdin, Lokalise and Phrase read it natively.
Full release notesEvery bullet, per package, in the repo changelog.

0.14.5

Release

Revision of 0.14.0: the review release

A hard audit of what 0.14.0 shipped found three real defects. All fixed here.

Highlights
  • The verbaly/devtools overlay no longer freezes the page: its observer now ignores its own writes.
  • "" now means untranslated everywhere: the runtime falls back instead of rendering a blank hole.
  • Locale mirrors get self-canonical rel="canonical" and og:url, so the hreflang set finally counts.
Full release notesEvery bullet, per package, in the repo changelog.

0.14.0

Release

Ships translated, and you can see why

Multi-locale SEO for the static renderer, runtime observability, hardened server-side use.

Highlights
  • verbaly render emits reciprocal hreflang alternates and an i18n sitemap: set render.baseUrl and go.
  • Opt-in verbaly/devtools: hover any element to see its key, status and source locale.
  • New onResolve hook + inspect(key), with zero cost when unused.
  • Server-side Node use hardened and locked with tests.
Full release notesEvery bullet, per package, in the repo changelog.

0.13.1

Release

README refresh: readable on npm dark mode

Docs-only patch; first release published from CI with npm provenance.

Highlights
  • The seven package READMEs are readable on npm’s dark theme and friendlier to skim.
  • MIT re-affirmed; releases now ship with npm provenance from the automatic workflow.
Full release notesEvery bullet, per package, in the repo changelog.

0.13.0

Release

verbaly doctor + adoption & trust

One command answers “why isn’t this translating?”, and a real path off i18next.

Highlights
  • verbaly doctor inspects config, catalogs, plugin wiring, stale types and orphan keys, and gives each finding its exact fix.
  • New Migrate from i18next guide: keep your keys and JSON, swap the runtime page by page.
  • Publishing moves to GitHub Actions with npm provenance: every package is publicly linked to its commit.
Full release notesEvery bullet, per package, in the repo changelog.

0.12.0

Release

verbaly init + the TypeScript 7 toolchain

From zero to translating in one command; the build moves to tsdown and native TS 7.

Highlights
  • verbaly init scaffolds config + catalogs and detects your bundler. It never overwrites.
  • Toolchain: tsup → tsdown; typecheck on native TypeScript 7 (~10× faster). No consumer-visible change.
Full release notesEvery bullet, per package, in the repo changelog.

0.11.0

Release

Rich links + the full modern Intl surface

Named links with hrefs that never come from catalogs, and the last missing Intl formatters.

Highlights
  • Named links in rich messages render real <a> elements. The href comes from your code, never the catalog, and unsafe schemes are blocked.
  • Relative time ({when:relative}), lists ({xs:list}) and CLDR units ({n:unit/kilometer}): all native Intl, zero dependencies.
  • License: Apache-2.0 → MIT across all seven packages.
Full release notesEvery bullet, per package, in the repo changelog.

0.10.0

Release

Static sites ship translated + Verbaly beyond Vite

The FOUC dies on SSG, and webpack/Rollup/esbuild/Rspack join via unplugin.

Highlights
  • verbaly render pre-fills your built HTML per locale with the real runtime, so static pages ship already translated.
  • New @verbaly/unplugin: the same virtual modules + build gate on webpack 5, Rollup, esbuild and Rspack.
  • verbaly pseudo: a QA locale (⟦Ĥéĺĺó ~⟧) that exposes hardcoded strings and clipped layouts.
  • <Trans> lands in Svelte.
Full release notesEvery bullet, per package, in the repo changelog.

0.9.0

Release

Machine translation closes the loop

write → extract → translate → check green: the whole cycle in one tool.

Highlights
  • verbaly translate fills the "" holes check reports: batched, with --dry-run and per-locale filters.
  • Structural validation: placeholders, variants and tags must survive verbatim or the translation is rejected.
  • Pluggable providers: Claude as the reference, and any (messages) => translations function works. Zero runtime impact.
Full release notesEvery bullet, per package, in the repo changelog.

0.8.0

Release

Readable keys + lazy catalogs

Human-readable ids when you want them, per-locale lazy loading when you need it.

Highlights
  • t.id('inbox.title')`Hello ${name}`: readable, namespaced keys; hashed stays the zero-config default.
  • loaders in createVerbaly: bundle only the source locale and load the rest on demand, flash-free with await loadLocale.
Full release notesEvery bullet, per package, in the repo changelog.

0.7.0

Release

Svelte joins the ecosystem

Idiomatic stores over the same reactive core, for Svelte 4 & 5.

Highlights
  • @verbaly/svelte: $t('key') re-renders on locale switch, and bind:value={$locale} just works.
  • Context via provideVerbaly/useVerbaly or store factories without a component tree.
Full release notesEvery bullet, per package, in the repo changelog.

0.6.0

Release

The compiler understands your whole codebase

Write JSX in place and the compiler extracts it.

Highlights
  • <Trans> write-in-place extraction: source text with real elements becomes key + components automatically.
  • verbaly.config.ts / .mts support (esbuild as optional peer).
Full release notesEvery bullet, per package, in the repo changelog.

0.5.0

Release

Hardening + performance, with receipts

Benchmarks vs i18next run on every release from here on.

Highlights
  • Hot path memoized end to end: lookup 31×, interpolation 10×, plurals 5× vs i18next.
  • Defensibility audit: cached fallback chains, per-element DOM args, capped AST cache.
Full release notesEvery bullet, per package, in the repo changelog.

0.4.0

Release

The DOM interpreter catches up

Rich text in plain HTML, safely, plus locale bootstrap helpers.

Highlights
  • data-verbaly-rich: tagged messages render real elements from a phrasing whitelist, never innerHTML.
  • resolveLocale / persistLocale: storage → navigator → fallback, SSR-safe.
Full release notesEvery bullet, per package, in the repo changelog.

0.3.0

Release

ICU escape-hatch + robustness

Bring your ICU strings: auto-detected, zero dependencies.

Highlights
  • ICU MessageFormat (plural, select, selectordinal) parses into the same engine, opt-in per message.
  • Hardened <Trans> tokenizer + build-time key-collision warnings.
Full release notesEvery bullet, per package, in the repo changelog.

0.2.0

Release

Ecosystem public + rich text

First npm publish of the ecosystem packages.

Highlights
  • @verbaly/compiler, @verbaly/vite, @verbaly/react and @verbaly/vue land on npm, version-aligned.
  • <Trans> in React and Vue: interpolate elements inside translated sentences with named tags.
Full release notesEvery bullet, per package, in the repo changelog.

0.1.0

Release

The first release

The core idea, working: write text, ship translations.

Highlights
  • Runtime: message format over native Intl, reactive locale store, DOM interpreter, type-level keys and params.
  • Compiler: AST extraction of t`…` into stable hashed keys, extract/check, typed verbaly.d.ts.
  • Vite plugin: live extraction, HMR, per-locale code-splitting, builds fail on missing translations.
Full release notesEvery bullet, per package, in the repo changelog.
Copied to clipboard