Littleweb, a small project in Symfony2 – Chapter 1: Translations

This posts starts a collection of little Symfony2 examples to create a typical website that we might be hired to create. Of course, most of this can be easily achieved with smaller frameworks like Silex, Kohana or even our own framework, but I think that this can be useful for most people struggling in the steep learning curve in Symfony2.

All this code is available in this Github repository.

This first chapter talks about localisation and translations in Symfony2.

To put this in context, let me say that I live in Barcelona, and here most people speak both Catalan and Spanish indistinctly. Political stuff apart (which is not the purpose of this blog), it is quite common that all companies here have their webpages translated in at least Catalan, Spanish and English.

Translations are solved absolutely briliant in Symfony2 and combining our routing definition with Twig command {% trans %} we can get our websites translated having no action in the controller.

Let’s see how we can do this:

1 – We will do our routing using annotations and translations will be introduced in YML format.

Let me just say that I love routing via annotations, it feels so natural and flexible. And more than that, routing and controllers are together in the same file, which helps development. Regarding translations, I like YML syntax as it is very clear. However, when working for a big multi-language website, with tons of text to translate to 13 languages, xliff format might be better as there are already applications to edit it inside a GUI and you could make one easy in PHP taking benefit of SimpleXML.

To achieve that, in our routing.yml

RicardclauLittlewebBundle:
    resource: "@RicardclauLittlewebBundle/Controller/"
    type:     annotation
    prefix:   /

And to enable translations, in our config.yml:

framework:
    translator:      { fallback: %locale% }

This enables translation, and makes the translation system fallback into language defined in parameters.ini if translation does not exist in the session language.

Translations are stored in src/Ricardclau/LittlewebBundle/Resources/translations, and files are named messages.{lang}.yml, being lang ca,es and en.

2 – We are defining languages accepted in littleweb.yml and loaded inside the Container

ricardclau_littleweb:
  langs: [ca, en, es]

And this gets loaded in the bundle with:

<?php
// src/Ricardclau/LittlewebBundle/DependencyInjection/Configuration.php
$rootNode
  ->children()
    ->arrayNode('langs')
      ->prototype('scalar')
    ->end()
  ->end()
->end();
 
// src/Ricardclau/LittlewebBundle/DependencyInjection/RicardclauLittlewebExtension.php
$container->setParameter('langs', $config['langs']);

3 – We will build 2 controllers: DefaultController and CompanyController.

DefaultController has got 2 methods: rootAction (which redirects user to its preferred language according to its browser configuration) and indexAction (which renders website homepage)

<?php
 
namespace Ricardclau\LittlewebBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 
class DefaultController extends Controller
{
    /**
     * Website root. Automatically redirect to preferred culture based on browser settings
     *
     * @Route("/", name="root")
     */
    public function rootAction()
    {
        $weblangs = $this->container->getParameter('langs');
        $lang = $this->getRequest()->getPreferredLanguage($weblangs);
 
        return $this->redirect(
                $this->generateUrl('index', array('_locale' => $lang))
                );
    }
 
    /**
     * Website Index. Culture must be set and be either es (Spanish), en (English) or ca (Catalan)
     *
     * @Route("/{_locale}", requirements={"_locale"="es|en|ca"}, name="index")
     * @Template()
     */
    public function indexAction()
    {
        return array();
    }
}

As you can see, most of the code is done through annotations, my good friend Christian Soronellas say thay maybe Symfony2 is introducing AOP (Annotation Oriented Programming) 🙂

CompanyController has 2 methods: companyAction and whoweareAction which render static web pages

<?php
namespace Ricardclau\LittlewebBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 
class CompanyController {
    /**
     * This controller renders "Company" Link. Routes are 100% translated
     *
     * @Route("/{_locale}/company", requirements={"_locale"="en"}, name="company_en")
     * @Route("/{_locale}/empresa", requirements={"_locale"="es"}, name="company_es")
     * @Route("/{_locale}/empresa", requirements={"_locale"="ca"}, name="company_ca")
     * @Template()
     */
    public function companyAction()
    {
        return array();
    }
 
    /**
     * This controller renders "Who we are" Link. Routes are 100% translated
     *
     * @Route("/{_locale}/whoweare", requirements={"_locale"="en"}, name="whoweare_en")
     * @Route("/{_locale}/quienessomos", requirements={"_locale"="es"}, name="whoweare_es")
     * @Route("/{_locale}/quisom", requirements={"_locale"="ca"}, name="whoweare_ca")
     * @Template()
     */
    public function whoweareAction()
    {
        return array();
    }
}

It can be seen that we can do almost everything through annotations, and no code at all.

@Route annotation defines the routes that our method resolves. Note that many routes can be defined, default parameters, restrictions, etc… making this system really flexible and powerful.

@Template annotation makes the framework render a Twig template inside a folder named as the controller, and the twig file named as the action. For instance in whoweareAction, Symfony2 looks for a template inside src/Ricardclau/LittlewebBundle/Resources/views/Company/whoweare.html.twig.

Isn’t that really cool?

4 – And finally, templates using Twig.

The skeleton is:

layout.html.twig that include menu.html.twig and footer.html.twig

Inside Default folder, there is index.html.twig, which extends layout
Inside Company folder, there are company.html.twig and whoweare.html.twig that also extend layout.

{# layout.html.twig #}
{% spaceless %}
<!DOCTYPE html>
<html lang="{{ app.request.get('_locale') }}">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta content="index,follow" name="robots"/>
        <meta content="{% block description %}{% trans %} head.description {% endtrans %}{% endblock %}" name="description"/>
        <meta content="{% block keywords %}{% trans %} head.keywords {% endtrans %}{% endblock %}" name="keywords"/>
        <meta content="7 days" name="revisit-after"/>
        <title>LittleWeb {% block title %}{% endblock %}</title>
    </head>
    <body>
        <header>
            {% include 'RicardclauLittlewebBundle::menu.html.twig' %}
        </header>
        <section>
            {% block content %}{% endblock %}
        </section>
        <footer>
            {% include 'RicardclauLittlewebBundle::footer.html.twig' %}
        </footer>
    </div>
    </body>
</html>
{% endspaceless %}
{# menu.html.twig #}
<nav>
    <ul>
    {# Small menu to access other languages #}
    {% for index, lang in {'ca':'Català', 'es':'Castellano', 'en':'English'} %}
       {% if index != app.request.get('_locale') %}
       <li><a href="{{ path('index', { '_locale': index }) }}">{{ lang }}</a></li>
       {% endif %}
    {% endfor %}
    </ul>
</nav>
<nav>
<ul>
    {# Website content #}
    <li><a href="{{ path('index', { '_locale': app.request.get('_locale')}) }}">{% trans %} menu.home {% endtrans %}</a></li>
    <li><a href="{{ path(['company_', app.request.get('_locale')]|join) }}">{% trans %} menu.company {% endtrans %}</a></li>
    <li><a href="{{ path(['whoweare_', app.request.get('_locale')]|join) }}">{% trans %} menu.whoweare {% endtrans %}</a></li>
</ul>
</nav>
{# footer.html.twig #}
<p>
Copyright 2011 <a href="http://www.ricardclau.com">www.ricardclau.com</a>
</p>

And the small layouts:

{# Default/footer.html.twig #}
{% extends 'RicardclauLittlewebBundle::layout.html.twig' %}
 
{% block content %}
{% trans %} sections.home {% endtrans %}
{% endblock %}
{# Company/company.html.twig #}
{% extends 'RicardclauLittlewebBundle::layout.html.twig' %}
 
{% block content %}
{% trans %} sections.company {% endtrans %}
{% endblock %}
{# Company/whoweare.html.twig #}
{% extends 'RicardclauLittlewebBundle::layout.html.twig' %}
 
{% block content %}
{% trans %} sections.whoweare {% endtrans %}
{% endblock %}

I guess the code is quite self-explanatory and not much further comments are needed.

Interesting things are the building of the URLs using path Twig function and _locale variable can be recovered using app.request.get(‘_locale’).
As you can see, translation messages are obtained with {{ trans }} path.to.text {{ endtrans }}

I am not a HTML5 expert so any improvements will be really welcome!!!!

And this is the end of the first chapter. As you have noticed I haven’t used any CSS, and this is because next chapter will cover the use of Assetic for CSS, JS and assets, so Stay tuned for more!!!

You may also like...

6 Responses

  1. cordoval says:

    very nice man, thanks!

  2. Ricard, congrats for this article! Interesting and simple to follow.

    You know if there is any way to make the Route requeriments for the languages dynamic, for example, if you have a website where you can add new languages from a backend?

    Thanks master!

  3. Ricard Clau says:

    Hi Javier, good to hear from you.

    I haven’t played much with such an issue, it can be really hard work if you want everything to be dynamic.

    Have a look at Doctrine Translatable extension for the database and for the routing, check BeSimpleI18nRoutingBundle (http://knpbundles.com/BeSimple/BeSimpleI18nRoutingBundle). It seems similar to what you are trying to acomplish.

    Regards!

  4. Hello Ricard!
    The Symfony BCN sessions yesterday remember me that subject. Thanks for the information. BeSimpleI18nRoutingBundle seems great, althougth is not compatible with your routing by annotation (its a shame). I think I’ll find an interesting solution using yml files.
    Regards!

  5. Aitor Suso says:

    I have just discovered that blog, and the translation routing system was incredible.

    We are working in our first project with symfony2 (we are basically migrating and redifinig our project from Symfony 1.1) and it is very flexible.

    As our project aims spanish and english people (maybe more in the future), we are always having problems with routing and translations, but it seems that it will solve a lot of problems for us.

    We will try to go to the next Symfony Barcelona group meeting

  6. Jan Machala says:

    AOP stands for Aspect-Oriented Programing. In fact you can use whole AOP without single annotation. Cheers.

Leave a Reply

Your email address will not be published. Required fields are marked *