i18n-keyless with Next.js (App Router)
The App Router (like Astro islands) has no single render hook to wrap, so
there's no runWithI18nKeyless here. You drive the component path with
<I18nKeylessProvider> (so <T> is SSR-correct), and seed the client store with
hydrateFromServer in a client boundary.
Read the SSR overview first.
Imperative getTranslation() in server output renders the primary language and
resolves to lang only after the client effect — prefer <T> for zero flash. This
guide mirrors the runnable examples/nextjs app.
1. Initialize once (server + client helpers)
// src/i18n.ts
import { init, type Lang } from "i18n-keyless-react";
export const SUPPORTED_LANGUAGES = ["fr", "en", "es"] as const;
const config = {
API_KEY: process.env.NEXT_PUBLIC_I18N_KEYLESS_API_KEY,
languages: { primary: "fr", supported: [...SUPPORTED_LANGUAGES] },
};
export const initI18nServer = () => init({ ...config }); // no storage
export const initI18nClient = () => init({ ...config, storage: window.localStorage });
export const normalizeLang = (value?: string | null): Lang =>
(SUPPORTED_LANGUAGES as readonly string[]).includes(value ?? "") ? (value as Lang) : "fr";
2. Fetch translations in the localized layout
app/[lang]/layout.tsx is a Server Component. Fetch the language's translations and hand
them to a client boundary. Next serializes them into the RSC payload automatically — no
manual <script>:
// src/app/[lang]/layout.tsx
import type { ReactNode } from "react";
import { getServerTranslations } from "i18n-keyless-react";
import { initI18nServer, normalizeLang, SUPPORTED_LANGUAGES } from "../../i18n";
import { Providers } from "../Providers";
export function generateStaticParams() {
return SUPPORTED_LANGUAGES.map((lang) => ({ lang }));
}
export default async function LangLayout({
children,
params,
}: {
children: ReactNode;
params: Promise<{ lang: string }>;
}) {
const lang = normalizeLang((await params).lang);
await initI18nServer();
const translations = await getServerTranslations(lang);
return (
<html lang={lang}>
<body>
<Providers lang={lang} translations={translations}>{children}</Providers>
</body>
</html>
);
}
3. The client boundary: Provider + hydrateFromServer
// src/app/Providers.tsx
"use client";
import { useEffect, type ReactNode } from "react";
import { I18nKeylessProvider, hydrateFromServer, type Translations } from "i18n-keyless-react";
import { initI18nClient } from "../i18n";
export function Providers({
lang,
translations,
children,
}: {
lang: string;
translations: Translations;
children: ReactNode;
}) {
// <I18nKeylessProvider> makes <T> SSR-correct (React context, available while Next
// server-renders client components). hydrateFromServer + initI18nClient run in an effect:
// getTranslation() renders the PRIMARY language on the server and the first client render
// (no mismatch), then resolves to `lang` after this effect.
useEffect(() => {
hydrateFromServer({ lang: lang as never, translations });
initI18nClient();
}, [lang, translations]);
return (
<I18nKeylessProvider lang={lang as never} translations={translations}>
{children}
</I18nKeylessProvider>
);
}
Now <T> / <I18nKeylessText> anywhere under [lang] is SSR-correct and flash-free.
getTranslation() in Server ComponentsThere's no render hook to wrap in the App Router, so getTranslation() evaluated during
the server render returns the primary language and resolves to the target language only
after the client effect (a brief flash on a cold cache). Prefer <I18nKeylessText> / <T>
for server-rendered text. (Pages Router isn't covered by the example app; the same client
boundary works there, fed from getServerSideProps.)
Checklist
-
initvia server (nostorage) and client (withstorage) helpers. -
app/[lang]/layout.tsxfetchesgetServerTranslations(lang)and renders<Providers>. -
Providersis a"use client"boundary:<I18nKeylessProvider>+hydrateFromServer/initI18nClientin an effect. - Prefer
<T>over imperativegetTranslation()for server-rendered text.