You already know how to create a simple CLI script. The CLI scripts are initialized with a different area – basically, with a separate CLI area\aplication, which lacks the standard frontend\admin localizations functionality. We faced that when developing a script for sending order emails via CLI – the emails were missing translations of the origin of the order. Let’s try to figure out “why?” and “how to fix?”.

The reason of the issue

If you try to check the bin/magento file, you will see that the application is represented by the Magento\Framework\Console\Cli class, while the index.php (or pub/index.php) is using Magento\Framework\App\Bootstrap and Magento\Framework\App\Http:

$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $params);
/** @var \Magento\Framework\App\Http $app */
$app = $bootstrap->createApplication('Magento\Framework\App\Http');

Magento Magento\Framework\App\Http is searching for a proper router for the provided action via “launch” method by calling \Magento\Framework\App\FrontController::dispatch method:

$frontController = $this->_objectManager->get('Magento\Framework\App\FrontControllerInterface');
$result = $frontController->dispatch($this->_request);
    /**
     * Perform action and generate response
     *
     * @param RequestInterface $request
     * @return ResponseInterface|\Magento\Framework\Controller\ResultInterface
     * @throws \LogicException
     */
    public function dispatch(RequestInterface $request)
    {
        \Magento\Framework\Profiler::start('routers_match');
        $routingCycleCounter = 0;
        $result = null;
        while (!$request->isDispatched() && $routingCycleCounter++ _routerList as $router) {
                try {
                    $actionInstance = $router->match($request);
                    if ($actionInstance) {
                        $request->setDispatched(true);
                        $this->response->setNoCacheHeaders();
                        if ($actionInstance instanceof \Magento\Framework\App\Action\AbstractAction) {
                            $result = $actionInstance->dispatch($request);
                        } else {
                            $result = $actionInstance->execute();
                        }
                        break;
                    }
                } catch (\Magento\Framework\Exception\NotFoundException $e) {
                    $request->initForward();
                    $request->setActionName('noroute');
                    $request->setDispatched(false);
                    break;
                }
            }
        }
        \Magento\Framework\Profiler::stop('routers_match');
        if ($routingCycleCounter > 100) {
            throw new \LogicException('Front controller reached 100 router match iterations');
        }
        return $result;
    }

Finally, any router is inherited from Magento\Framework\App\Action\AbstractAction. Still, you won’t find any “initTranslations” there. The source of the translations call is in the app/code/Magento/Store/etc/di.xml:

<type name="\Magento\Framework\App\Action\AbstractAction">
    <plugin name="storeCheck" type="Magento\Store\App\Action\Plugin\StoreCheck" sortOrder="10"/>
    <plugin name="designLoader" type="Magento\Framework\App\Action\Plugin\Design" sortOrder="30"/>
</type>

So, the Magento\Framework\App\Action\Plugin\Design plugin is basically called with every router execution. It is calling \Magento\Framework\View\DesignLoader::load, which in its part finally calls

$area->load(\Magento\Framework\App\Area::PART_TRANSLATE);

which is equal to \Magento\Framework\App\Area::_initTranslate method call:

    protected function _initTranslate()
    {
        $this->_translator->loadData(null, false);

        \Magento\Framework\Phrase::setRenderer(
            $this->_objectManager->get('Magento\Framework\Phrase\RendererInterface')
        );

        return $this;
    }

How to fix?

That’s how the Magento translations system is initiated within a proper application part. Pretty tricky, huh? But all what we need to do, in order to make translations work in our shell script (or other area without a translation initialization) is to make a static call like this:

Phrase::setRenderer(\Magento\Framework\Phrase\RendererInterface());

If we check the script from the original post, it will look like this:

<?php
/**
 * @author Atwix Team
 * @copyright Copyright (c) 2017 Atwix (https://www.atwix.com/)
 * @package Atwix_Shell
 * 
 * path: app/code/Atwix/Shell/Console/Command/TestCommand.php
 */

namespace Atwix\Shell\Console\Command;

use Magento\Framework\Phrase;
use Magento\Framework\Phrase\RendererInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Class TestCommand
 */
class TestCommand extends Command
{
    /**
     * Phrase renderer
     *
     * @var RendererInterface
     */
    protected $phraseRenderer;

    public function __construct(RendererInterface $phraseRenderer, $name = null)
    {
        parent::__construct($name)

        $this->phraseRenderer = $phraseRenderer;
    }


    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName('atwix:test')->setDescription('Test console command');
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        Phrase::setRenderer($this->phraseRenderer);
        $output->writeln("Hello world!");
    }
}

That’s all! Thanks for reading and learning!