Translate Laminas\Validator\ $messageTemplates

Hi all,
it’s the first time I have to ask a question myself because I wasn’t able to google an answer and ChathGPT was no help either.

Please be nice to me, I am neither a developer nor a student. Chef I have learned and php I teach myself.

On topic: I’m dabbling in OOP with laminas, installed the skeletton application via composer on xampp, followed the getting started tutorials and wrote my own modules. Since my site will be multilingual I created my own language files as phparray and configured the translator in global.php.

 'translator' => [
        'translation_files' => [
            [
                'type' => 'phparray',
                'filename' => getcwd() .  '/data/language/de_DE.mo',
                'locale' => 'de_DE',
            ],
        ],
        'translation_file_patterns' => [
            [
                'type'     => 'phparray',
                'base_dir' => getcwd() .  '/data/language',
                'pattern'  => '%s.mo',
            ],
        ],
        'event_manager_enabled' => true,
    ],

also in the global.php

    'service_manager' => [
        'factories' => [
            \Laminas\I18n\Translator\Translator::class => \Laminas\Mvc\I18n\TranslatorFactory::class,
            'MvcTranslator' => \Laminas\I18n\Translator\Translator::class,
            \StdAuth\Validator\StdAuthIdentical::class => \StdAuth\Factory\StdAuthIdenticalFactory::class,
        ],
       ...
        'aliases' => [
            'translator' => 'MvcTranslator',
        ],
        ...
    ],

in the views I successfully access the content via $this->translator->translate(‘key’) so that the output is as desired.

I have the unit User which deffinates the input filters for a form. some of the filters use Laminas\Validator\ validators and in these there are protected $messageTemplates with specified messages for various errors.
to translate the error messages as well ChatGPT recommends to replace the used Laminas\Validator with an own validator which extends the Laminas\Validator

namespace StdAuth\Validator;

use Laminas\Mvc\I18n\Translator;
use Laminas\Validator\Identical;

class StdAuthIdentical extends Identical{
    private $translator;

    public function __construct(Translator $translator) {
        $this->translator = $translator;
        parent::__construct();
    }

    protected $messageTemplates = [
        self::NOT_SAME => "Die beiden eingegebenen Passwörter stimmen nicht überein.",
    ];

    public function isValid($value, $context = null)
    {
        if ($this->translator !== null) {
            $this->abstractOptions['messageTemplates'] = [
                self::NOT_SAME => $this->translator->translate($this->messageTemplates[self::NOT_SAME]),
            ];
        }

        return parent::isValid($value, $context);
    }
}

and the factory

namespace StdAuth\Factory;

use Laminas\ServiceManager\Factory\FactoryInterface;
use StdAuth\Validator\StdAuthIdentical;

class StdAuthIdenticalFactory implements FactoryInterface{
    //put your code here
    public function __invoke(\Psr\Container\ContainerInterface $container, $requestedName, array $options = null){
        $translator = $container->get('translator');
        return new StdAuthIdentical($translator);
    }
}

the inputfilter :

namespace StdAuth\Model;

use Laminas\InputFilter\InputFilterAwareInterface;
use StdAuth\Validator\StdAuthIdentical;

class User implements InputFilterAwareInterface{
...
    public function getInputFilter(){
        if ($this->inputFilter){
            return $this->inputFilter;
        }
        $inputFilter = new InputFilter();
    ...
    $inputFilter->add([
            'name' => 'pwrepeat',
            'required' => true,
            'filters' => [
                ['name' => StripTags::class],
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StdAuthIdentical::class,
                    'options' => [
                        'token' => 'passwort',
                    ],
                ],
            ],
        ]);

but it seems that my factory is not used at all and as an error I get

($translator) must be of type Laminas\Mvc\I18n\Translator, array given, called in C:\xampp\htdocs\laminas\skeleton\vendor\laminas\laminas-servicemanager\src\Factory\InvokableFactory.php on line 25

the same error occurs if i delete the entry for \StdAuth\Validator\StdAuthIdentical::class => \StdAuth\Factory\StdAuthIdenticalFactory::class in the global.php

any hints? thank you for your time

Hello and welcome to our forums! :wink:

Correct, because the components of Laminas does not use any magic and that means for the input filter that it is independent of the whole application:

$inputFilter = new InputFilter();

This input filter doesn’t know that a factory for a validator exists and should be used. Therefore, this approach is not recommended at all.
Don’t bind the input filter to the entity, but add it to the form:

Now you factory works because this input filter use your application.


But the entire translation can be done easier:

With this step you have also installed the component installer, which is an important component that assists you in installing a component like laminas-i18n because it registers the component in your application for you.

See the description in the tutorials:

Assuming you are using laminas-component-installer (which is installed by default with the skeleton application), this will prompt you to install the component as a module in your application; make sure you select either application.config.php or modules.config.php for the location.

Once installed, this component exposes several services, including:

  • MvcTranslator, which implements the laminas-i18n TranslatorInterface, as well as the version specific to laminas-validator, providing an instance that can be used for all application contexts.
  • A “translator aware” router.

By default, until you configure translations, installation has no practical effect. So the next step is creating translations to use in your application.

This means that the following can be removed as it is already present via the installed components:

And that is superfluous and even wrong for translations. You can use the existing translation files for validators. Install laminas/laminas-i18n-resources and add the pattern to your configuration:

Extend your file global.php:

return [
    'translator' => [
        // …
       'translation_file_patterns' => [
            [
                'type'     => 'phparray',
                'base_dir' => getcwd() .  '/data/language',
                'pattern'  => '%s.mo',
            ],
            [
                'type'     => Laminas\I18n\Translator\Loader\PhpArray::class,
                'base_dir' => Laminas\I18n\Translator\Resources::getBasePath(),
                'pattern'  => Laminas\I18n\Translator\Resources::getPatternForValidator(),
            ],
        ],
       // …
    ],
];

But again, make sure that the input filter is not independent of the application!

Hello froschdesign,

Ö.Ö

Vielen Dank, muchas gracias and thank you very much,
has really helped me. Not to deffine the inputFilter in the form seemed strange to me right away …
I am sure that I have overlooked the corresponding notes here ^^

so far I had set the html properties for the error output in the view.phtml manually

view.phtml
...
$alias = $form->get('alias');
...
<div class="form-group">
    <?= $this->formElement($alias) ?>
    <?= $this->formElementErrors()->render($alias, ['class' => 'alert alert-danger', 'style' => 'max-width: 30%;']) ?>
</div>

how to define the formElementErrors in the Form.php?

is there a way to get the output of each form element in the div with the class=“form-group” ?

i implemented the changes in global.php, composer reports Using version ^2.9 for laminas/laminas-i18n-resources. Cleaned the User entity from all filters, made sure no new InfutFilter() is called.
to test the translation i used the Laminas\Validator\Identical instead of my own, made sure the keys wrongInput, notSame and missingToken are present in my translation files and found that still no translation of the errormessage occurs…

Kind regards

Follow the application integration tutorials and you are on the right way.

The options for the view helper formElementErrors can be set for the entire application:

I often see that the formRow helper is overwritten.
You will get more support for Bootstrap with TwbsHelper:

Or this one:

How do you instantiate your form? Via new ExampleForm() ?

1 Like

In the indexAction of my Controller wich extends Laminas\Mvc\Controller\AbstractActionController

$form = $this->formElementManager->get(RegistryForm::class);

for test i did it also with $form = new RegistryForm();
no change in translating behavior at all.

My Controlers 3. argument for the public function __construct(…) is Laminas\Form\FormElementManager $formElementManager and i bind it to the class option public $formElementManager;

the translatable strings defined in the form like the labels are translatet just by type the keys of the translation files ‘label’ => ‘registryForm.alias’. the validation works fine. only the error messages wont get german. maybe they hate to be german strings …

I found a solution who works for me.
inside of a Controllers action method:

if (!$form->isValid()){
            $errors = $form->getMessages();
            $translator = $this->serviceManager->get('MvcTranslator');
            foreach ($errors as $key => $value) {
                $messages = $translator->translate($key);
                foreach ($value as $i18nKey => $unused){
                    if (isset($messages[$i18nKey])){
                        $errors[$key][$i18nKey] = $messages[$i18nKey];
                    }
                }
            }
            $form->setMessages($errors);
            return ['form' => $form];
        }

Thanks a lot for all the help :slight_smile:

This may work for you but is not a correct solution! It it looks like that the input filter is independ so can not fetch the translator.
It is definitely wrong that the service manager is present in the form.

The form is free of the ServiceManager.

use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Form\Form;
use ... all the Filter and Validator of the formfields

class RegistryForm extends Form implements InputFilterProviderInterface{
    public function init(): void {
        parent::init();
        $this->add([
            'name' => 'id',
            'type' => Element\Hidden::class,
        ]);
        ... #add some more formfields
        }
    public function getInputFilterSpecification(): array {
        return [
            [
                'name' => 'id',
                'required' => true,
                'filters' => [
                    ['name' => ToInt::class],
                ],
            ],
            ...add some more filters
            [
                'name' => 'pwrepeat',
                'required' => true,
                'filters' => [
                    ['name' => StripTags::class],
                    ['name' => StringTrim::class],
                ],
                'validators' => [
                    [
                        'name' => Identical::class,
                        'options' => [
                            'token' => 'passwort',
                        ],
                    ],
                ],
            ],
        ];
    }
}

in my Controller Class i receive the serviceManager as a parameter

use StdAuth\Model\UserTable;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
use StdAuth\Form\RegistryForm;
use StdAuth\Form\LoginForm;
use StdAuth\Form\ActivationForm;
use StdAuth\Model\User;
use StdAuth\Email\RegistryMail as Mail;
use Laminas\Form\FormElementManager;
#use StdAuth\Model\Helper\UniqueKey;

/**
 * Description of StdAuth
 *
 * @author Herdle
 */
class StdAuthController extends AbstractActionController{
    
    private $table;
    private $serviceManager;
    public $formElementManager;
    
    public function __construct(
            UserTable $table,
            \Laminas\ServiceManager\ServiceManager $serviceManager,
            FormElementManager $formElementManager
            ) {
        $this->table = $table;
        $this->serviceManager = $serviceManager;
        $this->formElementManager = $formElementManager;
    }
    public function indexAction() {
        $form = $this->formElementManager->get(RegistryForm::class);
        assert($form instanceof RegistryForm);
        $form->setData($request->getPost());
        if (!$form->isValid()){
            $this->translateErrorMessages($form);
            return ['form' => $form];
        }
    }

    private function translateErrorMessages($form){
        $errors = $form->getMessages();
        var_dump($errors);
        $translator = $this->serviceManager->get('MvcTranslator');
        foreach ($errors as $key => $value) {
            $messages = $translator->translate($key);
            foreach ($value as $i18nKey => $unused){
                if (isset($messages[$i18nKey])){
                    $errors[$key][$i18nKey] = $messages[$i18nKey];
                }
            }
        }
        $form->setMessages($errors);
    }

In the controller it is also wrong.

Ok, to use $serviceManager = $this->getServiceLocator(); should be omited in a controller because it does not explicitly specify the dependencies. i think my way to use the serviceMananger is not really different from this approach, which is why i should always pass the actual needed services to a class.

public function __construct(
            UserTable $table,
            \Laminas\Mvc\Translator $translator,
            FormElementManager $formElementManager
            ) {
        $this->table = $table;
        $this->translator = $translator;
        $this->formElementManager = $formElementManager;
    }

Since the only reason I even inject the translator is that the validator error messages are not translated if I only set the key in my language files…

return [
    'login' => 'anmelden',
    'user' => 'benutzer',
    'notSame' => 'Die beiden angegebenen Passwörter stimmen nicht überein',
    'missingToken' => 'Es wurden keine Eingabe zum Abgleich festgestellt',
]

I can basically do without it once the translation of the error messages succeeds without any further action ^^
To set the inputFilter directly in the form was a great change. The label translation work directly with the keys from my files and here is no need for a instance of the translator anymore

$this->add([
            'name' => 'cerial',
            'type' => Element\Text::class,
            'options' => [
                'label' => 'regForm.cerial', # 'label' => $translator->translate('regForm.cerial');
            ],
        ]);

any more idea´s how to get the translation done without setting them explicitly like i do it in my translateErrorMessages method?

    private function translateErrorMessages($form){
        $errors = $form->getMessages();
        var_dump($errors);
        $translator = $this->serviceManager->get('MvcTranslator');
        foreach ($errors as $key => $value) {
            $messages = $translator->translate($key);
            foreach ($value as $i18nKey => $unused){
                if (isset($messages[$i18nKey])){
                    $errors[$key][$i18nKey] = $messages[$i18nKey];
                }
            }
        }
        $form->setMessages($errors);
    }

$errors = $form->getMessages() gives a array with the fildname as key and the Validator´s error masages key value pairs
$messages = $translator->translate($key) gives a array with the ‘notSame’ and ‘missingToken’ key value pairs
for this i changed my language files

return [
  ....
    'pwrepeat' => [
        'notSame' => 'Die beiden angegebenen Passwörter stimmen nicht überein',
        'missingToken' => 'Es wurden keine Eingabe zum Abgleich festgestellt',
    ],
]

The controller, form or anything else should not know that there is something like a dependency container. Therefore, only the services that are actually necessary for execution should be injected.

The translator doesn’t know about your elements and doesn’t need to. In fact, only the message identifier is of interest.

Your form looks good.
Have you set the correct locale? For the translation resources you need something like Locale::setDefault('de').

Here my test.

modules.config.php:

return [
    'Laminas\Mvc\I18n',
    'Laminas\I18n',
    'Laminas\Mvc\Plugin\FilePrg',
    'Laminas\Mvc\Plugin\FlashMessenger',
    'Laminas\Mvc\Plugin\Identity',
    'Laminas\Mvc\Plugin\Prg',
    'Laminas\Session',
    'Laminas\Form',
    'Laminas\Hydrator',
    'Laminas\InputFilter',
    'Laminas\Filter',
    'Laminas\Router',
    'Laminas\Validator',
    'Application',
];

global.php:

return [
    // …
    'translator'                 => [
        'translation_file_patterns' => [
            [
                'type'     => Laminas\I18n\Translator\Loader\PhpArray::class,
                'base_dir' => Laminas\I18n\Translator\Resources::getBasePath(),
                'pattern'  => Laminas\I18n\Translator\Resources::getPatternForValidator(
                ),
            ],
        ],
    ],
    // …
];

RegistryForm.php:

namespace Application\Form;

use Laminas\Filter\StringTrim;
use Laminas\Filter\StripTags;
use Laminas\Form\Element\Password;
use Laminas\Form\Element\Submit;
use Laminas\Form\Form;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Validator\Identical;

final class RegistryForm extends Form implements InputFilterProviderInterface
{
    public function init(): void
    {
        $this->add(
            [
                'name' => 'password',
                'type' => Password::class,
            ]
        );
        $this->add(
            [
                'name' => 'pwrepeat',
                'type' => Password::class,
            ]
        );
        $this->add(
            [
                'name' => 'send',
                'type' => Submit::class,
            ]
        );
    }

    public function getInputFilterSpecification(): array
    {
        return [
            [
                'name'       => 'pwrepeat',
                'required'   => true,
                'filters'    => [
                    ['name' => StripTags::class],
                    ['name' => StringTrim::class],
                ],
                'validators' => [
                    [
                        'name'    => Identical::class,
                        'options' => [
                            'token' => 'password',
                        ],
                    ],
                ],
            ],
        ];
    }
}

IndexController.php:

namespace Application\Controller;

use Application\Form\RegistryForm;
use Laminas\Form\FormElementManager;
use Laminas\Mvc\Controller\AbstractActionController;

/**
 * @method \Laminas\Http\Request getRequest()
 * @method \Laminas\Http\Response getResponse()
 */
class IndexController extends AbstractActionController
{
    public function __construct(readonly FormElementManager $formElementManager)
    {
    }

    public function indexAction()
    {
        \Locale::setDefault('de');

        /** @var RegistryForm $form */
        $form = $this->formElementManager->get(RegistryForm::class);
        $form->setData($this->getRequest()->getPost());
        $form->isValid();
        
        var_dump($form->getMessages()); // array(1) { ["pwrepeat"]=> array(1) { ["isEmpty"]=> string(30) "Es wird eine Eingabe benötigt" } }

        return [
            'form' => $form,
        ];
    }
}

And it works.

Also with custom messages:

global.php:

'translator'                 => [
    // …
    'translation_file_patterns' => [
        [
            'type'     => Laminas\I18n\Translator\Loader\PhpArray::class,
            'base_dir' => Laminas\I18n\Translator\Resources::getBasePath(),
            'pattern'  => Laminas\I18n\Translator\Resources::getPatternForValidator(
            ),
        ],
    ],
    'translation_files'         => [
        [
            'type'     => Laminas\I18n\Translator\Loader\PhpArray::class,
            'filename' => __DIR__ . '/../../data/languages/de/misc.php',
            'locale'   => 'de',
        ],
    ],
    // …
],

misc.php:

return [
    'notSame'      => 'Die beiden angegebenen Passwörter stimmen nicht überein',
    'missingToken' => 'Es wurden keine Eingabe zum Abgleich festgestellt',
];
array(1) { ["pwrepeat"]=> array(1) { ["notSame"]=> string(49) "Die zwei angegebenen Token stimmen nicht überein" } }

Used \Locale::setDefault('de'); in my controller.

Changed 'filename' => getcwd() . '/data/language/de_DE.mo', to filename' => getcwd() . '/data/language/de.mo
'locale' => 'de_DE', to 'locale' => 'de',
Renamed my translation files de_DE.php to de.php.
Errormessages are translated and my problem → just don’t check how the locale from ext/intl’s locale class looks like …

Thank you !!!

a var_dump(\Locale::getDefault()); gives me de_DE.
why did the validator error messages get just translated if i set \Locale::setDefault('de'); and change the filenames?

in my global.php i have

'translator' => [
        'translation_file_patterns' => [
            [
                'type'     => 'phparray',
                'base_dir' => getcwd() .  '/data/language',
                'pattern'  => '%s.mo',
            ],
            [
                'type'     => \Laminas\I18n\Translator\Loader\PhpArray::class,
                'base_dir' => \Laminas\I18n\Translator\Resources::getBasePath(),
                'pattern'  => \Laminas\I18n\Translator\Resources::getPatternForValidator(),
            ],
        ],
    ],

if i do not set the Locale to de and my translationfiles have de_DE, the strings in the form are translated but not the error messages of the validators.
if i set the locale to de and and my files have no prefix, the strings and the error messages are translated.

Locale is set to de, files have no sufix.
if i have just this part present in my translation_file_patterns section

            [
                'type'     => \Laminas\I18n\Translator\Loader\PhpArray::class,
                'base_dir' => \Laminas\I18n\Translator\Resources::getBasePath(),
                'pattern'  => \Laminas\I18n\Translator\Resources::getPatternForValidator(),
            ],

the formfields are not translated (it outputs the keys from translationfiles), the errormessges are translated.
if i have just this declaration

            [
                'type'     => 'phparray',
                'base_dir' => getcwd() .  '/data/language',
                'pattern'  => '%s.mo',
            ],

the formfields are translated, the error messages are not

Nothing special, because it is simply not supported. See in the related bugtracker; for example:

@LordSauron

You can set a fallback locale for the translator which then can handle de_DE and de.