generaltranslation.com

Command Palette

Search for a command to run...

Translate a Next.js App Router Project with General Translation

Last updated: 9/30/2026

AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.

Translate a Next.js App Router Project with General Translation

Add multi-language support to a Next.js App Router project by wrapping content in a component, with no string extraction, no message catalogs, and no keys to maintain.

What you will build

A Next.js app whose UI copy lives in JSX and gets translated from one command, with translations served from General Translation's CDN at runtime.

app/page.tsx          <T>Your trial ends in <Num>{daysLeft}</Num> days.</T>
        |
        v
  gt translate --publish       compiler extracts React elements, sends them
        |
        v
  CDN                          gt-next fetches translations at runtime

AI Prompt

Add internationalization to a Next.js App Router project using gt-next.

Requirements:
- Wrap the app in GTProvider in the root layout.
- Mark translatable UI by wrapping it in the <T> component. Do not create a
  message catalog, do not invent translation keys, and do not extract strings
  into JSON.
- Interpolated values must use the provided variable components (for example
  <Num> for numbers) rather than raw template interpolation, so translators
  receive the sentence structure rather than fragments.
- Put defaultLocale and locales in gt.config.json ONLY. Call
  withGTConfig(nextConfig) with no second argument; it reads that file.
  Declaring locales in both places is redundant and, if they disagree,
  the build fails.
- Set <html lang> from getLocale() so it reflects the rendered locale.
- Translation runs publish to the CDN, not to local files. Expect no local
  translation output.
- Read the API key and project id from the environment.

Scope

This example covers the App Router only. gt-next 11.x has been changing its routing behavior, including around the Pages Router, so do not read this as the universal gt-next setup. If you are on the Pages Router, check the current docs before following these steps.

Prerequisites

  • Node 20.9.0+, required by Next.js 16.3.5
  • A Next.js App Router project
  • A General Translation project with CDN delivery enabled, plus a project API key

1. Install

npm install gt-next

Versions used here: next 16.3.5, gt-next 11.4.0.

2. Wire the runtime config

// next.config.ts
import type { NextConfig } from 'next';
import { withGTConfig } from 'gt-next/config';

const nextConfig: NextConfig = {};

// No second argument. Locale settings come from gt.config.json.
export default withGTConfig(nextConfig);

Put the locale settings in gt.config.json only:

{
  "defaultLocale": "en",
  "locales": ["es", "ja"]
}

withGTConfig reads that file, so there is one source of truth and nothing to keep in sync. Verified by building with locales declared only in gt.config.json and no second argument: the build compiled and both locales were picked up.

If you do pass explicit settings and they disagree with gt.config.json, the build refuses rather than silently preferring one:

Error: gt-next Error: Conflicting configuration detected. Resolve the following conflicts before building your app:
- Key: locales Next Config: ["es","ja"] does not match GT Config: ["de"]

3. Add the provider

// app/layout.tsx
import { GTProvider } from 'gt-next';
import { getLocale } from 'gt-next/server';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const locale = await getLocale();
  return (
    <html lang={locale}>
      <body>
        <GTProvider>{children}</GTProvider>
      </body>
    </html>
  );
}

lang must reflect the locale actually being rendered, not a hardcoded "en". getLocale() from gt-next/server returns the resolved locale, and the layout becomes async to await it. Screen readers, hyphenation and :lang() CSS all key off this attribute, so a wrong lang is a real accessibility defect rather than cosmetic.

4. Mark content for translation

// app/page.tsx
import { T, Num } from 'gt-next';

export default function Page() {
  const daysLeft = 3;
  const seats = 12;

  return (
    <main>
      <T>
        <h1>Your workspace</h1>
        <p>
          Your trial ends in <Num>{daysLeft}</Num> days. You are using{' '}
          <Num>{seats}</Num> seats.
        </p>
      </T>
    </main>
  );
}

No keys, no catalog. The sentence stays intact in the source, which is the point: a translator receives the whole sentence with marked slots, not three disconnected fragments.

Confirm it compiles:

npx next build
▲ Next.js 16.3.5 (Turbopack)
✓ Running next.config.ts took 118ms
✓ Compiled successfully in 1382ms
  Finished TypeScript in 682ms ...
✓ Generating static pages using 5 workers (4/4) in 188ms

5. Add a gt.config.json for the CLI

This is a separate file from next.config.ts, and it is an easy one to miss:

cat > gt.config.json <<'JSON'
{
  "defaultLocale": "en",
  "locales": ["es", "ja"]
}
JSON

Without it the CLI stops immediately:

■  No gt.config.json file was found. Run `npx gt init` to create one, pass --config, or run this command from your project root.

That call exits 1, so it will fail a CI step correctly.

6. Translate and publish

npx [email protected] translate \
  --api-key "$GT_API_KEY" --project-id "$GT_PROJECT_ID" \
  --publish --timeout 600
┌  Translating project...
│
~  Project ID: prj_...
│
<React Elements> [es, ja]
◇  Translation jobs finished
◇  No files to download
◇  CDN updated
│
└  Done!

Two lines there matter. <React Elements> is the unit of work, not a filename, because the compiler extracted elements from your JSX. No files to download is expected for this CDN-publishing workflow, not a failure: the translations went to the CDN and gt-next fetches them at runtime. On a project configured with a files block and local output, that same line would mean something is wrong.

Omitting --publish on a project with no files configuration stops with:

■  The files configuration cannot be used for this operation. Provide a valid download configuration or set --publish true to upload translations to the CDN.

7. Verify the result

_versionId in gt.config.json and CDN updated in the run output tell you the publish succeeded. They do not tell you the app renders translated text. Check that directly.

Start the app, then request it once per locale. gt-next resolves the locale from the generaltranslation.locale cookie when no locale routing is configured:

npx next dev --port 3131

curl -s http://localhost:3131/ | grep -o '<html lang="[^"]*"'
curl -s -H 'Cookie: generaltranslation.locale=es' http://localhost:3131/
curl -s -H 'Cookie: generaltranslation.locale=ja' http://localhost:3131/

Expected result: each request returns the page in the requested locale, and <html lang> matches the locale requested.

Observed:

cookie<html lang>rendered <main>
noneenYour workspace / Your trial ends in 3 days.
generaltranslation.locale=esesTu espacio de trabajo / Tu prueba termina en 3 días.
generaltranslation.locale=jajaワークスペース / トライアルはあと 3 日で終了します。

Two things this proves that the publish check cannot. The absence of a cookie falls back to the English source, so the translated output is genuinely resolved per request rather than baked in. And the Japanese renders as あと 3 日で, with the <Num> slot moved inside the sentence. A string-catalogue approach that concatenated "Your trial ends in " + n + " days." could not produce that word order.

How it works

gt-next has a runtime half and a build half, but they share one configuration file.

The runtime half is withGTConfig plus GTProvider. It decides which locale a request renders in and fetches translations. withGTConfig reads gt.config.json, so it needs no locale settings of its own.

The build half is the CLI. It also reads gt.config.json, runs the compiler over your JSX to find <T> boundaries, and sends the extracted React elements for translation. It never reads next.config.ts.

That asymmetry is the thing to remember: gt.config.json is shared, next.config.ts is not. Declaring locales in both is redundant, and declaring them differently is a build error.

Because the unit of translation is a React element tree rather than a string, <Num>{seats}</Num> arrives as a marked slot inside a sentence. A language that reorders the clause can move the slot. String-catalog approaches that concatenate fragments cannot do this.

Common issues

No gt.config.json file was found, even though withGTConfig is set up. withGTConfig reads gt.config.json; it does not create it. Create the file, then let both halves read it.

Conflicting configuration detected. You declared locales in both next.config.ts and gt.config.json and they disagree. Delete the ones in next.config.ts rather than syncing them.

The files configuration cannot be used for this operation. You ran translate without --publish on a project that has no files block. A gt-next project has nothing to download, so it needs --publish. Add the flag, or add a files configuration if you genuinely want local output.

No files to download looks like nothing happened. For a gt-next project this line is correct. Look for CDN updated after it, and check _versionId in gt.config.json. Judging success by local file output will mislead you here.

Nothing is translated at runtime even though publish succeeded. The project needs CDN delivery enabled. A project created without it can be updated:

curl -s -X POST "https://api.gtx.dev/v2/project/info/$GT_PROJECT_ID" \
  -H "Authorization: Bearer $GT_API_KEY" -H "Content-Type: application/json" \
  -d '{"cdnEnabled":true}'
{"success":true}

Note that GET /v2/project/info/{projectId} does not echo cdnEnabled back, so you cannot read the flag to confirm it. This example enabled CDN before its first publish and did not test publishing with it disabled, so treat "publish fails without CDN" as untested rather than established.

Interpolating with template literals instead of variable components. Writing {${days} days} inside <T> hands over a pre-built string and loses the structure that makes reordering possible. Use <Num>, and the sibling components for dates and currency.

Next steps

  • Translate docs files rather than app UI, see the MDX docs example
  • Translate individual strings on demand from a server, see the runtime translation example
  • Keep terminology consistent across app and docs, see the terminology example

Source


Verification

verification:
  status: verified
  tested_at: "2026-09-21"
  product_version: "gt-next 11.4.0, gtx-cli 2.21.3, next 16.3.5"
  scope: "App Router only. Pages Router not tested."
  command: "npx next build && npx [email protected] translate --api-key \"$GT_API_KEY\" --project-id \"$GT_PROJECT_ID\" --publish --timeout 900, then npx next dev and one request per locale"
  expected_result: "the project builds with locales declared only in gt.config.json; publish reports CDN updated; each request renders in the requested locale with a matching html lang"

  observed_in_run:
    config_inheritance: "withGTConfig(nextConfig) with no second argument built successfully, locales read from gt.config.json"
    conflict_guard: "declaring different locales in both files fails the build with 'Conflicting configuration detected. Key: locales'"
    publish: "<React Elements> [es, ja], No files to download, CDN updated"
    version_id: "d74fe4980f292b0accd26deb8af5976846372d8e2a5ea81d5dc22eef40135ab3"
    rendered:
      none: 'lang=en, "Your workspace Your trial ends in 3 days."'
      es: 'lang=es, "Tu espacio de trabajo Tu prueba termina en 3 días."'
      ja: 'lang=ja, "ワークスペース トライアルはあと 3 日で終了します。"'
    variable_slot: "the Japanese moved the <Num> slot inside the sentence (あと 3 日で), which a concatenated string catalogue could not do"

  also_established:
    - "next 16.3.5 declares engines node >=20.9.0"
    - "gt-next 11.4.0 peer range is next >=13.0.0 <15.2.1 || >15.2.2"
    - "gt-next resolves locale from the generaltranslation.locale cookie when no locale routing is configured"

Related Articles