Symfony i18n-Routing: mehrsprachige URLs sauber strukturieren
AI generated
SF
{ }
Symfony · i18n · Routing · SEO
Symfony i18n-Routing
mehrsprachige URLs sauber strukturieren

Sobald eine Symfony-Anwendung mehr als eine Sprache bedient, stellt sich die Frage, wie sich das in der URL-Struktur widerspiegeln soll. Der eingebaute {_locale}-Routing-Parameter deckt den Grundfall ab, reicht aber nicht aus, sobald auch die Pfade selbst übersetzt werden sollen, etwa /de/produkte gegenüber /en/products. Wer diese Struktur von Anfang an sauber plant, spart sich später aufwendige Refactorings und vermeidet SEO-Probleme durch fehlende oder falsche Hreflang-Angaben.

16 Min. Lesezeit Symfony Routing i18n · Hreflang · SEO

1. Der {_locale}-Routing-Parameter vs. Subdomain-basierte Lokalisierung

Der klassische Ansatz in Symfony ist der spezielle {_locale}-Platzhalter, der in jeder Route verwendet werden kann und von Symfony automatisch als aktuelle Locale erkannt und in den Request-Kontext übernommen wird. Eine Route wie /{_locale}/produkte führt dazu, dass /de/produkte und /en/produkte beide funktionieren und Symfony den Locale-Wert automatisch für Übersetzungen, Formatierung von Zahlen und Daten sowie für die URL-Generierung über den UrlGenerator zur Verfügung stellt. Dieser Ansatz ist einfach umzusetzen und funktioniert gut, solange nur der Sprachcode variieren soll, nicht aber der restliche Pfad.

Subdomain-basierte Lokalisierung, etwa de.example.com und en.example.com, ist die Alternative, die vor allem dann gewählt wird, wenn verschiedene Sprachversionen technisch oder organisatorisch stärker getrennt werden sollen, beispielsweise weil unterschiedliche Teams für verschiedene Märkte verantwortlich sind oder weil regionale Hosting-Anforderungen eine physische Trennung nahelegen. Technisch lässt sich das in Symfony über host-Anforderungen in der Routing-Konfiguration abbilden, wobei jede Subdomain auf denselben oder auf unterschiedliche Locale-Werte gemappt werden kann.

2. _locale-Requirement mit erlaubten Sprachcodes

Ohne explizite Einschränkung würde der {_locale}-Parameter jeden beliebigen Wert akzeptieren, auch ungültige Sprachcodes, was zu unnötigen 404-Vermeidungen oder fehlerhaften Locale-Zuweisungen führen kann. Über requirements in der Routen-Konfiguration lässt sich der Parameter auf eine feste Liste erlaubter Codes einschränken, etwa de|en|fr, sodass Anfragen mit anderen Werten automatisch als nicht passend erkannt und mit einem regulären 404 beantwortet werden, statt eine unerwartete Locale zu setzen.

Diese Einschränkung sollte zentral an einer Stelle gepflegt werden, etwa als Parameter in der services.yaml, der dann in allen Routen wiederverwendet wird, statt die Liste erlaubter Sprachcodes in jeder einzelnen Route erneut auszuschreiben. Das reduziert das Risiko, dass beim Hinzufügen einer neuen Sprache eine einzelne Route vergessen wird und dadurch inkonsistent auf die neue Locale reagiert.


# config/routes.yaml
product_list:
    path: /{_locale}/{slug}
    controller: App\Controller\ProductController::list
    requirements:
        _locale: '%app.supported_locales%'
        slug: 'produkte|products'
    defaults:
        slug: produkte

3. Übersetzte Routen-Pfade mit Symfony-Translation

Für echte übersetzte Pfade wie /de/produkte gegenüber /en/products reicht der {_locale}-Parameter allein nicht aus, da er nur den Sprachcode, nicht aber den restlichen Pfad variiert. Eine Lösung ist, den übersetzbaren Teil des Pfads selbst als Übersetzungsschlüssel zu behandeln und über den Symfony-Translator in der Routen-Definition aufzulösen, etwa durch einen eigenen Loader, der beim Kompilieren der Routen die passenden Übersetzungen aus den translations/routes.de.yaml- und translations/routes.en.yaml-Dateien einliest.

Ein pragmatischerer Ansatz ohne eigenen Route-Loader ist, für jede Sprache eine eigene benannte Route mit demselben Controller, aber unterschiedlichem Pfad-Segment anzulegen, etwa product_list_de mit dem Pfad /de/produkte und product_list_en mit dem Pfad /en/products. Der Nachteil ist mehr Redundanz in der Routing-Konfiguration, der Vorteil ist deutlich einfachere Nachvollziehbarkeit ohne zusätzliche Abstraktionsebene, was besonders in kleineren bis mittelgroßen Projekten oft die praktikablere Wahl ist.

4. URL-Generierung in Templates unter Berücksichtigung der aktuellen Locale

Beim Generieren von URLs innerhalb von Twig-Templates übernimmt Symfony automatisch die aktuell aktive Locale für Routen, die den {_locale}-Parameter verwenden, sodass path('product_list') innerhalb einer deutschen Seite automatisch /de/produkte erzeugt, ohne dass die Locale manuell mitgegeben werden muss. Bei separaten Routen pro Sprache, wie im vorherigen Abschnitt beschrieben, muss dagegen im Template selbst zwischen product_list_de und product_list_en unterschieden werden, was sich am saubersten über ein kleines Twig-Extension-Funktion lösen lässt, die basierend auf der aktuellen Locale automatisch die richtige Route wählt.

Für den Sprachumschalter, also den Link, mit dem Nutzer zwischen Sprachversionen derselben Seite wechseln, ist es wichtig, nicht einfach nur die Locale in der aktuellen URL auszutauschen, sondern tatsächlich zur passenden übersetzten URL der aktuellen Seite zu verlinken. Das setzt voraus, dass zu jeder Seite bekannt ist, welche Route sie in welcher Sprache repräsentiert, was sich am einfachsten über eine gemeinsame, sprachneutrale Routen-ID lösen lässt, die intern auf die sprachspezifischen Pfade gemappt wird.

5. Canonical- und Hreflang-Implikationen für SEO

Jede Sprachversion einer Seite braucht ein eigenes canonical-Tag, das auf sich selbst zeigt, nicht auf eine andere Sprachversion. Ein häufiger Fehler ist, versehentlich immer auf die Standardsprache zu verlinken, wodurch Suchmaschinen die übersetzten Versionen als Duplicate Content der Hauptsprache interpretieren könnten und sie folglich schlechter oder gar nicht indexieren.

Zusätzlich zum Canonical-Tag sollten hreflang-Link-Elemente im HTML-Head jede verfügbare Sprachversion einer Seite explizit auflisten, inklusive eines x-default-Eintrags für Nutzer, deren Sprache nicht in der Liste vorkommt. Diese hreflang-Angaben lassen sich in Symfony am besten zentral über ein Twig-Fragment oder eine ViewModel-Methode erzeugen, die basierend auf der aktuellen Route und den bekannten Übersetzungen automatisch alle verfügbaren Sprachversionen samt ihrer korrekten URLs auflistet, statt sie auf jeder Seite manuell zu pflegen.

6. Mehrsprachige Sitemaps und robots.txt

Neben der eigentlichen Seitenstruktur sollten auch Sitemaps mehrsprachige URLs korrekt abbilden. Eine gängige Praxis ist eine Sitemap-Index-Datei, die auf sprachspezifische Einzel-Sitemaps verweist, wobei jeder url-Eintrag zusätzlich xhtml:link-Elemente für alternate Sprachversionen enthält, analog zu den hreflang-Angaben im HTML-Head. In Symfony lässt sich eine solche Sitemap über einen eigenen Controller generieren, der über alle bekannten Routen mit dem {_locale}-Parameter iteriert und für jede unterstützte Sprache automatisch einen eigenen Eintrag erzeugt, statt die Sitemap-Struktur manuell zu pflegen.

Für robots.txt gilt meist, dass keine sprachspezifische Sperrung nötig ist, solange Canonical- und Hreflang-Angaben korrekt gesetzt sind, da Suchmaschinen die Sprachversionen dann ohnehin korrekt als zusammengehörige Alternativen erkennen. Eine Ausnahme sind Vorschau- oder Staging-Subdomains einzelner Sprachversionen, die explizit über Disallow ausgeschlossen werden sollten, um zu verhindern, dass Suchmaschinen unfertige Übersetzungen indexieren, bevor sie offiziell veröffentlicht sind.

7. Default-Locale und Fallback-Verhalten konfigurieren

Symfony erlaubt es, über default_locale in der framework.yaml eine Standard-Sprache festzulegen, die verwendet wird, wenn keine explizite Locale ermittelt werden kann, etwa bei einem Request auf die Root-URL ohne Sprachpräfix. Wichtig ist, dieses Verhalten bewusst zu gestalten: Eine automatische Weiterleitung basierend auf dem Accept-Language-Header des Browsers kann nutzerfreundlich sein, sollte aber niemals dazu führen, dass dieselbe Seite unter mehreren URLs ohne klare Canonical-Zuordnung erreichbar ist.

Für Übersetzungslücken, also Seiten, die in einer bestimmten Sprache noch nicht existieren, empfiehlt sich ein bewusstes Fallback-Verhalten statt eines harten 404. Je nach Anwendungsfall kann das bedeuten, die Seite in der Standardsprache mit einem Hinweis auf die fehlende Übersetzung anzuzeigen, oder tatsächlich einen 404 zu liefern, wenn eine fehlende Übersetzung fachlich nicht akzeptabel ist, etwa bei rechtlich verpflichtenden Inhalten.

8. Ein LocaleListener für konsistente Locale-Ermittlung

Statt die Locale-Logik über mehrere Controller zu verstreuen, bietet sich ein eigener EventSubscriber an, der auf das kernel.request-Event hört und die Locale konsistent an einer zentralen Stelle ermittelt und im Request-Objekt setzt. Dieser Listener kann die Prioritätenkette klar definieren: zunächst der {_locale}-Routenparameter, falls vorhanden, dann eine im Session gespeicherte Nutzer-Präferenz, und erst zuletzt der Accept-Language-Header oder die konfigurierte Default-Locale als Fallback.

Diese zentrale Ermittlung verhindert, dass verschiedene Teile der Anwendung, etwa ein API-Controller und ein regulärer Web-Controller, die Locale auf unterschiedliche und potenziell widersprüchliche Weise bestimmen. Der Listener sollte mit einer ausreichend hohen Priorität registriert werden, damit die Locale bereits gesetzt ist, bevor andere Listener oder der eigentliche Controller darauf zugreifen wollen.

9. Mehrsprachige Routen sinnvoll testen

Funktionale Tests für mehrsprachige Routen sollten explizit prüfen, dass sowohl /de/produkte als auch /en/products auf denselben Controller mit korrekt gesetzter Locale führen, und dass eine Anfrage mit einem nicht unterstützten Sprachcode wie /xx/produkte tatsächlich einen 404 zurückliefert. Ebenso lohnt sich ein Test, der überprüft, dass die generierten hreflang-Tags für eine gegebene Seite exakt die Sprachversionen auflisten, die tatsächlich existieren, ohne verwaiste Einträge für noch nicht übersetzte Inhalte.

Bei Projekten mit vielen Sprachen empfiehlt sich ein datengetriebener Test, der über alle konfigurierten Locale-Werte iteriert und für jede Sprache dieselbe Grundprüfung durchführt, statt für jede Sprache einen separaten, weitgehend redundanten Testfall von Hand zu schreiben. Das macht das Hinzufügen einer neuen Sprache zu einem reinen Konfigurationsschritt, der automatisch von der bestehenden Testsuite mitabgedeckt wird.

Ansatz Beispiel-URL Vorteil Nachteil
{_locale}-Parameter /de/produkte, /en/produkte einfach umzusetzen Pfad selbst bleibt gleich
Übersetzte Pfade /de/produkte, /en/products SEO-freundlich pro Sprache mehr Konfigurationsaufwand
Subdomain de.example.com, en.example.com klare technische Trennung mehr Infrastruktur-Aufwand
Query-Parameter ?lang=de sehr einfach umzusetzen SEO-technisch ungeeignet

Mironsoft

Symfony-Architektur, saubere Domänenlogik und Legacy-Modernisierung

Symfony-Anwendungen, die nach zwei Jahren noch wartbar sind?

Wir prüfen bestehende Symfony-Projekte auf aufgeblähte Controller, fehlende Serviceabstraktionen und ungetestete Kernlogik und bauen daraus eine Architektur, die neue Features aufnimmt, ohne bei jedem Release brüchiger zu werden.

Architektur-Review

Bundle-Struktur, Dependency Injection und Serviceabstraktionen auf Wartbarkeit prüfen.

Legacy-Modernisierung

Schrittweise Migration veralteter Symfony-Versionen ohne kompletten Neuschrieb.

Test- und Qualitätssicherung

PHPUnit, PHPStan und CI-Pipelines für nachhaltige Codequalität aufbauen.

10. Zusammenfassung

i18n-Routing: Das Wichtigste auf einen Blick

_locale-Parameter

eingebauter Symfony-Mechanismus für sprachabhängige Routen

Übersetzte Pfade

eigene Route pro Sprache oder Route-Loader mit Übersetzungsdatei

Canonical

jede Sprachversion zeigt auf sich selbst, nie auf die Standardsprache

Hreflang

zentral generiert, inklusive x-default für nicht gelistete Sprachen

11. FAQ: i18n-Routing: Das Wichtigste auf einen Blick

1Was macht der {_locale}-Parameter in Symfony-Routen genau?
Er ist ein spezieller Platzhalter, der von Symfony automatisch als aktuelle Locale erkannt wird und für Übersetzungen, Zahlenformatierung und URL-Generierung zur Verfügung steht, sobald er in einer Route auftaucht.
2Wann lohnt sich Subdomain-basierte statt pfadbasierte Lokalisierung?
Vor allem wenn Sprachversionen technisch oder organisatorisch getrennt werden sollen, etwa durch unterschiedliche Teams oder regionale Hosting-Anforderungen, weniger aus reinen SEO-Gründen.
3Wie schränke ich den _locale-Parameter auf gültige Sprachcodes ein?
Über requirements in der Routen-Konfiguration mit einer regulären Ausdrucksliste wie de|en|fr, idealerweise als wiederverwendbarer Parameter statt in jeder Route einzeln definiert.
4Wie setze ich übersetzte Pfade wie /de/produkte und /en/products um?
Entweder über separate benannte Routen pro Sprache mit demselben Controller, oder über einen eigenen Route-Loader, der die Pfad-Segmente aus Übersetzungsdateien auflöst.
5Warum ist ein eigenes canonical-Tag pro Sprachversion wichtig?
Ohne eigenes canonical-Tag könnten Suchmaschinen übersetzte Seiten als Duplicate Content der Standardsprache einstufen und sie schlechter oder gar nicht indexieren.
6Was gehört in die hreflang-Angaben einer Seite?
Ein Link-Element pro verfügbarer Sprachversion mit korrekter URL, sowie ein zusätzlicher x-default-Eintrag für Nutzer, deren Sprache nicht explizit gelistet ist.
7Sollte eine automatische Sprachweiterleitung basierend auf dem Browser erfolgen?
Kann nutzerfreundlich sein, darf aber nicht dazu führen, dass dieselbe Seite ohne klare Canonical-Zuordnung unter mehreren URLs erreichbar wird.
8Was passiert bei fehlenden Übersetzungen einzelner Seiten?
Je nach Anwendungsfall entweder ein Fallback auf die Standardsprache mit Hinweis, oder ein bewusster 404, wenn eine fehlende Übersetzung fachlich nicht akzeptabel ist.
9Wofür wird ein zentraler LocaleListener empfohlen?
Um die Locale-Ermittlung konsistent an einer Stelle mit klarer Prioritätenkette zu bündeln, statt sie über mehrere Controller verstreut und potenziell widersprüchlich zu implementieren.
10Wie testet man mehrsprachige Routen effizient?
Mit einem datengetriebenen Test, der über alle konfigurierten Locale-Werte iteriert und pro Sprache dieselbe Grundprüfung durchführt, statt für jede Sprache einen eigenen, redundanten Testfall zu schreiben.