Problem/Motivation

Plural formula handling in PluralTranslatableMarkup::render() function ignores the Plural Formula Rule and hardcode the case for $count == 1 to use first index.

https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Co...

As you can see from the latest code, It is ignoring the getPluralIndex for case where count is 1.

     $index = $this->getPluralIndex();
    if ($this->count == 1 || $index == 0 || count($translated_array) == 1) {
      // Singular form.
      $return = $translated_array[0];
    }

This leads to a problem where Plural Form Rules for languages where case for "count = 1" uses any other index but 0, leads to wrong Plural Form shown to user.

Example,

I was testing Plural Formula for Arabic, where singular form has index 1.

"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n

Steps to reproduce

- On Standard Drupal installation enable locale module
- Add and enable "Arabic" language
- Upload a Arabic PO file with translation for "@count items", this way you can set the rules for Arabic.
- Update Plural Translation for "1 comment" by visiting Translation edit for "/admin/config/regional/translate" to something as follows,

Notice that the labels provided for translators are already wrong here, since the first item is pointing to form for "zero" and second item points to "Singular" form.
- Run following code to display translation for singular form in Arabic,

ddev drush eval "print_r(\Drupal::translation()->formatPlural(1, '1 comment', '@count comments', array(), array('langcode' => 'ar')) . PHP_EOL);"

Result
1 - zero
Expected Result
1 - one

Proposed resolution

TBD

Remaining tasks

TBD

User interface changes

TBD

Introduced terminology

TBD

API changes

TBD

Data model changes

TBD

Release notes snippet

TBD

Issue fork drupal-3496223

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

d34dman created an issue. See original summary.

d34dman’s picture

If anybody shed light on why the conditional check for $this->count == 1 was included, it would be super helpful.

ghost of drupal past’s picture

The logic change was introduced in #532512-38: Plural string storage is broken, editing UI is missing, likely a simple faulty assumption was made.

It seems to me the following simplification can be made as a step forward:

$translated_array = explode(PoItem::DELIMITER, $this->translatedString);
// No need to use plural formula if the translation only contains the singular case.
$index = count($translated_array) === 1 ? 0 : $this->getPluralIndex();
// Nth plural form, fallback to second plural form.
$return = $translated_array[$index] ?? $translated_array[1];

From if ($this->count == 1 || $index == 0 || count($translated_array) == 1) { I kept only the last condition. As this issue states, the presumption $this->count == 1 indicates $index = 0 is faulty, so that needs to be removed and the $index == 0 case is superfluous anyways because both the if and the else case is $return = $translated_array[0] for zero index.

I think the fallback only exists when somehow locale_get_plural doesn't exist or a translation is not provided for all cases of the plural formula and so while it's not quite correct to use the presumption that [1] is somehow appropriate it is also quite irrelevant. I think so but I am not sure. Perhaps the last translation should be used instead of the second as that's usually more of an "open" case?

Ps1.: please continue not crediting me on core issues.

Ps2: speaking of faulty presumptions in this area, #1273968-2: remove eval from locale.module presumed precomputing 0-199 is enough but quite a few languages including French, Italian, Portuguese and Spanish contain a special case for 1000000. And Cornish is a nightmare. Another issue should be filed to fix this . It'll be fun. https://php-gettext.github.io/Languages/

d34dman’s picture

Status: Active » Needs review

It seems to me the following simplification can be made as a step forward:

I have updated the MR with suggested changes.

>... precomputing 0-199

@ghost of drupal past, so we have a bug due to what seems to be a performance optimization. Thanks for the issues links which are quite helpful.

---

P.S. It seems like some comment/s are vanishing, like ghost comments! Is it Drupal.Orgs way of keeping discussion between a dead guy and a ghost spooky ¯\_(ツ)_/¯

smustgrave’s picture

Status: Needs review » Needs work

Don't see an MR. but it should have test coverage when opened please

d34dman’s picture

I am happy to write some test, but would like to clarify few things,

Question A:
Am not sure about core policy regarding test, I think we would need a kernel test for this one, Is that assumption correct?

Question B:
I see testing this is tricky. This refactor touches part which needs locale module to be enabled and configured in a certain way. So should this test be part of Locale Module testing scope? i.e. test be written under namespace Drupal\KernelTests\Core\Locale instead of Drupal\KernelTests\Core\StringTranslation

ghost of drupal past’s picture

Yeah, tests fail... here's a slightly better version, with comments:

    $translated_array = explode(PoItem::DELIMITER, $this->translatedString);
    // No need to check plural formulas if only one translation is provided.
    $index = count($translated_array) === 1 ? 0 : $this->getPluralIndex();
    // If the translation is not found, it's guesswork, and the best guess
    // is the most common case where the first plural form is for n = 1 and
    // the second is for everything else. It's not always correct but
    // ¯\_(ツ)_/¯
    $return = $translated_array[$index] ?? $translated_array[$this->count != 1];

The fallback was changed from fixed [1] to [$this->count != 1] (perhaps amend the comment to emphasize strict comparison doesn't work here -- it can be float for example). There are multiple cases when it's incorrect, like "0 is a special case" which what this issue is about but also say Czech has n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2 which means for all n > 4 the guess will use the second translation but the third would be correct. But: since either the plural formula is not accessible or there are no translations for this count , it's just impossible to know for sure which is the correct one. So it tries to do ... something. By this point we know there are two translations and the most common plural formula is n!=1 and so the highest chance of being correct is to just use the second translation. It's ... not possible to do much better because , really, if you have three Slovenian translations which does n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : (n % 100 == 3 || n % 100 == 4) ? 2 : 3 well good luck knowing which one is missing.

d34dman’s picture

@ghost of drupal past,

I am afraid, we are increasing the scope of the fix.

As far as the scope of this bug report is about, it feels removing the hardcoded condition for "$this->count == 1" should be enough. Trying to find a sane default for cases under following scenario should be followed up in a separate issue (or issues),

- Case A: Translation data is corrupted, i.e. Message array doesn't contain the string at a particular index.
- Case B: Translation index formula can be wrong because of using default plural formula for language where it doesn't make sense.
- Case C: Translation index formula can be wrong due to corrupted Plural Form header in the PO file.

The main source of above scenario would be content/configuration done by User. And as pointed out in comment #8, the outcome has too many variations to handle.

As reported in this issue, despite having configured messages and Plural Formula correctly, Drupal Core fails to use the correct index, and hence that needs fixing.

ghost of drupal past’s picture

Sure, it might be too much I was trying to optimize but it was just an idea.

sleitner made their first commit to this issue’s fork.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.