# @comvi/next

API reference for @comvi/next: createNextI18n, withComvi, ComviProvider and the server helpers, nextPlugin and nextPluginLazy, the manual setup, createMiddleware, navigation and routing, per entry point.

Canonical: https://comvi.io/docs/i18n/api/next

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

## Exports

| 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` |

## `createNextI18n(options)`

```ts
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'`.

## `nextPlugin(plugin, options?)`

```ts
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.

## `withComvi(options)`

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

```ts
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](#manual-setup). Test runners and `serverExternalPackages` never see the alias:
[Tests](/docs/i18n/guides/next-server-client#tests).

## Server helpers

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

```ts
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.

## `<ComviProvider>`

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

```ts
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.

## Manual setup

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

```ts
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.

### `<I18nProvider>`

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](/docs/i18n/guides/next-server-client#manual-setup).

## `createMiddleware(config)`

```ts
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.

### `LocaleCookieOptions`

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

### `LocaleDetectionConfig`

| 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`

```ts

export const routing = defineRouting({ locales: ['en', 'de'], defaultLocale: 'en' });
```

`middleware.ts`

```ts

export default createMiddleware({
...routing,
localeCookie: { name: 'locale', sameSite: 'strict' },
localeDetection: { order: ['query', 'cookie', 'accept-language'], queryParam: 'lang' },
});
```

## Navigation

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

```ts
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.

## Routing

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

```ts
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`.

## Read next

- [Next.js](/docs/i18n/next): the App Router walkthrough: `withComvi`, the config module, the layout provider, middleware.
- [`@comvi/react`](/docs/i18n/api/react): every hook and component this package re-exports.
- [Render on the server](/docs/i18n/ssr): hydration and the server/client split.
