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 translationsi18n.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/" });}); -
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:
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 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><I18nClientProvider locale={locale} messages={messages}>{children}</I18nClientProvider></body></html>);} -
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 (<I18nProvideri18n={i18n}locale={locale}messages={messages}routing={routing}autoInit={true}>{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"; 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 instancerouting: RoutingConfig— passed to middlewareuse(plugin, options?): this— register pluginsuseClient(plugin, options?): this— client-only pluginsuseServer(plugin, options?): this— server-only pluginsuseClientLazy(loadPlugin, options?): this— dynamic client importuseServerLazy(loadPlugin, options?): this— dynamic server import
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 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();}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 messages={messages} // optional pre-loaded translations autoInit={true} // auto-init on mount onError={(error) => {}} // error handler> {children}</I18nProvider>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:
- URL pathname (
/de/...→ localede) - Cookie (if set by plugin)
- Accept-Language header (first supported locale)
- Default locale
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”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());Common Patterns
Section titled “Common Patterns”Language Switcher
Section titled “Language Switcher”"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> );}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>;}Error Boundaries
Section titled “Error Boundaries”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> );}