i18n-keyless with Next.js
Next.js has two routers with different SSR models. The integration differs, so pick your section.
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.
App Router (recommended)
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.
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.
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.
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
-
initruns on server and client (nostorageon the server). - App Router:
<I18nKeylessProvider>inapp/[lang]/layout.tsxwith server-fetched translations. - Pages Router: translations from
getServerSideProps→<I18nKeylessProvider>in_app.tsx. - Server-Component text (App Router only) wrapped in
runWithI18nKeyless.