Skip to content

Integrations

Astro

The official Astro integration. One line in astro.config wires live extraction, typed keys and the build gate; if you use the pre-translated mirror flow, verbaly render runs by itself after every build.

What you can accomplish:

Installation

Install the package with your preferred package manager.

 pnpm add verbaly @verbaly/astro

Wire it up

astro.config.mjs
import { defineConfig } from 'astro/config';
import verbaly from '@verbaly/astro';

export default defineConfig({
  integrations: [verbaly()],
});

That single line injects the Vite plugin with your project root pinned: live extraction while you code, the typed virtual:verbaly module and the build gate. The generated types live inside Astro's own .astro folder, no file added to your project. Options are the same as the Vite plugin (locales, sourceLocale, dir, failOnMissing…), inline or in your verbaly.config.

Write the text in place

src/pages/index.astro
---
import { createRequestInstance } from 'virtual:verbaly';
const { t } = await createRequestInstance('en');
const title = t`Search and find`;
---

<h1>{title}</h1>
<p>{t`Hello ${name}, you have ${count} messages`}</p>
<img alt={t`Company logo`} src="/logo.png" />

The compiler reads your .astro files like any other source: the frontmatter and the markup expressions both extract, every message becomes a stable key with typed params, and an untranslated entry fails the build before it ships.

Two ways to ship the languages

Astro sites are static by default, so the language has to be decided at build time. Pick the flow that matches your routing:

Path-based pages

If Astro's i18n routing owns your URLs (/es/…, /pt/…), build one instance per request and use t as usual. Astro owns the routes, Verbaly owns the catalogs and the type safety. createRequestInstance waits for the catalog before it returns, which is what makes the page arrive translated instead of flashing, and Astro.currentLocale is empty outside a localized route, so the fallback is not decoration.

src/pages/[locale]/index.astro
---
import { createRequestInstance, sourceLocale } from 'virtual:verbaly';
const { t } = await createRequestInstance(Astro.currentLocale ?? sourceLocale);
---

Mirror mode

One page tree, pre-translated in every language. Bind your markup with data-verbaly attributes and add a render section to your config: after astro build, the integration mirrors the site into dist/es/, dist/pt/… with every message pre-filled, plus hreflang alternates and a per-locale sitemap when you set baseUrl. No flash of untranslated content and no client work.

verbaly.config.mjs
export default {
  locales: ['en', 'es', 'pt'],
  render: { baseUrl: 'https://example.com', sitemap: true },
};

The sitemap holds the pages a search engine should index. Your redirects and your 404 stay out of it, because they say so themselves, and they are still mirrored into every language so a visitor who reaches one gets it there. Leave more out with render.exclude.

A mirrored site can go further: text that only one page shows does not need to reach the browser at all, because the mirror already wrote it into the HTML. Name those groups under bundle and every other page stops downloading them.

verbaly.config.mjs
export default {
  locales: ['en', 'es', 'pt'],
  bundle: { exclude: ['changelog'] },
  render: { baseUrl: 'https://example.com', sitemap: true },
};

Each page brings only its own words

A mirrored page arrives already translated, and yet the visitor still downloads every message of that language, most of which the page never shows. Turn on inlineCatalog and the mirror writes into each page exactly the messages it renders, so the page needs nothing else.

verbaly.config.mjs
export default {
  locales: ['en', 'es', 'pt'],
  render: { baseUrl: 'https://example.com', inlineCatalog: true },
};

On this site the Spanish messages weigh 53 KB compressed. The home page uses 2.8 KB of them, so a first visit stops downloading about 50 KB, which is roughly nine times what the whole Verbaly runtime costs. The heaviest page here is the changelog, and even that one saves 32 KB.

If your page later asks for a message it did not show, Verbaly fetches the rest once and updates, rather than quietly showing your source language. Your source pages get nothing extra: their messages already travel with the app.

With <ClientRouter /> this keeps working as the visitor clicks around, and you write no code for it: the integration hands the runtime each page's own messages as that page swaps in.

to moveEnterto open
Copied to clipboard