docs

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

RequirementSupported
Next.js16 (>=16 <17)
React and React DOM19
RouterApp Router
BundlerTurbopack (default) or webpack (--webpack)
RuntimeNode.js
RenderingRequest-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

terminal
npm install @linguanahq/next
terminal
pnpm add @linguanahq/next
terminal
bun 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:

next.config.ts
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:

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

app/layout.tsx
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

app/page.tsx
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

terminal
npm run build

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

app/guide/page.tsx
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:

app/greeting.tsx
"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

app/language.tsx
"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:

  1. The linguana_locale cookie, or your runtime.cookieName.
  2. The browser's Accept-Language header.
  3. runtime.defaultLocale, or the project default from hosted configuration.
  4. 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:

.env.local — replace the placeholders
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-key

Next.js loads .env.local before it evaluates next.config.ts, so you can read the values directly:

next.config.ts
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: true is 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_TOKEN a build secret. Set LINGUANA_UPLOAD=false for 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

  1. Run npm run build and find your visible text in .linguana/manifest.json.

  2. Start the app and request a page with a target language:

    terminal
    curl -H 'Accept-Language: fr' http://localhost:3000

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

  3. Select a language with LanguageSelector and confirm Server and Client Components change together, without a hydration warning.

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

Did this page get you to a working result?

On this page

No Headings