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.
Inhaltsverzeichnis
- 1. Der {_locale}-Routing-Parameter vs. Subdomain-basierte Lokalisierung
- 2. _locale-Requirement mit erlaubten Sprachcodes
- 3. Übersetzte Routen-Pfade mit Symfony-Translation
- 4. URL-Generierung in Templates unter Berücksichtigung der aktuellen Locale
- 5. Canonical- und Hreflang-Implikationen für SEO
- 6. Mehrsprachige Sitemaps und robots.txt
- 7. Default-Locale und Fallback-Verhalten konfigurieren
- 8. Ein LocaleListener für konsistente Locale-Ermittlung
- 9. Mehrsprachige Routen sinnvoll testen
- 10. Zusammenfassung
- 11. FAQ
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