How to translate constraint messages of a Symfony 5 form (including non-mapped fields)

In Symfony 5 is quite easy to translate labels and placeholders inside FormTypes as you only need to type as the value of the property that you want to translate, the translation key defined in the translation files of the translations directory of your project.

According to the documentation of Symfony, there are multiple ways to translate stuff on your forms and I will provide you with a short overview in this article of how to make them work on every common use case.

For entities and mapped fields

The official Symfony documentation is quite specific on the translation of constraints for your forms. Considering that you have an Entity in your project, for example:

<?php
// app/src/Entity/User.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * User
 *
 * @ORM\Table(name="user")
 * @ORM\Entity
 */
class User implements UserInterface
{
    // ... ///

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="bigint", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string|null
     *
     * @ORM\Column(name="first_name", type="string", length=255, nullable=true, options={"default"="NULL"})
     */
    private $firstName;

    /**
     * @var string|null
     *
     * @ORM\Column(name="last_name", type="string", length=255, nullable=true, options={"default"="NULL"})
     */
    private $lastName;

    // ... ///
}

As you can see, in this short example we have 2 properties, firstName, and lastName. If we want to add some constraints to those fields, we would simply add them in the form type like this (in our case, the first name cannot be an empty field and the last name must be at least 6 characters long):

<?php

// app/src/Form/MyCustomFormType.php
namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\NotBlank;

class MyCustomFormType extends AbstractType
{   
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstName', TextType::class, [
                'label' => 'forms.user.firstName',
                'constraints' => [
                    // By default for NotBlank constraints in english, the message is: This value should not be blank.
                    // The message will be replaced though with our custom text in translations/validators.en_US.yaml
                    new NotBlank(),
                ]
            ])
            ->add('lastName', TextType::class, [
                'label' => 'forms.user.lastName',
                'constraints' => [
                    // By defau.lt, for the Length constraints in english, the message is: This value is too short. It should have {{ limit }} characters or more.
                    // The message will be replaced though with our custom text in translations/validators.en_US.yaml
                    new Length([
                        'min' => 6,
                        'max' => 64,
                    ]),
                ]
            ])
        ;
    }
}

In the form type, as you can see, there's no message specified with a string. If we don't customize the constraint message text, the default ones will appear. To customize them, we would simply create the validation.yaml file in your config/validator directory. This YAML file will define for every entity that you want under the properties key, the custom message for every constraint of every property of your entity. The message key contains the translation key in the validator of the validators.{locale}.yaml file in the translations directory of your project:

# config/validator/validation.yaml
App\Entity\User:
    properties:
        firstName:
            - NotBlank: { message: 'forms.user.firstName' }
        lastName:
            - Length: { message: 'forms.user.lastName' }

So, in our translation file for the validators, our messages are the following ones:

# app/translations/validators.en_US.yaml
forms:
    user:
        firstName: You need to provide your name.
        lastName: Your last name is way too short.

And that's it! Clear the cache of your project using php bin/console cache:clear and when the user submits the form with a constraint failing, the custom error messages would appear.

FormTypes without entities and non-mapped fields

The default approach of Symfony works pretty well with entities or fields that are mapped. However, what about the constraints of non-mapped fields of a self-built form? How are we supposed to translate them if they don't belong at all to an entity? In this case, you simply need to place the translation in the validators domain:

# app/translations/validators.en_US.yaml
forms:
    constraints:
        agreeTerms: You need to agree our terms and conditions

And provide the key in the message property of every constraint:

<?php

// app/src/Form/MyOwnFormType.php
namespace App\Form;

//  Default use
// ... 
//

class MyOwnFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // ... //
        $builder
            ->add('agreeTerms', CheckboxType::class, [
                'label' => 'forms.user.agreeTerms',
                // Note that this is a non-mapped field
                'mapped' => false,
                'constraints' => [
                    new IsTrue([
                        'message' => 'forms.constraints.agreeTerms',
                    ]),
                ],
            ])
        ;
    }
}

And that's it! It's way simpler than the approach with entities and mapped fields right?

Translating constraints with text from other translation domains

Is standard to store the translation of validations in the /app/translations directory with files under the validators domain (validators.{locale}.yaml) as that's where the translation of constraints will look for by default, however, if your translation is located in another domain for unknown reasons, like the messages one, you'll need to follow an extra step in your form type. Be sure to import the translator interface in your form type and inject it into the constructor to access it later in your class. Basically what you will do is to use the translator to translate any key that is defined in your translation files as you would do in any controller:

<?php

// app/src/Form/MyOwnFormType.php
namespace App\Form;

//  Default use
// ... 
//

// 1. Import the TranslatorInterface class
use Symfony\Contracts\Translation\TranslatorInterface;

class MyOwnFormType extends AbstractType
{
    // 2. Declare a locally accesible variable
    public $translator;
    
    // 3. Autowire the translator interface and update the local value with the injected one
    public function __construct(TranslatorInterface $translator)
    {
        $this->translator = $translator;
    }
    
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // ... //
        $builder
            ->add('agreeTerms', CheckboxType::class, [
                // Note that non-constraint labels are updated normally from the translation files
                'label' => 'forms.user.agreeTerms',
                'mapped' => false,
                // 4. However texts on constraints won't work as always, use the translator tool to translate them instead!
                'constraints' => [
                    new IsTrue([
                        // The trans method receives as first parameter the translation key, as second an array of parameters if there's any, and as third the domain name
                        // In our case, the translation is located in the messages.{locale}.yaml files, so the domain is messages
                        'message' => $this->translator->trans('forms.user.constraints.agreeTerms', [], "messages"),
                    ]),
                ],
            ])
        ;
    }
}

So, considering that the text is instead, located in the messages file:

# app/translations/messages.en_US.yaml

# Note that this structure is personal, you may use a different one
# and it will work anyway!
forms:
    myCustomLabels:
        constraints:
            agreeTerms: You should agree to our terms.

When the user submits the form and one constraint fails, the translation should be used without any issue:

Translate Constraint of non-mapped fields in Symfony

Happy coding ❤️!

This could interest you

Become a more social person