Skip to main content

i18n-keyless with Next.js

Next.js has two routers with different SSR models. The integration differs, so pick your section.

tip

Read the SSR overview first. This guide assumes you know what getServerTranslations, runWithI18nKeyless, and <I18nKeylessProvider> do.

Initialize once

Call init in a module that runs on both server and client — e.g. an i18n.ts you import from your root layout/_app. Omit storage on the server:

import { init } from "i18n-keyless-react";

await init({
languages: { primary: "en", supported: ["en", "fr", "de"] },
API_KEY: process.env.NEXT_PUBLIC_I18N_KEYLESS_API_KEY,
storage: typeof window === "undefined" ? undefined : window.localStorage,
ssr: true, // explicit read-only on serverless cold starts
});

For primary-language SSR, that's all. The rest is for localized, indexable HTML.

Use a localized segment, app/[lang]/layout.tsx. Fetch the translations on the server and render <I18nKeylessProvider> — a client component — around the tree. Next serializes the lang + translations props into the RSC payload and hydrates the same provider on the client, so it's flash-free with no manual <script> tag:

// app/[lang]/layout.tsx  (Server Component)
import { getServerTranslations, I18nKeylessProvider } from "i18n-keyless-react";

export default async function LangLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const translations = await getServerTranslations(lang);

return (
<I18nKeylessProvider lang={lang} translations={translations}>
{children}
</I18nKeylessProvider>
);
}

Any client component ("use client") below this layout — where the bulk of your interactive <T> text lives — now renders in lang and hydrates without a flash.

Using the getTranslation(key) function form?

The Provider seeds the store on mount (after the first render), which is correct for the <T> / <I18nKeylessText> component form but too late for the getTranslation function form on a cold cache — it would blink. To fix it, seed the store synchronously with hydrateFromServer({ lang, translations }) (≥ 2.2.0) from a top-of-tree client component that runs before paint, embedding the same translations you fetched in the layout. See Seed getTranslation synchronously. Prefer the component form in the App Router where you can.

Text rendered in Server Components

React context doesn't cross into child Server Components, so the Provider alone won't set the language for getTranslation(...) called directly inside an RSC. For server-rendered text, wrap that unit of work in runWithI18nKeyless so the request scope is active while it renders:

import { getServerTranslations, runWithI18nKeyless } from "i18n-keyless-react";

export default async function Page({ params }) {
const { lang } = await params;
const translations = await getServerTranslations(lang);
return runWithI18nKeyless({ lang, translations }, () => <ServerHeading />);
}

The simplest rule of thumb: keep translated text in client components and let the Provider handle it; reach for runWithI18nKeyless only when you specifically need translated text emitted by a Server Component.

Static params (optional)

Pre-render each language at build time:

import { getSupportedLanguages } from "i18n-keyless-react";

export function generateStaticParams() {
return getSupportedLanguages().map((lang) => ({ lang }));
}

Pages Router

In the Pages Router, fetch translations in getServerSideProps (or getStaticProps per locale) and feed them to <I18nKeylessProvider> in _app.tsx:

// pages/_app.tsx
import { I18nKeylessProvider } from "i18n-keyless-react";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
const { lang, translations, ...rest } = pageProps;
return (
<I18nKeylessProvider lang={lang} translations={translations}>
<Component {...rest} />
</I18nKeylessProvider>
);
}
// any page
import { getServerTranslations } from "i18n-keyless-react";
import type { GetServerSideProps } from "next";

export const getServerSideProps: GetServerSideProps = async ({ locale = "en" }) => {
const translations = await getServerTranslations(locale);
return { props: { lang: locale, translations } };
};

Next serializes pageProps, so the provider hydrates with the same data — flash-free.

Cloudflare / edge runtime

runWithI18nKeyless (used for Server-Component text) needs AsyncLocalStorage. On edge runtimes where it isn't available, the scope degrades to a no-op — the Provider path keeps working for client components.

Checklist

  • init runs on server and client (no storage on the server).
  • App Router: <I18nKeylessProvider> in app/[lang]/layout.tsx with server-fetched translations.
  • Pages Router: translations from getServerSideProps<I18nKeylessProvider> in _app.tsx.
  • Server-Component text (App Router only) wrapped in runWithI18nKeyless.