Bittest du Cursor oder Claude Code heute, i18n zu einer Next.js-App hinzuzufügen, bekommst du sehr wahrscheinlich middleware.ts, setRequestLocale in jedem Layout und useTranslations aufgerufen aus einer async-Seite — ein Setup, das für Next.js 15 korrekt war und auf Next.js 16 eine Generation veraltet ist. globalize.now ist KI-gestützte Lokalisierungsinfrastruktur, deshalb behalten wir genau diese Schicht im Blick: Die Routing-Verdrahtung hat sich geändert, der Katalog, den sie bedient, nicht. Im Folgenden: was sich verschoben hat, wie du erkennst, aus welcher Ära dein generierter Code stammt, und die drei Fixes, die einen Nachmittag statt einen Rewrite kosten.

Nichts davon ist ein Breaking Change. Genau deshalb übersieht man es so leicht.

Was hat sich in Next.js 16 für i18n geändert?

Die Datei, die dein Locale aushandelt, hat ihren Namen geändert. Next.js dokumentiert die middleware-Dateikonvention als veraltet und in proxy umbenannt, eingeführt in v16.0.0 — die eigene Begründung dreht sich um Begrifflichkeit: „middleware

Zwei Verhaltensänderungen kommen mit der Umbenennung in v16 mit. Proxy nutzt standardmäßig die Node.js-Runtime, und die dateispezifische runtime-Konfigurationsoption steht dort nicht mehr zur Verfügung — setzt man sie trotzdem, wirft Next.js einen Fehler. Proxy wird außerdem beim statischen Export nicht unterstützt, was relevant ist, falls die Locale-Verhandlung der einzige Grund ist, warum du nicht exportierst.

Speziell für das Locale-Routing zeigt der Setup-Guide von next-intl die Datei jetzt als src/proxy.ts und weist unmissverständlich darauf hin, dass sie bis Next.js 16 middleware.ts hieß. Der Import darin bleibt unverändert:

// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};

Achte auf die Asymmetrie, denn genau die bringt Leute ins Straucheln: Die Datei heißt jetzt proxy.ts, während der Importpfad weiterhin next-intl/middleware lautet. Den Import umzubenennen, ist eine typische Überkorrektur.

Warum hinkt KI-generierter i18n-Code immer eine Version hinterher?

Weil ein Coding-Agent das am häufigsten vertretene Muster vorhersagt — und das am häufigsten vertretene Muster ist das, das über Jahre Zeit hatte, sich anzusammeln. Jeder Blogpost, jede Stack-Overflow-Antwort und jedes GitHub-Beispiel zum Locale-Routing im App Router, das vor Ende 2026 geschrieben wurde, verwendet middleware.ts. Ein Modell, das diesen riesigen Korpus gegen ein paar Wochen v16-Dokumentation abwägt, greift selbstbewusst zur alten Form — ohne jede Warnung, dass eine Umbenennung stattgefunden hat.

Das ist derselbe Mechanismus hinter einem Problem, über das wir schon geschrieben haben: Cursor fügt nach dem i18n-Setup weiterhin hartcodierte Strings ein — der Agent reproduziert die statistisch normale Codebase, nicht deine. Aus demselben Grund beheben Instruction-Dateien das Problem bei GitHub Copilot nur teilweise: Regeln, die die Chat-Oberfläche liest, werden von der Inline-Vervollständigung nicht zwangsläufig gelesen.

Die praktische Konsequenz ist eng begrenzt, aber real. Dein generiertes Setup funktioniert, also schlägt nichts sichtbar fehl. Dann stößt du auf einen Bug, suchst danach, und jede aktuelle Antwort beschreibt Dateien, die du gar nicht hast.

Wie erkenne ich, aus welcher Ära mein generiertes Setup stammt?

Vier Greps. Führe sie im Projekt-Root aus, und in etwa einer Minute weißt du Bescheid.

# 1. Pre-16 locale negotiation file
ls middleware.ts src/middleware.ts 2>/dev/null

# 2. Legacy static-rendering API
grep -rn "setRequestLocale" app src 2>/dev/null

# 3. Hooks called inside async components
grep -rn -B3 "useTranslations" app | grep -n "async function"

# 4. Which next-intl era the config reads from
grep -rn "root-params\|await params" src/i18n/request.ts 2>/dev/null

Treffer bei 1 und 2 bedeuten Pre-16-Scaffolding. Ein Treffer bei 3 ist ein echter Laufzeitfehler, der nur auf die erste Anfrage wartet, die diese Komponente rendert. Kein Treffer bei 4 bedeutet, dass deine Request-Konfiguration das Locale noch auf die alte Art ausliest.

Muss ich middleware.ts in proxy.ts umbenennen?

Nicht dringend, und von Hand solltest du es sowieso nicht machen. Next.js liefert einen Codemod mit, der sowohl die Datei als auch die exportierte Funktion umbenennt:

npx @next/codemod@canary middleware-to-proxy .

Die Umbenennung ist eine Deprecation, keine Entfernung — ein bestehendes middleware.ts funktioniert also weiterhin. Der Grund, den Codemod trotzdem laufen zu lassen, sind die Wartungskosten: Sobald deine Dateinamen mit der aktuellen Dokumentation übereinstimmen, trifft jedes zukünftige Suchergebnis auf dein Repo zu. Lässt du es veraltet, zahlst du bei jeder Debugging-Session eine kleine Steuer — für immer.

Ist setRequestLocale in next-intl deprecated?

Es ist als Legacy markiert, was eine schwächere Aussage ist als deprecated und genau gelesen werden sollte. Die next-intl-Dokumentation beschreibt setRequestLocale als API, die existierte, bis next/root-params eingeführt wurde, gibt an, dass sie aus Gründen der Abwärtskompatibilität weiterhin unterstützt wird, und empfiehlt stattdessen next/root-params.

Die neuere Variante liest das ermittelte Locale direkt aus deiner Request-Konfiguration, statt es manuell durch jedes Layout und jede Page zu reichen:

// src/i18n/request.ts
import * as rootParams from 'next/root-params';
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({locale}) => {
  if (!locale) {
    const paramValue = await rootParams.locale();
    if (hasLocale(routing.locales, paramValue)) {
      locale = paramValue;
    } else {
      notFound();
    }
  }

  return {locale};
});

next/root-params ist ab Next.js 16.3 standardmäßig verfügbar; auf früheren Versionen muss es über experimental.rootParams aktiviert werden. Folgst du dem Setup auf diese Weise, erhältst du statisches Rendering ohne Zusatzaufwand – vorausgesetzt, du exportierst weiterhin generateStaticParams für das [locale]-Segment.

Der alte Ansatz verlangte, setRequestLocale in jeder Page und jedem Layout aufzurufen, das statisch gerendert werden sollte, und zwar vor jedem anderen next-intl-Aufruf – weil Next.js Layouts und Pages unabhängig voneinander rendert. Das ist eine Regel, die ein KI-Agent spätestens bei der fünften Datei vergisst. Fällt die Anforderung weg, fällt die ganze Fehlerklasse weg.

Warum stürzt useTranslations in meiner asynchronen Server Component ab?

Weil Hooks nicht aus async Komponenten aufgerufen werden können und useTranslations ein Hook ist. Das ist eine Einschränkung von React Server Components, keine Eigenheit von next-intl – und next-intls Antwort darauf ist ein paralleles Set awaitbarer Funktionen:

// Async component — await the server API
import {getTranslations} from 'next-intl/server';

export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getTranslations('ProfilePage');
  return <h1>{t('title', {username: user.name})}</h1>;
}
// Non-async component — the hook is correct here
import {useTranslations} from 'next-intl';

export default function UserDetails({user}) {
  const t = useTranslations('UserProfile');
  return <h2>{t('title')}</h2>;
}

getFormatter, getNow, getTimeZone, getMessages und getLocale folgen demselben Muster. Das zweite Beispiel lohnt einen genaueren Blick: Eine nicht-asynchrone Komponente ohne interaktive Features ist eine shared Komponente, und next-intl wählt die passende Implementierung, je nachdem, ob sie serverseitig oder clientseitig rendert. useTranslations in einer Server Component ist also kein Fehler – es aus einer async aufzurufen, schon.

Warum bekomme ich den NextIntlClientProvider-Context-Fehler?

Die Troubleshooting-Hinweise von next-intl nennen zwei Ursachen – und sie verlangen gegensätzliche Fixes. Entweder läuft die Komponente tatsächlich auf dem Client, ohne dass darüber ein Provider liegt – dann wrappe sie und übergib die benötigten Messages. Oder sie ist in einen Client-Modulgraphen abgedriftet, obwohl du serverseitiges Rendering erwartet hast – dann übergib sie stattdessen über children aus einer Server Component, statt sie innerhalb einer solchen zu importieren.

Der zweite Fall ist der, auf den KI-generierte Apps stoßen, weil Agents 'use client' großzügig einsetzen, um Interaktivität zum Laufen zu bringen – und die Direktive verbreitet sich durch den gesamten Import-Graphen. Das bevorzugte Muster: auf dem Server übersetzen und fertige Strings über die Grenze reichen:

import {useTranslations} from 'next-intl';
import Expandable from './Expandable'; // 'use client'

export default function FAQEntry() {
  const t = useTranslations('FAQEntry');
  return <Expandable title={t('title')}>{t('description')}</Expandable>;
}

Falls eine Komponente wirklich Messages auf dem Client braucht, kannst du einen Provider gezielt um genau diesen Teilbaum scopen, statt jede Message an den Browser zu schicken – messages={null} auf dem Root-Provider gibt gar keine weiter.

Was ändert sich dabei nicht?

Deine Locale-Dateien. Jeder Fix oben betrifft Routing und Rendering-Verdrahtung; das JSON, das deine App lädt, bleibt davon vollkommen unberührt. Und genau das ist der entscheidende Punkt: Die Verdrahtung ist ein Nachmittagsjob, der sich einmal im Jahr ändert – der Katalog dagegen ist das, was jede Woche verrottet, sobald neue UI ausgeliefert wird.

Genau für diese Trennung bauen wir. next-intl und seine Nachbarn liefern Übersetzungen zur Laufzeit aus – wir stehen nicht in Konkurrenz zu ihnen, und falls du dich noch zwischen ihnen entscheidest: next-intl vs react-i18next vs Lingui legt die Trade-offs offen. globalize.now setzt eine Ebene höher an und erzeugt die Keys und Locale-Dateien, die diese Runtimes lesen – weshalb die Next.js-Integration es egal ist, ob deine Negotiation in middleware.ts oder proxy.ts stattfindet.

Die Konvertierung passiert einmalig, direkt in der App: Du verbindest das Repository, globalize.now konvertiert die Codebase einmal und öffnet einen Pull Request mit dem Katalog. Danach übersetzen Push-Jobs neue Katalog-Einheiten, sobald sie auftauchen – das ist genau der Fehlermodus, den warum Übersetzungsdateien immer wieder aus dem Sync laufen beschreibt, und der Grund, diese Lücke vor deinem dritten Locale zu schließen, nicht danach.

Hat dein Projekt bereits ein handgeschriebenes next-intl-Setup, haben wir genau festgehalten, was wir anfassen und was nicht. Falls nicht, ist die Cursor-Anleitung der kürzeste Weg hinein – mit dem Vorbehalt, dass die generierte Datei nach der älteren Konvention benannt ist, also führe im Anschluss den Codemod aus.

Wo du anfängst

Führe die vier Greps aus. Bekommst du bei den ersten beiden Treffer, führe den Codemod aus, verschiebe deine Request-Konfiguration auf next/root-params, und du bist auf dem aktuellen Stand. Schau dir danach die Locale-Dateien an, denn das ist der Teil, der schon nächsten Monat wieder aus dem Sync läuft. Baust du eine KI-generierte App und willst den Katalog verwaltet statt gepflegt haben, ist die Vibe-Coder-Seite der Überblick, und Preise hat eine eigene Seite.

globalize.now konvertiert hartcodierte App-Texte in übersetzungsreife Locale-Dateien und hält sie aktuell, während Sie veröffentlichen.

Probiere globalize.now kostenlos