Several constraints carry a message that changes with the number it speaks
about. Symfony writes every form of such a message into one string, separated
by |:
// Symfony\Component\Validator\Constraints\Count
public $minMessage = 'This collection should contain {{ limit }} element or more.'
. '|This collection should contain {{ limit }} elements or more.';How many forms there are, and which one a given number takes, belongs to the locale. English has two. Ukrainian has three: one form for 1, one for 2 to 4, and one for 5 and above. Arabic has six.
The form is chosen while the message is translated, where the locale is known
and the limit is already part of the constraint, so the browser receives the
one form it has to show. Count(min: 5) on a Ukrainian page produces:
Ця колекція повинна містити {{ limit }} елементів чи більше.
and not the form for 2 to 4, which is what a rule of "one for 1, one for everything else" would give.
These mirror the setPlural() calls in Symfony's own validators, and they are
the whole of it: a message of Symfony's own that is not in this table keeps
every form it came with.
| Constraint | Message | Chosen by |
|---|---|---|
Choice |
minMessage |
min |
Choice |
maxMessage |
max |
Count |
minMessage |
min |
Count |
maxMessage |
max |
Count |
exactMessage |
min |
Length |
minMessage |
min |
Length |
maxMessage |
max |
Length |
exactMessage |
min |
File |
filenameTooLongMessage |
filenameMaxLength |
WordCount |
minMessage |
min |
WordCount |
maxMessage |
max |
Count and Length reach for exactMessage only when min and max are
equal, which is what exactly sets, so choosing its form by min is the same
as choosing it by max. Where the two differ the message is never shown, by
the browser or by the server.
WordCount is in the table for the sake of the messages the server renders;
the browser has no validator for that constraint yet, so nothing of it is
checked before the form is submitted.
A message of Symfony's own that the table does not name is left alone even when
an option beside it looks like a count. Range has a min next to its
minMessage and Image a minRatio next to its minRatioMessage, but neither
constraint pluralizes anything, and reading them as counts would cut a
translation at a | that was never a plural separator. A constraint Symfony
pluralizes in a later release needs a row here before it is chosen from; until
then its message keeps every form, which is where this started.
The same holds for a constraint of your own that extends one of Symfony's. It
inherits Symfony's options and is validated by Symfony's validator, which calls
setPlural() for the rows above and for nothing else, so the rows above are all
it is pluralized by. Extend Constraint directly to get the naming convention
below.
Symfony's constraints name the option a message is pluralized by after the
message itself, and a constraint of your own that keeps the convention is
covered without being registered anywhere. A minMessage is chosen by a numeric
min option, a maxMessage by max:
<?php
// src/Validator/Constraints/MinimumTags.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class MinimumTags extends Constraint
{
public $min = 5;
public $minMessage = 'Add at least {{ limit }} tag.|Add at least {{ limit }} tags.';
}A message whose name does not end in Message, or whose matching option is
absent or not a number, is translated without a count, exactly as before. A
limit that is not a whole number is truncated towards zero, the same cast
Symfony's own setPlural(int $number) applies to it.
A translation can offer fewer forms than the locale needs — two forms of a
Ukrainian message, say, where the number lands on the third. Nothing can be
chosen then, so the whole message is handed to the browser and
prepareMessage() falls back to its own two-form split. That keeps a
half-finished translation from taking the form down; the message is only as
wrong as it was before.
A message that reaches the browser still carrying its | separators is the only
sign of this, so the factory also reports it to the application logger as a
warning — that message is what the form actually shows — naming the entry of the
catalogue that needs finishing:
Could not choose a plural form for the message "This collection should contain
{{ limit }} element or more.|...", handing every form to the browser: Unable to
choose a translation for "один елемент|багато елементів" with locale "uk" for
value "5".
The logger is optional. Without one the message still reaches the browser whole — it simply goes unreported.
translateMessage() takes the message and its parameters, as it always has. A
message that carries plural forms goes through translatePluralMessage() first,
which decides the count and then hands the message to translateMessage() with
that count as the %count% parameter. A factory of your own that overrides
translateMessage() therefore sees every message, pluralized or not, including
the ones no form could be chosen for. Override translatePluralMessage() as
well to take over the choice of the count itself.