This example is going to teach us how to creating a multilingual application with symfony. We will be using static and dynamic messages in English en and Turkish tr. To translate message codes to human readable strings, we will be using trans function in twig templates and translator service in controller.


Basics


Cache


You'll often need to clear cache to see changes you did. This applies to all environments.


Message list


You can use $ php bin/console debug:translation en InanzzzApplicationBundle command to list all messages for given language en and bundle InanzzzApplicationBundle.


URL structure


As you can see below, URLs contain en and tr parameter.


http://myapp.dev/app_dev.php/en/
http://myapp.dev/app_dev.php/tr/
http://myapp.dev/app_dev.php/en/translator
http://myapp.dev/app_dev.php/tr/translator

URL correction


Every URL must contain _locale parameters which are en and tr. If a user tries to access an URL without en or tr parameter, our event listener will intercept the request to automatically add en or tr parameter to the current URL and redirect user to it. Shortly, you'll see how it is done.


Configuration


Config.yml


parameters:
locale: en
default_locale: en
valid_locales: en|tr

framework:
translator: { fallbacks: ["%locale%"] }

Routing.yml


inanzzz_application:
resource: "@InanzzzApplicationBundle/Controller/"
type: annotation
prefix: /{_locale}/
requirements:
_locale: "%valid_locales%"
defaults:
_locale: "%default_locale%"

Routing_dev.yml


_wdt:
resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
prefix: /{_locale}/_wdt

_profiler:
resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
prefix: /{_locale}/_profiler

_errors:
resource: "@TwigBundle/Resources/config/routing/errors.xml"
prefix: /{_locale}/_error

_main:
resource: routing.yml

Messages


Messages.en.xlf


#src/Inanzzz/ApplicationBundle/Resources/translations/messages.en.xlf

<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="application.greeting">
<source>application.greeting</source>
<target>Welcome to our multilingual application!</target>
</trans-unit>
<trans-unit id="user.greeting">
<source>user.greeting</source>
<target>Hello %name% %surname%!</target>
</trans-unit>
<trans-unit id="page.translator.header">
<source>page.translator.header</source>
<target>Translator</target>
</trans-unit>
</body>
</file>
</xliff>

$ php bin/console debug:translation en InanzzzApplicationBundle
---------- ---------- ------------------------ ------------------------------------------
State Domain Id Message Preview (en)
---------- ---------- ------------------------ ------------------------------------------
messages application.greeting Welcome to our multilingual application!
unused messages user.greeting Hello %name% %surname%!
unused messages page.translator.header Translator
---------- ---------- ------------------------ ------------------------------------------

Messages.tr.xlf


#src/Inanzzz/ApplicationBundle/Resources/translations/messages.tr.xlf

<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="tr" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="application.greeting">
<source>application.greeting</source>
<target>Çok dilli web uygulamamıza hoşgeldiniz!</target>
</trans-unit>
<trans-unit id="user.greeting">
<source>user.greeting</source>
<target>Merhabe %name% %surname%!</target>
</trans-unit>
<trans-unit id="page.translator.header">
<source>page.translator.header</source>
<target>Çevirmen</target>
</trans-unit>
</body>
</file>
</xliff>

$ php bin/console debug:translation tr InanzzzApplicationBundle
---------- ---------- ------------------------ -----------------------------------------
State Domain Id Message Preview (tr)
---------- ---------- ------------------------ -----------------------------------------
messages application.greeting Çok dilli web uygulamamıza hoşgeldiniz!
unused messages user.greeting Merhabe %name% %surname%!
unused messages page.translator.header Çevirmen
---------- ---------- ------------------------ -----------------------------------------

Controllers


DefaultController


namespace Inanzzz\ApplicationBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("", service="inanzzz_application.controller.default")
*/
class DefaultController
{
private $templating;

public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}

/**
* @Method({"GET"})
* @Route("/")
*
* @return Response
*/
public function indexAction()
{
return $this->templating->renderResponse(
'InanzzzApplicationBundle:Default:index.html.twig'
);
}
}

TranslatorController


namespace Inanzzz\ApplicationBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Translation\TranslatorInterface;

/**
* @Route("/translator", service="inanzzz_application.controller.translator")
*/
class TranslatorController
{
private $templating;
private $translator;

public function __construct(
EngineInterface $templating,
TranslatorInterface $translator
) {
$this->templating = $templating;
$this->translator = $translator;
}

/**
* @Method({"GET"})
* @Route("/")
*
* @return Response
*/
public function indexAction()
{
return $this->templating->renderResponse(
'InanzzzApplicationBundle:Translator:index.html.twig',
[
'page_header' => $this->translator->trans('page.translator.header'),
'user_greeting' => $this->translator->trans(
'user.greeting',
[
'%name%' => 'Inanzzz',
'%surname%' => 'Zzznani',
]
)
]
);
}
}

services:

inanzzz_application.controller.default:
class: Inanzzz\ApplicationBundle\Controller\DefaultController
arguments:
- '@templating'

inanzzz_application.controller.translator:
class: Inanzzz\ApplicationBundle\Controller\TranslatorController
arguments:
- '@templating'
- '@translator'

Templates


Default


#src/Inanzzz/ApplicationBundle/Resources/views/Default/index.html.twig

{% extends '::base.html.twig' %}

{% block body %}
<h3>{{ 'application.greeting'|trans }}</h3>
{% endblock %}

Translator


#src/Inanzzz/ApplicationBundle/Resources/views/Translator/index.html.twig

{% extends '::base.html.twig' %}

{% block body %}
<h3>{{ page_header }}</h3>
<p>{{ user_greeting }}</p>
{% endblock %}

LocaleRedirectListener


If the URL doesn't contain en or tr, application throws a 404 NotFoundHttpException. In such case this event listener kicks in and handles it as shown below.


  1. Catches NotFoundHttpException.

  2. Makes sure that the user caused this error.

  3. Extracts URL parts.

  4. Checks if en and tr are missing in extracted URL parts.

  5. Constructs URL to redirect.

    1. Checks user's browser language.

    2. If the browser language is in the valid langue list (en or tr) then use it otherwise use application default language en.

    3. Attach language (en or tr) to beginning of the extracted URL parts.

    4. Return new URL.

  6. Redirect user to corrected URL.

If the application throws a 404 NotFoundHttpException error even if the URL has language part than this event listener will ignore redirection because the user is actually trying to access a non-existent URL which is fine.


namespace Inanzzz\ApplicationBundle\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class LocaleRedirectListener
{
private $validLocales;

public function __construct($validLocales)
{
$this->validLocales = explode('|', $validLocales);
}

public function onKernelException(GetResponseForExceptionEvent $event)
{
// 1
if (!$event->getException() instanceof NotFoundHttpException) {
return;
}

// 2
if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
return;
}

// 3
$request = $event->getRequest();
$uriParts = explode('/', $request->getPathInfo());

// 4
if ($this->isLocaleMissing($uriParts)) {
// 5
$response = new RedirectResponse($this->constructRedirectUri($request));
// 6
$event->setResponse($response);
}
}

private function isLocaleMissing(array $uriParts)
{
return !isset($uriParts[1]) || !$uriParts[1] || !in_array($uriParts[1], $this->validLocales);
}

private function constructRedirectUri(Request $request)
{
// 5.1
$browserLanguage = $request->getPreferredLanguage($this->validLocales);
// 5.2
$locale = in_array($browserLanguage, $this->validLocales)
? $browserLanguage
: $request->getDefaultLocale();
// 5.3
$find = str_replace('/', '\/', $request->getPathInfo());

// 5.4
return preg_replace(
'/'.$find.'$/',
'/'.$locale.'/'.trim($request->getPathInfo(), '/'),
$request->getUri()
);
}
}

services:

inanzzz_application.listener.locale_redirect:
class: Inanzzz\ApplicationBundle\EventListener\LocaleRedirectListener
arguments:
- "%valid_locales%"
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

Readings