Next.js
@transglot/next: load a published bundle on the server and hydrate the client from it, so the first render is already translated.
@transglot/next fetches a locale's published bundle on the server, hands your page a serializable snapshot, and seeds a client provider from it. Server-rendered HTML contains translations, and the browser does not fetch the seeded locale again.
It works with the App Router and the Pages Router, on Next.js 14 or 15 with React 18 or 19.
Install
npm install @transglot/next@transglot/runtime and @transglot/react-i18n are installed with it. The package has two entry points:
| Import | Runs on | Exports |
|---|---|---|
@transglot/next | Server and client. Has no React import, so it is safe in Server Components, route handlers and getServerSideProps. | loadTranslations, createSeededClient, and createClient, RuntimeError, parseBundle, interpolate from the runtime |
@transglot/next/client | Client. Marked 'use client'. | NextTranslateProvider, and TransglotProvider, useTranslations, T from @transglot/react-i18n |
Keep the CDN key in a server environment variable, for example TRANSGLOT_CDN_KEY. The snapshot does not contain it.
App Router
Load the snapshot in a Server Component and render the provider around your tree:
import { loadTranslations } from '@transglot/next';
import { NextTranslateProvider } from '@transglot/next/client';
import type { ReactNode } from 'react';
export default async function LocaleLayout({
children,
params,
}: {
children: ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const snapshot = await loadTranslations({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: process.env.TRANSGLOT_CDN_KEY!,
locale,
});
return (
<html lang={locale}>
<body>
<NextTranslateProvider key={locale} snapshot={snapshot}>
{children}
</NextTranslateProvider>
</body>
</html>
);
}In Next.js 14, params is a plain object: type it as { locale: string } and drop the await.
Translate in Client Components with useTranslations or T:
'use client';
import { T, useTranslations } from '@transglot/next/client';
export function Header() {
const { t } = useTranslations();
return (
<header>
<h1>{t('home.title')}</h1>
<T keypath="home.greeting" params={{ name: 'Ada' }} />
</header>
);
}The provider builds its client once per mount. The key={locale} remounts it when the route moves to another locale, so it picks up the new snapshot.
In a Server Component
Hooks do not run in Server Components. Build a seeded client from the snapshot and call its t directly:
import { createSeededClient, loadTranslations } from '@transglot/next';
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const snapshot = await loadTranslations({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: process.env.TRANSGLOT_CDN_KEY!,
locale,
});
const { t } = createSeededClient({ snapshots: snapshot, dev: false });
return <h1>{t('home.title')}</h1>;
}Pages Router
import { loadTranslations, type TranslateSnapshot } from '@transglot/next';
import { NextTranslateProvider, useTranslations } from '@transglot/next/client';
import type { GetServerSideProps } from 'next';
type Props = { snapshot: TranslateSnapshot };
export const getServerSideProps: GetServerSideProps<Props> = async ({ locale }) => ({
props: {
snapshot: await loadTranslations({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: process.env.TRANSGLOT_CDN_KEY!,
locale: locale ?? 'en',
}),
},
});
function Title() {
const { t } = useTranslations();
return <h1>{t('home.title')}</h1>;
}
export default function Home({ snapshot }: Props) {
return (
<NextTranslateProvider key={snapshot.locale} snapshot={snapshot}>
<Title />
</NextTranslateProvider>
);
}Switching locale in the browser
setLocale from useTranslations switches between locales the provider already holds. You have two ways to give it more than one:
-
Seed several locales. Load a snapshot for each and pass them as
snapshots. Switching between them needs no network request.const snapshots = await Promise.all( ['en', 'fr', 'de'].map((locale) => loadTranslations({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, locale, }), ), ); <NextTranslateProvider snapshots={snapshots} locale="en"> {children} </NextTranslateProvider>; -
Pass a
loader. With CDN coordinates inloader, switching to a locale that was not seeded fetches it from the browser. The loader's CDN key is a prop, so it is sent to the browser. A CDN key is read-only and reaches only published bundles, but if you do not want it in the page, seed every locale instead. SetauthIn: 'query', because the CDN does not answer the CORS preflight a header would need (see In a browser).<NextTranslateProvider snapshot={snapshot} loader={{ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, authIn: 'query', }} > {children} </NextTranslateProvider>
Without a loader, switching to a locale that was not seeded keeps the current locale and logs a dev warning.
Freshness
The seeded client does not poll and does not revalidate the seeded locale: its refresh() does nothing for a locale it already holds. New publishes reach users when the server runs loadTranslations again. How often that happens depends on how Next.js renders and caches the route.
Next.js extends fetch with its own caching options. To decide how long the server reuses a bundle, pass a fetchImpl that sets them:
const snapshot = await loadTranslations({
baseUrl: 'https://api.transglot.ai',
project: 42,
cdnKey: process.env.TRANSGLOT_CDN_KEY!,
locale,
fetchImpl: (input, init) => fetch(input, { ...init, next: { revalidate: 60 } }),
});API
loadTranslations(options)
loadTranslations(options: LoadTranslationsOptions): Promise<TranslateSnapshot> fetches one locale with the runtime client (the same URL, authentication, 429 retries and error mapping) and returns the raw body with its headers.
Prop
Type
TranslateSnapshot
Every field is a JSON primitive, so the snapshot crosses the Server Component boundary and survives getServerSideProps.
Prop
Type
NextTranslateProvider
Pass exactly one of snapshot or snapshots.
Prop
Type
Inside the provider, useTranslations() and T work as described on the React page.
createSeededClient(options)
createSeededClient(options: CreateSeededClientOptions): RuntimeClient returns a client whose cache is filled synchronously from snapshots, so its first t call already returns translations. NextTranslateProvider uses it; you can use it directly in Server Components or with your own TransglotProvider.
Prop
Type
The returned client has the full RuntimeClient shape. hydrate(locale?) resolves true when the locale is seeded, and stop() does nothing.
Errors
loadTranslations rejects with the runtime's RuntimeError, with the slugs listed on the JavaScript runtime page. Catch it to render a fallback, or let it reach your error boundary.
createSeededClient throws:
Error('[transglot] createSeededClient() requires at least one snapshot.')for an emptysnapshotsarray.RuntimeErrorwith the slugunsupported-formatwhen a snapshot body is not a JSON-family bundle. Set the project's Delivery format tojson_flat,json_nested,laravel_jsonorflutter_arb.