We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Volt custom function for translate adapter

Hi, I'm trying to create basic multi-lingual project. After studing documentation and github sample, I found very cumbersome to pass and especially use \Phalcon\Translate\Adapter\NativeArray object in view even though it's only two extra symbols in volt. Just feels wrong to me. Here is what I've come up with:

// index.volt
{{ _("main") }}
//services.php
$di->set('translate', function() {
    $language = "uk";

    if (file_exists("../app/messages/".$language.".php")) {
        require "../app/messages/".$language.".php";
    } else {
        require "../app/messages/en.php";
    }

    return new \Phalcon\Translate\Adapter\NativeArray(array(
        "content" => $messages
    ));
});

$volt->getCompiler()->addFunction('_', function($translateKey, $placeholders=null) use ($di) {
  $t = $di->get('translate');
  $str = $t->_($translateKey, $placeholders);
  return $str;
});

I've spent some time debugging this peace and can't understand what's going on. $tranlateKey value in debuger is main', with a single quote at the end and placeholders not null even though as you can see nothiing was passed from the view. In browser I always get "main" without quotes. First thought that's a default behaviour if key not found in array, so I added "main'" entry to array - noting changes. Also I tried explicitly call $t method _ without placeholders parameter - no luck.

Why this code is brocken?



18.5k
Accepted
answer
edited Mar '14

Look at https://docs.phalcon.io/en/latest/reference/volt.html#id3 .

You function "_" doesn't recieves exact same variable as you passing them in to view. View sends php string concatenation.. so, look at example

In Volt ====> In PHP/Function args ($resolvedArgs value), this is string

$variable, [1,2,3] ====> "$variable, array(1, 2 ,3)"

'string', 'other' ====> "'string', 'other'"



7.2k
edited Mar '14

Missed that. Thank you. fixed function:

$volt->getCompiler()->addFunction('_', function($resolvedArgs, $exprArgs) use ($t) {
    return "'".$t->_($exprArgs[0]['expr']['value'])."'";
});

P.S. Fatal error: Call to a member function increaseKarma() on a non-object in /usr/share/nginx/html/app/controllers/DiscussionsController.php on line 503

This forum is still under development =)



7.2k
edited Mar '14

Epic mistake. After adding routing

$router->add("/([a-z]{2})/:controller/:action/:params", array(
  "lang" => 1,
  "controller" => 2,
  "action" => 3,
  "params" => 4,
));

I realized I can't reach dispatcher from services.php. Although I could parse uri but moving code into controller feels more appropriate. So

class ControllerBase extends \Phalcon\Mvc\Controller
{
    public function initialize() {
        $language = $this->dispatcher->getParam("lang");

        if (file_exists("../app/messages/".$language.".php")) {
            require "../app/messages/".$language.".php";
        } else {
            require "../app/messages/en.php";
        }

        $t = new \Phalcon\Translate\Adapter\NativeArray(array(
            "content" => $messages
        ));

        $volt = new \Phalcon\Mvc\View\Engine\Volt($this->view, $this->di);
        $volt->getCompiler()->addFunction('_', function($resolvedArgs, $exprArgs) use ($t) {
            return "'".$t->_($exprArgs[0]['expr']['value'])."'";
        });
    }
}

class IndexController extends ControllerBase
{
    public function initialize() {
        parent::initialize();
    }

    public function indexAction() {

    }
}

gives PhalconException: Undefined function '_' in C:\xampp\htdocs\lib\app\config/../../app/views/templates/base.volt on line 50.

Looks like phalcon compiles templates before exec controller. What can I do with it?

edited Mar '14

You are not setting $volt into DI back in this code.

 $volt = new \Phalcon\Mvc\View\Engine\Volt($this->view, $this->di);
        $volt->getCompiler()->addFunction('_', function($resolvedArgs, $exprArgs) use ($t) {
            return "'".$t->_($exprArgs[0]['expr']['value'])."'";
        });

Althrough - it's bad behaviour... better use some kind of bootstrap for you application... In that bootstrap you will have DI... and from DI you can get request or dispatcher ($di->get('request') / $di->get('dispatcher')) ... Meanwhile... you can get dispatcher from DI in you services.php

If you don't want to recompile volt every time you can use this approach:

$di->get('volt')->getCompiler()->addFunction('_', function ($resolvedArgs, $exprArgs) use ($di) {
  return sprintf('$this->translate->query(\'%s\')', $exprArgs[0]['expr']['value']);
});


7.2k
edited Mar '14

Puting back, if I understand and done it correctly:

        $volt = new \Phalcon\Mvc\View\Engine\Volt($this->view, $this->di);
        $volt->getCompiler()->addFunction('_', function($resolvedArgs, $exprArgs) use ($t) {
            return "'".$t->_($exprArgs[0]['expr']['value'])."'";
        });
        $this->view->registerEngines(array(
            ".volt" => function($view, $di) use($volt) {
                return $volt;
            }
        ));

doesn't work - same error.

Neat hack from Tomasz, works like a charm. Here is the final code:

// service.php
$di->set('translate', function() use($di) {
    $dispatcher = $di->get('dispatcher');
    $language = $dispatcher->getParam("lang");

    if (file_exists("../app/messages/".$language.".php")) {
        require "../app/messages/".$language.".php";
    } else {
        require "../app/messages/en.php";
    }

    return new \Phalcon\Translate\Adapter\NativeArray(array(
        "content" => $messages
    ));
});

$di->set('view', function () use ($config) {

    $view = new View();

    $view->setViewsDir($config->application->viewsDir);

    $view->registerEngines(array(
        '.volt' => function ($view, $di) use ($config) {

            $volt = new VoltEngine($view, $di);

            $volt->setOptions(array(
                'compiledPath' => $config->application->cacheDir,
                'compiledSeparator' => '_',
                'compileAlways' => true
            ));

            $volt->getCompiler()->addFunction('_', function ($resolvedArgs, $exprArgs) use ($di) {
                return sprintf('$this->translate->query(\'%s\')', $exprArgs[0]['expr']['value']);
            });

            return $volt;
        }
    ));

    return $view;
}, true);

//ControllerBase.php
class ControllerBase extends \Phalcon\Mvc\Controller
{
    public function initialize() {
        $uri_lang = $this->dispatcher->getParam("lang");

        if (!isset($uri_lang)) {
            $this->session->destroy();
            if ($this->session->has("lang")) {
                $lang = $this->session->get("lang");
            } else {
                $lang = substr($this->request->getBestLanguage(), 0, 2);
                switch ($lang) {
                    case "uk": break;
                    case "en": break;
                    default: $lang = "en";
                }
                $this->session->set("lang", $lang);
            }

            $this->response->redirect("/$lang");

        } else {
            if ($this->session->has("lang") && $this->session->get("lang") != $uri_lang) {
                $this->session->set("lang", $uri_lang);
            }
        }
    }
}

//IndexController.php
class IndexController extends ControllerBase
{
    public function initialize() {
        parent::initialize();
    }

    public function indexAction() {
    }
}

Yea, this is right, but set translation as Shared... to avoid translation object creation each time you request it...

by-the-way I think using extended view helper (Phalcon/Tag) is more comfortable:

add a function like this to your custom Tag helper:

static function _($string, array $params = []) {
        return DI::getDefault()->getShared('t9n')->_($string, $params);
}

then use it like below:

// simple usage
{{ tag._('signup') }}

// mised usage
// 'hello_name' => 'Hello %name%'
{{ tag._('hellow_name', ['name' : 'aboozar']) }}

What about simply leaving \Phalcon\Translate\Adapter\NativeArray in the view and creating a shorthand when registering the volt service:

// service.php
$di->set('view', function () use ($config) {

    $view = new View();

    $view->setViewsDir($config->application->viewsDir);

    $view->registerEngines(array(
        '.volt' => function ($view, $di) use ($config) {

            $volt = new VoltEngine($view, $di);

            $volt->setOptions(array(
                'compiledPath' => $config->application->cacheDir,
                'compiledSeparator' => '_',
                'compileAlways' => true
            ));

            // Shorthand here:
            $volt->getCompiler()->addFunction('_', function($resolvedArgs) {
                return '$t->_(' . $resolvedArgs . ')';
            });

            return $volt;
        }
    ));

    return $view;
}, true);

//ControllerBase.php
class ControllerBase extends \Phalcon\Mvc\Controller
{
    public function initialize()
    {
        $this->view->t = $this->getTranslation();
    }

    protected function getTranslation()
    {
        // Ask browser what is the best language
        $language = $this->request->getBestLanguage();

        // Check if we have a translation file for that lang
        if (file_exists(APP_PATH . 'app/my_module/' . $this->router->getControllerName() . '/' . $language . '.php'))
            require APP_PATH . 'app/my_module/' . $this->router->getControllerName() . '/' . $language . '.php';
        else
            require APP_PATH . 'app/my_module/' . $this->router->getControllerName() . '/en-US.php';

        // Return a translation object
        return new \Phalcon\Translate\Adapter\NativeArray(
            array(
                "content" => $translations
            )
        );
    }
}

//IndexController.php
class IndexController extends ControllerBase
{
    public function initialize() {
        parent::initialize();
    }

    public function indexAction() {
    }
}

As a result you would now use {{ _("Your string") }} and still be able to perform different operations in your controller before translating, for instance redirecting the user to the appropriate URL, checking for user settings in the db....

edited Oct '16

The final code by @fedrch works perfectly to print translated variables in the view. I have a question about it:

How can I print translations that include a variable, eg:

'numbervalidation-field' => '%field% is a number',

'Teams' => 'Teams',

If I add php directly into volt, it works this way and prints: 'Teams is a number' correctly.

<?php echo $this->translate->("numbervalidation-field", array("field" => $this->translate->('Teams'))); ?>

But I haven't been able to print this in Volt... below I've pasted my last attempt which prints '%field% is a number':

{{('numbervalidation-field', ['field': ('Teams')])}}

The final code by @fedrch works perfectly to print translated variables in the view. I have a question about it:

How can I print translations that include a variable, eg:

'numbervalidation-field' => '%field% is a number',

'Teams' => 'Teams',

If I add php directly into volt, it works this way and prints: 'Teams is a number' correctly.

<?php echo $this->translate->("numbervalidation-field", array("field" => $this->translate->('Teams'))); ?>

But I haven't been able to print this in Volt... below I've pasted my last attempt which prints '%field% is a number':

{{('numbervalidation-field', ['field': ('Teams')])}}

You can do it like

{{ _("numbervalidation-field", ['firstname': ("Teams") ]) }}

And a simpler example with only a (not translated) variable $firstName as a placeholder in a translation:

{{ _("numbervalidation-field"", ['first_name': firstName ]) }}

Hi @Alexander Bohndorf, I've tried the first option you posted but it didn't work. I'm not sure where you get "firstname" from, but I'm assuming you meant "field", both ways it doesn't work. The not translated variable is not an option, I need everything translated. And single translations already work, are the ones with variables the ones giving me a hard time in volt. Thanks for trying