Eine Anwendung lokalisieren
Mit seinem internationalen Nutzerkreis ist Symfony seit jeher in der Lage, Internationalisierung (i18n) und Lokalisierung (l10n) ohne weiteres zu bewältigen. Bei der Lokalisierung einer Anwendung geht es nicht nur um die Übersetzung der Benutzeroberfläche, sondern auch um Mehrzahlformen, Datums- und Währungsformatierung, URLs und mehr.
URLs internationalisieren
Der erste Schritt zur Internationalisierung der Website ist die Internationalisierung der URLs. Bei der Übersetzung einer Website sollten die URLs pro Sprache unterschiedlich sein, damit HTTP-Caches problemlos funktionieren (verwende niemals die gleiche URL und speichere die Sprache in der Session).
Nutze den speziellen Routen-Parameter _locale
, um die Sprache in Routen zu verwenden:
1 2 3 4 5 6 7 8 9 10 11
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -28,7 +28,7 @@ class ConferenceController extends AbstractController
) {
}
- #[Route('/', name: 'homepage')]
+ #[Route('/{_locale}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
return $this->render('conference/index.html.twig', [
Auf der Homepage wird die Sprache nun intern abhängig von der URL gesetzt; z. B. gibt $request->getLocale()
uns fr
zurück, wenn Du /fr/
eintippst.
Da Du den Inhalt wahrscheinlich nicht in allen Sprachen übersetzen kannst, beschränke die zulässigen _locale
-Werte auf die Sprachen, die Du unterstützen möchtest:
1 2 3 4 5 6 7 8 9 10 11
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -28,7 +28,7 @@ class ConferenceController extends AbstractController
) {
}
- #[Route('/{_locale}/', name: 'homepage')]
+ #[Route('/{_locale<en|fr>}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
return $this->render('conference/index.html.twig', [
Jeder Routen-Parameter kann durch einen regulären Ausdruck innerhalb <
>
eingeschränkt werden. Die homepage
-Route passt jetzt nur noch, wenn der Routen-Parameter _locale
en
oder fr
ist. Versuche /es/
einzutippen, Du solltest einen 404-Fehler bekommen, da keine Route passt.
Da wir die gleiche Anforderung in fast allen Routen verwenden werden, verschieben wir sie in einen Container-Parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -9,6 +9,7 @@ parameters:
admin_email: "%env(string:default:default_admin_email:ADMIN_EMAIL)%"
default_base_url: 'http://127.0.0.1'
router.request_context.base_url: '%env(default:default_base_url:SYMFONY_DEFAULT_ROUTE_URL)%'
+ app.supported_locales: 'en|fr'
services:
# default configuration for services in *this* file
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -28,7 +28,7 @@ class ConferenceController extends AbstractController
) {
}
- #[Route('/{_locale<en|fr>}/', name: 'homepage')]
+ #[Route('/{_locale<%app.supported_locales%>}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
return $this->render('conference/index.html.twig', [
Das Hinzufügen einer Sprache kann durch Aktualisieren des app.supported_languages
-Parameters erfolgen.
Füge den anderen URLs das gleiche lokale Routenpräfix hinzu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -35,7 +35,7 @@ class ConferenceController extends AbstractController
])->setSharedMaxAge(3600);
}
- #[Route('/conference_header', name: 'conference_header')]
+ #[Route('/{_locale<%app.supported_locales%>}/conference_header', name: 'conference_header')]
public function conferenceHeader(ConferenceRepository $conferenceRepository): Response
{
return $this->render('conference/header.html.twig', [
@@ -43,7 +43,7 @@ class ConferenceController extends AbstractController
])->setSharedMaxAge(3600);
}
- #[Route('/conference/{slug}', name: 'conference')]
+ #[Route('/{_locale<%app.supported_locales%>}/conference/{slug}', name: 'conference')]
public function show(
Request $request,
Conference $conference,
Wir sind fast fertig. Wir haben keine Route mehr, die zu /
passt. Fügen wir sie wieder ein und leiten sie nach /en/
weiter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -27,6 +27,12 @@ class ConferenceController extends AbstractController
) {
}
+ #[Route('/')]
+ public function indexNoLocale(): Response
+ {
+ return $this->redirectToRoute('homepage', ['_locale' => 'en']);
+ }
+
#[Route('/{_locale<%app.supported_locales%>}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
Da nun alle Hauptrouten einen `_locale``-Parameter haben, sieht man, dass die generierten URLs auf den Seiten automatisch die aktuelle Sprache berücksichtigen.
Einen Sprachwechsler hinzufügen
Damit Benutzer*innen von der Standard-Sprache en
zu einer anderen wechseln können, fügen wir einen Sprachwechsler im Header hinzu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -34,6 +34,16 @@
Admin
</a>
</li>
+<li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle" href="#" id="dropdown-language" role="button"
+ data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ English
+ </a>
+ <ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-language">
+ <li><a class="dropdown-item" href="{{ path('homepage', {_locale: 'en'}) }}">English</a></li>
+ <li><a class="dropdown-item" href="{{ path('homepage', {_locale: 'fr'}) }}">Français</a></li>
+ </ul>
+</li>
</ul>
</div>
</div>
Um zu einem anderen Sprache zu wechseln, übergeben wir explizit den Routen-Parameter _locale
an die path()
-Funktion.
Aktualisiere das Template, um den aktuellen Sprach-Namen anstelle des fest geschriebenen "English" anzuzeigen:
1 2 3 4 5 6 7 8 9 10 11
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -37,7 +37,7 @@
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dropdown-language" role="button"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- English
+ {{ app.request.locale|locale_name(app.request.locale) }}
</a>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-language">
<li><a class="dropdown-item" href="{{ path('homepage', {_locale: 'en'}) }}">English</a></li>
app
ist eine globale Twig-Variable, die den Zugriff auf den aktuellen Request ermöglicht. Um das Sprachkürzel in ein lesbares Wort zu konvertieren, verwenden wir den Twig-Filter locale_name
.
Abhängig von der Sprache wird der Name der Sprache nicht immer großgeschrieben. Um Sätze richtig groß zu schreiben, benötigen wir einen entsprechenden Filter, der mit Unicode umgehen kann, wie ihn die Symfony String-Komponente und ihre Twig-Implementierung bereitstellen:
1
$ symfony composer req twig/string-extra
1 2 3 4 5 6 7 8 9 10 11
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -37,7 +37,7 @@
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dropdown-language" role="button"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- {{ app.request.locale|locale_name(app.request.locale) }}
+ {{ app.request.locale|locale_name(app.request.locale)|u.title }}
</a>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-language">
<li><a class="dropdown-item" href="{{ path('homepage', {_locale: 'en'}) }}">English</a></li>
Du kannst nun über den Sprachwechsler von Französisch auf Englisch umschalten und die gesamte Oberfläche passt sich schön an:
Das Interface übersetzen
Die Übersetzung jedes einzelnen Satzes auf einer großen Website kann mühsam sein, aber glücklicherweise haben wir nur eine Handvoll Nachrichten (messages) auf unserer Website. Beginnen wir mit allen Sätzen auf der Homepage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -20,7 +20,7 @@
<nav class="navbar navbar-expand-xl navbar-light bg-light">
<div class="container mt-4 mb-3">
<a class="navbar-brand me-4 pr-2" href="{{ path('homepage') }}">
- 📙 Conference Guestbook
+ 📙 {{ 'Conference Guestbook'|trans }}
</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#header-menu" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Show/Hide navigation">
--- a/templates/conference/index.html.twig
+++ b/templates/conference/index.html.twig
@@ -4,7 +4,7 @@
{% block body %}
<h2 class="mb-5">
- Give your feedback!
+ {{ 'Give your feedback!'|trans }}
</h2>
{% for row in conferences|batch(4) %}
@@ -21,7 +21,7 @@
<a href="{{ path('conference', { slug: conference.slug }) }}"
class="btn btn-sm btn-primary stretched-link">
- View
+ {{ 'View'|trans }}
</a>
</div>
</div>
Der Twig-Filter trans
sucht nach einer Übersetzung der gegebenen Eingabe in die aktuelle Sprache. Wenn sie nicht gefunden wird, wird sie auf die Standard-Sprache zurückgesetzt, die in config/packages/translation.yaml
konfiguriert ist:
1 2 3 4 5 6
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
Beachte, dass der Translation-"Tab" in der Web-Debug-Toolbar rot geworden ist:
Das sagt uns, dass 3 Nachrichten noch nicht übersetzt sind.
Klicke auf den "Tab", um alle Nachrichten aufzulisten, für die Symfony keine Übersetzung gefunden hat:
Übersetzungen erstellen
Wie Du vielleicht schon in config/packages/translation.yaml
gesehen hast, werden Übersetzungen in einem translations/
-Stammverzeichnis gespeichert, das automatisch für uns erstellt wurde.
Verwende den translation:extract
-Befehl, anstatt die Übersetzungsdateien manuell zu erstellen:
1
$ symfony console translation:extract fr --force --domain=messages
Dieser Befehl erzeugt eine Übersetzungsdatei (--force
Flag) für die Sprache fr
und die messages
-Domäne. Die messages
-Domäne enthält alle Anwendungsmeldungen, die nicht von Symfony selbst kommen, wie beispielsweise Validierungs- oder Sicherheitsfehler.
Bearbeite die translations/messages+intl-icu.fr.xlf
-Datei und übersetze die Nachrichten auf Französisch. Sprichst Du kein Französisch? Ich kann Dir helfen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
--- a/translations/messages+intl-icu.fr.xlf
+++ b/translations/messages+intl-icu.fr.xlf
@@ -7,15 +7,15 @@
<body>
<trans-unit id="eOy4.6V" resname="Conference Guestbook">
<source>Conference Guestbook</source>
- <target>__Conference Guestbook</target>
+ <target>Livre d'Or pour Conferences</target>
</trans-unit>
<trans-unit id="LNAVleg" resname="Give your feedback!">
<source>Give your feedback!</source>
- <target>__Give your feedback!</target>
+ <target>Donnez votre avis !</target>
</trans-unit>
<trans-unit id="3Mg5pAF" resname="View">
<source>View</source>
- <target>__View</target>
+ <target>Sélectionner</target>
</trans-unit>
</body>
</file>
Beachte, dass wir nicht alle Vorlagen übersetzen werden, aber zögere nicht, dies zu tun:
Formulare übersetzen
Form-Labels werden von Symfony automatisch über das Übersetzungssystem angezeigt. Gehe auf eine Konferenzseite und klicke auf den Tab "Translation" in der Web-Debug-Toolbar; Du solltest alle Labels sehen, die übersetzt werden:
Das Datum übersetzen
Wenn Du zu Französisch wechselst und zu einer Konferenz Website gehst, die ein paar Kommentare hat, wirst Du merken dass das Datum eines Kommentares automatisch angepasst wurde. Das funktioniert, weil wir den Twig-Filter format_datetime
benutzen, der die Sprache berücksichtigt ({{ comment.createdAt|format_datetime('medium', 'short') }}
).
Die Lokalisierung funktioniert für Datum, Zeiten (format_time
), Währungen (format_currency
) und Zahlen (format_number
) im Allgemeinen (Prozent, Dauer, Buchstabieren, ...).
Mehrzahlformen übersetzen
Die Verwaltung von Mehrzahlformen in Übersetzungen ist Teil des üblichen Problems, eine Übersetzung basierend auf einer Bedingung auszuwählen.
Auf einer Konferenzseite wird die Anzahl der Kommentare angezeigt: There are 2 comments
. Für 1 Kommentar zeigen wir There are 1 comments
an, was falsch ist. Ändere die Vorlage, um den Satz in eine übersetzbare Nachricht zu konvertieren:
1 2 3 4 5 6 7 8 9 10 11
--- a/templates/conference/show.html.twig
+++ b/templates/conference/show.html.twig
@@ -44,7 +44,7 @@
</div>
</div>
{% endfor %}
- <div>There are {{ comments|length }} comments.</div>
+ <div>{{ 'nb_of_comments'|trans({count: comments|length}) }}</div>
{% if previous >= 0 %}
<a href="{{ path('conference', { slug: conference.slug, offset: previous }) }}">Previous</a>
{% endif %}
Für diese Nachricht haben wir eine andere Übersetzungsstrategie gewählt. Anstatt die englische Version in der Vorlage zu behalten, haben wir sie durch einen eindeutigen Identifier ersetzt. Diese Strategie funktioniert bei komplexen und großen Textmengen besser.
Aktualisiere die Übersetzungsdatei, indem Du die neue Nachricht hinzufügst:
1 2 3 4 5 6 7 8 9 10 11 12 13
--- a/translations/messages+intl-icu.fr.xlf
+++ b/translations/messages+intl-icu.fr.xlf
@@ -17,6 +17,10 @@
<source>Conference Guestbook</source>
<target>Livre d'Or pour Conferences</target>
</trans-unit>
+ <trans-unit id="Dg2dPd6" resname="nb_of_comments">
+ <source>nb_of_comments</source>
+ <target>{count, plural, =0 {Aucun commentaire.} =1 {1 commentaire.} other {# commentaires.}}</target>
+ </trans-unit>
</body>
</file>
</xliff>
Wir sind noch nicht fertig, da wir nun die englische Übersetzung zur Verfügung stellen müssen. Erstelle die translations/messages+intl-icu.en.xlf
-Datei:
Funktionale Tests aktualisieren
Vergiss nicht, die funktionalen Tests zu aktualisieren, um URLs und Inhaltsänderungen aufzunehmen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
--- a/tests/Controller/ConferenceControllerTest.php
+++ b/tests/Controller/ConferenceControllerTest.php
@@ -11,7 +11,7 @@ class ConferenceControllerTest extends WebTestCase
public function testIndex()
{
$client = static::createClient();
- $client->request('GET', '/');
+ $client->request('GET', '/en/');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h2', 'Give your feedback');
@@ -20,7 +20,7 @@ class ConferenceControllerTest extends WebTestCase
public function testCommentSubmission()
{
$client = static::createClient();
- $client->request('GET', '/conference/amsterdam-2019');
+ $client->request('GET', '/en/conference/amsterdam-2019');
$client->submitForm('Submit', [
'comment[author]' => 'Fabien',
'comment[text]' => 'Some feedback from an automated functional test',
@@ -41,7 +41,7 @@ class ConferenceControllerTest extends WebTestCase
public function testConferencePage()
{
$client = static::createClient();
- $crawler = $client->request('GET', '/');
+ $crawler = $client->request('GET', '/en/');
$this->assertCount(2, $crawler->filter('h4'));
@@ -50,6 +50,6 @@ class ConferenceControllerTest extends WebTestCase
$this->assertPageTitleContains('Amsterdam');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h2', 'Amsterdam 2019');
- $this->assertSelectorExists('div:contains("There are 1 comments")');
+ $this->assertSelectorExists('div:contains("There is one comment")');
}
}