After spending hours looking around online for an answer to the problem I was having setting the locale of a multi-lingual Symfony2 application based on the domain name, I finally sat down and more or less found my own solution. As is often the case, an answer to a question on Stack Overflow got me set on the right path.
Disclaimer: Yes, I still use Symfony2. Yes, I know Symfony3 is out. No, I want to use the LTS version. Ergo, my solution works for Symfony2 and might work for Symfony3, but I haven’t tested it. Now that that is taken care of, we can carry on.
The first thing I did was create an event listener class which I named LocalListener. Essentially, this class listens to all requests and sets the locale based on the domain. This is what the class looks like:
LocaleListener.php
namespace AppBundle\Listeners;
use \Symfony\Component\HttpKernel\Event\GetResponseEvent;
use \Symfony\Component\HttpKernel\KernelEvents;
use \Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
class LocaleListener implements EventSubscriberInterface
{
protected $domainLocales;
protected $defaultLocale;
public function __construct($container, $defaultLocale)
{
$this->domainLocales = $container->getParameter('domain_locales');
$this->defaultLocale = $defaultLocale;
}
/**
* Set default locale
*
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$host = $request->getHost();
$locale = $this->defaultLocale;
$domainLocale = array_search($host, $this->domainLocales);
if (!empty($domainLocale)) {
$locale = $domainLocale;
}
$request->setLocale($locale);
}
/**
* {@inheritdoc}
*/
static public function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
The onKernalRequest() function checks to see if the current hostname (domain name) is in a key-value array that I’ve defined in config.yml where the key is the locale and the value is the hostname. If the hostname cannot be found, it defaults to the default locale I’ve set in the config.yml. This is what the parameters section of my config.yml looks like where I’ve defined the possible locales, the default locale (called only “locale” in the configuration) and the “domain_locales” key-value array that I check in the event listener class above:
config.yml
parameters:
locale: en
app.locales: en|de
domain_locales:
en: www.example.com
de: www.beispiel.de
Once that is taken care of, we need to setup the configuration so that it also works locally in a development environment. One solution to this is the simply add the domains “www.example.com” and “www.beispiel.de” to your hosts file, but that means you will constantly have to edit the host file in order to switch between your local environment and your productive website. Symfony thankfully provides us with a file called config_dev.yml which overrides the parameters in config.yml when working with Symfony’s built-in server. That means you can create two new domains that will only work locally.
In order to do this, I’ve overwritten the “domain_locals” key-value array as follows:
config_dev.yml:
parameters:
domain_locales:
en: symfony.localhost.com
de: symfony.localhost.de
Of course, they need to be added to your hosts file as well so that your browser will know to look locally:
Hosts file
127.0.0.1 symfony.localhost.com
127.0.0.1 symfony.localhost.de
The last thing that needs to be done is to connect the event listener class in services.yml. You can use the following example, changing the location to match the actual location of your class:
services.yml
locale_listener:
class: YourBundle\Folder\LocaleListener
tags:
- { name: kernel.event_subscriber }
arguments: [@service_container,%locale%]
Now, we can start Symfony’s built-in server and browse to the English and German versions of the application:
English: http://symfony.localhost.com:8000
German: http://symfony.localhost.de:8000
Of course, we can’t forget to add port 8000 locally. Depending on your setup, adding the port will most likely be unnecessary when accessing your application on production.
Once the application has been deployed and the server is properly configured so that both domains point to the same application (one could be setup as an alias of the other, for example), you can access your application in German or in English or whatever other locale you’ve defined via the domains you defined in config.yml.
The benefit of this setup is that you can add as many locales and domains as needed by simply changing or adding to the “domain_locales” array so that the key matches Symfony’s locale while the value matches the domain.
Update for Symfony 3
Reader Joseph B. wrote a comment about how he made this code compatible with Symfony 3:
There were some differences from Symfony 2:
– “parameters” was already defined in my “config.yml” file so I just needed to add the “domain_locales” data in there.
– Rather than editing the “LocaleListener” class, I needed to add your changes into “LocaleSubscriber” instead.
– Services.yml’s syntax is slightly different; needed this instead of what was in the article:
AppBundle\EventSubscriber\LocaleSubscriber:
arguments: [‘%kernel.default_locale%’,’@service_container’]
tags: [kernel.event_subscriber]
Thank you, that helped me a lot! I just modified it a little adding an array of domains for each locale, instead of just one. I even added a check for locale routing parameters:
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
// the locale has already been set as a routing parameter
$routingLocale = $request->attributes->get('_locale');
if ($routingLocale) {
return;
}
// default locale
$locale = $this->defaultLocale;
// gets the current host
$host = $request->getHost();
// gets the locale from a list of domains
foreach ($this->domainLocales as $domainLocale => $domains) {
foreach ($domains as $domain) {
if (stripos($host, $domain) !== false) {
$locale = $domainLocale;
break 2;
}
}
}
$request->setLocale($locale);
}
I‘m glad it helped you! And thank you for posting the additions!
How would I adapt these steps for Symfony 3? I tried yours but it produced errors. For example, it didn’t like having $container in the __construct function and it can’t find ‘@service_container,%locale%’ when I add that to services.yml.
Never mind, I got it working. There were some differences from Symfony 2:
– “parameters” was already defined in my “config.yml” file so I just needed to add the “domain_locales” data in there.
– Rather than editing the “LocaleListener” class, I needed to add your changes into “LocaleSubscriber” instead.
– Services.yml’s syntax is slightly different; needed this instead of what was in the article:
AppBundle\EventSubscriber\LocaleSubscriber:
arguments: [‘%kernel.default_locale%’,’@service_container’]
tags: [kernel.event_subscriber]
I didn’t bother touching the dev config and my hosts file; I just added a condition in twig to detect whether the current domain is within domain_locales. If it’s not, then use the regular language switch; otherwise alternate between domains.
I’m very happy to hear you got it working. Fortunately it looks like there aren’t too many changes that are needed in order to make it work in Symfony 3. Thank you very much for posting them!