Skip to content

Latest commit

 

History

History
131 lines (103 loc) · 5.82 KB

3_23.md

File metadata and controls

131 lines (103 loc) · 5.82 KB

3.23 Pluralized messages

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.

Which message is pluralized by which option

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.

Custom constraints

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.

When no form can be chosen

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.

Replacing the factory

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.