In a current project I needed a way to replace the wording of selected form labels and messages for certain users. Symfony's translation catalogues seemed the right place to start with as I could simply add different catalogues for those users and use the translation_domain argument in forms.

Problem: A lot of redundancy and administration effort plus this wouldn't work with validators which are configured to use their own catalogue. The latter can be easily solved by combining the messages and validators catalogues, configure validation to use the default by adding the following to app/config/parameters.yml:

parameters:
    validator.translation_domain: messages

and finally change the form errors block in Twig. Now for the fun part. When switching the translation domain from messages to something else I'd have to copy the whole catalogue which would also always be loaded in full. What I needed was a fallback translation domain so I could add a catalogue that only contains those labels that differ and use the default catalogue messages for everything else. After taking a look at Symfony\Component\Translation\Translator I decided to change the trans() method to use the default translation domain messages if a key wasn't found in the provided domain. The framework bundle implements its own translator class which extends the components' so I had to extend the frameworks' in turn:

<?php

namespace My\Bundle\Translation;

use Symfony\Bundle\FrameworkBundle\Translation\Translator;

class FallbackTranslator extends Translator
{
    /**
     * {@inheritdoc}
     *
     * @api
     */
    public function trans($id, array $parameters = array(), $domain = null, $locale = null)
    {
        if (null === $locale) {
            $locale = $this->getLocale();
        }

        if (null === $domain) {
            $domain = 'messages';
        }

        if (!isset($this->catalogues[$locale])) {
            $this->loadCatalogue($locale);
        }

        // Change translation domain to 'messages' if a translation can't be found in the
        // current domain
        if ('messages' !== $domain && false === $this->catalogues[$locale]->has((string) $id, $domain)) {
            $domain = 'messages';
        }

        return strtr($this->catalogues[$locale]->get((string) $id, $domain), $parameters);
    }
}

Thanks to Symfony's DI container it's easy to configure the framework to use the changed class:

<?xml version="1.0" encoding="UTF-8"?>

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

    <parameters>
        <parameter key="translator.class">My\Bundle\Translation\FallbackTranslator</parameter>
        [...]
    </parameters>
</container>

That's it. To make this solution more universal one could make this new behavior read the fallback domain configurable.

comments powered by Disqus