Next.js
Next.js integration for Intl-T
Overview
intl-t offers special integration with Next.js for server-side rendering and routing:
For Static Rendering you will need to generate static params for each locale.
In dynamic pages with just await getTranslation() you can get the translation with current locale from headers.
getTranslation also has getTranslations as an alias.
Note:
intl-t/nextis for Next.js App with RSC. For Next.js Pages you should useintl-t/reactinstead, andintl-t/navigationfor Next.js Navigation and Routing tools.
// Important: use intl-t/next or @intl-t/next
import { createTranslation } from "intl-t/next";
import en from "./messages/en.json";
import es from "./messages/es.json";
export const { Translation, useTranslation, getTranslation } = await createTranslation({ locales: { en, es } });Navigation
Import createNavigation from intl-t/navigation and pass the allowed locales. Don't import createNavigation from intl-t/next in order to use it from middleware.
import { createNavigation } from "intl-t/navigation";
export const { middleware, Link, generateStaticParams } = createNavigation({ allowedLocales: ["en", "es"], defaultLocale: "en" });import { Translation } from "@/i18n/translation";
export { generateStaticParams } from "@/i18n/navigation";
interface Props {
params: Promise<{ locale: string }>;
children: React.ReactNode;
}
export default async function RootLayout({ children, params }: Props) {
const { locale } = await params;
if (!Translation.locales.includes(locale as typeof Translation.locale)) return;
return (
<html lang={locale}>
<body>
<Translation>{children}</Translation>
</body>
</html>
);
}That translation component is a React Server Component that handles the current locale and the corresponding translations to be sent to the client and its context.
Also, Translation will work too as a client-side translation component.
export { middleware as default } from "@/i18n/navigation";
export const config = {
// middleware matcher config
};If you need to customize your middleware or chain multiple middlewares, you can use the withMiddleware function to wrap your middleware in a chain.
import { createNavigation } from "intl-t/navigation";
export const { withMiddleware, Link, generateStaticParams, useRouter } = createNavigation({ allowedLocales, defaultLocale });
// middleware.ts
import { withMiddleware } from "intl-t/navigation";
function middleware(request, event) {
// do something
}
export default withMiddleware(middleware);withMiddleware and middleware both return the response. middleware function also can receive the response as the last argument, so you can configure it in a flexible way.
middleware(request, event, response);From createNavigation you can get:
middleware: Middleware function to be used inmiddleware.tsgenerateStaticParams: Function to generate static paramsuseRouter: React hook to get router config with bindedlocaleandpathnamevaluesLink: React component to create links with bindedlocaleandpathnamevaluesredirect: Binded Next.jsredirectfunctionpermanentRedirect: Binded Next.jspermanentRedirectfunctiongetLocale: Function to get current locale at serveruseLocale: React hook to get current localeusePathname: React hook to get current pathname without locale prefix if existgetPathname: Function to get current pathname without locale prefix if exist
Router Hook
useRouter hook is a wrapper for Next.js useRouter hook, but it will resolve the locale and pathname at client and server dynamically.
const router = useRouter();
router.push("/hello", { locale: "fr" }); // Handles automatically the locale
router.pathname; // "/fr/hello"
router.locale; // "fr"Pathname and locale are resolved through other hooks with getters, so you can use them dynmically when need, like old Next.js useRouter hook.
Resolvers Config
When creating navigation, you can configure the routing structure using resolvers like resolvePath and resolveHref to match the correct locale and path.
interface Config {
pathPrefix?: "always" | "default" | "optional" | "hidden";
pathBase?: "always-default" | "detect-default" | "detect-latest";
strategy?: "domain" | "param" | "headers";
redirectPath?: string;
}-
pathPrefix: Controls how the locale appears in the URL path."always": The locale is always included as a path prefix."default": The default locale is hidden in the path, while other locales are shown."optional": The locale prefix can be present or absent, depending on the accessed URL."hidden": The locale is never shown in the path prefix.
Default is"default".
-
pathBase: Determines the behavior when no locale is specified in the path."always-default": The path base/always routes to the default locale."detect-default": On the first visit, the user's locale is detected and redirected; subsequent visits at path base go to the default locale."detect-latest": On the first visit, the user's locale is detected and redirected; subsequent visits at path base go to the most recently used locale.
Default is"detect-default".
-
strategy: Specifies how to match the locale and path. The default is to use the[locale]param with Next.js, but you can determine it, including the parameter name. -
redirectPath: Sets a custom path for redirecting users to the appropriate locale.
For example, if you are sending an email and don't know the user's locale, you can use a prefix path like/rto redirect to the default locale, or set it to any path you prefer. -
detect: Callback function to detect the locale from the Next Request. E. g. from domain, geolocation, etc.
All these configurations are compatible and are used internally throughout the intl-t tools.
You can set these options in the createNavigation function.
There are also additional configuration options you may want to explore.
import { createNavigation } from "intl-t/navigation";
export const { middleware, Link, generateStaticParams, useRouter } = createNavigation({
allowedLocales: ["en", "es"],
defaultLocale: "en",
// custom
pathPrefix: "hidden",
pathBase: "always-default",
});import en from "@/public/locales/en.json";
import es from "@/public/locales/es.json";
import { createTranslation } from "intl-t";
export const { t } = createTranslation({
locales: {
en,
es,
},
});NotFound Page Warning
When using the param strategy ([locale]) with middleware in Next.js, you may encounter unexpected behavior when a page is not found: Next.js will redirect to app/not-found.tsx, which is outside the [locale] wrapper. To resolve this, use a [...404] dynamic param inside [locale] to catch all unmatched routes. Then, call the notFound function from Next.js to redirect users to the correct not-found.tsx page within the expected locale folder.
This workaround will likely not be needed in future versions of Next.js.
Static Rendering
import { Translation } from "intl-t/next";
export const { getTranslation, setLocale } = new Translation({ locales: { en: "Hello world" } });import { getTranslation, setLocale } from "@/i18n/translation";
import { setRequestLocale /* or setLocale */ } from "intl-t/next";
export default function Page({ params }) {
const { locale } = await params;
setRequestLocale(locale); // or
// setLocale(locale); // Same as setRequestLocale but typed with available locales
const t = getTranslation(); // It works like useTranslation
return <div>{t}</div>; // hello world
}Then in a sub-component, setRequestLocale is not needed.
import { getTranslation } from "@/i18n/translation";
export default function Component() {
const { t } = getTranslation();
return <div>{t("greeting", { name: "Ivan" })}</div>;
}New Next.js feature
rootParamswill be implemented.setRequestLocalewill be no longer needed in pages and layout, except in therootLayout
// Already available but not directly implemented in getTranslation logic
import { getRootParamsLocale } from "intl-t/next";Dynamic Rendering
Same configuration. No need any more to set locale in dynamic pages.
export default async function Page() {
const t = await getTranslation(); // Get locale from headers from middleware with its navigation settings
return <div>{t}</div>; // hello world
}If you want to use your own strategy to load locales dynamically, you can and avoid the [locale] param in your app routes.
When creating navigation, you can configure its strategy to load locales always dinamically and don't route to the locale path with param. (Also it can be shown or hidden as you want configuring the pathPrefix and pathBase options)
createNavigation({ allowedLocales, strategy: "headers" });Then is no more needed to wrap your application routes into [locale] param.
import { Translation } from "@/i18n/translation";
import { getRequestLocale /* or getLocale */ } from "intl-t/next";
export default function RootLayout({ children }) {
const locale = getRequestLocale();
return (
<html lang={locale}>
<body>
<Translation>{children}</Translation>
</body>
</html>
);
}Usage
With React Server Components (Static):
import { getTranslation } from "@/i18n/translation";
export default function Component() {
const t = getTranslation();
return <div>{t("greeting", { name: "Ivan" })}</div>;
}With React Server Components (Dynamic):
If you don't provide a Translation Provider or don't use setRequestLocale if required, you can use await getTranslation() for dynamic rendering in Next.js.
import { getTranslation } from "@/i18n/translation";
export default function Component() {
const t = await getTranslation();
return <div>{t("greeting", { name: "Ivan" })}</div>;
}With Server Actions:
The locale is automatically detected from headers.
"use server";
import { getTranslation } from "@/i18n/translation";
export function greeting() {
const t = await getTranslation(); // use await to get locale from headers
return t("greeting", { name: "Ivan" });
}With Client Components (Hydration):
"use client";
import { useTranslation } from "@/i18n/translation";
export default function Component() {
const { t } = useTranslation();
return <div>{t("greeting", { name: "Ivan" })}</div>;
}For easier migration from other i18n libraries, you can use the getTranslations and useTranslations aliases, exactly the same and keep type safety. getTranslation and useTranslation are functionally the same and adapt depending on the environment.
You can also use them as translation object directly, e.g., useTranslation.greeting.es({ name: "Ivan" })—it's modular, type-safe, and flexible.
With metadata:
// layout.tsx
export async function generateMetadata({ params }) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslation();
return t.metadata.toJSON();
}Link Navigation Component:
import { Link } from "@/i18n/navigation";
import { Translation } from "@/i18n/translation";
export default function LanguageSwitcher() {
const { Translation, t } = useTranslation("languages");
return (
<nav>
<h2>{t("title")}</h2>
<ul>
{t.allowedLocales.map(locale => (
<Link locale={locale} key={locale}>
<Translation.change variable={{ locale }} /> {/* example of Translation component */}
</Link>
))}
</ul>
</nav>
);
}Router Hook:
import { useRouter } from "@/i18n/navigation";
export default function Component() {
const router = useRouter();
function onClick() {
router.push("/hello", { locale: "fr" });
}
return (
<div onClick={onClick}>
{router.locale} {router.pathname}
</div>
);
}Advanced Technical Warning.
Warning: When calling directly the t object from getTranslation("...") with dynamic rendering in a React Server Component (RSC) without await, and the locale is not yet loaded or cached, and t is not destructured, and the t expected is not the translation root t, you may find unexpected behaviour when calling:
Translation did not load correctly through the Proxy. Try using
await getTranslation,t.t(...args)orconst { t } = getTranslation()"
This only occurs in this specific case, as it returns an incorrect t object when called due to how proxies work. If you use await getTranslation() or set request locale as normal, there will be no problem.
The recommended approach is to use await getTranslation() when there is no locale so that the locale is loaded dynamically from headers in order to use dynamic rendering. The warning above only applies to this example of flexible usage pattern of getTranslation. The getTranslation when is not awaited works as a fallback that is not callable if you don't destructure const { t } = getTranslation().
Static Rendering together with Dynamic Import Warning
The previous problem only applies for dynamic rendering with next, but if you are using static rendering with dynamic import, keep in mind that sometimes pages load before the layout. Therefore, you may need to await getTranslation at the top of your static page to preload your locale translations (It keeps static). After this initial preload, you won't need to await the getTranslation in your components.
Next.js React patch
import patch from "intl-t/react";
import React from "react";
import jsx from "react/jsx-runtime";
process.env.NODE_ENV !== "development" && patch(React, jsx);import "./patch";Warning: The only situation where you may encounter unexpected behavior with this Patch is when passing Translation Nodes as JSX attributes from a React Server Component (RSC) to a React Client Component. In this case, you cannot send the function object directly. Instead, convert the translation node to a string using t.toString(), t.base, or to JSON with t.toJSON().