18ways-Blog
Wie man einer bestehenden Next.js-App mehrsprachige i18n und Lokalisierung hinzufügt, ohne der SEO zu schaden oder alles von Grund auf neu aufzubauen.
I18n (Internationalisierung), l10n (Lokalisierung), mehrsprachige Unterstützung … wie auch immer du es nennst, irgendwann musst du deine Next.js-App so aktualisieren, dass sie mehr als eine Sprache unterstützt.
Die Schwierigkeit liegt nicht darin, einen Satz zu übersetzen. Es geht darum, mehrere Sprachen zu einer echten Next.js-App hinzuzufügen, ohne SEO zu beschädigen, deinen Codebase-Knoten zu verheddern oder dir selbst ein Wartungsproblem einzubauen.
Wenn du direkt zum Code springen möchtest, schau dir die 18ways-next GitHub-Beispiele an.
Bevor wir irgendetwas anderes tun, müssen wir unsere Pakete installieren. Wir richten die 18ways-Bibliotheken ein, aber Tools wie i18next funktionieren gut, wenn dein Projekt einfach ist und nur statische Inhalte hat.
npm install @18ways/next @18ways/reactErstelle deine Konfigurationsdatei:
// 18ways.config.js
module.exports = {
apiKey: 'pk_dummy_demo_token',
baseLocale: 'en-GB',
router: 'app', // 'app', or 'path' depending on which Next.js router you are using
};Wickle deine Next.js-Konfiguration ein:
// next.config.js
const { withWays } = require('@18ways/next/config');
const nextConfig = {
/*
* your normal Next.js config here
*/
};
module.exports = withWays(nextConfig);Füge einen Root-Proxy hinzu, damit / zur richtigen Locale weiterleiten kann:
// proxy.js
export { default, config } from '@18ways/next/proxy';Dann füge ein lokalisiertes Layout hinzu:
// app/layout.jsx
import './styles.css';
import { WaysRoot } from '@18ways/next/server';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className="next-demo-body">
<WaysRoot>{children}</WaysRoot>
</body>
</html>
);
}Und fertig! Du hast jetzt:
Du kannst jetzt deine erste Seite übersetzen. Tools wie i18next verlangen, dass du all deinen Text in Übersetzungsschlüssel aufteilst und diese dann in deinem Code referenzierst. Wie genau das aussieht, hängt von der von dir gewählten Bibliothek ab.
Wenn du 18ways verwendest, kannst du den Text, den du übersetzen möchtest, einfach in eine <T>-Komponente einhüllen:
// src/components/MyExampleComponent.jsx
import { useT, T } from '@18ways/react';
export default function MyExampleComponent() {
const t = useT();
return (
<div>
<T>Hello world!</T>
<img
src="https://example.com/image.png"
alt={t('Example image')}
/>
</div>
);
}Du möchtest Nutzer vermutlich ihre Sprachwahl wechseln lassen:
// src/components/Footer.jsx
import { T, LanguageSwitcher } from '@18ways/react';
export default function Footer() {
return (
<footer>
<T>My footer content</T>
<LanguageSwitcher />
</footer>
);
}Sobald die Grundstruktur steht, gibt es ein paar Wege, wie diese Projekte meist schiefgehen.
Serverseitiges Rendern (SSR) ermöglicht es deiner Next.js-App, HTML serverseitig vorgerendert auszuliefern. Das ist sowohl für SEO als auch dafür entscheidend, dass Nutzer keine kurzen Aufblitzer falscher Inhalte oder der falschen Sprache sehen.
Wenn du eine Bibliothek wie i18next verwendest, musst du sehr genau darauf achten, dass deine Übersetzungen beim serverseitigen Rendern geladen und eingefügt werden. Du kannst das testen, indem du dir den view-source: deiner Seite ansiehst, z. B. view-source:http://localhost:3000/. Das solltest du auch in der Produktion prüfen, um sicherzustellen, dass es auch in deinem Produktiv-Build funktioniert.
Wenn du 18ways verwendest, musst du dir darüber keine Gedanken machen. Das wird alles für dich erledigt.
Wenn du 18ways verwendest, musst du dir darüber überhaupt keine Gedanken machen. 18ways braucht keine Übersetzungsschlüssel; du kannst deinen Text ganz normal an Ort und Stelle lassen.
Viele i18n-Systeme verlangen, dass du deinen Code in Übersetzungsschlüssel aufteilst. Dabei ist es wichtig, bei der Benennung dieser Schlüssel sorgfältig zu sein.
Schlechte Schlüssel sind vage:
// bad-keys.en-GB.js
module.exports = {
title: 'Continue',
button: 'Pay now',
label: 'Home',
};Diese Schlüssel verraten Übersetzenden und Entwicklerinnen und Entwicklern fast nichts darüber, wo der Text erscheint.
Bessere Schlüssel enthalten Kontext:
// better-keys.en-GB.js
module.exports = {
'checkout.payment.primaryButton': 'Pay now',
'checkout.payment.stepTitle': 'Complete your payment',
'account.sidebar.homeLink': 'Home',
};Vermeide außerdem, Schlüssel dynamisch zu erzeugen:
// bad-dynamic-keys.js
const key = `checkout.${status}.${buttonType}`;
const translatedText = t(key);Das bringt IDE-Tools durcheinander, die Übersetzungsschlüssel weniger unerträglich machen sollen. Außerdem wird es extrem schwierig, alte Übersetzungsschlüssel zu suchen und aufzuräumen.
// better-dynamic-keys.js
const keyMap = {
success: {
primary: t('checkout.success.primary'),
default: t('checkout.success.default'),
},
error: {
primary: t('checkout.error.primary'),
default: t('checkout.error.default'),
},
};
const translatedText = keyMap[status][buttonType];Das ist weniger DRY, aber der beste Weg, zu verhindern, dass Übersetzungsschlüssel unüberschaubar werden.
Noch besser ist es, ein Tool wie 18ways zu verwenden; dann brauchst du überhaupt nie Übersetzungsschlüssel:
// app/[lang]/checkout/page.jsx
<T>Pay now</T>Die Lokalerkennung bedeutet, vor dem manuellen Wechsel zu entscheiden, welche Sprache ein Nutzer sehen soll.
Das umfasst meist eine Kombination aus:
Accept-Language-Header des BrowsersIn normalem Next.js-Middleware sieht das oft so aus:
// middleware.js
/**
* You don't need any of this if you're using 18ways
*/
import { NextResponse } from 'next/server';
const acceptedLocales = ['en-GB', 'fr-FR'];
export function middleware(request) {
const savedLocale =
request.cookies.get('preferred-locale')?.value;
const browserLocale =
request.headers
.get('accept-language')
?.split(',')[0] || 'en-GB';
const locale = acceptedLocales.includes(savedLocale)
? savedLocale
: acceptedLocales.includes(browserLocale)
? browserLocale
: 'en-GB';
if (request.nextUrl.pathname === '/') {
return NextResponse.redirect(
new URL(`/${locale}`, request.url)
);
}
return NextResponse.next();
}Manche Bibliotheken haben Hilfsfunktionen, die das in unterschiedlichem Maße erleichtern. Mit 18ways werden die anfängliche Erkennung und die Weiterleitungsebene für dich erledigt.
Unterschiedliche Locale formatieren Daten, Zahlen und Geldbeträge unterschiedlich.
Zum Beispiel:
04/05/2026 kann den 4. Mai oder den 5. April bedeuten€1.999,00 und 1.999,00 € sind je nach Gebietsschema beide gültigIn reinem JavaScript erledigst du das selbst mit Intl:
// formatting-dates-and-currency.js
const myLocale = getCurrentLocale(); // depends on your lib
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const dateLabel = new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'long',
}).format(someTimestamp);
const someMoney = {
amount: 1999,
currency: 'EUR',
};
const moneyLabel = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: someMoney.currency,
}).format(someMoney.amount);
const translatedText = t(
'my.translation.key',
{ dateLabel, moneyLabel }
);Wenn du 18ways verwendest, wird das für dich erledigt:
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const someMoney = {
amount: 1999,
currency: 'EUR',
};
<T>My text with {{ someTimestamp }} and {{ someMoney }}</T>Baue keine übersetzte UI, indem du Fragmente zusammensetzt:
// bad-string-joining.jsx
const clickHereText = t('click.here');
const toGetStartedText = t('to.get.started')
<p><a href="#">{clickHereText}</a> {toGetStartedText}.</p>Das wird in mehreren Sprachen kaputtgehen.
Auf Englisch würdest du vielleicht „👉👉Hier klicken👈👈, um loszulegen“ sagen, aber auf Französisch ist es natürlicher, „Pour commencer, 👉👉cliquez ici👈👈“ zu sagen. Manche Sprachen wie Japanisch brauchen sogar Wörter sowohl davor als auch danach, etwa „始めるには👉👉こちら👈👈をクリックしてください“.
Die Satzstruktur kann sich ändern, daher macht es die Übersetzung viel schwerer, wenn man den Text in einzelne Teile zerlegt.
Vollständige Sätze lassen sich besser übersetzen, weil Übersetzer Wörter natürlich umstellen und die Bedeutung als ganze Einheit erfassen können.
Wenn du JSX so übersetzt:
// rich-text-message.jsx
<p>
Something <strong>that we want to be bold</strong>
</p>du musst dazu in deiner i18n-Bibliothek nachsehen, denn das wird von den verschiedenen Bibliotheken sehr unterschiedlich gehandhabt.
Wenn du 18ways verwendest, kannst du einfach den gesamten JSX-Block wie gewohnt übersetzen.
// rich-text-message.jsx
<p>
<T>Something <strong>that we want to be bold</strong></T>
</p>Mit Variablen kannst du den Satz als Ganzes beibehalten und trotzdem Laufzeitwerte einfügen.
Die meisten i18n-Bibliotheken unterstützen das in irgendeiner Form. Mit 18ways:
// app/[lang]/page.jsx
<T>Hello {{ name: 'Ada' }}</T>Dieses Muster funktioniert gut für:
Der Satz bleibt gut lesbar, und der Wert bleibt eindeutig.
Wenn auch die Variable selbst übersetzt werden muss, achte darauf, sie ebenfalls in ein t(...) zu packen:
const animal = t('dog');
<T>Favourite animal: {{ animal }}</T>Pluralregeln unterscheiden sich je nach Sprache. Englisch hat eine Pluralform (1 year, 2 years usw.). Polnisch hat mehrere Pluralformen (1 rok, 2 lata, 5 lat). Japanisch hat überhaupt keine Pluralform (1 年, 2 年, 3 年).
Die meisten i18n-Bibliotheken erlauben dir, eine ICU-ähnliche Syntax für Pluralformen anzugeben:
// app/[lang]/page.jsx
<T>
{{
unreadCount,
format:
'plural, =0{No unread messages} =1{One unread message} other{{unreadCount} unread messages}',
}}
</T>Das Obige funktioniert auf 18 Wegen, aber in den meisten Fällen kannst du es auch einfach so machen:
// app/[lang]/page.jsx
<T>{{ unreadCount }} unread messages</T>18ways kümmert sich für dich um Pluralformen!
Das Hinzufügen mehrerer Sprachen zu einer Next.js-App muss nicht zu einem Neuaufbau-Projekt werden.
Wenn du keine Lösung wie 18ways verwendest — achte darauf, dass Routing und SSR korrekt eingerichtet sind, und arbeite dann daran, deinen Text in Übersetzungsschlüssel auszulagern. Vermeide dabei die üblichen i18n-Fallstricke.
Wenn du 18ways verwendest, wird das alles für dich erledigt, und du musst deinen Text nur noch in <T>-Blöcke einhüllen!