Autor:
    Erstellung:2026-09-26Letzte Aktualisierung:2026-09-26

    Wie Sie Ihre TanStack Start-Anwendung 2026 mit Lingui internationalisieren

    Inhaltsverzeichnis

    Was ist Lingui?

    Lingui ist eine i18n-Bibliothek, die auf Makros und Nachrichtenextraktion aufbaut. Sie schreiben den Quelltext direkt in Ihre Komponenten ( t`Hello` , <Trans>Hello</Trans>), lingui extract sammelt jede Nachricht in Katalogen (standardmäßig PO-Dateien), Übersetzer befüllen diese und das Vite-Plugin kompiliert sie zu kompaktem JavaScript. Nachrichten nutzen das ICU MessageFormat, sodass Pluralformen und Selects unterstützt werden.

    TanStack Start enthält von Haus aus keine i18n-Schicht, daher bindet diese Anleitung Lingui von Grund auf ein:

    • Durch Babel kompilierte Makros über @rolldown/plugin-babel (erforderlich bei @vitejs/plugin-react v6 und Vite 8).
    • Locale-Routing mit einem optionalen {-$locale}-Segment (/about, /fr/about).
    • Ein Katalog pro Locale, geladen bei Bedarf, und eine I18n-Instanz pro Rendervorgang, damit gleichzeitige SSR-Anfragen niemals ein Locale teilen.
    • Vollständiges mehrsprachiges SEO: übersetzter <title> und Beschreibung, kanonische URL, hreflang mit x-default, Open-Graph-Locales, JSON-LD, Sitemap, robots.txt, Pre-Rendering und lokalisierte 404-Seiten.
    Suchen Sie nach einem anderen Stack? Lesen Sie den TanStack Start + use-intl Leitfaden, den TanStack Start + Paraglide Leitfaden oder den TanStack Start + Intlayer Leitfaden.
    Nutzen Sie Next.js? Lesen Sie den Next.js + Lingui Leitfaden. Vergleichen Sie Bibliotheken? Lesen Sie Lingui vs. Intlayer.

    Was der Benchmark über Lingui auf TanStack Start aussagt

    Der i18n-Benchmark führt dieselbe TanStack Start-App mit 10 Seiten und 10 Sprachen mit jeder wichtigen Bibliothek aus und misst, was der Browser tatsächlich herunterlädt.

    Dynamisches JSON-Laden

    Lädt Übersetzungen während der Laufzeit verzögert

    Gescoptes JSON (Namespacing)

    Übersetzungs-Namespaces pro Seite

    I18n Performance-Benchmark

    Was ist diese Metrik?

    Die gesamte gzip-komprimierte Größe des Internationalisierungs-Bibliothekspakets. Es enthält nur den Provider und die Inhaltsabruflogik nach Tree-Shaking und Minimierung.

    Warum ist das wichtig?

    Eine kleinere Bibliotheksgröße reduziert die anfängliche JavaScript-Nutzlast, was zu schnelleren Download- und Ausführungszeiten auf dem Client führt.

    Ansehen als

    Wichtige Kennzahlen für @lingui/core@6.6.0, gemessen am 26.09.2026 (gzip):

    SetupBibliotheksgrößeJS pro SeiteLeak anderer LocalesLeak anderer Seiten
    Kein i18n (Basis-App)-111.0 KB0%0%
    Lingui (Setup dieser Anleitung)56.7 KB115.2 KB9.3%0%
    @intlayer/lingui (Kompatibilität)9.8 KB136.7 KB9.9%0%
    react-intlayer (natives Intlayer)4.5 KB126.8 KB0%0%

    Die wichtigsten Erkenntnisse:

    • Laden Sie einen Katalog pro Locale bei Bedarf. Dadurch bleibt die Seitengröße nahe an der Basis-App.
    • Die Laufzeit bleibt schwer (~57 KB gzip). Der @intlayer/lingui-Kompatibilitätsadapter (Schritt 16) behält Ihre Makros bei und reduziert sie auf ~10 KB.
    Vollständige Daten finden Sie im TanStack Start Benchmark-Bericht und im Benchmark-Repository.

    Funktionsvergleich auf TanStack Start

    Wie Lingui im Vergleich zu anderen gängigen Bibliotheken auf TanStack Start abschneidet:

    Funktionreact-intlayer (Intlayer)use-intlParaglide JSLingui
    Übersetzungen nahe an Komponenten✅ Ko-lokalisiert❌ Zentrales JSON❌ Eine JSON-Datei pro Locale⚠️ Quelltext in Komponenten
    TypeScript-Integration✅ Automatisch generierte Typen✅ Über AppConfig✅ Typisierte Nachrichtenfunktionen⚠️ Nur Makros
    Erkennung fehlender Übersetzungen✅ Typfehler und Build-Warnungen⚠️ Laufzeit-Fallback⚠️ Fällt auf Basis-Locale zurück⚠️ Fällt auf Quelltext zurück
    Rich Content (JSX, Markdown)✅ Direkte Unterstützung⚠️ Tags über t.rich⚠️ Zeichenketten✅ JSX innerhalb von <Trans>
    Lokalisiertes Routing✅ Integriert❌ Manuelles {-$locale}✅ urlPatterns + Router-Rewrite❌ Manuelles {-$locale}
    Sprachwechsel ohne Neuladen✅ Ja✅ Ja❌ Vollständiger Seiten-Reload✅ Ja
    Pluralisierung✅ Aufzählungsbasiert✅ ICU✅ Varianten✅ ICU
    ICU MessageFormat✅ Über format: "icu"✅ Nativ⚠️ Über ein inlang-Plugin✅ Nativ
    Inhaltsformate✅ .ts, .json, .md, .yaml...⚠️ .json⚠️ inlang JSON✅ PO, JSON, CSV
    KI-Übersetzung✅ Eigener Anbieter und Schlüssel❌ Nein❌ Nein❌ Nein
    Visueller Editor / CMS✅ Lokaler Editor + optionales CMS❌ Externe Plattformen⚠️ inlang-Ökosystem-Apps❌ Externe Plattformen
    SEO-Helfer (hreflang, Sitemap)✅ Integriert❌ Manuell⚠️ Lokalisierte URLs, Rest manuell❌ Manuell
    Laufzeitgröße (gzip, Benchmark)4.5 KB75.9 KB1.8 KB56.7 KB
    Leak, bestes Setup (Locale / Seite)0% / 0%0% / 0%49.7% / 0%8.6% / 0%
    Fehlende Übersetzungen in CI✅ npx intlayer test⚠️ Nicht integriert⚠️ Nicht integriert✅ lingui compile --strict
    Laufzeitgröße und Leak-Werte stammen aus dem TanStack Start Benchmark. Der Leak wird anhand des besten Setups der jeweiligen Bibliothek gemessen.
    Weitere TanStack Start-Leitfäden: use-intl, Paraglide JS und Intlayer.

    Best Practices, die Sie befolgen sollten

    • Setzen Sie lang und dir auf <html> basierend auf dem Routen-Locale, damit sie im Server-HTML korrekt sind.
    • Behalten Sie eine URL pro Locale bei mit einem Präfix, damit jede Sprachversion indexierbar ist.
    • Erstellen Sie eine I18n-Instanz pro Locale, mutieren Sie während SSR niemals eine globale Instanz: Zwei gleichzeitige Anfragen würden sonst gegenseitig das Locale überschreiben.
    • Laden Sie nur den aktiven Katalog, importieren Sie niemals alle Kataloge im Client-Code.
    • Wählen Sie einen Makro-Stil (useLingui + t in Komponenten, msg für Lazy Descriptors) und bleiben Sie dabei. Das Mischen von t, i18n._, i18n.t und <Trans> erschwert die Lesbarkeit für Menschen und KI-Assistenten.
    • Führen Sie lingui extract in CI aus, damit eine neue Nachricht niemals unübersetzt ausgeliefert wird.
    • Übersetzen Sie Ihre Metadaten und deklarieren Sie canonical, hreflang und x-default auf jeder Seite.
    • Generieren Sie eine mehrsprachige Sitemap und robots.txt und führen Sie Pre-Rendering für jedes Locale durch.
    • Verwenden Sie echte Links für den Sprachwechsler, damit Webcrawler alle Sprachen finden können.
    Siehe auch unseren Leitfaden zu Internationalisierung und SEO und den hreflang-Leitfaden.

    Schritt-für-Schritt-Anleitung zur Einrichtung von Lingui in einer TanStack Start-Anwendung

    Hier ist die Projektstruktur, die wir erstellen werden:

    bash
    .
    ├── lingui.config.ts
    ├── vite.config.ts
    └── src
        ├── locales
        │   ├── en
        │   │   └── messages.po     # Generiert durch `lingui extract`
        │   ├── fr
        │   │   └── messages.po
        │   └── es
        │       └── messages.po
        ├── start.ts                # Request-Middleware (Locale-Weiterleitung)
        ├── i18n
        │   ├── config.ts           # Locales, URL-Helfer
        │   ├── lingui.ts           # Katalog-Loader, I18n-Instanzen
        │   ├── negotiateLocale.ts  # Parsen von Accept-Language
        │   └── seo.ts              # head()-Builder
        ├── components
        │   ├── LocaleSwitcher.tsx
        │   ├── LocalizedLink.tsx
        │   └── NotFound.tsx
        └── routes
            ├── __root.tsx
            ├── sitemap[.]xml.ts
            ├── robots[.]txt.ts
            └── {-$locale}
                ├── route.tsx       # Locale-Layout + I18nProvider
                ├── index.tsx
                ├── about.tsx
                └── $.tsx           # Lokalisierte 404-Seite
    
    1. Abhängigkeiten installieren

      bash
      npm install @lingui/core @lingui/react
      npm install -D @lingui/cli @lingui/vite-plugin @lingui/babel-plugin-lingui-macro @lingui/format-po @rolldown/plugin-babel
      
      • @lingui/core / @lingui/react: Laufzeit, I18nProvider und die Makros (@lingui/core/macro, @lingui/react/macro).
      • @lingui/cli: lingui extract, um Nachrichten in Katalogen zu sammeln.
      • @lingui/vite-plugin: kompiliert .po-Kataloge beim Import, sodass lingui compile nicht erforderlich ist.
      • @lingui/babel-plugin-lingui-macro + @rolldown/plugin-babel: transformieren die Makros zur Build-Zeit.
    2. Zentralisieren Sie Ihre Locale-Konfiguration

      Das Standard-Locale bleibt ohne Präfix (/about), andere Locales erhalten ein Präfix (/fr/about).

      src/i18n/config.ts
      export const locales = ["en", "fr", "es"] as const;
      
      export type Locale = (typeof locales)[number];
      
      export const defaultLocale: Locale = "en";
      
      /** Public origin, used for canonical URLs, hreflang and the sitemap. */
      export const siteUrl = "https://example.com";
      
      /** Cookie storing the locale explicitly chosen by the visitor. */
      export const localeCookieName = "locale";
      
      /** Open Graph expects `language_TERRITORY` codes. */
      export const openGraphLocales: Record<Locale, string> = {
        en: "en_US",
        fr: "fr_FR",
        es: "es_ES",
      };
      
      export const isLocale = (value: unknown): value is Locale =>
        typeof value === "string" && (locales as readonly string[]).includes(value);
      
      /** Maps the optional `{-$locale}` route param to a supported locale. */
      export const resolveLocale = (localeParam: string | undefined): Locale =>
        isLocale(localeParam) ? localeParam : defaultLocale;
      
      /** The value to pass as `locale` param: `undefined` for the default locale. */
      export const toLocaleParam = (locale: Locale): Locale | undefined =>
        locale === defaultLocale ? undefined : locale;
      
      const rightToLeftLanguages = new Set(["ar", "fa", "he", "ur", "ps", "yi"]);
      
      export const getTextDirection = (locale: string): "ltr" | "rtl" =>
        rightToLeftLanguages.has(new Intl.Locale(locale).language) ? "rtl" : "ltr";
      
      /** `localizePath("/about", "fr")` → `/fr/about`, default locale unprefixed. */
      export const localizePath = (path: string, locale: Locale): string => {
        if (locale === defaultLocale) return path;
      
        return path === "/" ? `/${locale}` : `/${locale}${path}`;
      };
      
      export const getAbsoluteUrl = (path: string, locale: Locale): string =>
        `${siteUrl}${localizePath(path, locale)}`;
      
      export const getLocaleName = (locale: Locale): string =>
        new Intl.DisplayNames([locale], { type: "language" }).of(locale) ?? locale;
      
    3. Lingui konfigurieren

      Die Lingui-Konfiguration verwendet dieselbe Locale-Liste wieder, sodass die Kataloge, der Router und die Sitemap stets synchron bleiben.

      lingui.config.ts
      import { defineConfig } from "@lingui/cli";
      import { formatter } from "@lingui/format-po";
      import { defaultLocale, locales } from "./src/i18n/config";
      
      export default defineConfig({
        sourceLocale: defaultLocale,
        locales: [...locales],
        catalogs: [
          {
            path: "<rootDir>/src/locales/{locale}/messages",
            include: ["src"],
          },
        ],
        format: formatter({ lineNumbers: false }),
      });
      

      Fügen Sie die Extraktions-Skripte hinzu:

      package.json
      {
        "scripts": {
          "i18n:extract": "lingui extract --clean",
          "i18n:check": "lingui extract --clean && git diff --exit-code src/locales"
        }
      }
      

      i18n:check schlägt in der CI fehl, wenn eine Komponente eine Nachricht enthält, die nicht extrahiert und committet wurde.

    4. Vite konfigurieren

      Mit @vitejs/plugin-react v6 ist Babel nicht mehr integriert. @rolldown/plugin-babel führt das Lingui-Makro-Plugin aus, und linguiTransformerBabelPreset verarbeitet nur Dateien, die ein Makro importieren, was Builds schnell hält.

      vite.config.ts
      import { lingui, linguiTransformerBabelPreset } from "@lingui/vite-plugin";
      import babel from "@rolldown/plugin-babel";
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { defineConfig } from "vite";
      
      export default defineConfig({
        plugins: [
          tanstackStart(),
          viteReact(),
          lingui(),
          babel({ presets: [linguiTransformerBabelPreset()] }),
        ],
      });
      
    5. Kataloge pro Locale laden

      Das Template-Literal in import() sorgt dafür, dass Vite einen Chunk pro Katalog erzeugt, und das Lingui-Plugin kompiliert die .po-Datei hinein. Ein französischsprachiger Besucher lädt ausschließlich den französischen Katalog herunter.

      Die kompilierten Nachrichten sind reine Daten, sodass sie von einem Routen-Loader zurückgegeben, in das HTML serialisiert und bei der Hydratisierung wiederverwendet werden können.

      src/i18n/lingui.ts
      import { type I18n, type Messages, setupI18n } from "@lingui/core";
      import type { Locale } from "./config";
      
      /**
       * Loads the compiled catalog of one locale (one chunk per locale).
       */
      export const loadCatalog = async (locale: Locale): Promise<Messages> => {
        const { messages } = await import(`../locales/${locale}/messages.po`);
      
        return messages;
      };
      
      /**
       * Creates an isolated I18n instance: safe for concurrent SSR requests.
       */
      export const createI18n = (locale: Locale, messages: Messages): I18n =>
        setupI18n({ locale, messages: { [locale]: messages } });
      
      /**
       * Loads a catalog and returns a ready-to-use instance, for loaders and
       * server functions.
       */
      export const loadI18n = async (locale: Locale): Promise<I18n> =>
        createI18n(locale, await loadCatalog(locale));
      

      Damit TypeScript den .po-Import akzeptiert, deklarieren Sie das Modul einmalig:

      src/i18n/po.d.ts
      declare module "*.po" {
        import type { Messages } from "@lingui/core";
      
        export const messages: Messages;
      }
      
    6. Das Root-Dokument erstellen

      Die Root-Route liest den optionalen Locale-Parameter, um lang und dir auf dem serverseitig gerenderten <html> festzulegen.

      src/routes/__root.tsx
      import {
        createRootRoute,
        HeadContent,
        Scripts,
        useParams,
      } from "@tanstack/react-router";
      import type { ReactNode } from "react";
      import { getTextDirection, resolveLocale } from "@/i18n/config";
      
      export const Route = createRootRoute({
        head: () => ({
          meta: [
            { charSet: "utf-8" },
            { name: "viewport", content: "width=device-width, initial-scale=1" },
          ],
        }),
        shellComponent: RootDocument,
      });
      
      function RootDocument({ children }: { children: ReactNode }) {
        const { locale: localeParam } = useParams({ strict: false });
        const locale = resolveLocale(localeParam);
      
        return (
          <html lang={locale} dir={getTextDirection(locale)}>
            <head>
              <HeadContent />
            </head>
            <body>
              {children}
              <Scripts />
            </body>
          </html>
        );
      }
      
    7. Die Locale-Layout-Route erstellen

      Der Ordner {-$locale} erstellt ein optionales Pfadsegment: /about und /fr/about passen beide zu /{-$locale}/about. Das Layout weist unbekannte Präfixe ab, lädt den Katalog des aktuellen Locales und stellt eine dedizierte I18n-Instanz bereit.

      src/routes/{-$locale}/route.tsx
      import { I18nProvider } from "@lingui/react";
      import { createFileRoute, notFound, Outlet } from "@tanstack/react-router";
      import { useMemo } from "react";
      import { Header } from "@/components/Header";
      import { NotFound } from "@/components/NotFound";
      import { isLocale, resolveLocale } from "@/i18n/config";
      import { createI18n, loadCatalog } from "@/i18n/lingui";
      
      export const Route = createFileRoute("/{-$locale}")({
        beforeLoad: ({ params }) => {
          if (params.locale !== undefined && !isLocale(params.locale)) {
            throw notFound();
          }
        },
        loader: async ({ params }) => {
          const locale = resolveLocale(params.locale);
      
          return { locale, messages: await loadCatalog(locale) };
        },
        // A catalog never changes for a given locale
        staleTime: Infinity,
        component: LocaleLayout,
        notFoundComponent: NotFound,
      });
      
      function LocaleLayout() {
        const { locale, messages } = Route.useLoaderData();
      
        // One instance per locale, never shared between requests
        const i18n = useMemo(() => createI18n(locale, messages), [locale, messages]);
      
        return (
          <I18nProvider i18n={i18n}>
            <Header />
            <main>
              <Outlet />
            </main>
          </I18nProvider>
        );
      }
      
    8. Übersetzungen in Ihren Seiten nutzen

      Schreiben Sie den Quelltext direkt in die Komponente. Die Makros wandeln ihn zur Build-Zeit in Nachrichten-IDs um und lingui extract erfasst ihn.

      • <Trans> für JSX-Inhalte, einschließlich verschachtelter Elemente;
      • useLingui().t für Zeichenketten (Attribute, Props);
      • <Plural> für ICU-Pluralformen.
      src/routes/{-$locale}/about.tsx
      import { msg } from "@lingui/core/macro";
      import { Plural, Trans, useLingui } from "@lingui/react/macro";
      import { createFileRoute } from "@tanstack/react-router";
      import { useState } from "react";
      import { resolveLocale } from "@/i18n/config";
      import { loadI18n } from "@/i18n/lingui";
      import { buildLocalizedHead } from "@/i18n/seo";
      
      export const Route = createFileRoute("/{-$locale}/about")({
        // Translate the metadata in the loader: head() stays synchronous
        loader: async ({ params }) => {
          const i18n = await loadI18n(resolveLocale(params.locale));
      
          return {
            metadata: {
              title: i18n._(msg`About us`),
              description: i18n._(
                msg`Learn who we are and why we built this application.`
              ),
            },
          };
        },
        staleTime: Infinity,
        head: ({ params, loaderData }) =>
          loaderData
            ? buildLocalizedHead({
                path: "/about",
                locale: resolveLocale(params.locale),
                ...loaderData.metadata,
              })
            : {},
        component: AboutPage,
      });
      
      function AboutPage() {
        const { t } = useLingui();
        const [count, setCount] = useState(0);
      
        return (
          <>
            <h1>
              <Trans>About us</Trans>
            </h1>
            <p>
              <Plural
                value={count}
                _0="No clicks yet"
                one="# click"
                other="# clicks"
              />
            </p>
            <button
              type="button"
              aria-label={t`Counter`}
              onClick={() => setCount((value) => value + 1)}
            >
              <Trans>Increment</Trans>
            </button>
          </>
        );
      }
      
      Der dynamische import() eines Katalogs wird vom Modulsystem zwischengespeichert, sodass der Aufruf von loadI18n in mehreren Loadern den Katalog nicht mehrfach herunterlädt.
    9. Nachrichten extrahieren und übersetzen

      Führen Sie die Extraktion aus. Lingui schreibt jede Nachricht in den jeweiligen Sprachkatalog:

      bash
      npm run i18n:extract
      

      Übersetzen Sie anschließend den msgstr jedes Eintrags:

      src/locales/fr/messages.po
      msgid "About us"
      msgstr "À propos"
      
      msgid "Learn who we are and why we built this application."
      msgstr "Découvrez qui nous sommes et pourquoi nous avons créé cette application."
      
      msgid "Increment"
      msgstr "Incrémenter"
      
      msgid "Counter"
      msgstr "Compteur"
      
      msgid "{count, plural, =0 {No clicks yet} one {# click} other {# clicks}}"
      msgstr "{count, plural, =0 {Aucun clic} one {# clic} other {# clics}}"
      
      src/locales/es/messages.po
      msgid "About us"
      msgstr "Sobre nosotros"
      
      msgid "Learn who we are and why we built this application."
      msgstr "Descubre quiénes somos y por qué creamos esta aplicación."
      
      msgid "Increment"
      msgstr "Incrementar"
      
      msgid "Counter"
      msgstr "Contador"
      
      msgid "{count, plural, =0 {No clicks yet} one {# click} other {# clicks}}"
      msgstr "{count, plural, =0 {Ningún clic} one {# clic} other {# clics}}"
      
      Standardmäßig sind Nachrichten-IDs Hashes des Quelltexts: Durch das Ändern des englischen Textes entsteht eine neue Nachricht. Verwenden Sie explizite IDs (<Trans id="about.title">About us</Trans>) für Texte, die sich häufig ändern.
    10. Optional

      Jede Route befindet sich unter {-$locale}, daher müssen Links den aktuellen Locale-Parameter beibehalten.

      src/components/LocalizedLink.tsx
      import { useLingui } from "@lingui/react";
      import { Link, type LinkComponentProps } from "@tanstack/react-router";
      import { type Locale, toLocaleParam } from "@/i18n/config";
      
      type LocalizedLinkProps = Omit<LinkComponentProps, "params">;
      
      export const LocalizedLink = (props: LocalizedLinkProps) => {
        const { i18n } = useLingui();
      
        return (
          <Link
            {...props}
            params={{ locale: toLocaleParam(i18n.locale as Locale) }}
          />
        );
      };
      
    11. Sprache des Inhalts wechseln

      Optional

      Rendern Sie den Sprachwechsler als Links, damit Suchmaschinen-Crawler jede Sprachversion finden können. to="." behält die aktuelle Seite bei und ersetzt den Locale-Parameter. Der Loader des Locale-Layouts ruft daraufhin den neuen Katalog ab.

      src/components/LocaleSwitcher.tsx
      import { useLingui } from "@lingui/react/macro";
      import { Link } from "@tanstack/react-router";
      import {
        getLocaleName,
        type Locale,
        localeCookieName,
        locales,
        toLocaleParam,
      } from "@/i18n/config";
      
      const persistLocale = (locale: Locale) => {
        document.cookie = `${localeCookieName}=${locale}; Path=/; Max-Age=31536000; SameSite=Lax`;
      };
      
      export const LocaleSwitcher = () => {
        // The macro version also returns the i18n instance
        const { i18n, t } = useLingui();
      
        return (
          <nav aria-label={t`Change language`}>
            <ul>
              {locales.map((locale) => (
                <li key={locale}>
                  <Link
                    to="."
                    params={(previous) => ({
                      ...previous,
                      locale: toLocaleParam(locale),
                    })}
                    hrefLang={locale}
                    lang={locale}
                    aria-current={locale === i18n.locale ? "page" : undefined}
                    onClick={() => persistLocale(locale)}
                  >
                    {getLocaleName(locale)}
                  </Link>
                </li>
              ))}
            </ul>
          </nav>
        );
      };
      
    12. Metadaten internationalisieren

      Optional

      Jede Sprachversion kann separat ranken, vorausgesetzt, jede Seite stellt einen übersetzten <title> und eine übersetzte Beschreibung, eine selbstreferenzierende Canonical-URL, ein hreflang pro Locale plus x-default, Open-Graph-Locales und JSON-LD mit inLanguage bereit. Die Metadaten werden im Loader übersetzt (Schritt 8), und dieser Helfer baut den Rest auf:

      src/i18n/seo.ts
      import {
        defaultLocale,
        getAbsoluteUrl,
        type Locale,
        locales,
        openGraphLocales,
      } from "./config";
      
      type LocalizedHeadOptions = {
        /** Path without locale prefix, e.g. "/about" */
        path: string;
        locale: Locale;
        title: string;
        description: string;
      };
      
      export const buildLocalizedHead = ({
        path,
        locale,
        title,
        description,
      }: LocalizedHeadOptions) => {
        const url = getAbsoluteUrl(path, locale);
      
        return {
          meta: [
            { title },
            { name: "description", content: description },
            { property: "og:type", content: "website" },
            { property: "og:title", content: title },
            { property: "og:description", content: description },
            { property: "og:url", content: url },
            { property: "og:locale", content: openGraphLocales[locale] },
            ...locales
              .filter((alternateLocale) => alternateLocale !== locale)
              .map((alternateLocale) => ({
                property: "og:locale:alternate",
                content: openGraphLocales[alternateLocale],
              })),
          ],
          links: [
            { rel: "canonical", href: url },
            ...locales.map((alternateLocale) => ({
              rel: "alternate",
              hrefLang: alternateLocale,
              href: getAbsoluteUrl(path, alternateLocale),
            })),
            {
              rel: "alternate",
              hrefLang: "x-default",
              href: getAbsoluteUrl(path, defaultLocale),
            },
          ],
          scripts: [
            {
              type: "application/ld+json",
              children: JSON.stringify({
                "@context": "https://schema.org",
                "@type": "WebPage",
                name: title,
                description,
                url,
                inLanguage: locale,
              }),
            },
          ],
        };
      };
      
    13. Sitemap und robots.txt internationalisieren

      Optional

      Die Sitemap listet jede URL jedes Locales auf, wobei jeder Eintrag alle seine Alternativen mit xhtml:link deklariert. Die Datei robots.txt blockiert private Routen in jeder Sprache und verweist auf die Sitemap. Entfernen Sie public/robots.txt, falls das Starter-Template eine solche Datei erstellt hat.

      src/routes/sitemap[.]xml.ts
      import { createFileRoute } from "@tanstack/react-router";
      import { defaultLocale, getAbsoluteUrl, locales } from "@/i18n/config";
      
      type SitemapPage = {
        path: string;
        changeFrequency: "daily" | "weekly" | "monthly";
        priority: number;
      };
      
      export const sitemapPages: SitemapPage[] = [
        { path: "/", changeFrequency: "daily", priority: 1.0 },
        { path: "/about", changeFrequency: "monthly", priority: 0.8 },
      ];
      
      const buildAlternateLinks = (path: string): string =>
        [
          ...locales.map(
            (locale) =>
              `<xhtml:link rel="alternate" hreflang="${locale}" href="${getAbsoluteUrl(path, locale)}"/>`
          ),
          `<xhtml:link rel="alternate" hreflang="x-default" href="${getAbsoluteUrl(path, defaultLocale)}"/>`,
        ].join("");
      
      const buildSitemap = (): string => {
        const urls = sitemapPages.flatMap((page) =>
          locales.map(
            (locale) =>
              `<url><loc>${getAbsoluteUrl(page.path, locale)}</loc>${buildAlternateLinks(page.path)}<changefreq>${page.changeFrequency}</changefreq><priority>${page.priority}</priority></url>`
          )
        );
      
        return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls.join("")}</urlset>`;
      };
      
      export const Route = createFileRoute("/sitemap.xml")({
        server: {
          handlers: {
            GET: () =>
              new Response(buildSitemap(), {
                headers: { "Content-Type": "application/xml; charset=utf-8" },
              }),
          },
        },
      });
      
      src/routes/robots[.]txt.ts
      import { createFileRoute } from "@tanstack/react-router";
      import { locales, localizePath, siteUrl } from "@/i18n/config";
      
      const privatePaths = ["/dashboard", "/admin"];
      
      const buildRobots = (): string =>
        [
          "User-agent: *",
          "Allow: /",
          ...privatePaths.flatMap((path) =>
            locales.map((locale) => `Disallow: ${localizePath(path, locale)}`)
          ),
          "",
          `Sitemap: ${siteUrl}/sitemap.xml`,
        ].join("\n");
      
      export const Route = createFileRoute("/robots.txt")({
        server: {
          handlers: {
            GET: () =>
              new Response(buildRobots(), {
                headers: { "Content-Type": "text/plain; charset=utf-8" },
              }),
          },
        },
      });
      
    14. Pre-Rendering für jedes Locale durchführen

      Optional

      Listen Sie alle lokalisierten Pfade auf, damit TanStack Start beim Build alle Sprachversionen vorrendert:

      vite.config.ts
      import { lingui, linguiTransformerBabelPreset } from "@lingui/vite-plugin";
      import babel from "@rolldown/plugin-babel";
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { defineConfig } from "vite";
      import { locales, localizePath } from "./src/i18n/config";
      
      const pagePaths = ["/", "/about"];
      
      const localizedPages = pagePaths.flatMap((path) =>
        locales.map((locale) => ({
          path: localizePath(path, locale),
          prerender: { enabled: true },
        }))
      );
      
      export default defineConfig({
        plugins: [
          tanstackStart({
            prerender: { enabled: true, crawlLinks: true },
            pages: [
              ...localizedPages,
              { path: "/sitemap.xml", prerender: { enabled: true } },
              { path: "/robots.txt", prerender: { enabled: true } },
            ],
          }),
          viteReact(),
          lingui(),
          babel({ presets: [linguiTransformerBabelPreset()] }),
        ],
      });
      
    15. Erstbesucher weiterleiten und 404-Seiten handhaben

      Optional

      Eine Request-Middleware leitet Besucher, die auf / landen, zu ihrer bevorzugten Sprache weiter (zuerst Cookie, dann Accept-Language). Deep-Links werden niemals umgeleitet, sodass Crawler und geteilte URLs stets genau die angeforderte Seite erhalten.

      src/i18n/negotiateLocale.ts
      import { isLocale, type Locale } from "./config";
      
      /** "fr-CA,fr;q=0.9,en;q=0.8" → "fr" */
      export const negotiateLocale = (
        acceptLanguage: string | null | undefined
      ): Locale | undefined => {
        if (!acceptLanguage) return undefined;
      
        return acceptLanguage
          .split(",")
          .map((part) => {
            const [tag = "", quality] = part.trim().split(";q=");
      
            return {
              language: tag.toLowerCase().split("-")[0],
              quality: quality ? Number(quality) : 1,
            };
          })
          .sort((first, second) => second.quality - first.quality)
          .map(({ language }) => language)
          .find(isLocale);
      };
      
      src/start.ts
      import { redirect } from "@tanstack/react-router";
      import { createMiddleware, createStart } from "@tanstack/react-start";
      import { getCookie } from "@tanstack/react-start/server";
      import { defaultLocale, isLocale, localeCookieName } from "@/i18n/config";
      import { negotiateLocale } from "@/i18n/negotiateLocale";
      
      const localeRedirectMiddleware = createMiddleware().server(
        ({ request, next }) => {
          if (new URL(request.url).pathname !== "/") return next();
      
          const cookieLocale = getCookie(localeCookieName);
          const preferredLocale = isLocale(cookieLocale)
            ? cookieLocale
            : negotiateLocale(request.headers.get("accept-language"));
      
          if (preferredLocale && preferredLocale !== defaultLocale) {
            throw redirect({ href: `/${preferredLocale}`, statusCode: 307 });
          }
      
          return next();
        }
      );
      
      export const startInstance = createStart(() => ({
        requestMiddleware: [localeRedirectMiddleware],
      }));
      

      Für 404-Seiten rendert eine Catch-All-Route die lokalisierte notFoundComponent des Layouts. Markieren Sie sie mit noindex: React 19 verschiebt das <meta> automatisch in den <head>.

      src/components/NotFound.tsx
      import { Trans } from "@lingui/react/macro";
      import { LocalizedLink } from "./LocalizedLink";
      
      export const NotFound = () => (
        <div>
          <meta name="robots" content="noindex" />
          <h1>
            <Trans>Page not found</Trans>
          </h1>
          <LocalizedLink to="/{-$locale}">
            <Trans>Back to home</Trans>
          </LocalizedLink>
        </div>
      );
      
      src/routes/{-$locale}/$.tsx
      import { createFileRoute, notFound } from "@tanstack/react-router";
      
      export const Route = createFileRoute("/{-$locale}/$")({
        beforeLoad: () => {
          throw notFound();
        },
      });
      
    16. Makros beibehalten, Laufzeit mit Intlayer reduzieren

      Optional

      Der Kompatibilitätsadapter @intlayer/lingui lässt Ihren Quellcode unverändert: Die Makros werden exakt wie zuvor kompiliert, und die resultierenden Aufrufe von i18n._(), useLingui() und <Trans> werden von kompilierten Intlayer-Wörterbüchern bedient. Im Benchmark sinkt die Laufzeitgröße von ~56.7 KB auf ~9.8 KB gzip.

      bash
      npm install @intlayer/lingui intlayer @intlayer/sync-json-plugin
      npx intlayer init
      

      Fügen Sie das Plugin nach der Makro-Transformation ein, sodass es @lingui/core und @lingui/react auf den Adapter umleitet:

      vite.config.ts
      import { lingui as linguiIntlayer } from "@intlayer/lingui/plugin";
      import { linguiTransformerBabelPreset } from "@lingui/vite-plugin";
      import babel from "@rolldown/plugin-babel";
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { defineConfig } from "vite";
      
      export default defineConfig({
        plugins: [
          tanstackStart(),
          viteReact(),
          babel({ presets: [linguiTransformerBabelPreset()] }),
          linguiIntlayer(),
        ],
      });
      

      Kataloge werden mit dem JSON-Sync-Plugin (JSON-Kataloge) oder dem PO-Sync-Plugin (PO-Kataloge) synchronisiert. Die vollständige Einrichtung finden Sie im Lingui-Kompatibilitätsleitfaden und einen direkten Vergleich in Lingui vs. @intlayer/lingui.

    17. Übersetzungen mit Intlayer automatisieren

      Optional

      Lingui extrahiert Nachrichten, aber das manuelle Ausfüllen von Dutzenden Katalogen nimmt die meiste Zeit in Anspruch. Intlayer ist kostenlos und Open Source, und seine Tools arbeiten nahtlos mit Lingui zusammen:

      • Mit KI übersetzen unter Verwendung Ihres eigenen API-Schlüssels und Anbieters. Siehe Auto-Fill und das CLI.
      • PO-Dateien beibehalten als Source of Truth mit dem PO-Sync-Plugin.
      • Fehlende Übersetzungen in CI testen. Siehe Übersetzungen testen.
      • Bereitgestellte Website auditieren auf fehlende hreflang-Tags, falsche Canonicals und Sprachlecks mit dem Scan-Befehl.

    Häufig gestellte Fragen

    Ja. Lingui hat keine dedizierte TanStack Start-Integration, aber sein Vite-Plugin und das Babel-Makro-Plugin funktionieren unverändert. Die beiden entscheidenden Punkte sind die Ausführung der Makros über @rolldown/plugin-babel (Vite 8 und @vitejs/plugin-react v6 enthalten Babel nicht mehr) und die Erstellung einer I18n-Instanz pro Locale anstelle der Aktivierung einer globalen Instanz während SSR.

    Auf dem Server verarbeitet ein einzelner Prozess viele Anfragen gleichzeitig. Der Aufruf von i18n.activate("fr") auf einem geteilten Objekt würde die Sprache einer parallel auf Englisch gerenderten Anfrage ändern. setupI18n erstellt eine isolierte Instanz pro Locale, was sicher ist.

    Nein. @lingui/vite-plugin kompiliert .po-Kataloge direkt beim Importieren. Sie führen lediglich lingui extract aus, um neue Nachrichten zu sammeln.

    Deklarieren Sie sie mit dem msg-Makro und übersetzen Sie sie im Routen-Loader mit i18n._(msg`...`). Der Loader gibt einfache Zeichenketten zurück, sodass head() synchron bleibt und die Werte für die Hydratisierung serialisiert werden. Schritt 8 und Schritt 12 zeigen die vollständige Einrichtung.

    Der Benchmark misst ~56.7 KB gzip für die Laufzeit. Wenn ein Katalog pro Locale bei Bedarf geladen wird, wiegen Seiten ~115 KB gegenüber 111 KB ohne i18n. Das statische Importieren aller Kataloge erhöht das Gewicht auf ~152 KB.

    Ja. Der Adapter @intlayer/lingui behält die Makros bei und tauscht die Laufzeit aus. Anschließend können Sie Komponenten schrittweise auf useIntlayer umstellen. Siehe auch die Kompatibilitätsadapter.

    Kommentare

    Noch keine Kommentare. Seien Sie der Erste, der seine Gedanken teilt.

    Ähnliche Beiträge

    Letzte Beiträge