Thursday, December 11, 2014

Symfony site localization based on domain name

Problem: I want to localize my site based on the domain name.

Symfony strongly suggests that you use paths (like '/en' or '/fr') after the domain name to determine what the locale should be. This is ideal for a site with only one domain name, but for a site that has a different one for each localization, it's unnecessary. You should be able to determine the language based on the domain.

Solution: Use an event listener.

With an event listener, you can catch the request, parse the domain name, and set the locale appropriately. For this blog's purposes, let's say that a site has www.endomain.com for its English site and www.frdomain for its French site.

Create this folder/file in your bundle: EventListener/LocaleListener.php. Inside, put this:

namespace My\CustomBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;


class LocaleListener
{
  public function setLocale(GetResponseEvent $event)
  {
    if (strstr(strtolower($_SERVER['HTTP_HOST']), strtolower('frdomain')))
    {
      $request = $event->getRequest();
      $request->setLocale('fr');
    }

  }
}

If the HTTP_HOST contains the string 'frdomain', then set the locale to 'fr'.

Now register the listener in your bundle's services.yml file (it should be in Resources/config/). Inside, put this:

services:
    my_custom.language.kernel_request_listener:
        class: My\CustomBundle\EventListener\LocaleListener
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: setLocale }

Now every time a request is sent to Symfony, this listener runs first and sets the locale to 'fr' if it detects 'frdomain' in HTTP_HOST. Otherwise, it keeps the default locale (in this case, it's en).

That should work! Happy coding!

Monday, January 13, 2014

Symfony, internalization/localization, and 404 pages

Background

Given 1) the Symfony framework and 2) the need for localization, one shouldn't be surprised that there's a package that takes care of that already (somewhat): JMSI18nRoutingBundle. It works so that in your config.yml, you can specify the default locale, the locales that your site works with, and what the domain names are for each locale. I guess I'm not using this bundle correctly, though, because although my config.yml file looks like this:

jms_i18n_routing:
  default_locale: %locale%
  locales: [en, fr]
  strategy: custom
  hosts:
    en: www.englishVersion.com
    fr: www.frenchVersion.com
  redirect_to_host: true

typing in the French locale into the address bar redirects to the English site. (And if you happen to have an inkling of what I'm doing wrong, do let me know!)

This necessitates the extra steps of 1) creating a separate /fr path that serves up French content and 2) configuring .htaccess to redirect www.frenchVersion.com to that path.

The Problem

Given that 1) www.frenchVersion.com originally serves up English content and 2) needs coaxing in .htaccess to redirect, I hit the problem of English 404 pages appearing under the French domain. Ie., www.frenchVersion.com/path-does-not-exist will serve up the English 404 message.

The Solutions

The halfway solution - detect the language in the template

In my custom 404 page (in project_root\app\Resources\TwigBundle\views\Exception\error404.html.twig), I included an if-statement that checked the domain name and then set the variable lang appropriately.

{% set lang = ('frenchVersion' in app.request.getHost()) ? 'fr' : 'en' %}

{{ 'projname.error404.copy' | trans({}, "messages", lang) }}

This solution only worked halfway, though. While the 404 message was in French, the surrounding layout.html.twig was still in English.

Full solution - create a listener for any exceptions thrown

With the guidance of this Stackoverflow answer, I was able to check for the domain name before anything was rendered when an exception is thrown. This is a two-file (or two-part) solution. First, you create a LanguageListener in projectroot\src\GenericName\SpecificNameBundle\EventListener. Code it like:


namespace GenericName\SpecificNameBundle\EventListener;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;


class LanguageListener
{
  public function setLocale(GetResponseEvent $event)
  {
    if (strstr($_SERVER['HTTP_HOST'], 'frenchVersion'))
    {
      $request = $event->getRequest();
      $request->setLocale('fr');
    }
  }
}

And then in projectroot\src\GenericName\SpecificNameBundle\Resources\config\services.yml:

services:
  # ...
  genericname.language.kernel_request_listener:
    class: GenericName\SpecificNameBundle\EventListener\LanguageListener
    tags:
      - { name: kernel.event_listener, event: kernel.exception, method: setLocale }

And there you go! Even your layout should be rendered using the correct locale. Go try it out! Happy coding!