Loading...
Nextjs 14 Part5
Author Cloudapp
E.G.

Next.js 14 - Komplettes Beispiel - Hinzufügen von Internationalisierung / Mehrsprachigkeit zu unserem Blog basierend auf Contentful/GraphQL - Teil 5

29. März 2024
Inhaltsverzeichnis

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/

Spoiler part5
Spoiler part5

Hinzufügen von neuen NPM Paketen für die Mehrsprachigkeit

npm i i18next i18next-browser-languagedetector i18next-resources-to-backend
npm i react-i18next

Die 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.

i18n folder
i18n folder

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.

Common.json en-US
Common.json en-US

Es gibt drei zusätzlche Dateien im Ordner “i18n”:

  1. client.ts

    Übersetzungen für Client Komponenten

  2. server.ts

    Übersetzungen für Server Komponenten

  3. 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.

Contentful Locales Step1
Contentful Locales Step1
Contentful Locales Step2
Contentful Locales Step2
Contentful Locales Step3
Contentful Locales Step3
Contentful Locales Step4
Contentful Locales Step4

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

Contentful Locales Step5
Contentful Locales Step5
Contentful Locales Step6
Contentful Locales Step6

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.

Contentful Locales Step7
Contentful Locales Step7
Contentful Locales Step8
Contentful Locales Step8

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.

Changed Files Part5
Changed Files Part5

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.

Contentful Contenttype flag
Contentful Contenttype flag
contentful flags
contentful flags

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

Verwandte Artikel