Een applicatie internationaliseren
Met een internationaal publiek is Symfony vrijwel vanaf het begin in staat geweest om de internationalisering (i18n) en lokalisatie (l10n) op een eenvoudige manier aan te pakken. Het lokaliseren van een applicatie gaat niet alleen over het vertalen van de interface, het gaat ook over meervouden, datum- en valutaopmaak, URL's, en meer.
URL's internationaliseren
De eerste stap om de website te internationaliseren, is het internationaliseren van de URL's. Bij het vertalen van een website interface, moet de URL verschillend zijn per locale, om goed om te kunnen gaan met HTTP caches (gebruik nooit dezelfde URL en sla de locale op in de sessie).
Gebruik de speciale _locale
routeparameter om te verwijzen naar de locale in routes:
1 2 3 4 5 6 7 8 9 10 11
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -33,7 +33,7 @@ class ConferenceController extends AbstractController
$this->bus = $bus;
}
- #[Route('/', name: 'homepage')]
+ #[Route('/{_locale}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
$response = new Response($this->twig->render('conference/index.html.twig', [
Op de homepage wordt de locale nu ingesteld op basis van de URL; bijvoorbeeld, op /fr/
, geeft $request->getLocale()
nu fr
terug.
Omdat je waarschijnlijk niet in staat zult zijn om de inhoud naar alle geldige locales te vertalen, beperk je je tot de locales die je wilt ondersteunen:
1 2 3 4 5 6 7 8 9 10 11
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -33,7 +33,7 @@ class ConferenceController extends AbstractController
$this->bus = $bus;
}
- #[Route('/{_locale}/', name: 'homepage')]
+ #[Route('/{_locale<en|fr>}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
$response = new Response($this->twig->render('conference/index.html.twig', [
Elke routeparameter kan worden beperkt door een regular expression tussen <
>
. De homepage
route komt nu alleen nog maar overeen als de _locale
parameter en
of fr
is. Probeer /es/
op te vragen, je zou een 404 te zien moeten krijgen omdat er geen route overeenkomt.
Aangezien we dezelfde vereiste in bijna alle routes zullen gebruiken, verplaatsen we deze naar een containerparameter:
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
@@ -7,6 +7,7 @@ parameters:
default_admin_email: admin@example.com
default_domain: '127.0.0.1'
default_scheme: 'http'
+ app.supported_locales: 'en|fr'
router.request_context.host: '%env(default:default_domain:SYMFONY_DEFAULT_ROUTE_HOST)%'
router.request_context.scheme: '%env(default:default_scheme:SYMFONY_DEFAULT_ROUTE_SCHEME)%'
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -33,7 +33,7 @@ class ConferenceController extends AbstractController
$this->bus = $bus;
}
- #[Route('/{_locale<en|fr>}/', name: 'homepage')]
+ #[Route('/{_locale<%app.supported_locales%>}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
$response = new Response($this->twig->render('conference/index.html.twig', [
Het toevoegen van een taal doen we door de app.supported_languages
parameter bij te werken.
Voeg dezelfde locale route prefix toe aan de andere URLs:
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
@@ -44,7 +44,7 @@ class ConferenceController extends AbstractController
return $response;
}
- #[Route('/conference_header', name: 'conference_header')]
+ #[Route('/{_locale<%app.supported_locales%>}/conference_header', name: 'conference_header')]
public function conferenceHeader(ConferenceRepository $conferenceRepository): Response
{
$response = new Response($this->twig->render('conference/header.html.twig', [
@@ -55,7 +55,7 @@ class ConferenceController extends AbstractController
return $response;
}
- #[Route('/conference/{slug}', name: 'conference')]
+ #[Route('/{_locale<%app.supported_locales%>}/conference/{slug}', name: 'conference')]
public function show(Request $request, Conference $conference, CommentRepository $commentRepository, NotifierInterface $notifier, string $photoDir): Response
{
$comment = new Comment();
We zijn bijna klaar. We hebben geen route meer die overeenkomt met /
. Laten we deze weer ondersteunen en omleiden naar /en/
:
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
@@ -33,6 +33,12 @@ class ConferenceController extends AbstractController
$this->bus = $bus;
}
+ #[Route('/')]
+ public function indexNoLocale(): Response
+ {
+ return $this->redirectToRoute('homepage', ['_locale' => 'en']);
+ }
+
#[Route('/{_locale<%app.supported_locales%>}/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
Nu dat alle hoofdroutes voorzien zijn van de locale, zie je dat de gegenereerde URL's op de pagina's automatisch rekening houden met de huidige locale.
Een locale switcher toevoegen
Om gebruikers in staat te stellen van de standaard en
locale naar een andere locale over te schakelen, voegen we bovenin een switcher toe:
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>
Om over te schakelen naar een andere locale, geven we expliciet de _locale
routeparameter door aan de path()
functie.
Update de template om de huidige locale naam weer te geven in plaats van de hard-coded waarde "English":
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
is een globale Twig-variabele die toegang geeft tot de huidige request. Om de locale om te zetten naar een menselijk leesbare string, gebruiken we de Twig-filter locale_name
.
Afhankelijk van de locale wordt de localenaam niet altijd met een hoofdletter geschreven. Om op de juiste manier om te gaan met hoofdletters, hebben we een filter nodig die Unicode-bewust is, zoals de Symfony String-component en de Twig-implementatie ervan:
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>
Je kunt nu wisselen van Frans naar Engels en de hele interface past zich netjes aan:
Vertalen van de interface
Het vertalen van elke zin op een grote website is een hele klus, maar gelukkig hebben we maar een handvol berichten op onze website. Laten we beginnen met alle zinnen op de 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>
De trans
Twig-filter zoekt een vertaling van de gegeven invoer naar de huidige locale. Indien niet gevonden, valt het terug naar de standaard locale zoals geconfigureerd in config/packages/translation.yaml
:
1 2 3 4 5 6
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
Merk op dat het vertaling "tabblad" van de web debug toolbar rood is geworden:
Het vertelt ons dat er 3 berichten nog niet vertaald zijn.
Klik op de "tab" om alle berichten te tonen waarvoor Symfony geen vertaling heeft gevonden:
Vertalingen ondersteunen
Zoals je misschien al gezien hebt in config/packages/translation.yaml
, worden vertalingen opgeslagen in een translations/
hoofdmap, die automatisch voor ons is aangemaakt.
In plaats van de vertaalbestanden met de hand aan te maken, gebruik je het translation:extract
commando:
1
$ symfony console translation:extract fr --force --domain=messages
Dit commando genereert een vertaalbestand ( --force
vlag) voor de fr
locale en het messages
domein. Het messages
domein bevat alle applicatie berichten, behalve degene die uit Symfony zelf komen zoals validatie of beveiligingsfouten.
Bewerk het translations/messages+intl-icu.fr.xlf
bestand en vertaal de berichten in het Frans. Spreek je geen Frans? Laat me je helpen:
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>
Merk op dat we niet alle templates zullen vertalen, maar voel je vrij om dit wel te doen:
Vertalen van formulieren
Formulierlabels worden door Symfony automatisch weergegeven door middel van het vertaalsysteem. Ga naar een conferentiepagina en klik op het "Vertaling" tabblad van de web debug toolbar; je zou alle labels die beschikbaar zijn voor vertaling moeten zien:
Lokaliseren van datums
Als je overschakelt naar Frans en een conferentiepagina met reacties bezoekt, zul je zien dat de datum van de reacties automatisch gelokaliseerd zijn. Dit werkt omdat we de Twig format_datetime
filter gebruikt hebben, die op de hoogte is van locales ({{ comment.createdAt|format_datetime('medium', 'short') }}
).
De lokalisatie werkt voor datums, tijden (format_time
), valuta (format_currency
) en getallen (format_number
) in het algemeen (procenten, duurtijden, spelling, ....).
Vertalen van meervouden
Het vertalen van meervouden is een voorbeeld van een algemener probleem, waarbij je een vertaling moet selecteren op basis van een voorwaarde.
Op een conferentiepagina tonen we het aantal reacties: There are 2 comments
. Voor 1 reactie tonen we foutief There are 1 comments
. Wijzig de template zodat de zin om te zetten is in een vertaalbaar bericht:
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 %}
Voor deze boodschap hebben we een andere vertaalstrategie gebruikt. In plaats van de Engelse versie in de template te behouden, hebben we deze vervangen door een unieke identifier. Die strategie werkt beter voor complexe en grote hoeveelheden tekst.
Update het vertaalbestand door het nieuwe bericht toe te voegen:
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>
We zijn nog niet klaar, omdat we nu de Engelse vertaling nog moeten ondersteunen. Maak het translations/messages+intl-icu.en.xlf
bestand aan:
Functionele tests bijwerken
Vergeet niet om de functionele tests bij te werken door URL's en inhoudelijke wijzigingen over te nemen:
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_form[author]' => 'Fabien',
'comment_form[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")');
}
}