# General Translation Knowledge Base: How do I migrate from next-intl to gt-next?
URL: https://generaltranslation.com/en-US/kb/migrate-from-next-intl.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Swap next-intl's plugin, middleware, and provider for gt-next's, match its locale prefix setting, and load messages/en.json as a gt-next dictionary so existing ICU messages and translations keep working. Map any localized pathnames before deleting next-intl's routing, then move copy inline into `<T>` one route at a time.
Category: Internationalization
Last updated: 2026-11-11
Authors: Kevin Wu
Published: 2026-11-11

next-intl and gt-next both localize Next.js App Router apps with locale-prefixed routes and Server Components, so the app's structure barely changes. Your next-intl messages are already ICU MessageFormat, which is also what gt-next dictionaries use. That lets you keep `messages/en.json` and your translated catalogs while you switch libraries, then move copy inline into [`<T>`](/docs/react/reference/components/t) at your own pace.

If your catalogs work and only producing translations is slow, you may not need to migrate. If you keep next-intl, the `gt` CLI translates its catalogs in place. (See [next-intl vs. gt-next](/kb/next-intl-vs-gt-next).)

## How do next-intl APIs map to gt-next?

| next-intl | gt-next |
| --- | --- |
| `defineRouting({ locales, defaultLocale })` | `defaultLocale` and `locales` in `gt.config.json` |
| `createNextIntlPlugin()` in `next.config.ts` | [`withGTConfig`](/docs/react/nextjs/config) from `gt-next/config` |
| `createMiddleware(routing)` in `proxy.ts` | [`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) from `gt-next/middleware` |
| `localePrefix: 'as-needed'` / `'always'` | `prefixDefaultLocale: false` (the default) / `true` |
| `getRequestConfig` in `i18n/request.ts` | A root `getLocale.ts` reading `next/root-params`, plus the `dictionary` option |
| `NextIntlClientProvider` | [`GTProvider`](/docs/react/reference/components/gt-provider) in `app/[locale]/layout.tsx` |
| `useTranslations('ns')` | [`useTranslations`](/docs/react/reference/hooks/use-translations)`('ns')` from `gt-next` |
| `await getTranslations('ns')` | `await` [`getTranslations`](/docs/react/nextjs/reference/functions/get-translations)`('ns')` from `gt-next/server` |
| `t.rich` and `t.markup` | [`<T>`](/docs/react/reference/components/t) wrapping the real JSX |
| `useFormatter` / `getFormatter` | [`<Num>`](/docs/react/reference/components/num), [`<Currency>`](/docs/react/reference/components/currency), [`<DateTime>`](/docs/react/reference/components/datetime), and [`<RelativeTime>`](/docs/react/reference/components/relative-time), or ICU arguments in messages |
| [`useLocale` / `getLocale`](https://next-intl.dev/docs/usage/configuration#locale) | [`useLocale`](/docs/react/reference/hooks/use-locale) / [`getLocale`](/docs/react/nextjs/reference/functions/get-locale) |
| `setRequestLocale(locale)` | Not needed for pages and layouts with `getLocale.ts`; [`registerLocale`](/docs/react/nextjs/reference/functions/register-locale) for route handlers and image routes |
| `Link` from `createNavigation` | [`Link`](/docs/react/nextjs/link) from `gt-next/link` |
| `useRouter` / `getPathname` / `redirect` | Next.js's own APIs with [`useLocalizeHref`](/docs/react/nextjs/reference/hooks/use-localize-href) or [`getLocalizeHref`](/docs/react/nextjs/reference/functions/get-localize-href) |
| `NEXT_LOCALE` cookie | `generaltranslation.locale`, renamable with `headersAndCookies.localeCookieName` |

## Step 1: Replace the plugin and keep your messages

Install `gt-next` and the `gt` CLI, then list your languages in `gt.config.json`. Point `withGTConfig` at your existing source catalog, and match next-intl's URL shape:

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

const nextConfig: NextConfig = {};

export default withGTConfig(nextConfig, {
  dictionary: './messages/en.json',
  prefixDefaultLocale: true, // next-intl's default, localePrefix: 'always'
  headersAndCookies: { localeCookieName: 'NEXT_LOCALE' }, // keep returning visitors' language
});
```

Leave out `prefixDefaultLocale` if you used `localePrefix: 'as-needed'`. Set routing options on `withGTConfig` rather than only on the middleware, because gt-next's `<Link>` reads them from there.

This example covers standard locale prefixes, such as `/de/about`. If your `defineRouting` call customizes routes, carry each customization over before Step 2 deletes it:

- **Localized pathnames** (`pathnames`, such as `/de/ueber-uns`): map each one with gt-next's [`pathConfig`](/docs/react/nextjs/reference/functions/create-next-middleware#path-config) option, which takes the same shape of a shared path and a localized path per locale.
- **Custom prefixes, domain-based routing, or `localePrefix: 'never'`:** gt-next has no direct equivalent. Decide on the new URLs and add permanent redirects from the old ones before you remove next-intl's routing.

To keep serving your reviewed translations, add a `loadDictionary.ts` file at the project root. gt-next detects it automatically:

```ts title="loadDictionary.ts"
export default async function loadDictionary(locale: string) {
  try {
    return (await import(`./messages/${locale}.json`)).default;
  } catch {
    return {}; // a new locale without a catalog uses General Translation's translations
  }
}
```

Entries in these files take precedence. General Translation's translation renders for any key a catalog lacks. If `gt.config.json` already lists these catalogs under `files`, remove that entry: gt-next now loads them as dictionaries, and each string should have one translation owner.

## Step 2: Swap the middleware, layout, and request config

Replace next-intl's middleware in `proxy.ts` (`middleware.ts` on Next.js 15 and earlier):

```ts title="proxy.ts"
import { createNextMiddleware } from 'gt-next/middleware';

export default createNextMiddleware();

export const config = {
  matcher: ['/((?!api|static|.*\\..*|_next).*)'],
};
```

Once its routes are mapped, delete `i18n/request.ts` and `i18n/routing.ts`. In their place, add a root `getLocale.ts` so pages and layouts can render statically without `setRequestLocale`:

```ts title="getLocale.ts"
import { locale } from 'next/root-params';

export default async function getLocale() {
  return await locale();
}
```

`next/root-params` needs Next.js 15.5 or later; see the [middleware guide](/docs/react/nextjs/app-router-middleware#root-locale) for 15.2 through 15.4. Then replace the provider in your locale layout:

```tsx title="app/[locale]/layout.tsx"
import { GTProvider, getLocales } from 'gt-next';
import { getLocale } from 'gt-next/server';

export function generateStaticParams() {
  return getLocales().map((locale) => ({ locale }));
}

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

By default, gt-next falls back to `defaultLocale` for an unsupported locale segment. To return a 404 as next-intl's `hasLocale` check does, test the segment with [`isLocaleSupported`](/docs/react/nextjs/reference/functions/is-locale-supported) and call `notFound()`.

## Step 3: Change the imports

With the catalog loaded as a dictionary, most components change only their import line:

```tsx
// Before
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { Link } from '@/i18n/navigation';

// After
import { useTranslations } from 'gt-next';
import { getTranslations } from 'gt-next/server';
import Link from 'gt-next/link';
```

`t('heading')`, namespaces, `{name}` placeholders, and ICU `plural` and `select` messages work unchanged, including in `generateMetadata`. Three patterns need edits:

- **`t.rich` and `t.markup`.** gt-next's [`t()`](/docs/react/reference/hooks/use-translations) returns a string. Move rich text inline into [`<T>`](/docs/react/reference/components/t) with real elements.
- **`t.raw` with arrays.** gt-next has no `t.raw`, and it reads a dictionary array only as a message with its options. Restructure lists as nested objects and read them with [`t.obj()`](/docs/react/guides/translating-with-dictionaries#objects) or one key at a time.
- **Missing keys.** next-intl logs an error and renders a fallback; gt-next throws, so search for a key before you delete it from the catalog.

Then run [`npx gt validate`](/docs/cli/reference/commands/validate) to check for entries that aren't valid ICU, and [`npx gt translate --dictionary ./messages/en.json`](/docs/cli/reference/commands/translate) to generate translations for keys your catalogs lack and for any new locales. The CLI finds a dictionary on its own only when it's named `dictionary.json`, `.ts`, or `.js` in the project root or `src/`, so pass `--dictionary ./messages/en.json` to both commands, locally and in CI, for as long as you keep the catalog:

```bash
npx gt validate --dictionary ./messages/en.json
npx gt translate --dictionary ./messages/en.json
```

## Step 4: Move copy inline, one route at a time

Keys keep working, so you can stop here. To keep copy beside the JSX it renders, move it into [`<T>`](/docs/react/reference/components/t) route by route, starting with `t.rich` messages:

```tsx
// Before: "terms": "By continuing you accept the <link>terms</link>."
t.rich('terms', { link: (chunks) => <a href="/terms">{chunks}</a> });

// After
<T>
  By continuing you accept the <a href="/terms">terms</a>.
</T>
```

In async Server Components, use [`getGT`](/docs/react/nextjs/reference/functions/get-gt) for plain strings. The same [`npx gt translate`](/docs/cli/reference/commands/translate) command generates new translations for copy you move inline, using the surrounding JSX as context. Reviewers can correct them in the [Dashboard](/docs/platform/dashboard/guides/reviewing-translations), and a [Context Group](/docs/platform/dashboard/guides/defining-context-for-translations) Glossary keeps your terminology. Delete each key from your catalogs once nothing reads it.

## Step 5: Remove next-intl

When a search for `next-intl` returns nothing, remove the dependency and `createNextIntlPlugin`, and test a production build in every locale. Then add [`npx gt translate`](/docs/cli/reference/commands/translate) to your build or [CI](/kb/translations-in-ci) so new copy ships translated, with `--dictionary ./messages/en.json` if you kept the catalog.

## Common questions

### Will my URLs change?

Not for standard locale prefixes, if you match next-intl's setting: `localePrefix: 'as-needed'` corresponds to gt-next's default, and `'always'` to `prefixDefaultLocale: true`. gt-next also uses an `app/[locale]` segment. Localized pathnames need an explicit `pathConfig` mapping, and custom prefixes and domain-based routing have no direct gt-next equivalent, so redirect those URLs and test every route before removing next-intl's routing.

### Do I lose my existing translations?

No. Load your next-intl catalogs with [`loadDictionary`](/docs/react/reference/functions/load-dictionary), and they render as before. General Translation translates only the keys they're missing, and any copy you later move inline into [`<T>`](/docs/react/reference/components/t).

### Does gt-next support static rendering?

Yes. Add a root `getLocale.ts` and `generateStaticParams`. (See [static site generation](/docs/react/nextjs/app-router-static-site-generation).)

### Is gt-next free?

gt-next is MIT-licensed and renders bundled translations without an account. Generating translations with General Translation is [usage-based](https://generaltranslation.com/pricing).

## Key takeaways

- Replace next-intl's plugin, middleware, request config, and provider with `withGTConfig`, [`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware), `getLocale.ts`, and [`GTProvider`](/docs/react/reference/components/gt-provider).
- Keep `messages/en.json` as a gt-next dictionary and your translated catalogs through [`loadDictionary`](/docs/react/reference/functions/load-dictionary); ICU messages work unchanged.
- Match `localePrefix` with `prefixDefaultLocale` and keep the `NEXT_LOCALE` cookie so standard locale URLs and saved preferences carry over; map localized pathnames with `pathConfig` before deleting next-intl's routing.
- Rewrite `t.rich` as [`<T>`](/docs/react/reference/components/t), then move other copy inline one route at a time.

## Related questions

- [next-intl alternatives: gt-next vs. next-intl](https://generaltranslation.com/en-US/kb/next-intl-vs-gt-next.mdx): next-intl renders translations you supply in a message catalog for each language. gt-next renders translations too, and General Translation also generates them from copy written inline in your JSX. Both are MIT-licensed and support the App Router and Server Components. If your next-intl catalogs already work, you can keep them and use General Translation to translate them without migrating.
- [How do I add i18n to a Next.js App Router app?](https://generaltranslation.com/en-US/kb/add-i18n-to-nextjs.mdx): Start with gt init to configure General Translation in a supported App Router project, then mark source content, generate translations, and verify language switching. gt-next keeps copy in JSX with translation context; the CLI, Dashboard editor, and Locadex handle generation, review, and ongoing updates. This guide also shows the explicit local-file setup.
- [How do I set up SEO for a multilingual Next.js app?](https://generaltranslation.com/en-US/kb/multilingual-nextjs-seo.mdx): Give each language its own prefixed URL, such as /en/about and /es/about, then publish html lang, a self-referencing canonical, hreflang alternates with x-default, a translated title and description, and a sitemap entry for each published language. Mark unpublished versions noindex. General Translation's gt-next routes each language to its own URL and translates titles with getGT.
- [Language redirects and SEO: URL, cookie, or Accept-Language?](https://generaltranslation.com/en-US/kb/locale-routing-seo.mdx): Let the URL decide the language of every page you want indexed, because Googlebot sends no Accept-Language header and crawls mostly from US addresses. Use a saved cookie choice, then Accept-Language, only to pick where a neutral root such as / redirects, with a temporary 302 or 307.
- [How do I keep translations up to date on every deploy?](https://generaltranslation.com/en-US/kb/translations-in-ci.mdx): Run translation as a pipeline step before each production build, so new copy ships translated with the deploy that adds it. General Translation's gt CLI does this with npx gt translate, using a project API key and project ID stored as CI secrets.

## Previous and next questions

- Previous question: [Which translation tools charge for usage instead of seats?](https://generaltranslation.com/en-US/kb/translation-pricing-without-seat-fees.mdx)
- Next question: [How do I migrate from react-i18next to gt-react?](https://generaltranslation.com/en-US/kb/migrate-from-react-i18next.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/en-US/kb/sitemap.md) for every article.
