Extending Laravel's Translator (IoC): Lang setter image

Extending Laravel's Translator (IoC): Lang setter

 

It may be the case that you need to set translations dynamically, so it will be necessary to replace the Translator with one that adds this functionality. To achieve this, we will extend the service provider and bind our vitamined version to the IoC container.

First we will create our Translator class. We will repress the urge to reinvent the wheel (because it is frowned upon, today we are lazy and I escape from a possible squared), so we will extend the Laravel translator:

namespace Acme\Extensions\Translation;

use Illuminate\Translation\Translator as LaravelTranslator;

class Translator extends LaravelTranslator
{
    /**
     * Set translation.
     *
     * @param  string  $key
     * @param  mixed   $value
     * @param  string  $locale
     * @return void
     */
    public function set($key, $value, $locale = null)
    {
        list($namespace, $group, $item) = $this->parseKey($key);

        if(null === $locale)
        {
            $locale = $this->locale;
        }

        // Load given group defaults if exists
        $this->load($namespace, $group, $locale);

        array_set($this->loaded[$namespace][$group][$locale], $item, $value);
    }

    /**
     * Set multiple translations.
     *
     * @param  array   $items   Format: [group => [key => value]]
     * @param  string  $locale
     * @return void
     */
    public function add(array $items, $locale = null)
    {
        if(null === $locale)
        {
            $locale = $this->locale;
        }

        foreach($items as $group => $translations)
        {
            // Build key to parse
            $key = $group.'.'.key($translations);

            list($namespace, $group) = $this->parseKey($key);

            // Load given group defaults if exists
            $this->load($namespace, $group, $locale);

            foreach($translations as $item => $value)
            {
                array_set($this->loaded[$namespace][$group][$locale], $item, $value);
            }
        }
    }
}

Now, we are going to create a new Translator service provider which, again, extend the provider offered by Laravel. This is where we bind our Translator class to the Container, replacing the Laravel one's:

namespace Acme\Providers;

use Illuminate\Translation\TranslationServiceProvider;

class Translator extends TranslationServiceProvider
{
    public function boot()
    {
        $this->app->bindShared('translator', function($app)
        {
            $loader = $app['translation.loader'];
            $locale = $app['config']['app.locale'];

            $trans = new \Acme\Extensions\Translation\Translator($loader, $locale);

            $trans->setFallback($app['config']['app.fallback_locale']);

            return $trans;
        });

        parent::boot();
    }
}

Finally, we will tell our application which provider have to use:

// app/config/app.php
'providers' => array(
    ...
    //'Illuminate\Translation\TranslationServiceProvider',
    'Acme\Providers\Translator',
    ...
);

That's all. We take our wheel and...

// Set/replace single translation
Lang::set('group.string', 'translation');

// Set/replace multiple translations
Lang::add([
    'group' => [
        'january' => 'Enero',
        'february' => 'Febrero',
        ...
        'week.monday' => 'Lunes',
        'week.tuesday' => 'Martes',
        ...
    ],
]);

In both cases we can use dot notation.


It should be clarified that, while this procedure is proposed in the official documentation, given that the boot() method of the service provider is used to bind/replace the class, it's possible that another portion of the application tries to access the instance before that method has been executed, getting, in this case, an instance of the Laravel Translator class.

For example, the boot() method from another service provider makes a call to our Translator before it has been "booted". Probably this is more the exception than the rule, but in any case, I propose a couple of solutions:

  1. I suppose the most obvious is to stop using the boot() method in favor of the register(), as it should be. We can imagine why not, but since we have this problem, we can make the change and be blessed at the same time (I don't take responsibility for the heat that you can suffer in the future).

  2. Probably the best option is to register a booted listener and put there the rebel code; that will be executed after the application has booted.

    App::booted(function($app) {});

    See: Illuminate\Foundation\Application::booted().

Last, note that registering the provider manually, as shown below, does not imply the immediate execution of the boot() method.

App::register('Acme\Providers\Translator');

See: Illuminate\Foundation\Application::register().

Documentation:

 

comments powered by Disqus