Runtime SDKs

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-dom

The package has three entry points:

ImportUse it inExports
@transglot/astroastro.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/islandReact component filesTransglotIsland, and TransglotProvider, useTranslations, T from @transglot/react-i18n
@transglot/astro/middlewareRegistered for you by the integrationonRequest

Setup

Add the integration to your Astro config:

astro.config.mjs
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:

src/pages/index.astro
---
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.

src/components/Menu.tsx
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>
  );
}
src/components/MenuIsland.tsx
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>
  );
}
src/pages/index.astro
---
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 of snapshot. Switching between them needs no request.
  • Pass a loader. With CDN coordinates in loader, 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. Set authIn: '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:

src/middleware.ts
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:

src/middleware.ts
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.

On this page