Zum Inhalt springen

Wissen

Eine Next.js-Plattform in 22 Sprachen: Erkenntnisse aus GAGA

Locale-Routing, Serbisch in Latein und Kyrillisch, Intl-Formatierung, Schriften, Übersetzungs-CI und aktuelle Kurse: Lehren aus einem Projekt in 22 Sprachen.

Engineering-Team von sigmacode.io10 Min. Lesezeit

Auf dieser Seite (11)
  1. Jede Sprachversion braucht eine eigene URL
  2. Zwei Schriften, eine Sprache
  3. Zahlen, Währungen und Datumsangaben mit Intl formatieren
  4. Schriften und Glyphenabdeckung
  5. Ein Übersetzungsprozess, der 22 Sprachen standhält
  6. Echtzeitdaten ohne veraltete Werte
  7. Eine Quelle der Wahrheit für PDF, Excel, CSV und XML
  8. SEO über viele Locales
  9. Barrierefreiheit
  10. Tests: Routen × Locales
  11. Was Sie einplanen sollten

GAGA Menjačnica ist eine Wechselstube und ein Händler für Edelmetalle mit mehreren Filialen in Novi Sad, Serbien. Unser Team hat die Plattform menjacnicegaga.rs mit Next.js und React auf Vercel umgesetzt. Sie läuft in 22 Sprachen, darunter Serbisch in lateinischer und kyrillischer Schrift, Deutsch, Chinesisch, Russisch, Türkisch, Ukrainisch, Griechisch und Bulgarisch. Die Plattform zeigt aktuelle An- und Verkaufskurse neben den Referenzkursen der Serbischen Nationalbank und bietet einen Währungsrechner, Kurslisten zum Download in fünf Formaten, eine Filialsuche sowie Ratgeber zu Anlagegold und -silber.

Bei 22 Sprachen ist Internationalisierung kein Feature mehr, sondern prägt die Architektur. Dieser Artikel fasst zusammen, worauf es aus unserer Sicht bei einer solchen Plattform ankommt. Er richtet sich an CTOs, Product Owner und Frontend-Entwickler, die ein vergleichbares Projekt planen. Details zum Projekt selbst finden Sie in der GAGA-Fallstudie.

Jede Sprachversion braucht eine eigene URL#

Die wichtigste Entscheidung steht am Anfang: Jede Sprachversion jeder Seite braucht eine eigene, stabile und crawlbare URL. Wechseln Sie die Sprache nicht per Cookie oder durch Auswertung von Accept-Language. Suchmaschinen können das nicht indexieren, Nutzer können es nicht teilen, und CDNs können es nicht sauber cachen.

Wir empfehlen ein Sprachpräfix im Pfad, etwa /sr/..., /de/... und /zh/.... Im Next.js App Router bedeutet das ein [locale]-Segment an der Wurzel der Anwendung. Der Request-Proxy (proxy.ts, früher Middleware) kann einen Erstbesuch anhand der Browsersprache auf einen sinnvollen Standard umleiten. Danach ist die URL die einzige Quelle der Wahrheit, und ein Sprachumschalter muss auf dieselbe Seite in der anderen Sprache verlinken, nicht auf deren Startseite.

Legen Sie Ihre Locale-Kennungen früh fest und verwenden Sie durchgängig BCP-47-Tags. Allein Serbisch braucht zwei davon: sr-Latn und sr-Cyrl. Wer die Zuordnung zu URL-Segmenten, hreflang-Werten und Intl-Locales an einer einzigen Stelle pflegt, erspart sich später viel Ärger.

hreflang, x-default und Sitemaps#

Jede Seite sollte alle ihre Sprachalternativen einschließlich sich selbst auflisten und zusätzlich ein x-default für Nutzer angeben, deren Sprache Sie nicht unterstützen. Im App Router übernimmt das die Metadata-API:

ts
// app/[locale]/rates/page.tsx
import type { Metadata } from "next";

const BASE = "https://example.com";
const LOCALES = ["sr-Latn", "sr-Cyrl", "en", "de", "zh", "ru"] as const;
const segment = (l: string) => l.toLowerCase(); // "sr-Latn" -> "sr-latn"

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
  const { locale } = await params;
  const path = "/rates";
  const languages: Record<string, string> = Object.fromEntries(
    LOCALES.map((l) => [l, `${BASE}/${segment(l)}${path}`])
  );
  languages["x-default"] = `${BASE}/en${path}`;

  return {
    alternates: { canonical: `${BASE}/${locale}${path}`, languages },
  };
}

Erzeugen Sie die Sitemap aus derselben Locale-Liste und demselben Routenverzeichnis, mit Alternativen für jeden Eintrag. Sobald hreflang-Tags und Sitemap-Einträge aus getrennten Codepfaden stammen, werden sie irgendwann voneinander abweichen, und Suchmaschinen vertrauen dann stillschweigend keinem von beiden.

Zwei Schriften, eine Sprache#

Serbisch wird sowohl in lateinischer als auch in kyrillischer Schrift geschrieben, und viele Leser haben eine klare Präferenz. Behandeln Sie beide als vollwertige Locales, nicht als eine Locale mit Anzeigeschalter. Jede bekommt ihre eigene URL, ihren eigenen hreflang-Wert und ihr eigenes lang-Attribut.

Es ist verlockend, alles in einer Schrift zu schreiben und die andere automatisch zu transliterieren. Für Fließtext kann ein gut getesteter Transliterationsschritt ein vernünftiger Ausgangspunkt sein. Wenden Sie ihn aber nicht blind auf alles an:

  • Eigennamen und Marken. Firmen- und Produktnamen sowie Fremdwörter behalten auch in kyrillischem Text oft ihre lateinische Form. Ein mechanischer Konverter macht daraus „hilfsbereit“ etwas, das so niemand geschrieben hat.
  • Mehrdeutige Digraphe. Die lateinischen Buchstabenfolgen nj, lj und entsprechen meist einem einzelnen kyrillischen Buchstaben, aber nicht immer. Komposita und Lehnwörter brechen die Regel. Von Kyrillisch nach Lateinisch ist die Umwandlung eindeutig, in die Gegenrichtung nicht.
  • URLs, Codes und Kennungen. Währungscodes wie EUR, E-Mail-Adressen, Slugs und alles innerhalb von Interpolations-Platzhaltern dürfen niemals transliteriert werden.
  • Suche und Sortierung. Nutzer tippen auf einer kyrillischen Seite womöglich lateinisch ins Suchfeld. Normalisieren Sie beide Seiten, bevor Sie vergleichen.

Unsere Empfehlung: Speichern Sie die kyrillische Quelle (die eindeutige Richtung) oder pflegen Sie zwei getrennte Kataloge, und führen Sie eine kleine Ausnahmeliste, für die ein Muttersprachler verantwortlich ist. Alles, was eine Maschine erzeugt hat, sollte vor dem Launch geprüft werden.

Zahlen, Währungen und Datumsangaben mit Intl formatieren#

Auf einer Wechselkursseite sind die Zahlen das Produkt. Serbisch verwendet ein Komma als Dezimaltrennzeichen, Deutsch gruppiert Tausender mit einem Punkt, und chinesische Nutzer erwarten wieder andere Konventionen. Formatieren Sie nicht von Hand. Verwenden Sie die eingebauten Intl-APIs und übergeben Sie das vollständige Locale-Tag:

ts
const rateFormatter = (locale: string) =>
  new Intl.NumberFormat(locale, {
    minimumFractionDigits: 4,
    maximumFractionDigits: 4,
  });

const moneyFormatter = (locale: string, currency: string) =>
  new Intl.NumberFormat(locale, { style: "currency", currency });

const asOf = (locale: string, date: Date) =>
  new Intl.DateTimeFormat(locale, {
    dateStyle: "long",
    timeStyle: "short",
    timeZone: "Europe/Belgrade",
  }).format(date);

rateFormatter("sr-Latn").format(117.1234);   // "117,1234"
rateFormatter("de").format(117.1234);        // "117,1234"
moneyFormatter("en", "EUR").format(1250);    // "€1,250.00"
asOf("sr-Cyrl", new Date());                 // e.g. "18. септембар 2026. 10:30" (exact output depends on the ICU version)

Einige Punkte, die Sie einplanen sollten: Setzen Sie die Zeitzone explizit. Serverseitig gerenderte Seiten verwenden sonst die Zeitzone des Servers, meist UTC. Erzeugen Sie Formatter einmal und verwenden Sie sie wieder, denn das wiederholte Anlegen summiert sich in großen Tabellen. Und legen Sie die Anzahl der Nachkommastellen auch in der Datenschicht fest, nicht nur in der Oberfläche, damit Exporte und Bildschirm übereinstimmen.

Schriften und Glyphenabdeckung#

22 Sprachen bedeuten Lateinisch, erweitertes Lateinisch (Serbisch, Kroatisch, Türkisch), Kyrillisch (Serbisch, Russisch, Ukrainisch, Bulgarisch), Griechisch und CJK-Zeichen. Nur wenige Hausschriften decken all das ab, und diejenigen, die es tun, sind groß.

Worauf es ankommt:

  • Prüfen Sie die Abdeckung je Schrift, bevor Sie eine Schriftart wählen. Testen Sie mit echten Texten, nicht mit „Lorem ipsum“. Serbisches Kyrillisch hat eigene Buchstaben und lokale Kursivformen, Ukrainisch hat Buchstaben, die im Russischen fehlen, und Bulgarisch bevorzugt eigene Glyphenformen.
  • Nach Schrift subsetten und je Locale laden. next/font unterstützt Subsets wie latin, latin-ext, cyrillic und greek. Eine deutsche Seite sollte keine kyrillischen Glyphen herunterladen.
  • Hosten Sie keine vollständige CJK-Schrift auf jeder Seite selbst. Chinesische Schriften können mehrere Megabyte groß sein. Ein System-Font-Stack für CJK, etwa "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif, ist oft der richtige Kompromiss.
  • Definieren Sie einen expliziten Fallback-Stack mit metrisch kompatiblen Ersatzschriften, damit der Text beim Laden der Webschrift nicht springt.

Ein Übersetzungsprozess, der 22 Sprachen standhält#

Bei zwei oder drei Sprachen genügen eine Tabelle und etwas Disziplin. Bei 22 brauchen Sie eine Pipeline.

Message-Kataloge mit stabilen Schlüsseln. Verwenden Sie eine JSON-Datei pro Locale, mit Schlüsseln, die nach Bedeutung benannt sind, etwa rates.table.buy, und niemals nach dem englischen Text. Nutzen Sie ICU MessageFormat für Pluralformen und Interpolation. Die Pluralregeln unterscheiden sich stark: Russisch, Ukrainisch und Serbisch haben mehrere Formen, Chinesisch hat keine.

Schlüsselparität in der CI. Ein fehlender Schlüssel in einer Locale ist der häufigste Fehler auf mehrsprachigen Websites und zugleich der am leichtesten automatisch zu findende:

ts
// scripts/check-i18n.ts — run in CI, fail the build on drift
import { readdirSync, readFileSync } from "node:fs";

const dir = "messages";
const flatten = (o: Record<string, unknown>, p = ""): string[] =>
  Object.entries(o).flatMap(([k, v]) =>
    v && typeof v === "object" ? flatten(v as Record<string, unknown>, `${p}${k}.`) : [`${p}${k}`]
  );

const load = (f: string) => new Set(flatten(JSON.parse(readFileSync(`${dir}/${f}`, "utf8"))));
const source = load("en.json");
let failed = false;

for (const file of readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "en.json")) {
  const keys = load(file);
  const missing = [...source].filter((k) => !keys.has(k));
  const extra = [...keys].filter((k) => !source.has(k));
  if (missing.length || extra.length) {
    failed = true;
    console.error(`${file}: missing ${missing.length}, extra ${extra.length}`, { missing, extra });
  }
}
process.exit(failed ? 1 : 0);

Erweitern Sie dasselbe Skript um eine Prüfung, ob die Interpolations-Platzhalter in allen Locales übereinstimmen. Ein übersetzter Platzhaltername bricht erst zur Laufzeit, nicht beim Build.

KI-gestützte Übersetzung mit menschlicher Prüfung. Maschinelle und LLM-basierte Übersetzung liefert heute gute erste Entwürfe, und bei 22 Sprachen verändert das die Wirtschaftlichkeit. Eine Wechselstube hat aber mit Geld und Vertrauen zu tun. Kursbezeichnungen, rechtliche Hinweise und Anlageratgeber sollten vor der Veröffentlichung von einer Person mit sicherer Sprachkenntnis geprüft werden. Geben Sie Übersetzern Kontext: Screenshots, Zeichenlimits und ein Glossar fester Begriffe wie „Ankaufskurs“, „Verkaufskurs“ und „Referenzkurs“.

Echtzeitdaten ohne veraltete Werte#

Wechselkurse ändern sich im Laufe des Tages. Die Falle besteht darin, sie genauso aggressiv zu cachen wie den Rest einer statisch ausgerichteten Next.js-Website.

Allgemeine Strategien, die wir empfehlen:

  • Trennen Sie Hülle und Daten. Layout, Übersetzungen und Ratgeber können statisch sein oder selten revalidiert werden. Die Kurstabelle sollte ein eigenes, kurzes Revalidierungsfenster haben oder clientseitig geladen bzw. gestreamt werden.
  • Halten Sie die Revalidierung kurz und bewusst gewählt. Wählen Sie ein Fenster, das zur tatsächlichen Änderungsfrequenz der Kurse passt, und dokumentieren Sie es. Nutzen Sie nach Möglichkeit On-Demand-Revalidierung bei der Veröffentlichung neuer Kurse, statt sich nur auf einen Timer zu verlassen.
  • Cachen Sie an der Edge mit Bedacht. Ein kurzes s-maxage mit stale-while-revalidate hält Seiten schnell, aber stellen Sie sicher, dass das Fenster für veraltete Werte fachlich vertretbar ist.
  • Zeigen Sie immer einen „Stand“-Zeitstempel, je Locale formatiert und in der Zeitzone der Filiale. Er ist die ehrliche Antwort auf die Frage, die jedes Caching-System aufwirft: Wie aktuell ist das?

Eine Quelle der Wahrheit für PDF, Excel, CSV und XML#

GAGA stellt seine Kursliste als PDF, JPG, Excel, CSV und XML zum Download bereit. Fünf Formate sind fünf Gelegenheiten, bei denen die Zahlen voneinander abweichen können.

Die Regel: Erzeugen Sie jeden Export aus derselben normalisierten Datenstruktur, die auch die Tabelle auf dem Bildschirm rendert. Rundung, Reihenfolge und Währungsmetadaten gehören in diese Struktur, nicht in den einzelnen Exporter. Jedes Format wird dann zu einem schlanken Serializer. Einzuplanen ist Folgendes:

  • CSV braucht ein festgelegtes Trennzeichen und eine Kodierung. Excel erwartet in vielen europäischen Locales ein Semikolon und verarbeitet UTF-8 mit BOM zuverlässiger.
  • Excel sollte echte numerische Zellen mit Zahlenformaten erhalten, keine vorformatierten Strings, damit Nutzer damit rechnen können.
  • XML braucht ein stabiles, dokumentiertes Schema, denn irgendjemand wird eine Integration darauf aufbauen.
  • PDF und Bilder müssen Schriften einbetten, die die angeforderte Schrift abdecken. Damit sind Sie wieder bei der Glyphenabdeckung.

Nehmen Sie den „Stand“-Zeitstempel auch in jeden Export auf.

SEO über viele Locales#

Über hreflang und Sitemaps hinaus:

  • Übersetzen Sie Titel, Beschreibungen und Open-Graph-Metadaten je Locale. Lassen Sie keine englischen Metadaten auf einer griechischen Seite stehen.
  • Lokalisieren Sie Slugs nur, wenn Sie sie stabil halten können. Ein geänderter Slug in einer Locale bedeutet Weiterleitungen und einen gebrochenen hreflang-Verbund.
  • Vermeiden Sie dünne Duplikate. Wenn eine Locale nur teilweise Inhalte hat, prüfen Sie, ob sie schon indexiert werden sollte.
  • Canonical-URLs sollten auf die Seite selbst zeigen, niemals auf eine andere Sprachversion.

Barrierefreiheit#

Setzen Sie lang am html-Element für jede Locale, mit dem vollständigen Tag (sr-Latn, sr-Cyrl), damit Screenreader die richtige Stimme und Aussprache wählen. Wenn innerhalb einer Seite eine Wendung in einer anderen Sprache vorkommt, kennzeichnen Sie sie mit einem eigenen lang.

Auch wenn Sie heute keine rechts-nach-links geschriebene Sprache unterstützen, sollten Sie dafür vorsorgen. Verwenden Sie logische CSS-Eigenschaften wie margin-inline-start statt margin-left und leiten Sie dir aus der Locale ab. Arabisch später hinzuzufügen ist deutlich günstiger, wenn das Layout nicht von links nach rechts ausgeht.

Tests: Routen × Locales#

Bei 22 Locales übersieht man leicht einen Fehler, der nur in einer davon auftritt. Wir empfehlen einen automatisierten Smoke-Test, der über jede öffentliche Route und jede Locale läuft und die Grundlagen prüft:

  • die Seite liefert 200 und rendert ohne Laufzeitfehler;
  • html hat das korrekte lang;
  • die hreflang-Alternativen sind vollständig und verweisen auf existierende URLs;
  • es sind keine rohen Message-Schlüssel (wie rates.table.buy) oder leeren Strings sichtbar;
  • Rechner und Kurstabelle stellen Zahlen im erwarteten Format dar.

Ergänzen Sie visuelle Snapshots für die längsten Sprachen. Deutsche und griechische Beschriftungen sprengen oft Layouts, die auf Englisch gut aussahen.

Was Sie einplanen sollten#

Wenn Sie eine mehrsprachige Next.js-Plattform beginnen, sind dies die Entscheidungen für den ersten Tag: eine URL-Strategie mit Sprachpräfix, ein zentrales Locale-Verzeichnis, das Routing, Metadaten und Sitemaps steuert, eine klare Regelung für Schriften und Transliteration, Intl für jede Formatierung, ein Schriftkonzept je Schriftsystem, CI-Prüfungen der Übersetzungsparität, eine ausdrückliche Aktualitätsregel für Live-Daten und eine einzige Quelle der Wahrheit für jeden Export.

Nichts davon ist exotisch, aber es ist wesentlich günstiger, es von Anfang an einzuplanen, als es nachzurüsten. Unsere Arbeit wird von einem Tech Lead mit mehr als 20 Jahren Erfahrung geleitet, und genau auf diese Grundlagen konzentrieren wir uns im Bereich Web & Plattformen. Wenn Sie etwas Ähnliches planen, nehmen Sie Kontakt auf.

Sie planen ein Projekt?

Erzählen Sie uns, was Sie vorhaben. Sie erhalten eine ehrliche Einschätzung, einen klaren Umfang und ein Angebot mit Festpreis oder Meilensteinen – meist innerhalb weniger Werktage.

Lieber zuerst schreiben? Nachricht schreiben