In Teil 1 haben wir die Basis unseres Next.js 14-Projekts erstellt. In Teil 2 haben wir das Frontend mit Contentful als Headless-CMS, Tailwindcss für das Styling und Typescript für die Programmierung erstellt. In Teil 3 haben wir einen Header und Footer hinzugefügt, sowie die sogenannte Entwurfs-/Vorschaufunktionalität, die von Contentful angeboten wird, und nicht zuletzt den heutzutage obligatorischen Umschalter für den Dunkelmodus. In Teil 4 werden wir eine benutzerdefinierte 404-Seite hinzufügen, eine Lade-UI (SVG-Spinner) und wir fügen unserem tailwind.config.ts einige zusätzliche Farben hinzu. In Teil 5 fügen wir Mehrsprachigkeit mit dem Paket i18next und einem Sprachumschalter hinzu. Wir werden zeigen, wie Sie die Internationalisierungsfunktion für Client- und Serverkomponenten nutzen können und natürlich im Zusammenspiel mit Contentful und der GraphQL API.
Hier ist bereits ein Spoiler zum Endergebnis des 5. Teils -> https://nextjs14-internationalization-with-contentful-i18next.vercel.app/

Hinzufügen von neuen NPM Paketen für die Mehrsprachigkeit
npm i i18next i18next-browser-languagedetector i18next-resources-to-backend
npm i react-i18nextDie Pakete sind installiert und wir können loslegen.
Edge Middleware — Verwaltung der Web-Anfragen
Wir werden die Datei middleware.ts direkt unter src erstellen. Edge Middleware (Funktionen) führt Code aus, der ausgeführt wird, bevor eine Anfrage auf einer Website verarbeitet wird, um Ihren Benutzern Geschwindigkeit und Personalisierung zu bieten. Funktionen, die die Edge-Laufzeit verwenden, werden im Datenzentrum ausgeführt, das dem Benutzer am nächsten liegt. In unserem Fall ist das fallbackLng „en-US“ und daher wird dieses URL-Segment mit einen "Redirect" aus dem Pfad entfernt, sodass /en-US/about zu /about wird. Wenn das Locale fehlt, führen wir einen "Rewrite" durch, sodass unser Blog das Locale (fallbackLng) erhält.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { fallbackLng, locales } from "@/app/i18n/settings";
export function middleware(request: NextRequest) {
// Check if there is any supported locale in the pathname
const pathname = request.nextUrl.pathname;
// Check if the default locale is in the pathname
if (
pathname.startsWith(`/${fallbackLng}/`) ||
pathname === `/${fallbackLng}`
) {
return NextResponse.redirect(
new URL(
pathname.replace(
`/${fallbackLng}`,
pathname === `/${fallbackLng}` ? "/" : ""
),
request.url
),
301
);
}
// Check if the pathname is missing any locale
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const RewriteUrl = request.nextUrl;
RewriteUrl.pathname = `/${fallbackLng}${pathname}`;
return NextResponse.rewrite(new URL(RewriteUrl, request.url));
}
}
export const config = {
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
matcher: ["/((?!api|_next/static|_next/image|images|favicon.ico).*)"],
};Jetzt fügen wir den Ordner i18n unter src/app hinzu. Dieser Ordner hat einen Unterordner namens locales, in dem wir einen neuen Unterordner für jedes Locale erstellen, das wir behandeln möchten.

In diesen Beispiel fügen wir die Sprache “de-DE” hinzu. In der Datei common.json verwalten wir die Übersetzungen für Labels, Buttons usw. Die restlichen Übersetzungen werden innerhalb Contentful verwaltet.

Es gibt drei zusätzlche Dateien im Ordner “i18n”:
client.ts
Übersetzungen für Client Komponenten
server.ts
Übersetzungen für Server Komponenten
settings.ts
(Generelle Optionen)
import type { InitOptions } from "i18next";
export const fallbackLng = "en-US";
export const locales = [fallbackLng, "de-DE"] as const;
export type LocaleTypes = (typeof locales)[number];
export const defaultNS = "common";
export function getOptions(lang = fallbackLng, ns = defaultNS): InitOptions {
return {
// debug: true, // Set to true to see console logs
supportedLngs: locales,
fallbackLng,
lng: lang,
fallbackNS: defaultNS,
defaultNS,
ns,
};
}Wir importieren "useTranslation" für die Client Komponenten und "createTranslation" für die Server Komponenten.
import { useTranslation } from "@/app/i18n/client";
// Internationalization, get the translation function
const { t } = useTranslation(locale, "common");
// We use it then in the code
{t("languages.en")}or
import { createTranslation } from "@/app/i18n/server";
// Internationalization, get the translation function
const { t } = await createTranslation(params.locale as LocaleTypes, "common");
// We use it then in the code
{t("languages.en")}common bezieht sich auf den namespace "common" -> common.json unter src/app/i18n/locales/de-DE/
Eine neue Sprache (de-DE) in Contentful hinzufügen
Jetzt springen wir kurz zu Contentful, da wir eine neue Sprache hinzufügen müssen.




Nun können wir das Post “TestBlogPost” öffnen und die zweite Sprache aktivieren.


Falls noch nicht bei der Erstellung des Inhaltstyps geschehen, müssen Sie die Lokalisierung des Feldes aktivieren, das dies benötigt, wie den Titel, Untertitel, Inhalt usw.


Bitte überprüfen Sie Ihre Inhalte, fügen Sie den benötigten übersetzten Inhalt hinzu und veröffentlichen Sie sie erneut, damit wir die Inhaltsobjekte in beiden Sprachen bereit haben.
Neue/angepasste/gelöschte Komponenenten/Seiten
Jetzt werden wir einen neuen Ordner [locale] unter src/app hinzufügen. Verschieben Sie den Ordner [slug] und die Dateien page.tsx und layout.tsx in diesen neuen Ordner und erstellen Sie die Datei layout.tsx unter src/app.
import "./globals.css";
export const metadata = {
title: "Example Blog",
description: "Your Example Blog Description",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<head></head>
<body>{children}</body>
</html>
);
}Schauen wir uns die verschobene Datei layout.tsx, jetzt im Verzeichnis src/app/[locale] etwas genauer an.
import type { Metadata } from "next";
import { Urbanist } from "next/font/google";
import { draftMode } from "next/headers";
import "@/app/globals.css";
import Header from "@/components/header/header.component";
import Footer from "@/components/footer/footer.component";
import { Providers } from "@/components/header/providers";
import getAllNavitemsForHome from "@/components/header/navbar.menuitems.component";
import getAllFooteritemsForHome from "@/components/footer/footer.menuitems.component";
import ExitDraftModeLink from "@/components/header/draftmode/ExitDraftModeLink.component";
import { locales } from "@/app/i18n/settings";
const urbanist = Urbanist({ subsets: ["latin"], variable: "--font-urbanist" });
export async function generateStaticParams() {
return locales.map((lng) => ({ lng }));
}
export const metadata: Metadata = {
title: "Example Blog",
description: "Your Example Blog Description",
};
type LayoutProps = {
children: React.ReactNode;
params: { locale: string };
};
export default async function Layout({ children, params }: LayoutProps) {
const locale = params.locale;
const headerdata = await getAllNavitemsForHome(locale);
const footerdata = await getAllFooteritemsForHome(locale);
return (
<main className={`${urbanist.variable} font-sans dark:bg-gray900`}>
<Providers>
<Header menuItems={headerdata} />
{draftMode().isEnabled && (
<p className="bg-emerald-400 py-4 px-[6vw]">
Draft mode is on! <ExitDraftModeLink className="underline" />
</p>
)}
{children}
<Footer footerItems={footerdata} />
</Providers>
</main>
);
}Wir haben den Import hinzugefügt
import { locales } from "@/app/i18n/settings";und wir übergeben einen neuen Typ „LayoutProps“ mit unseren "Children" und dem Locale darin. Wir verwenden diesen neuen Typ dann in unserer Layout-Funktion, weil wir das Locale-Prop an unsere Funktionen/Komponenten übergeben müssen.
Unsere GraphQL Dateien wurden bereits mit der Variable “locale” vorbereitet
query pageBlogPost($slug: String!, $locale: String, $preview: Boolean) {
pageBlogPostCollection(
limit: 1
where: { slug: $slug }
locale: $locale
preview: $preview
) {
items {
...PageBlogPostFields
}
}
}Jetzt können wir den Inhalt in der richtigen Sprache abfragen.
Als letzten Schritt müssen wir unsere Seiten/Komponenten ändern, damit die neue Sprache richtig funktioniert.
Hier alle geänderten Dateien.

Sprachenumschalter
Wir fügen neuen Code zu unserer navbar.component.tsx für den Sprachumschalter hinzu. Oben fügen wir diese neuen Importe hinzu:
import { Fragment } from "react";
// Internationalization
import { useTranslation } from "@/app/i18n/client";
import type { LocaleTypes } from "@/app/i18n/settings";
import {
useRouter,
usePathname,
useParams,
useSelectedLayoutSegments,
} from "next/navigation";
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";Unterhalb “export default function Navbar…..” fügen wir diese neuen Konstanten hinzu und die asynchrone Funktion für den Sprachwechsel.
// Internationalization
const locale = useParams()?.locale as LocaleTypes;
const pathname = usePathname();
const currentRoute = pathname;
const { t } = useTranslation(locale, "common");
const { push } = useRouter();
const router = useRouter();
const urlSegments = useSelectedLayoutSegments();
async function handleLocaleChange(event: any) {
const newLocale = event;
// This is used by the Header component which is used in `app/[locale]/layout.tsx` file,
// urlSegments will contain the segments after the locale.
// We replace the URL with the new locale and the rest of the segments.
router.push(`/${newLocale}/${urlSegments.join("/")}`);
}Im Bereich “Desktop View” fügen wir die neue locale für unsere Links hinzu.
<div className="hidden lg:ml-6 lg:flex lg:space-x-8">
{/* Desktop View */}
{menuItems.map((menuItem: any, index: number) => (
<NavigationLink
key={index}
// href={menuItem.href}
href={`/${locale}${menuItem.href}`}
name={menuItem.name}
/>
))}
</div>Und natürlich auch den Code für das eigentliche Sprachenauswahlmenü.
{/* Language dropdown */}
<Menu as="div" className="relative flex-shrink-0 ml-4">
<div>
<Menu.Button className="flex text-sm bg-white rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span className="sr-only">
{" "}
{t("user.languageswitcher")}
</span>
<GlobeAmericasIcon
className="w-8 h-8 hover:text-gray-500"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 block w-48 py-1 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<a
onClick={() => {
handleLocaleChange("en-US");
}}
className={classNames(
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700"
)}
>
🇺🇸 {t("languages.en")}
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {
handleLocaleChange("de-DE");
}}
className={classNames(
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700"
)}
>
🇩🇪 {t("languages.de")}
</a>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>Tipp: Diese hübschen Flaggen können Sie hier finden -> https://emojipedia.org/
Die Icons von emojipedia machen sich auch bei den Inhaltstypen in Contentful sehr gut. Einfach den Inhaltstyp bearbeiten und per copy & paste das Icon ins Feld einfügen und speichern.


Wie immer finden Sie unten den Link zum Github Repo mit allen Codebeispielen bzw. dem kompletten Projekt, welches auch eine aktualisierte export.json für den Contenfulimport bereitstellt. Bleiben Sie dran, denn in den nächsten Beiträgen werden wir noch diese Funktionen umsetzen:
On-Site-Suche mit Algolia (Erweiterte Datensynchronisation zwischen Contentful und Algolia über eine Azure Logic App)
Hinzufügen eines neuen Seiteninhalts-Typs zusätzlich zu einem deep dive mit GraphQL
Registrierung/Anmeldung/Benutzerrollenverwaltung mit Next-Auth und Azure Entra ID
und vieles mehr. Wie immer werden wir nur Dienste im kostenlosen Tarif nutzen
Wenn Ihnen gefällt, was Sie sehen, dann unterstützen Sie mich bitte mit einem "Clap" oder folgen Sie mir auf medium.com.
Github Repo mit vollständigen Code
https://github.com/cloudapp-dev/nextjs14-typescript-app-router-contentful/tree/nextjs14-part5




