Runtime SDKs

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:

ImportRuns onExports
@transglot/nextServer 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/clientClient. 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:

app/[locale]/layout.tsx
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:

app/[locale]/header.tsx
'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:

app/[locale]/page.tsx
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

pages/index.tsx
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 in loader, 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. Set authIn: '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 empty snapshots array.
  • RuntimeError with the slug unsupported-format when a snapshot body is not a JSON-family bundle. Set the project's Delivery format to json_flat, json_nested, laravel_json or flutter_arb.

On this page