Next.js
Linguana translates Next.js App Router apps without translation keys. Write ordinary JSX in Server Components and Client Components. A source loader extracts static copy before Next.js compiles it, and the runtime renders the language selected for each request, then hydrates Client Components with the same data.
The Next.js integration is a separate package, @linguanahq/next. It does not use @linguanahq/vite.
Requirements
| Requirement | Supported |
|---|---|
| Next.js | 16 (>=16 <17) |
| React and React DOM | 19 |
| Router | App Router |
| Bundler | Turbopack (default) or webpack (--webpack) |
| Runtime | Node.js |
| Rendering | Request-time rendering. output: "export" and cacheComponents: true are rejected |
Pages Router, localized URL routing, Edge runtime certification, MDX extraction, and a native SWC plugin are not included. The CLI templates and linguana link do not support Next.js yet; set up the integration with this guide.
1. Install the package
npm install @linguanahq/nextpnpm add @linguanahq/nextbun add @linguanahq/next@linguanahq/next ships compiled JavaScript, so Next.js needs no extra loader settings, Babel configuration, or SWC plugin.
2. Wrap your Next.js configuration
Start with local extraction. No account or token is required:
import type { NextConfig } from "next";
import { withLinguana } from "@linguanahq/next/plugin";
const nextConfig: NextConfig = {
// Your existing configuration.
};
export default withLinguana({
projectId: "my-app",
sourceLocale: "en",
environment: "development",
upload: false,
include: ["app/**/*.{js,jsx,ts,tsx}", "components/**/*.{js,jsx,ts,tsx}"],
})(nextConfig);my-app is a local identifier. Replace it with your dashboard project ID before connecting hosted translations.
withLinguana(options) returns a function that accepts your configuration object, or a configuration function, including an async one. It works in next.config.ts, next.config.mjs, and next.config.js:
const { withLinguana } = require("@linguanahq/next/plugin");
module.exports = withLinguana({ projectId: "my-app", upload: false })({
// Your existing configuration.
});The wrapper adds the Linguana loader to both Turbopack and webpack, ahead of your existing rules, and writes the public runtime configuration to .linguana/next/runtime.json. Set root when the application root differs from the directory where you run Next.js. See the withLinguana options.
3. Add the root provider
import type { ReactNode } from "react";
import { LinguanaProvider, getLocale } from "@linguanahq/next/server";
import "@linguanahq/next/styles.css";
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang={await getLocale()}>
<body>
<LinguanaProvider>{children}</LinguanaProvider>
</body>
</html>
);
}LinguanaProvider resolves the language and catalogs once per request, renders Server Components in that language, and hydrates Client Components with the same state. getLocale() returns the same language for the lang attribute. The stylesheet is optional and styles the language selector.
4. Write your components normally
export default function Page() {
return (
<main>
<h1>Welcome to your store</h1>
<input aria-label="Search" placeholder="Search products" />
<button title="Save your changes">Save</button>
</main>
);
}Static text in Server and Client Components is extracted automatically, along with supported attributes such as title, placeholder, alt, and aria-label. Shared components imported by both kinds of component work in both. Extraction uses the same exclusions and message IDs as the Vite integration: add data-no-translate to keep content as written, and use explicit messages for dynamic or rich text. See Keep product text and code separate.
5. Build and check the strings
npm run buildnext build writes .linguana/manifest.json. Find a visible string from your app in it, then run next dev and confirm the page still renders its source language. Nothing is uploaded with upload: false, and development servers never upload.
Add .linguana/ to .gitignore. Do not commit it or .next/.
Explicit messages
Use T for rich text and getTranslations() for strings you build in server code, such as metadata:
import { T } from "@linguanahq/next";
import { getTranslations } from "@linguanahq/next/server";
export async function generateMetadata() {
const { t } = await getTranslations();
return { title: String(t("Our products")) };
}
export default function Page() {
return (
<T
id="guide-link"
defaultMessage="Read <link>the guide</link>"
components={{ link: <a href="/guide">the guide</a> }}
/>
);
}In Client Components, use useTranslation() from @linguanahq/next/client:
"use client";
import { T } from "@linguanahq/next";
import { useTranslation } from "@linguanahq/next/client";
export function Greeting({ name }: { name: string }) {
const { t } = useTranslation();
return (
<>
<p>{t("Welcome, {name}", { name })}</p>
<T id="client-help" defaultMessage="Need help? Contact support." />
</>
);
}Import T from @linguanahq/next in both environments; it selects the server or client implementation automatically. Never import @linguanahq/next/server into a Client Component. Message syntax, values, plurals, and rich slots match Dynamic text and links.
Let people choose a language
"use client";
import { LanguageSelector } from "@linguanahq/next/client";
export function Language() {
return <LanguageSelector />;
}Render the selector from any Client Component under the provider. It validates the target catalog first, stores the linguana_locale cookie, and refreshes server content with router.refresh(). Unrelated client state is preserved, and the URL does not change. If the catalog cannot be loaded, the current language stays and a diagnostic is recorded.
For a custom control, call setLocale(code) from useTranslation() and disable the control while ready is false. availableLocales lists the choices.
How the language is chosen
For each request, the provider picks the first supported match from:
- The
linguana_localecookie, or yourruntime.cookieName. - The browser's
Accept-Languageheader. runtime.defaultLocale, or the project default from hosted configuration.- The source language.
Supported languages come from runtime.availableLocales when set. Otherwise, with a publishable key and endpoint, they come from your project's hosted runtime configuration; without those, from the embedded catalogs. The source language is always available.
Load translations
Embedded catalogs. Pass verified catalogs in runtime.catalogs. Matching embedded catalogs take priority over hosted ones and need no network request.
Hosted catalogs. Set runtime.publishableKey and runtime.catalogEndpoint (your API origin) to load published catalogs. Server requests time out after five seconds, are deduplicated within one render, and are not cached by Next.js between requests. Every catalog is checked for project, environment, source language, and integrity before use.
Unavailable, invalid, or tampered catalogs render the source text. Runtime keys are publishable keys, never build tokens.
Connect hosted translations
Upload is off by default. To upload your text, give the build your project ID, API origin, and a build-only project token. Keep these values in .env.local locally and in your host's build environment:
LINGUANA_PROJECT_ID=your-project-id
LINGUANA_ENVIRONMENT=development
LINGUANA_API_URL=https://your-linguana-api.example.com
LINGUANA_PROJECT_TOKEN=your-project-token
LINGUANA_UPLOAD=true
LINGUANA_PUBLISHABLE_KEY=your-publishable-keyNext.js loads .env.local before it evaluates next.config.ts, so you can read the values directly:
import type { NextConfig } from "next";
import { withLinguana } from "@linguanahq/next/plugin";
const nextConfig: NextConfig = {};
export default withLinguana({
projectId: process.env.LINGUANA_PROJECT_ID ?? "my-app",
environment: process.env.LINGUANA_ENVIRONMENT ?? "development",
sourceLocale: "en",
include: ["app/**/*.{js,jsx,ts,tsx}", "components/**/*.{js,jsx,ts,tsx}"],
apiUrl: process.env.LINGUANA_API_URL,
token: process.env.LINGUANA_PROJECT_TOKEN,
upload: process.env.LINGUANA_UPLOAD === "true",
runtime: {
catalogEndpoint: process.env.LINGUANA_API_URL,
publishableKey: process.env.LINGUANA_PUBLISHABLE_KEY,
},
})(nextConfig);apiUrl and token are used only by the build and never reach browser bundles. Only the runtime values become public. No NEXT_PUBLIC_ prefix is needed, because the wrapper reads these values when Next.js loads its configuration. Restart next dev after changing them, and rebuild for production.
With upload: true, next build uploads the manifest after compilation succeeds. Then enable a language, translate, review, and publish in the dashboard as described in Connect hosted translations. Your build and runtime must use the same project and environment as the published catalog.
Use credentials from the CLI
linguana link can create a project and write a development build token for a Next.js app, although it prints Vite instructions. See Next.js apps. It stores the publishable key as VITE_LINGUANA_PUBLISHABLE_KEY, which you can read in next.config.ts:
runtime: {
catalogEndpoint: process.env.LINGUANA_API_URL,
publishableKey: process.env.VITE_LINGUANA_PUBLISHABLE_KEY,
},The other values use the names in the configuration above. env pull --environment production writes .env.production.local, which Next.js loads for next build and next start. Next.js does not load .env.staging.local.
Translate during development builds
Translation is a separate opt-in for development builds:
translation: {
enabled: true,
maxChargeMicros: 100_000,
waitTimeoutMs: 300_000,
publish: "development",
},It requires upload: true, a build token, and environment: "development". Production publication remains an explicit release action. Upload and translation run after successful compilation, before Next.js type checking and static generation, so they can finish even if a later build stage fails. Remote failures fail builds that opted in. Cached builds still rescan current source and remove deleted messages.
Local translation preview is not available for Next.js. Development servers never upload or request translations.
Rendering and caching
Because the root layout reads the request's cookies and headers, routes under LinguanaProvider render at request time. This is what lets each visitor see their own language.
output: "export"is rejected: cookie-based translation requires a Next.js server.cacheComponents: trueis rejected for now.- Do not place automatically translated, request-dependent content inside a
"use cache"scope.
Concurrent requests in different languages are isolated from each other.
Deploy
Deploy to any host that runs next build and serves the app with the Node.js runtime, such as next start.
- Set
LINGUANA_*values in the host's build environment. Runtime options are captured when Next.js builds the app. - Keep
LINGUANA_PROJECT_TOKENa build secret. SetLINGUANA_UPLOAD=falsefor builds that should not upload, such as previews. - Use a production token,
environment: "production", and a production publishable key together, and publish the production catalog in the dashboard. - Routes under the provider run on the Node.js runtime; the Edge runtime is not certified.
Verify your integration
-
Run
npm run buildand find your visible text in.linguana/manifest.json. -
Start the app and request a page with a target language:
terminal curl -H 'Accept-Language: fr' http://localhost:3000When French is a supported language, the response has
<html lang="fr">. With an available French catalog, the text is French. If the catalog cannot be loaded, the language stays selected, the text falls back to the source language, and the server logs a[linguana]warning. -
Select a language with
LanguageSelectorand confirm Server and Client Components change together, without a hydration warning. -
Check the server log for
[linguana]warnings, which report catalog and translation fallbacks.
Example app
The Linguana repository's examples/next-app runs without credentials, using embedded French translations. From a repository checkout, run bun run dev:example:next to try it, and bun run verify:next to check both bundlers, server rendering, concurrent language isolation, and cached-build manifests.
Translate Next.js 16 App Router Server and Client Components with Turbopack or webpack.
Last updated October 6, 2026
Did this page get you to a working result?