Needs review
Project:
Drupal core
Version:
main
Component:
locale.module
Priority:
Normal
Category:
Bug report
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
20 Oct 2026 at 13:43 UTC
Updated:
2 Nov 2026 at 13:26 UTC
Jump to comment: Most recent
Comments
Comment #3
gábor hojtsyComment #4
gábor hojtsyComment #5
gábor hojtsyComment #6
gábor hojtsySimplify table to make it easier to see where is wrong :)
Comment #7
gábor hojtsyComment #8
d34dman commented@gábor hojtsy,
One thought on what we test against. The MR makes JS follow PHP, but I think both should follow GNU gettext.
A Plural-Forms formula is written for gettext's evaluator: a C-like expression with C precedence, evaluated on an unsigned integer n. Whoever wrote the formula, whether a translation team, the gettext manual or a 3rd party reference list, expected ngettext() behaviour. So for each row in the summary table, the question is "what would ngettext() return?" and not "what does PHP do today?". There is an arguably better ux here for both being equally wrong, but lets do better.
That reference settles several rows on its own:
- Fractional counts are truncated (1.5 counts as 1).
- With no formula, the default is n != 1.
- Javanese (n != 0) with n = 1 gives variant 1, so JS was right there and the old $count == 1 shortcut in PHP was wrong.
I ran into this while writing string_plural_form. It ships 26 plural formulas as plugins, and I checked each one against gettext, for example the Breton test: https://git.drupalcode.org/project/string_plural_form/-/blob/1.x/tests/U...
I compared all 26 formulas against gettext for core and for this MR. Details follow in the next comment (AI Assisted research).
Comment #9
d34dman commentedLLM disclosure
Generated with the help of an LLM
Investigation: core plural selection against GNU gettext
Method. I took the 26 plural formulas from string_plural_form 1.x. Each one went into a .po header, was compiled with
msgfmt, and was evaluated with libintl's owndngettext()(GNU gettext-runtime 1.0). That is gettext's real evaluator, not a port. I used every count from 0 to 1199 plus large values (1000, 10000, 100000, 1000000, 2000000, … up to 109). I compared the results with three implementations:mainin PHP:TranslationManager::selectPluralForm()with locale'sgetPluralIndex(), using the formula table fromPoHeader::parsePluralForms()mainin JS:Drupal.formatPlural()As a control, string_plural_form's hand-ported
getIndex()matches gettext for all 26 formulas and every count tested.Core JS today is wrong above 199 for 15 of the 26 formulas, because it uses the
defaultentry. Example: Russian 201 gives 2 instead of 0. The MR fixes this.Core PHP today is wrong at n = 1 for Arabic, Welsh, Cornish and Kölsch, where index 0 is the "zero" form and index 1 is "one". The
$count == 1shortcut returns 0. The MR fixes this, because it drops the shortcut.With the MR applied, 4 formulas still disagree with gettext. All of them come from the
100 + n % 100fallback above 199:… : ((n != 0 && n % 1000000 == 0) ? 3 : 4)(n == 1) ? 0 : ((n != 0 && n % 1000000 == 0) ? 1 : 2)(n == 0 || n == 1) ? 0 : ((n != 0 && n % 1000000 == 0) ? 1 : 2)… n % 1000 == 0 && (n % 100000 >= 1000 && n % 100000 <= 20000 || …) …The comment in
PoHeader::parsePluralForms()says "All known plural formula's are repetitive above 100". That holds for the classic formulas, but not for the CLDR-derived three-form formulas that have a separate "many" form for millions. A site using one of those formulas shows the wrong variant for "1 000 000 items", in both PHP and JS.Suggestions