Translations and Internationalisation in FOSUserBundle


In this video we are going to look at Translations inside FOSUserBundle, and cover a potential workaround to a 'gotcha' that might affect you if you use Translations out of the box.

FOSUserBundle translations, or internationalisation, works just as though you were doing translations yourself in your own project. There is nothing different happening just because this is FOSUserBundle.

If Translations are new to you, I have covered them before in the Symfony Translations course here on CodeReviewVideos. I would also highly recommend reading the Translations chapter of the Symfony documentation.

As long as you followed the FOSUserBundle installation step to enable Translations, you should be all set to start using the available translations in your project:

# app/config/config.yml

framework:
    translator: ~

If you haven't already been wowed by how awesome FOSUserBundle is, consider this:

Even if you could spend the time to implement exactly the same feature set, with the same level of edge case bug fixes, modularity, and reusability that FOSUserBundle provides - for free - out of the box, it is highly unlikely that you would also have the time to provide 39 different language translations for your code.

These translations apply to the whole bundle. From the 'Log In' link text, through to the Welcome email that new users receive upon registation, even down to the Password Reset email, and validation error messages.

Personally, I find this amazing. That the community can provide such immediate value to my projects is astounding. That I can do Star Trek level universal translator goodness with barely no effort is world class. The Symfony community, nay, the Open Source community as a whole is fantastic. Thank you!

Getting Started with FOSUserBundle Translations

Getting started is very straightforward. Remember to enable the translator component (as above) if you haven't done so already.

If we are happy with the translations that come pre-configured with FOSUserBundle then good news, there is very little else we need to do at this point, aside from telling Symfony to start paying attention to the locale.

To get Symfony interested in our current locale, the easiest way is to add the locale as a part of our current URL, e.g.:

http://mysite.com/fr/

Which wouldn't really do much, unless we also update our routing with the special _locale parameter:

# app/config/routing.yml
fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"
    prefix:   /{_locale}

some_other_routes:
    resource: "@YourBundle/Controller/"
    type:     annotation
    prefix:   /{_locale}

As soon as you have done this, and assuming you have been following along with the course so far, browsing to e.g.:

http://mysite.com/de/

This will show the FOSUserBundle text in the language passed in via the locale. You are able to use any of the 39 different locales configured in FOSUserBundle and see the text automatically translate to the requested language.

To clarify, this will only translate the FOSUserBundle text, not all the text on your site. At least, not unless you have provided a translation file and configured your text to be translatable!

Firewall Issues

However, we have created a problem for ourselves.

FOSUserBundle's firewall entry will not appreciate having the locale added to the URL in this way. We must add in a little extra config to our firewall as the URL structure will no longer be using the default values.

That is to say, the login form will no longer be accessible at /login, but rather at /{_locale}/login, and the same for the other URL's that are part of the login / logout process.

We need to make the changes in our security.yml file:

# app/config/security.yml
security:

    # ...

    firewalls:
        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: security.csrf.token_manager 

                # NEW BITS
                # the login form will POST here on submit
                # the default value would be /login_check
                check_path: fos_user_security_check

                # the user is redirected here when they need to log in
                # the default would be /login
                login_path: fos_user_security_login

                # We will fix this shortly, but for now, the default
                # would be '/', but we would need it to be e.g. 
                # /en/
                default_target_path: some_route_name_you_have

            logout:       
                # default is /logout
                path: fos_user_security_logout
                # default is /
                target: some_route_name_you_have
            anonymous:    true

The gist of this change is that routes like /logout are no longer going to work.

We have set up our configuration to only make available e.g. /en/logout or /ru/logout. If we don't override these values then our login flow is going to break.

Note, for each path, we have simply passed in a route name. All the route names beginning with fos_user_security_ are being pulled out of the fos_user resource we configured in our routing.yml file:

# app/config/routing.yml
fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

If you dig in to that file, specifically following the link to the security.xml file (@FOSUserBundle/Resources/config/routing/security.xml), you will find the three route names we have used:

<!-- vendor/friendsofsymfony/user-bundle/Resources/config/routing/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="fos_user_security_login" path="/login" methods="GET POST">
        <default key="_controller">FOSUserBundle:Security:login</default>
    </route>

    <route id="fos_user_security_check" path="/login_check" methods="POST">
        <default key="_controller">FOSUserBundle:Security:check</default>
    </route>

    <route id="fos_user_security_logout" path="/logout" methods="GET">
        <default key="_controller">FOSUserBundle:Security:logout</default>
    </route>

</routes>

And the default_target_path and logout.target we provided is any route name that exists in your project that you want your User to be sent to on successful login / logout, if a more specific route isn't specified.

Fixing This Problem

In solving one problem: determining the current locale via the URL, we have created ourselves another: changing our URL structure.

This might not be so bad, but adding /en/ or /de/ or whatever to every route is not great. Assuming we are in the UK or other English speaking country, we could make the assumption that our default user base is going to want our site in English. If we make this assumption then we could also say that by default / and /en/ are the same. If that is the case, we could remove /en/ from the URL, and when not specified, default to English.

Of course, feel free to change the default to meet your own site needs.

Unfortunately, fixing this is going to require the addition of another Bundle to our project. Whilst this isn't the end of the world, it never the less involves adding more dependencies to our project.

The bundle I would recommend for this is the JMS I18N Routing Bundle.

In the video I opt for prefixing all my routes with the locale, except those of the default locale. This is described in the documentation under scenario 2.

If you do implement this bundle, remember to remove the prefix section from your routing.yml file, as this the JMSI18nRoutingBundle is going to handle that part for us. If you don't, you will end up with a confusing error situation.

This will also remove the default locale route entirely for us. That is to say, if our default locale was en, then the route /en/ would not be available. Instead, only /, would work for en, but /de/ or /fr/ or other configured routes would still need to have the locale given.

Overriding FOSUserBundle's Translations

If you do wish to override or customise the provided translations then we can do that easily enough.

Much like how we have customised the FOSUserBundle templates, we can also do the same for the translations.

The translations for FOSUserBundle live in individual files - 2 files per locale.

One file is for the site text, email messages, and so on.

The other file is for the validation error messages.

For the English translations, these would be at:

vendor/friendsofsymfony/user-bundle/Resources/translations/FOSUserBundle.en.yml
vendor/friendsofsymfony/user-bundle/Resources/translations/validators.en.yml

To override these, copy any / all of the files that you wish to customise to /app/Resources/FOSUserBundle/translations, e.g.:

app/Resources/FOSUserBundle/translations/FOSUserBundle.en.yml
app/Resources/FOSUserBundle/translations/validators.en.yml

For Symfony 4, the pathing is simpler:

translations/FOSUserBundle.en.yml
translations/validators.en.yml

Make sure to clear your cache after doing this: php app/console cache:clear, otherwise your customisations very likely won't display.

Any translation files that you do not copy over will fall back to using the ones in the vendor directory.

Code For This Course

Get the code for this course.

Episodes