Astro
@transglot/astro: an Astro integration that loads a published bundle per request, renders translated .astro HTML, and seeds React islands from the same snapshot.
@transglot/astro fetches a locale's published bundle on the server, stores it on Astro.locals, and lets both your .astro markup and your React islands read it. Islands render translated HTML on the server and hydrate without fetching the seeded locale again.
It supports Astro 4 and 5. The island entry uses React 18 or 19.
Install
npm install @transglot/astro@transglot/runtime and @transglot/react-i18n are installed with it. For React islands, add Astro's React renderer:
npm install @astrojs/react react react-domThe package has three entry points:
| Import | Use it in | Exports |
|---|---|---|
@transglot/astro | astro.config, .astro frontmatter, middleware, endpoints. Has no React import. | transglot, getLocaleBundle, createTranslationMiddleware, readSnapshot, createSeededClient, DEFAULT_LOCALS_KEY, VIRTUAL_CONFIG_ID, and createClient, RuntimeError, parseBundle, interpolate from the runtime |
@transglot/astro/island | React component files | TransglotIsland, and TransglotProvider, useTranslations, T from @transglot/react-i18n |
@transglot/astro/middleware | Registered for you by the integration | onRequest |
Setup
Add the integration to your Astro config:
import react from '@astrojs/react';
import { transglot } from '@transglot/astro';
import { defineConfig } from 'astro/config';
export default defineConfig({
integrations: [
react(),
transglot({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: process.env.TRANSGLOT_CDN_KEY,
defaultLocale: 'en',
}),
],
});The integration registers a middleware that runs before your own. For each request it picks a locale (Astro's currentLocale when you use Astro's i18n routing, otherwise defaultLocale), fetches that locale's bundle, and stores the snapshot on Astro.locals.transglot. If no locale resolves, it does nothing.
Translate in .astro files
Read the snapshot and build a seeded client. Its t returns translations straight away:
---
import { createSeededClient, readSnapshot } from '@transglot/astro';
const snapshot = readSnapshot(Astro.locals);
const { t } = createSeededClient({ snapshots: snapshot!, dev: false });
---
<h1>{t('home.title')}</h1>
<p>{t('cart.items', { count: 3 })}</p>Translate in a React island
Put TransglotIsland and the components that call useTranslations in the same React component, and hydrate that component. Astro renders children passed to an island from an .astro file as separate static HTML, outside the island's React tree, so they would not see the provider.
import { useTranslations } from '@transglot/astro/island';
export function Menu() {
const { t, locale, setLocale } = useTranslations();
return (
<nav>
<a href="/">{t('nav.home')}</a>
<button onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')}>{locale}</button>
</nav>
);
}import type { TranslateSnapshot } from '@transglot/astro';
import { TransglotIsland } from '@transglot/astro/island';
import { Menu } from './Menu';
export default function MenuIsland({ snapshot }: { snapshot: TranslateSnapshot }) {
return (
<TransglotIsland snapshot={snapshot}>
<Menu />
</TransglotIsland>
);
}---
import { readSnapshot } from '@transglot/astro';
import MenuIsland from '../components/MenuIsland';
const snapshot = readSnapshot(Astro.locals)!;
---
<MenuIsland client:load snapshot={snapshot} />The snapshot holds the bundle and its headers, not the CDN key, so passing it to an island does not expose the key.
Switching locale in an island
setLocale switches between locales the island already holds:
- Seed several locales. Pass
snapshots(an array) instead ofsnapshot. Switching between them needs no request. - Pass a
loader. With CDN coordinates inloader, the island fetches a locale that was not seeded from the browser. The key is then part of the island's props and reaches the browser. SetauthIn: 'query', because the CDN does not answer the CORS preflight a header would need (see In a browser).
Without a loader, switching to a locale that was not seeded keeps the current locale and logs a dev warning. The island builds its client once per mount; to give it a new snapshot, remount it with a React key.
Freshness
The middleware fetches the bundle each time it handles a request, and the seeded client does not poll. On pages rendered on demand, a new publish shows up on the next request. On prerendered pages, Astro runs the middleware while it builds, so the HTML keeps the bundle from build time until you rebuild.
Every fetch goes to the CDN, which rate-limits reads per key and client address. For busy on-demand pages, wire your own middleware with a fetchImpl that caches responses.
API
transglot(options)
The Astro integration. Throws [transglot] transglot() requires { baseUrl, project, cdnKey }. when one of those is missing.
Prop
Type
The integration also exposes its options to your build as the virtual module virtual:@transglot/astro/config (the VIRTUAL_CONFIG_ID constant), which the built-in middleware reads.
createTranslationMiddleware(options)
Builds the same middleware, for src/middleware.ts. Use it with middleware: false when you need your own locale resolution or a custom fetch:
import { createTranslationMiddleware } from '@transglot/astro';
export const onRequest = createTranslationMiddleware({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: import.meta.env.TRANSGLOT_CDN_KEY,
defaultLocale: 'en',
resolveLocale: (context) => context.url?.searchParams.get('lang') ?? undefined,
});The locale is the first of: resolveLocale(context), Astro's context.currentLocale, defaultLocale.
Prop
Type
readSnapshot(locals, localsKey?)
readSnapshot(locals: Record<string, unknown>, localsKey?: string): TranslateSnapshot | undefined returns the snapshot the middleware stored, or undefined when it did not run or resolved no locale.
getLocaleBundle(options)
getLocaleBundle(options: GetLocaleBundleOptions): Promise<TranslateSnapshot> fetches one locale yourself, without the middleware. Its options are baseUrl, project, cdnKey, locale, and optionally version, authIn, fetchImpl and dev (default false).
---
import { getLocaleBundle } from '@transglot/astro';
const snapshot = await getLocaleBundle({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: import.meta.env.TRANSGLOT_CDN_KEY,
locale: Astro.currentLocale ?? 'en',
});
---TranslateSnapshot has the same fields as in the Next.js package. See TranslateSnapshot.
createSeededClient(options)
Returns a RuntimeClient filled synchronously from snapshots, with the options snapshots, locale, loadSnapshot and dev. It behaves as described in createSeededClient.
TransglotIsland
Pass exactly one of snapshot or snapshots.
Prop
Type
Inside it, useTranslations() and T work as described on the React page.
Errors
getLocaleBundle and the middleware reject with the runtime's RuntimeError (see Errors). A failure in the middleware fails the request, so Astro renders its error page. To degrade instead, wire your own middleware that catches it:
import { RuntimeError, getLocaleBundle } from '@transglot/astro';
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
try {
context.locals.transglot = await getLocaleBundle({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: import.meta.env.TRANSGLOT_CDN_KEY,
locale: context.currentLocale ?? 'en',
});
} catch (error) {
if (!(error instanceof RuntimeError)) throw error;
console.error(error.slug, error.hint);
}
return next();
});With this middleware, readSnapshot can return undefined, so check it before seeding a client.