@transglot/astro
v0.1.0
Published
SSR-aware Astro integration over @transglot/runtime: a per-request getLocaleBundle() (+ middleware) that returns a serializable snapshot for .astro server rendering, and a hydration-safe <TransglotIsland> (a React island wrapper of @transglot/react-i18n)
Downloads
136
Maintainers
Readme
@transglot/astro
SSR-aware Astro integration over
@transglot/runtime. Fetch a
locale's published bundle per request on the server, render translated
.astro HTML from it, and hand the same snapshot to a React island so
hydration has nothing to refetch and nothing to flash.
- Server entry
@transglot/astrohas no React import, so it is safe in.astrofrontmatter, a route handler, middleware, andastro.config. - Client entry
@transglot/astro/islandis the React island wrapper:TransglotIsland, plususeTranslationsand<T>re-exported from@transglot/react-i18n. - Middleware entry
@transglot/astro/middlewareis the auto-wiredonRequestthe integration registers for you. You rarely import it directly.
astro is an optional peer dependency: the Astro types this package needs are
declared structurally, so it type-checks and builds without Astro installed.
Install
npm install @transglot/astro @transglot/runtimeThe island entry additionally needs React and Astro's React renderer:
npm install @astrojs/react react react-domQuickstart: the integration
Add transglot() to astro.config. It exposes your CDN coordinates to the rest
of the build and registers a per-request middleware that seeds each request's
bundle onto Astro.locals.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import { transglot } from '@transglot/astro';
export default defineConfig({
integrations: [
react(),
transglot({
baseUrl: 'https://app.example.com',
project: 42,
cdnKey: process.env.TRANSGLOT_CDN_KEY,
defaultLocale: 'en',
}),
],
});| option | required | meaning |
| --- | --- | --- |
| baseUrl | yes | origin of the app / CDN, e.g. https://app.example.com |
| project | yes | numeric project id (the CDN URL segment) |
| cdnKey | yes | read-only taicdn_… key |
| defaultLocale | no | locale the middleware loads when nothing else resolves one |
| version | no | pin every request to an immutable …/v{n} instead of "latest" |
| authIn | no | present the key in a header (default) or the query |
| localsKey | no | the Astro.locals key for the snapshot (default transglot) |
| middleware | no | set false to wire createTranslationMiddleware yourself |
Render translated .astro HTML
Read the per-request snapshot with readSnapshot, then build a synchronously
seeded client from it. createSeededClient parses the bundle at construction, so
client.t(...) returns translations on the very first (and only) server render.
---
import { readSnapshot, createSeededClient } from '@transglot/astro';
const snapshot = readSnapshot(Astro.locals);
const client = snapshot ? createSeededClient({ snapshots: snapshot }) : null;
---
<h1>{client?.t('home.title') ?? 'home.title'}</h1>
<p>{client?.t('cart.items', { count: 3 })}</p>Not using the middleware? Load a bundle explicitly instead. getLocaleBundle
returns the same serializable TranslateSnapshot:
---
import { getLocaleBundle } from '@transglot/astro';
const snapshot = await getLocaleBundle({
baseUrl: 'https://app.example.com',
project: 42,
cdnKey: import.meta.env.TRANSGLOT_CDN_KEY,
locale: Astro.currentLocale ?? 'en',
});
---Hydrate an island from the same snapshot
Pass the snapshot as a prop to <TransglotIsland> and mark it with a client:*
directive. Its first render, server-side and on hydration, is already translated,
because it seeds from the snapshot the surrounding HTML rendered from.
---
import { readSnapshot } from '@transglot/astro';
import { TransglotIsland } from '@transglot/astro/island';
import Menu from '../components/Menu'; // a React island calling useTranslations()
const snapshot = readSnapshot(Astro.locals)!;
---
<TransglotIsland client:load snapshot={snapshot}>
<Menu />
</TransglotIsland>// src/components/Menu.tsx
import { useTranslations } from '@transglot/astro/island';
export default function Menu() {
const { t, locale, setLocale } = useTranslations();
return (
<nav>
<span>{t('nav.home')}</span>
<button onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')}>{locale}</button>
</nav>
);
}Seed several locales at once with snapshots={[en, fr]} and switching between
them is instant, with no network at all. To let a visitor switch to a locale you
did not seed, give the island a loader (the CDN coordinates) and it fetches
that locale on the client:
<TransglotIsland
client:load
snapshot={snapshot}
loader={{ baseUrl, project: 42, cdnKey }}
/>Without a loader the client is seed-only: switching to an unseeded locale is a
no-op that keeps the current locale (and warns in dev) rather than rendering raw
keys. Note that a client-side loader puts the read-only cdnKey in the browser;
omit it if you would rather keep the key server-side and reload the page instead.
The client is built once per mount (a useState initializer). To swap the
snapshot on navigation, remount the island with a React key.
API
Server (@transglot/astro)
transglot(options): the Astro integration. Onastro:config:setupit publishes the resolved config through a virtual module and, unlessmiddleware: false, registers the per-request middleware.getLocaleBundle(options)→Promise<TranslateSnapshot>: fetches and validates one locale's published bundle. It drives the runtime client for the entire wire contract (URL shape, key auth,Retry-Afterbackoff, RFC 7807 errors, format detection and parse validation), so failures arrive as the runtime's typedRuntimeError.createTranslationMiddleware(options): the middleware factory, if you want to wire it as your ownsrc/middleware.ts. TakesresolveLocale(context)to pick the locale from a URL, cookie or header; falls back to Astro'scontext.currentLocale, thendefaultLocale. Resolves nothing, does nothing.readSnapshot(locals, localsKey?)→ the snapshot the middleware stored, orundefinedwhen it did not run or resolved no locale.createSeededClient({ snapshots, locale?, loadSnapshot?, dev? })→ aRuntimeClientwhose cache is filled synchronously from the snapshots. It has no background refresh and no persistent storage:hydratereports whether a locale is already seeded,refreshre-runsloadLocalefor the active locale, andstophas no timer to detach.DEFAULT_LOCALS_KEY,VIRTUAL_CONFIG_ID, and the runtime re-exportscreateClient,RuntimeError,parseBundle,interpolate.
Client (@transglot/astro/island)
<TransglotIsland snapshot|snapshots locale? loader?>: seeds a client from the server snapshot and provides it to@transglot/react-i18n.useTranslations(),<T keypath params>,TransglotProvider: re-exported from@transglot/react-i18nso an island file needs only one import.
The snapshot carries the raw bundle body plus its delivery format, version and
ETag. It deliberately does not carry the cdnKey: seeding never refetches the
seeded locale, so the key need not cross to the browser.
Missing keys return the key (never throw); network and HTTP errors surface as the
runtime's typed RuntimeError. The full runtime client contract is documented in
@transglot/runtime.
