Skip to content

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.

Terminal window
npm install @comvi/next @comvi/core

Peer dependencies:

  • next: ^14.0.0 || ^15.0.0
  • react: ^18.0.0 || ^19.0.0
  1. 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 server
    export const { i18n, routing } = nextI18n;
    // Register translations
    i18n.registerLoader({
    en: () => import("../locales/en.json"),
    de: () => import("../locales/de.json"),
    "en:admin": () => import("../locales/admin/en.json"),
    });
    // Plugin registration (optional)
    nextI18n.useServer(async () => {
    const { FetchLoader } = await import("@comvi/plugin-fetch-loader");
    return FetchLoader({ cdnUrl: "https://cdn.example.com/" });
    });
  2. 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 localePrefix mode.

  3. Create the locale layout:

    app/[locale]/layout.tsx
    import { setRequestLocale, loadTranslations } from "@comvi/next/server";
    import { I18nClientProvider } from "./I18nClientProvider";
    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 functions
    setRequestLocale(locale);
    // Pre-load translations on server and pass them to the client provider
    const messages = await loadTranslations(locale);
    return (
    <html lang={locale}>
    <head>
    <meta charSet="utf-8" />
    </head>
    <body>
    <I18nClientProvider locale={locale} messages={messages}>
    {children}
    </I18nClientProvider>
    </body>
    </html>
    );
    }
  4. Wrap client-side in I18nProvider:

    app/[locale]/I18nClientProvider.tsx
    "use client";
    import { I18nProvider } from "@comvi/next/client";
    import type { MessagesMap } from "@comvi/next/client";
    import { i18n, routing } from "@/i18n/config";
    export function I18nClientProvider({
    children,
    locale,
    messages,
    }: {
    children: React.ReactNode;
    locale: string;
    messages: MessagesMap;
    }) {
    return (
    <I18nProvider
    i18n={i18n}
    locale={locale}
    messages={messages}
    routing={routing}
    autoInit={true}
    >
    {children}
    </I18nProvider>
    );
    }
  5. 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>
    );
    }
  6. 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>
    );
    }
interface CreateNextI18nOptions {
// Routing (required)
locales: string[];
defaultLocale: string;
localePrefix?: "always" | "as-needed" | "never";
pathnames?: Record<string, Record<string, string>>; // locale-specific routes
// i18n (optional)
apiKey?: string;
ns?: string[];
fallbackLocale?: string | string[];
defaultNs?: string;
devMode?: boolean;
basicHtmlTags?: string[];
onMissingKey?: (info: MissingKeyInfo) => void;
}

Returns an object with:

  • i18n: I18n — core instance
  • routing: RoutingConfig — passed to middleware
  • use(plugin, options?): this — register plugins
  • useClient(plugin, options?): this — client-only plugins
  • useServer(plugin, options?): this — server-only plugins
  • useClientLazy(loadPlugin, options?): this — dynamic client import
  • useServerLazy(loadPlugin, options?): this — dynamic server import

Store locale in async context. Call at the start of each Server Component:

import { setRequestLocale } from "@comvi/next/server";
export default async function Page({ params }) {
const { locale } = await params;
setRequestLocale(locale); // Must be called before getI18n
const { t } = await getI18n();
}

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>;
}

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
}

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> */}</>;
}

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
i18n={i18n}
locale="en" // hydration locale
messages={messages} // optional pre-loaded translations
autoInit={true} // auto-init on mount
onError={(error) => {}} // error handler
>
{children}
</I18nProvider>

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:

  1. URL pathname (/de/... → locale de)
  2. Cookie (if set by plugin)
  3. Accept-Language header (first supported locale)
  4. Default locale
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)

The use(), useClient(), and useServer() methods detect runtime automatically:

// Client only (typeof window !== 'undefined')
nextI18n.useClient(LocaleDetectorPlugin({ /* ... */ }));
// Server only (process.env.NEXT_RUNTIME === 'nodejs')
nextI18n.useServer(FetchLoader({ /* ... */ }));
// Both (registered on both)
nextI18n.use(MyCustomPlugin());
"use client";
import { useI18n } from "@comvi/next/client";
import { useRouter } from "next/navigation";
export function LanguageSwitcher() {
const { locale, setLocale } = useI18n();
const router = useRouter();
const handleSwitch = async (newLocale: string) => {
await setLocale(newLocale);
// Next.js router.push to locale path if needed
router.push(`/${newLocale}`);
};
return (
<select value={locale} onChange={(e) => handleSwitch(e.target.value)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
);
}
import { Link } from "@comvi/next/navigation";
export function LocaleLink({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
return <Link href={href}>{children}</Link>;
}
import { I18nProvider } from "@comvi/next/client";
export function ErrorBoundary({
children,
locale,
}: {
children: React.ReactNode;
locale: string;
}) {
return (
<I18nProvider
i18n={i18n}
locale={locale}
onError={(error) => {
console.error("i18n error:", error);
}}
>
{children}
</I18nProvider>
);
}