Learn how to create a symfony 3 application that supports routing internationalization (locale user friendly URLs).

How to work with routing internationalization in Symfony 3 using the BeSimpleI18nRoutingBundle

Nowadays many companies need to provide support for client from around all the world. Pitifully, not everybody speaks the same language so that's a little limitation, but not for the market and neither for the developers. Creating internationalized and localized applications is never an easy task, so there are as well many tools that provide you with easy ways to add multilingual support for websites and applications. But, not many provide you with tools to internationalize the routes of your websites, so they are benefited from SEO as well. BeSimpleI18nRoutingBundle is an useful tool that helps you to avoid the copy and paste of your routes for different languages. Additionally it allows to translate given routing parameters between languages in Router#match and UrlGenerator#generate using either a Symfony Translator or a Doctrine DBAL (+Cache) based backend.

Although the explanation of this approach in this article may not fill every requirement on very complexes application (thing that the bundle does), it's pretty useful for people that only use Symfony to create websites or applications that doesn't require quite complexes routes definitions. However, don't forget that the bundle do support complex patterns for example with the translation of slugs using your own database so be sure to read the documentation of the library as well.

1. Install BeSimpleI18nRoutingBundle

Install the bundle with composer running the following command:

composer require besimple/i18n-routing-bundle

After the installation of the bundle you will be able to configure the routes. For more information about this bundle and documentation, please visit the official repository at Github here.

2. Configure default locales on your application

As second step, you need to define which languages will support the routing of your application. We will define 2 parameters in the config.yml file of your project with 2 names namely locale (usually locale is already defined) and locales that specifies which locales are supported by your application as you obviously won't redirect to a page with russian locale if you don't support this language. In this example, we'll provide support for the English, French, Spanish and German languages so define the parameters in your config.yml:

parameters:
    locale: en
    locales: en|fr|es|de

Besides you need to be sure that you give an answer to the question "What if the user's locale hasn't been determined?" by providing as default locale the same defined as the locale parameter (config.yml as well):

framework:
    # Set those properties with the locale parameter
    translator: { fallbacks: ['%locale%'] }
    default_locale: '%locale%'

With this configuration, our application will use as default locale the english language always if the users locale is not providen in the URL. Finally you need to specify the same locales but in an array format in the supported property of the be_simple_i18n_routing:

# app/config/config.yml
be_simple_i18n_routing:
    locales:
        supported: ['en', 'es', 'fr', 'de']
        filter: true
        strict: true

This bundle work as well in older versions of Symfony, so if you face the following exception after installing the bundle or in some point after the configuration:

[Symfony\Component\Config\Exception\FileLoaderLoadException]                                                                                                                    
    Warning: trim() expects parameter 1 to be string, array given in /app/config/routing.yml (which is being imported from "/app/config/routing_dev.yml").           

Please, modify your routing_dev.yml file and add the type parameter to the _main property:

_main:
    resource: routing.yml
    type: be_simple_i18n

This will fix your problem and you will be able to continue with the configuration of your routing.

3. Configure basic routing

As an expected behaviour, every URL will have as prefix the locale, even the default language of your application e.g English. However, if you try to access your application from the main domain e.g www.yourapp.com after configuring the basic routing, you will see an error that says that the / route is not defined. That happens because indeed not any route will be used to define the root address of your domain because we'll use the locale as prefix on every route. This would mean a problem for the user, as he or she would need to write the URL with the locale e.g www.yourapp.com/en which is obviously annoying, so the expected behaviour of the user is being redirected to its homepage with the default locale. That's why you will need to define the root parameter in your main routing file (app/config/routing.yml) that will use the urlRedirect action of the framework bundle to redirect the user to the mentioned URL using the locale parameters. Then create a route definer that in this case will be app_main and define the prefixes there (following the locale pattern). This definer will target another routing file that in this example is inside the AppBundle/Resources/config/routing/routing.yml (you can change the file path obviously if you want) and we'll create that file on the next step:

# app/config/routing.yml
# Entry point of your application that redirects the user to the main routing
# using a locale, that means If the user goes to:
# www.yourapp.com
# Is redirected automatically to:
# www.yourapp.com/en/
root:
    pattern: /
    defaults:
        _controller: FrameworkBundle:Redirect:urlRedirect
        path: /%locale%/
        permanent: true
    requirements:
        _locale: "%locales%"

# The main routing of your application whose content will be defined on
# the next step !
app_main:
    resource: "@AppBundle/Resources/config/routing/routing.yml"
    type: "be_simple_i18n"
    # Add the locale prefix to every URL
    # e.g Contact page English : /en/contact
    #     Homepage French      : /fr/
    #     Services page Spanish: /es/servicios
    #     Contact page German  : /de/kontakt
    prefix:
        en: "/en"
        fr: "/fr"
        es: "/es"
        de: "/de"

4. Create main routing

As mentioned in the previous step, the app_main definer targets another routing file that will handle the routes itself of your application. Our application will have only 3 routes, namely / (homepage), /services and /contact. They all will be naturally translated to the user languages so we can have an user friendly routing. The content to create the 3 routes so they can work on 3 different languages is the following:

# AppBundle/Resources/config/routing/routing.yml
main_homepage:
    locales:
        en: "/"
        fr: "/"
        es: "/"
        de: "/"
    defaults: { _controller: AppBundle:Default:homepage }
    methods:  [GET]
    requirements:
        _locale: "%locales%"

main_services:
    locales: 
        en: "/services"
        fr: "/services"
        es: "/servicios"
        de: "/dienstleistungen"
    defaults: { _controller: AppBundle:Default:contact }
    methods:  [GET]
    requirements:
        _locale: "%locales%"

main_contact:
    locales: 
        en: "/contact"
        fr: "/contact"
        es: "/contacto"
        de: "/kontakt"
    defaults: { _controller: AppBundle:Default:services }
    methods:  [GET]
    requirements:
        _locale: "%locales%"

As you can see, we define a different route pattern for every locale easily and readable. This is very nice even for the programmer that won't have headache reading another files to see the slug of every route etc. Note that we need to set the _locale requirement for every route as this will update as well the user locale in the session automatically.

5. Create Controller for Routes

As you saw in the previous step, we use the same controller for every route even with different locales. That's why this bundle is pretty awesome, you don't need to write necessarily logic for every language as you can work peacefully with the translator module or return a different view in the controller according to the locale. In this case, our controller is pretty simple and returns a view, there are totally 3 actions (the defined on the main routing file):

Note

We return the same view as we want to use the translation module as well to translate things on the view, however that doesn't belong to this tutorial.

<?php
// AppBundle/Controller/DefaultController.php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    public function homepageAction(Request $request)
    {
        return $this->render("default/homepage.html.twig");
    }

    public function servicesAction(Request $request)
    {
        return $this->render("default/services.html.twig");
    }

    public function contactAction(Request $request)
    {
        return $this->render("default/contact.html.twig");
    }
}

As mentioned, you can either filter in the controller using the locale to return a different file if you want:

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    public function homepageAction(Request $request)
    {
        // Switch locale and return a file according to your needs
        switch($request->getLocale()){
            case "es":
                return $this->render("default/homepage_spanish.html.twig");
            case "fr":
                return $this->render("default/homepage_french.html.twig");
            case "de":
                return $this->render("default/homepage_german.html.twig");
            case "en":
            default:
                return $this->render("default/homepage_english.html.twig");
        }
    }
}

However in our opinion implementing the symfony translator may be cleaner and more simple that maintaining multiple files.

6. Creating Redirect Links on Views

Till this point, your application (if has the mentioned controller and routes) should be able now to respond to routes like /en/homepage, /es/contacto, /de/dienstleistungen accesing them directly in the Browser Navigation Bar. However, the user needs to be able to navigate through your website clicking some links, so be sure to create them in the views. For example, some of our routes can be created using the default locale of the user:

{# /en/ #}
{{ path('main_homepage') }}

{# /en/services #}
{{ path('main_services') }}

{# /en/conctact #}
{{ path('main_contact') }}

But if you want to redirect to some special locale e.g See this view in other language, you can force the locale as well:

{#
    Redirect to the homepage in english
    /en/
#}
{{ path('main_homepage.en') }}
{{ path('main_homepage', { 'locale': 'en' }) }}

{#
    Redirect to the homepage in french
    /fr/
#}
{{ path('main_homepage.fr') }}
{{ path('main_homepage', { 'locale': 'fr' }) }}

{#
    Redirect to the homepage in German 
    /de/
#}
{{ path('main_homepage.de') }}
{{ path('main_homepage', { 'locale': 'de' }) }}

7. Clear the Project Cache using the cache:clear command before testing

As last step, super important is to clear the cache of your project using the command line. I'll repeat, is very, very important to clear the cache using the cache:clear command of Symfony, otherwise if you only remove the cache folder manually the locale won't be cleared and you may get frustrated. To make sure that everything works in order, please clear the cache of your project using the following command:

Pro tip

Opening an Incognito Window of Google Chrome after cleaning the cache manually is pretty useful to make test as the browsers starts from scratch and nothing is stored, so if there's a problem it isn't in the browser but your code.

php bin/console cache:clear

Remember as well that as we defined as requirement on any URL the special parameter _locale, the locale of the user will be automatically updated everytime he changes the URL locale e.g from /en (english) to /es (spanish), so you can easily create a language switcher that simply redirects to the main_homepage route with the desired locale and that's it, the user will be redirected to the homepage with the desired language sticked to the locale. For more information about this approach read more about the locale and the URL in the documentation of Symfony.

Happy coding !


Senior Software Engineer at Software Medico. Interested in programming since he was 14 years old, Carlos is a self-taught programmer and founder and author of most of the articles at Our Code World.

Sponsors