Next.js
Comvi i18n integrates with Next.js 14+ via the @comvi/next package, supporting the App Router, Server Components, middleware-based locale routing, and streaming SSR.
Installation
Section titled “Installation”npm install @comvi/next @comvi/corePeer dependencies:
next: ^14.0.0 || ^15.0.0react: ^18.0.0 || ^19.0.0
Bootstrap
Section titled “Bootstrap”-
Create the i18n configuration:
i18n/config.ts import { createNextI18n } from "@comvi/next";export const nextI18n = createNextI18n({locales: ["en", "de", "fr"],defaultLocale: "en",localePrefix: "as-needed", // or "always" / "never"fallbackLocale: "en",apiKey: process.env.COMVI_API_KEY,});// Export for middleware and serverexport const { i18n, routing } = nextI18n;// Register translations.// Keys are "locale" (default namespace) or "locale:namespace".i18n.registerLoader({en: () => import("../locales/en.json"),de: () => import("../locales/de.json"),"en:admin": () => import("../locales/admin/en.json"),});// Plugin registration (optional).// useServerLazy takes a loader that resolves to a plugin — use it for plugins// you only want on the server, dynamically imported. (useServer takes an// already-resolved plugin; useClient / useClientLazy do the same on the client.)nextI18n.useServerLazy(() =>import("@comvi/plugin-fetch-loader").then((m) =>m.FetchLoader({ cdnUrl: "https://cdn.comvi.io/your-distribution-id" }),),); -
Register the i18n instance for server use:
getI18n(),getLocale(), andloadTranslations()read from a module-level i18n reference that you must set once withsetI18n(). Do this in a server-only side-effect file:i18n/server.ts import "server-only";import { setI18n } from "@comvi/next/server";import { i18n } from "./config";setI18n(i18n);// Re-export the server helpers for convenienceexport { i18n, routing } from "./config";export { getI18n, getLocale, loadTranslations } from "@comvi/next/server";Then import this file once at the top of your root layout (
app/[locale]/layout.tsx) so the side effect runs:import "@/i18n/server";If you skip this,
getI18n()throws"[comvi/next] i18n not configured. Call setI18n(i18n) in your i18n configuration file." -
Add middleware:
middleware.ts import { createMiddleware } from "@comvi/next/middleware";import { routing } from "./i18n/config";export default createMiddleware(routing);export const config = {matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\.png).*)"],};The middleware detects locale from the URL, cookies, or Accept-Language header, and handles redirects according to
localePrefixmode. -
Create the locale layout:
Import
@/i18n/serveronce here sosetI18n()runs before any Server Component callsgetI18n().app/[locale]/layout.tsx import "@/i18n/server";import { setRequestLocale, loadTranslations } from "@comvi/next/server";import { ComviProvider } from "./ComviProvider";export async function generateStaticParams() {return [{ locale: "en" }, { locale: "de" }, { locale: "fr" }];}export default async function LocaleLayout({children,params,}: {children: React.ReactNode;params: Promise<{ locale: string }>;}) {const { locale } = await params;// Set locale in async context for server functionssetRequestLocale(locale);// Pre-load translations on server and pass them to the client providerconst messages = await loadTranslations(locale);return (<html lang={locale}><head><meta charSet="utf-8" /></head><body><ComviProvider locale={locale} messages={messages}>{children}</ComviProvider></body></html>);} -
Wrap client-side in I18nProvider:
The
i18nclass instance can’t be serialized from server to client, so the client provider lives in its own"use client"file that importsi18ndirectly.app/[locale]/ComviProvider.tsx "use client";import { I18nProvider } from "@comvi/next/client";import type { MessagesMap } from "@comvi/next/client";import { i18n, routing } from "@/i18n/config";export function ComviProvider({children,locale,messages,}: {children: React.ReactNode;locale: string;messages: MessagesMap;}) {return (<I18nProvideri18n={i18n}locale={locale}messages={messages}routing={routing}>{children}</I18nProvider>);} -
Use in Server Components:
app/[locale]/page.tsx import { getI18n, setRequestLocale } from "@comvi/next/server";export default async function HomePage({params,}: {params: Promise<{ locale: string }>;}) {const { locale } = await params;setRequestLocale(locale);const { t } = await getI18n();return (<main><h1>{t("home.title")}</h1><p>{t("home.description", { count: 5 })}</p></main>);} -
Use in Client Components:
app/[locale]/components/Switcher.tsx "use client";import { useI18n } from "@comvi/next/client";export function LocaleSwitcher() {const { locale, setLocale } = useI18n();const handleChange = async (newLocale: string) => {await setLocale(newLocale);// Note: Next.js client-side routing may be needed for full page switch};return (<select value={locale} onChange={(e) => handleChange(e.target.value)}><option value="en">English</option><option value="de">Deutsch</option><option value="fr">Français</option></select>);}
createNextI18n Options
Section titled “createNextI18n Options”interface CreateNextI18nOptions { // Routing (required) locales: string[]; defaultLocale: string; localePrefix?: "always" | "as-needed" | "never"; // default "as-needed" pathnames?: Record<string, Partial<Record<string, string>>>; // locale-specific public slugs
// i18n (optional) apiKey?: string; ns?: string[]; // namespaces to load on init translation?: Record<string, Record<string, TranslationValue>>; // seed translations (no loader) fallbackLocale?: string | string[]; // default: same as defaultLocale defaultNs?: string; // default "default" devMode?: boolean; // default: NODE_ENV === "development" basicHtmlTags?: string[]; // tags allowed in tag interpolation // The core callback receives a MissingKeyInfo ({ key, locale, namespace }) and may return // a replacement TranslationResult, or void to use the default fallback chain. onMissingKey?: (info: MissingKeyInfo) => TranslationResult | void;}Returns an object with:
i18n: I18n— core instance (use with<I18nProvider>andsetI18n)routing: Required<RoutingConfig>— passed to middleware and navigationuse(plugin, options?): this— register a resolved plugin (chainable)useClient(plugin, options?): this— client-only plugin (resolved)useServer(plugin, options?): this— server-only plugin (resolved)useClientLazy(loadPlugin, options?): this— client-only plugin, dynamically importeduseServerLazy(loadPlugin, options?): this— server-only plugin, dynamically imported
The options for useClient/useServer/useClientLazy/useServerLazy is ScopedPluginOptions
— the standard plugin options plus environment?: "all" | "development" | "production" (default "all"),
which lets you, for example, register the in-context editor plugin only in dev builds.
Server APIs (@comvi/next/server)
Section titled “Server APIs (@comvi/next/server)”setRequestLocale(locale)
Section titled “setRequestLocale(locale)”Store locale in async context. Call at the start of each layout/page Server Component
that uses getI18n() (and required for static rendering with generateStaticParams()):
import { setRequestLocale, getI18n } from "@comvi/next/server";
export default async function Page({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; setRequestLocale(locale); // Must be called before getI18n const { t } = await getI18n(); return <h1>{t("home.title")}</h1>;}getI18n(options?): Promise<ServerI18n>
Section titled “getI18n(options?): Promise<ServerI18n>”Get the i18n instance with the locale from the async context:
import { getI18n, setRequestLocale } from "@comvi/next/server";
export default async function Page({ params }) { const { locale } = await params; setRequestLocale(locale);
const { t, hasTranslation } = await getI18n();
return <h1>{t("key")}</h1>;}getLocale() → Promise<string>
Section titled “getLocale() → Promise<string>”Get the current request’s locale. Async because it may read from next/headers:
import { getLocale, setRequestLocale } from "@comvi/next/server";
export default async function Page({ params }) { const { locale } = await params; setRequestLocale(locale);
const currentLocale = await getLocale(); // currentLocale === locale}loadTranslations(locale, options?)
Section titled “loadTranslations(locale, options?)”Pre-load translations on the server. Returns a serializable map keyed by "locale:namespace" that you can pass to the client provider for hydration. Defaults to the default namespace; pass { namespaces: [...] } to load more:
import { loadTranslations } from "@comvi/next/server";
export default async function Layout({ params }) { const { locale } = await params;
// Default namespace only const messages = await loadTranslations(locale);
// Or specific namespaces const adminMessages = await loadTranslations(locale, { namespaces: ["common", "admin"], });
return <>{/* pass messages to <I18nProvider> */}</>;}Client APIs (@comvi/next/client)
Section titled “Client APIs (@comvi/next/client)”useI18n(ns?) Hook
Section titled “useI18n(ns?) Hook”Same as React useI18n but works in Next.js client components:
"use client";
import { useI18n } from "@comvi/next/client";
export function MyComponent() { const { t, locale, setLocale } = useI18n("admin");
return ( <div> <h1>{t("dashboard.title")}</h1> <button onClick={() => setLocale("de")}>Deutsch</button> </div> );}I18nProvider Props
Section titled “I18nProvider Props”<I18nProvider i18n={i18n} locale="en" // hydration locale (required) — server locale messages={messages} // optional pre-loaded translations from loadTranslations() routing={routing} // optional — enables locale-aware <Link>/usePathname/router autoInit // default true — auto-init i18n on mount onError={(error) => {}} // optional — init error handler (defaults to console)> {children}</I18nProvider>This is the Next-aware provider from @comvi/next/client — it accepts a plain locale
prop (rather than @comvi/react’s ssrInitialLocale), keeps i18n.locale in sync across
client-side navigation, and adds the pre-loaded messages to the cache before children render.
Middleware (@comvi/next/middleware)
Section titled “Middleware (@comvi/next/middleware)”createMiddleware(routing)
Section titled “createMiddleware(routing)”Creates a middleware function that handles locale detection and routing:
import { createMiddleware } from "@comvi/next/middleware";import { routing } from "./i18n/config";
export default createMiddleware(routing);
export const config = { matcher: ["/((?!api|_next/static|.*\\..*).*)"],};Middleware detects locale in this order:
- Custom
detectLocale(request)callback, if you configured one (highest priority) - URL pathname (
/de/...→ localede) - The configured
localeDetection.order— by default["cookie", "accept-language"](cookie nameNEXT_LOCALE) - Default locale
See MiddlewareConfig in the API reference for the full
detection-config shape (order, cookieName, cookieSecure, headerName, resolveAcceptLanguage, detectLocale).
Locale Routing Modes
Section titled “Locale Routing Modes”localePrefix: "always" → All routes prefixed: /en/about, /de/about, /about (redirects)localePrefix: "as-needed" → Only non-default: /en/about, /about (English)localePrefix: "never" → No prefix: /about (routing via cookie/header)Plugin Scoping
Section titled “Plugin Scoping”use(), useClient(), and useServer() register an already-resolved plugin; the
client/server variants only run the plugin on the matching runtime (detected from
NEXT_RUNTIME / typeof window). The *Lazy variants take a loader function that
resolves to the plugin — handy when the plugin should be code-split:
import { LocaleDetector } from "@comvi/plugin-locale-detector";
// Client only (typeof window !== 'undefined')nextI18n.useClient(LocaleDetector({ /* ... */ }));
// Server only (NEXT_RUNTIME present)nextI18n.useServerLazy(() => import("@comvi/plugin-fetch-loader").then((m) => m.FetchLoader({ /* ... */ })),);
// Dev-only client plugin (e.g. the in-context editor)nextI18n.useClientLazy( () => import("@comvi/plugin-in-context-editor").then((m) => m.InContextEditorPlugin()), { environment: "development", required: false },);
// Both (registered everywhere)nextI18n.use(MyCustomPlugin());Common Patterns
Section titled “Common Patterns”Language Switcher
Section titled “Language Switcher”For localePrefix: "always" / "as-needed", switching locale also means navigating to
the localized path. Use the locale-aware router from @comvi/next/navigation:
"use client";
import { useI18n } from "@comvi/next/client";import { usePathname, useLocalizedRouter } from "@comvi/next/navigation";
export function LanguageSwitcher() { const { locale } = useI18n(); const pathname = usePathname(); // current path without locale prefix const router = useLocalizedRouter();
return ( <select value={locale} onChange={(e) => router.push(pathname, e.target.value)} > <option value="en">English</option> <option value="de">Deutsch</option> </select> );}(setLocale() from useI18n() updates the active locale in the i18n instance; the
navigation above keeps the URL in sync. With localePrefix: "never" you can call
setLocale(newLocale) alone since the middleware reads the locale cookie.)
Locale-Aware Links
Section titled “Locale-Aware Links”import { Link } from "@comvi/next/navigation";
export function LocaleLink({ href, children,}: { href: string; children: React.ReactNode;}) { return <Link href={href}>{children}</Link>;}Validating Locale Params
Section titled “Validating Locale Params”In layouts/pages under app/[locale]/, validate the locale segment with hasLocale
from @comvi/next/routing and notFound() for unknown values — it also narrows the type:
import { hasLocale } from "@comvi/next/routing";import { notFound } from "next/navigation";import { routing } from "@/i18n/config";
export default async function Page({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; if (!hasLocale(routing.locales, locale)) { notFound(); } // locale is now typed as a member of routing.locales return <div>{locale}</div>;}Error Boundaries
Section titled “Error Boundaries”"use client";
import { I18nProvider } from "@comvi/next/client";import { i18n, routing } from "@/i18n/config";
export function ErrorBoundary({ children, locale,}: { children: React.ReactNode; locale: string;}) { return ( <I18nProvider i18n={i18n} locale={locale} routing={routing} onError={(error) => { console.error("i18n error:", error); }} > {children} </I18nProvider> );}