Skip to content
Comvi

@comvi/next

View .md

App Router pieces on top of @comvi/react: a composed i18n instance, a next.config plugin, server helpers, locale middleware, navigation and routing, in seven entries.

Entry point Values Types
@comvi/next createNextI18n, nextPlugin, nextPluginLazy, createI18n, I18n, icuCompiler, loader, attachLoader, plugins, definePlugin, devtools CreateNextI18nOptions, CreateNextI18nResult, UseInstanceIdentityError, NextComposedI18n, NextPluginOptions, LazyPluginModule, LazyPluginLoader, RoutingConfig, LocalePrefixMode, MiddlewareConfig, GetI18nOptions, I18nProviderProps, DevtoolsOptions, every @comvi/core type
@comvi/next/plugin withComvi WithComviOptions, WithComvi, NextConfigFn
@comvi/next/server getI18n, ComviProvider, getLocale, loadMessages, setRequestLocale, setI18n, createNextI18nFromInstance, createI18n, icuCompiler, loader, attachLoader, plugins, definePlugin, devtools, nextPlugin, nextPluginLazy ComviProviderProps, NextServerInstance, CreateNextI18nFromInstanceOptions, CreateNextI18nFromInstanceResult, ServerI18n, ServerTranslationFunction, TranslationFunction, HasTranslationOptions, GetI18nOptions, LoadMessagesOptions, MessagesMap, NextRoutingOptions, NextPluginOptions, LazyPluginModule, LazyPluginLoader, DevtoolsOptions
@comvi/next/client every @comvi/react hook and T, the Next-aware I18nProvider, createI18n, the same installers as /server, nextPlugin, nextPluginLazy UseI18nReturn, UseI18nLoaderReturn, UseI18nPluginsReturn, UseSetLocaleTransitionReturn, UseFormattersReturn, TProps, ComponentMap, I18nProviderProps, MessagesMap, NextPluginOptions, LazyPluginModule, LazyPluginLoader, DevtoolsOptions
@comvi/next/middleware createMiddleware MiddlewareConfig, LocaleCookieConfig, LocaleCookieOptions, LocaleDetectionConfig, LocaleDetectionSource, ResolveAcceptLanguage
@comvi/next/navigation Link, usePathname, useLocalizedRouter LocalizedLinkProps, LocalizedRouter
@comvi/next/routing defineRouting, hasLocale, createGetPathname RoutingConfig, LocalePrefixMode, LocaleCookieConfig, LocaleCookieOptions, GetPathnameOptions
function createNextI18n<D>(options: CreateNextI18nOptions<D>): CreateNextI18nResult<D>
interface CreateNextI18nResult<D, C extends NextComposedI18n<D> = NextComposedI18n<D>> {
readonly i18n: C;
readonly routing: Required<RoutingConfig>;
use<T extends C>(installer: (i18n: C) => T): CreateNextI18nResult<D, T>;
}
type NextComposedI18n<D> = I18n<D> & Omit<I18nLoaderApi, 'registerLoader'> & I18nPluginApi & {
registerLoader(loader: LoaderFn): void;
registerLoader(importMap: LoaderImportMap): void;
}
Option Type Default Meaning
locales readonly string[] required Supported locales
defaultLocale string required Default locale
localePrefix 'always' | 'as-needed' | 'never' 'as-needed' URL prefix mode
pathnames RoutingConfig['pathnames'] none Localized slugs
localeCookie RoutingConfig['localeCookie'] 'NEXT_LOCALE' The locale cookie, kept in result.routing
apiKey string none Forwarded to the i18n instance
ns string[] none Namespaces init() loads; omitted, only defaultNs loads
messages I18nOptions['messages'] none Inline catalogs
defaultParams DefaultTranslationParams none Interpolation defaults
fallbackLocale string | string[] defaultLocale Fallback chain
defaultNs string 'default' Default namespace
devMode boolean NODE_ENV === 'development' Read by plugins
compiler MessageCompiler none Pass icuCompiler for ICU
basicHtmlTags string[] none Tags rendered without a handler
onMissingKey I18nOptions['onMissingKey'] none Miss hook

The instance comes composed: ambient tags, the loader with both registerLoader overloads, the plugin capability and devtools discovery. use(f) is f(result.i18n); an installer returning another object throws code: 'E_USE_INSTANCE_IDENTITY'.

function nextPlugin(plugin: I18nPlugin, options?: NextPluginOptions): <T extends I18n<any>>(i18n: T) => T & I18nPluginApi
function nextPluginLazy(load: LazyPluginLoader, options?: NextPluginOptions): <T extends I18n<any>>(i18n: T) => T & I18nPluginApi
type LazyPluginLoader = () => Promise<I18nPlugin | { default: I18nPlugin }>
Option Type Default Meaning
runtime 'client' | 'server' both Which runtime runs the plugin
environment 'all' | 'development' | 'production' 'all' Which build runs it
required, timeout, onError PluginOptions Forwarded to registerPlugin

Both attach the plugin capability if absent and register the scoped plugin; nextPluginLazy also defers the import to the runtime the scope names. Compose localeDetector only through nextPlugin(localeDetectorPlugin(), { runtime: 'client' }): the server instance is process-wide, so a detector there runs once at the first init(), detects nothing, and never runs per request.

From @comvi/next/plugin, Node only: export default withComvi({ config: './src/i18n/config.ts' })(nextConfig).

function withComvi(options: { config: string }): WithComvi
interface WithComvi {
(nextConfig?: NextConfig): NextConfig;
(nextConfig: NextConfigFn): NextConfigFn; // the (phase, { defaultConfig }) form, sync or async
}

It aliases the internal specifier @comvi/next/config to your module, under webpack and under Turbopack, keeping your own webpack function and every alias already there; the server helpers and <ComviProvider> read the i18n export through that alias. config names the module exporting i18n (required) and routing (optional), resolved from the directory next runs in; in a monorepo pass an absolute path, such as fileURLToPath(new URL('./src/i18n/config.ts', import.meta.url)). A missing path throws while next.config loads, naming the resolved path. The module is evaluated in the server build and in the client build, so it must be client-safe; a server-only loader belongs to the manual setup. Test runners and serverExternalPackages never see the alias: Tests.

From @comvi/next/server, which imports server-only: a Client Component importing it fails the build.

function getI18n(options?: { locale?: string; ns?: string }): Promise<ServerI18n>
function getLocale(): Promise<string>
function loadMessages(locale: string, options?: { namespaces?: string[] }): Promise<MessagesMap>
interface ServerI18n {
t: TranslationFunction;
hasTranslation(key: string, options?: { ns?: string; locale?: string }): boolean;
locale: string;
}
type MessagesMap = Record<string, Record<string, TranslationValue>> // @comvi/core's, re-exported by /server and /client
Helper Meaning
getI18n({ locale, ns? }) Binds t, hasTranslation and locale to the locale you pass; neither reads nor sets the request locale, so two calls in one request may name different locales
getI18n() The same, with the locale from the request store (<ComviProvider> or setRequestLocale()) or the middleware header; throws [@comvi/next] Locale not set with neither
getLocale() The request locale from the same two sources. The x-comvi-locale header counts only when its value is in routing.locales, whether routing comes from the config module or from createNextI18nFromInstance(factory, routing), or, with no routing, is a well-formed BCP 47 tag, singleton subtags included; any other value is ignored and routing.defaultLocale applies. With neither source, or an ignored header and no routing, it throws [@comvi/next] Unable to determine locale.
loadMessages(locale, options?) Preloads and returns a map keyed "locale:namespace", for a manual client provider or a namespace getI18n() does not load; with no loader returns what is cached

getI18n() loads one namespace (options.ns or the instance’s default) for that locale and every locale in the fallbackLocale chain, then returns a synchronous t; another namespace needs loadMessages() first. A locale routing.locales does not list still renders, from the fallback chain, with one development warning per locale: notFound() stays the layout’s job. generateMetadata, not-found.tsx and anything outside the <ComviProvider> tree must pass the locale; the no-argument form needs an established request context.

From @comvi/next/server, for the locale layout of an app with withComvi in next.config.

function ComviProvider(props: ComviProviderProps): Promise<JSX.Element>
interface ComviProviderProps {
locale: string;
namespaces?: string[]; // default: the instance's default namespace
children: React.ReactNode;
}

locale is the [locale] param; validate it with hasLocale in your layout, with notFound(), <html lang> and generateStaticParams(). The provider sets the request locale for the no-argument helpers under it, loads namespaces (default: the instance’s default namespace) for that locale and its fallback chain, and renders the library’s "use client" provider with that data and the i18n and routing the client graph reads from your config module. A namespace absent here is missing on the client until something loads it there. Without withComvi it throws.

The pieces withComvi and <ComviProvider> stand in for, for an app whose config imports a server-only loader.

function setI18n(i18n: NextServerInstance): void
function createNextI18nFromInstance<D, C extends NextServerInstance<D>>(factory: () => C, options: CreateNextI18nFromInstanceOptions): { readonly i18n: C; readonly routing: Required<RoutingConfig> }
function setRequestLocale(locale: string): void
type NextServerInstance<D = {}> = WrapperI18nInstance<D> & I18nLoaderApi
Helper Meaning
setI18n(i18n) Registers an instance you built as the one the server helpers read; repeating with the same instance is a no-op, a different one throws
createNextI18nFromInstance(factory, routing) Calls factory() once, eagerly, and registers the result; the options carry routing only; no use pipe
setRequestLocale(locale) Stores the locale in React’s request cache() for the no-argument helpers; needed at the top of a layout or page that renders statically and calls getI18n() without a locale

A registered instance wins over the withComvi alias; two different objects get one development warning naming both sources, and the check runs before createNextI18nFromInstance() calls its factory.

From @comvi/next/client. Extends the React provider, minus ssrLocale and targetLocale, both set from locale. <ComviProvider> renders it for you; render it yourself from a "use client" file that imports the instance, because a class instance does not cross the RSC boundary.

Prop Type Meaning
locale string Required; forwarded to React’s ssrLocale when it is in routing.locales, otherwise an error is reported and the instance’s locale renders
messages MessagesMap What loadMessages() returned; seeds the client cache
routing RoutingConfig Read by <Link> and useLocalizedRouter(); without it they prefix unconditionally

The layout calls setRequestLocale(locale), then loadMessages(locale, { namespaces }), and passes the map as messages; it holds the locale and its whole fallbackLocale chain, so a key rendered from a fallback hydrates to the same text. The files are on Server and client APIs.

function createMiddleware(config: MiddlewareConfig): (request: NextRequest) => NextResponse
interface MiddlewareConfig extends RoutingConfig {
detectLocale?: (request: NextRequest) => string | undefined;
localeDetection?: false | LocaleDetectionConfig;
}

/_next*, /api and /api/* are skipped. Order: detectLocale (a value outside locales becomes defaultLocale), the URL path, localeDetection.order, defaultLocale. Then it applies localePrefix and pathnames, sets the x-comvi-locale header and, on a document load, writes the cookie when its value differs; a switch to the default locale under as-needed is recorded too. localeDetection: false leaves the path alone in charge and localeCookie: false persists nothing. @comvi/nuxt takes the same two options with the same values and defaults; only the default cookie name differs.

Option Type Default Meaning
name string 'NEXT_LOCALE' Cookie name
maxAge number 31536000 Lifetime in seconds, one year
secure boolean true, off in development Secure flag; NODE_ENV=development drops it so localhost over HTTP keeps the cookie
sameSite 'lax' | 'strict' | 'none' 'lax' SameSite attribute
domain string none, the current domain only Send the cookie to this domain and its subdomains
Option Type Default Meaning
order Array<'query' | 'cookie' | 'header' | 'accept-language'> ['query', 'cookie', 'accept-language'] Sources after the URL path; query and header do nothing until queryParam and headerName are set
queryParam string none Query parameter read for 'query' ('lang' reads ?lang=de); values outside locales are ignored
headerName string none Request header read for 'header', matched case-insensitively
resolveAcceptLanguage (acceptLanguage, locales, defaultLocale) => string | undefined built-in matcher Replaces the Accept-Language matcher
i18n/routing.ts
import { defineRouting } from '@comvi/next/routing';
export const routing = defineRouting({ locales: ['en', 'de'], defaultLocale: 'en' });
middleware.ts
import { createMiddleware } from '@comvi/next/middleware';
import { routing } from './i18n/routing';
export default createMiddleware({
...routing,
localeCookie: { name: 'locale', sameSite: 'strict' },
localeDetection: { order: ['query', 'cookie', 'accept-language'], queryParam: 'lang' },
});

From @comvi/next/navigation; all read routing from the provider.

const Link: React.ForwardRefExoticComponent<LocalizedLinkProps & React.RefAttributes<HTMLAnchorElement>> // Next's Link props plus locale?: string
function usePathname(): string
function useLocalizedRouter(): {
push: (href: string, locale?: string) => void;
replace: (href: string, locale?: string) => void;
back: () => void; forward: () => void; refresh: () => void;
prefetch: (href: string, locale?: string) => void;
}

<Link> applies localePrefix to href for the current locale, or for its locale prop, which also records that locale in the cookie on click. usePathname() strips the locale prefix and never adds one.

From @comvi/next/routing, server-safe.

function defineRouting<T extends string>(config: RoutingConfig<T>): Required<RoutingConfig<T>>
function hasLocale<T extends string>(locales: readonly T[], locale: string): locale is T
function createGetPathname<T extends string>(config: Required<RoutingConfig<T>>): (options: { locale: string; href: string }) => string
interface RoutingConfig<T extends string = string> {
locales: readonly T[];
defaultLocale: T;
localePrefix?: 'always' | 'as-needed' | 'never'; // default 'as-needed'
localeCookie?: false | string | LocaleCookieOptions; // default 'NEXT_LOCALE'
pathnames?: Record<string, Partial<Record<T, string>>>;
}

Locale matching is segment-based and a trailing slash survives. createGetPathname applies localePrefix and pathnames, for sitemaps and hreflang.

  • Next.js: the App Router walkthrough: withComvi, the config module, the layout provider, middleware.
  • @comvi/react: every hook and component this package re-exports.
  • Render on the server: hydration and the server/client split.