fix: pluralize only the messages Symfony pluralizes - #22
Merged
Merged
Conversation
The naming convention that covers a constraint of an application's own -- a "minMessage" is chosen by a numeric "min" -- was also applied to Symfony's own constraints, which the table in the factory already covers in full. It matched options that count nothing: Range::$min sits beside a minMessage Symfony never pluralizes, and so do Image::$minRatio and Count::$divisibleBy. Nothing broke while those translations held no "|", but one that did was cut at a separator that was never a plural one, and a Ukrainian Range(min: 1) lost everything after the first pipe. Answer Symfony's constraints from the table and from nothing else. A constraint Symfony pluralizes in a later release needs a row before it is chosen from, and keeps every form until it gets one, which is where the library stood before the table existed. translateMessage() takes its two arguments again. The class of the factory is a documented extension point, "svaroh_js_form_validator.factory.class", and a factory of an application's own that overrides the method with the signature it has always had would have died on a fatal error. The plural forms move to translatePluralMessage(), which falls back through translateMessage(), so an override still sees every message that is not pluralized and every pluralized one whose form could not be chosen. A message no form could be chosen for is now reported to the application logger, optional and at debug level. It reaches the browser carrying its "|" separators either way, and nothing else said which entry of the catalogue was unfinished. The table gets the tests it was missing. Every row of it is load-bearing now that the convention no longer stands behind it, and File's filenameTooLongMessage, chosen by filenameMaxLength, is one the convention could never have reproduced. The two tests that read Symfony's own English wording bring a catalogue of their own instead, so a rewording upstream cannot fail them for a reason that has nothing to do with choosing a form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that keeps the naming convention off Symfony's own constraints looked at the class itself, while the table beside it is matched with "instanceof". A constraint of an application's own that extends one of Symfony's fell between the two: it took the table's rows, correctly, and then took the convention for every message the table does not name. An "AppRange extends Range" was still choosing a form for its minMessage by Range::$min, and an "AppCount extends Count" for its divisibleByMessage by Count::$divisibleBy, which is the same cut at a separator that was never a plural one. Look at the whole ancestry instead: such a constraint inherits Symfony's options and is validated by Symfony's validator, which calls setPlural() for the table's rows and nothing else. Hand the count to translateMessage() as the "%count%" parameter it has always been, rather than reaching past it to the translator. The method is a documented extension point, and routing the plural path through it costs nothing -- the call it made was the call translateMessage() makes -- while an override now sees every message rather than only the ones that carry no plural forms. Report a message no form could be chosen for as a warning rather than a debug note. That message is what the form actually shows, separators and all, so it is a defect in the rendering and not a diagnostic detail. Require psr/log ^3.0. The wider range promised more than is tested: the logger the tests collect records with cannot be declared against psr/log 1, whose LoggerInterface::log() leaves $message untyped, so narrowing it in a subclass is a fatal error. Share one validator between the factory and the form in the tests, as the container does, and give a pluralized message its own test for an overridden translateMessage(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to 39493ab, which chose the plural form of a constraint message by the
locale. A review of it turned up one behaviour bug, one BC break, and a gap in
the tests. This fixes all three, plus what a review of this branch turned up.
The convention reached constraints Symfony does not pluralize
getPluralCount()fell back to a naming convention — aminMessageis chosen bya numeric
min— for anything absent fromPLURAL_COUNT_OPTIONS. That fallbackis meant for a constraint of an application's own, but it also answered for
Symfony's, whose pluralized messages the table already covers in full:
Range(min: 5, max: 10)minMessage/maxMessageCount(divisibleBy: 3)divisibleByMessageImage(minRatio: 1.5)minRatioMessageImage(minWidth: 100)minWidthMessageHarmless while those translations carry no
|, since a single-part message comesback unchanged. One that does carry a
|is cut at it:Symfony's constraints are now answered from the table and from nothing else.
Across all of Symfony's own
validators.*.xlfcatalogues a|appears inexactly 11 message ids, and the table has a row for each, so nothing of
Symfony's is lost by this. A constraint pluralized in a later Symfony release
needs a row before it is chosen from, and keeps every form until it gets one —
the behaviour the library had before the table existed, which is the safe side
of the trade.
A constraint that extends one of Symfony's is answered the same way. The
table is matched with
instanceof, soAppCount extends Countalready tookCount's rows; the guard beside it looked at the class itself, so the same
constraint then took the convention for every message the table does not name.
AppRange extends Rangewas still choosing a form for itsminMessagebyRange::$min. Such a constraint inherits Symfony's options and is validated bySymfony's validator, which calls
setPlural()for the table's rows and nothingelse, so the whole ancestry is looked at now.
translateMessage()takes two arguments againsvaroh_js_form_validator.factory.classis the documented way to replace thefactory, and
translateMessage($message, ?array $parameters = null)predates allof this. A third parameter on it means PHP fatals on any subclass that overrides
the method with the signature it has always had — not a deprecation, a
Declaration ... must be compatibleat class-declaration time. 39493ab is not inany tag yet, so nothing shipped broken and only a
dev-maincheckout could havehit it; this keeps the break from reaching a release.
The plural path moved to
translatePluralMessage(), which decides the count andthen hands the message to
translateMessage()with that count as the%count%parameter — the call it was making directly is the call
translateMessage()makes. An override therefore sees every message: pluralized, not pluralized,
and the ones no form could be chosen for. Three tests cover it, and declaring
the test's
LegacyFactorywould itself be a fatal error if the signature everslipped again.
A message no form could be chosen for is reported
The
catch (\InvalidArgumentException)was silent. Swallowing it is right — anunfinished translation should not take a form down — but the only trace left was
a message still showing its
|separators in the browser. The factory now logsit through an optional PSR-3 logger, naming the catalogue entry to go and finish.
It is a warning, not a debug note: that message is what the form actually
shows, which is a defect in the rendering rather than a diagnostic detail.
psr/logjoinsrequireat^3.0; it was already present transitively throughsymfony/http-kernel. The range is deliberately narrow — the logger the testscollect records with cannot be declared against psr/log 1, whose
LoggerInterface::log()leaves$messageuntyped, so narrowing it in a subclassis a fatal error.
Tests for the table
Every row of
PLURAL_COUNT_OPTIONSis load-bearing now that the convention nolonger stands behind it, and one row always was:
filenameTooLongMessageischosen by
filenameMaxLength, which no convention could derive. It had no test.Neither did
Choice,WordCount, orCount::exactMessage. They do now, alongwith a constraint extending
Rangeand one extendingCount— the latter bothkeeping the table's rows and losing the convention for the rest.
The helper the tests parse a constraint through also had to learn that a
Fileconstraint is exported as a plain option list rather than as itself, which is why
a
Filetest could not have been written against it before. It now shares onevalidator between the factory and the form, as the container does.
The two tests that asserted on Symfony's own English wording bring a catalogue of
their own now, so a rewording upstream cannot fail them for a reason that has
nothing to do with choosing a form.
Verified locally on PHP 8.5
composer test: 95 tests, 268 assertions, green (81 / 250 before).composer phpstan: no errors.composer validate --strict: valid.setLoggercall, resolving tologger.narrowing it back to the class itself fails exactly the two new subclass cases;
reaching past
translateMessage()again fails exactly the new override test.Each test locks its fix rather than the code.
Not changed
prepareMessage()in the browser still splits on|, because the fallback stillneeds it. Its two-form rule is now reachable only through a translation with too
few forms, which
3_23.mdsays.🤖 Generated with Claude Code