Problem/Motivation

Using eval is bad. Bad for performance and bad for PHP source code compiler. Eval is used to proces the Gettext plural formula. This processing should therefore be removed and replaced by an alternative.

Proposed resolution

Replace the plural formula parser by a script that evaluates the Gettext plural formula and stores the result as an array of plural formula data. This data is used by both a PHP function (locale_get_plural) and a JavaScript function (Drupal.formatPlural) to determine which plural formula should be used for which (@count) value in the string.

Some references:

Remaining tasks

How to test manually

Since the Drupal test scripts can not test JavaScript yes, the JavaScript translation can not be tested automatically. Use the steps below to test.

Although the Drupal.formatPlural() is called only twice in Drupal core, it is not easy to trigger those occurrences manually. Therefore this script is made.
(core/modules/comment/js/node-new-comments-link.js, core/modules/contextual/js/contextual.toolbar.js)

  1. Use the latest D8 and apply the latest patch of this issue.
  2. Enable the interface translation module.
  3. Download Serbian translation from http://ftp.drupal.org/files/translations/8.x/drupal/drupal-8.0-alpha10.s...
  4. Import the downloaded PO file at admin/config/regional/translate/import. This will add the Serbian language too.
  5. Add this two line to the /core/themes/bartik/bartik.info.yml file
    scripts:
      - js/bartik.format_plural_tester.js
    
  6. Create the file "core/themes/bartik/js/bartik.format_plural_tester.js" with the following content:
    Drupal.behaviors.bartikFormatPluralTester = {
      attach: function (context, settings) {
        // We make sure the source string is included in the JS translations file.
        // The string will be detected and included in the JS translation file.
        Drupal.t('1 hour');
    
        var
          args = {},
          options = {},
          t = '';
        t = Drupal.formatPlural(1, 'Some untranslated singular text.', 'Some untranslated singular text.', {}, {});
        console.log(t);
    
        t = Drupal.formatPlural(2, 'Some untranslated plural text.', 'Some untranslated plural text.', {}, {});
        console.log(t);
    
        for (var i = 1; i < 120; i++) {
          t = Drupal.formatPlural(i, '1 hour', '@count hours', args, options);
          console.log(t);
        }
      }
    };
    
  7. Open the console window of your browser
  8. Visit the front page with English interface language. The output should be:
    Some untranslated plural text.
    Some untranslated plural text.
    1 hours
    2 hours
    3 hours
    4 hours
    5 hours 
    ...
    
  9. Visit the front page with Serbian interface language (http://example.com/sr). (Except for the first two lines) the console output should be the translated to Serbian.
    Output should look like:
    Some untranslated singular text.
    Some untranslated plural text.
    1 час
    2 часа
    3 часа
    4 часа
    5[2] часова
    6[2] часова
    ...
    20[2] часова
    21 час
    22 часа
    23 часа
    24 часа
    25[2] часова
    26[2] часова
    ...
    30[2] часова
    31 час
    32 часа
    33 часа 
    ...
     

Of course you can use any other language than the Serbian language.

Original report by chx

That's seriously uncool. I will write more tests.

CommentFileSizeAuthor
#85 the_last_eval-1273968-85.patch12.81 KBSweetchuck
#80 the_last_eval-1273968-80-interdiff-75-78.txt4.57 KBSweetchuck
#80 the_last_eval-1273968-80-78-clean.patch13.6 KBSweetchuck
#80 the_last_eval-1273968-80-75-reroll.patch13.31 KBSweetchuck
#78 the_last_eval-1273968-78.patch22.74 KBSweetchuck
#77 the_last_eval-1273968-76.patch21.49 KBSweetchuck
#75 the_last_eval-1273968-75.patch13.25 KBSutharsan
#75 interdiff-1273968-74-75.txt1.88 KBSutharsan
#74 the_last_eval-1273968-74.patch11.7 KBSutharsan
#73 the_last_eval-1273968-73.patch0 bytesSutharsan
#53 the_last_eval-1273968-49.patch11.7 KBchx
#53 interdiff.txt542 byteschx
#49 interdiff.46-49.txt1.16 KBpenyaskito
#49 the_last_eval-1273968-49.patch11.7 KBpenyaskito
#46 interdiff.42-46.txt1.5 KBpenyaskito
#46 the_last_eval-1273968-46.patch11.82 KBpenyaskito
#42 interdiff.40-42.txt927 bytespenyaskito
#42 the_last_eval-1273968-42.patch12.41 KBpenyaskito
#40 the_last_eval-1273968-40.patch11.75 KBSutharsan
#40 interdiff-1273968-34-40.txt1.93 KBSutharsan
#34 the_last_eval-1273968-34.patch11.64 KBSutharsan
#34 interdiff-1273968-23-24.txt6.58 KBSutharsan
#32 the_last_eval-1273968-32.patch11.7 KBSutharsan
#32 interdiff-1273968-26-32.txt8.21 KBSutharsan
#28 stackrun.txt1.65 KBchx
#26 the_last_eval-1273968-26.patch8.22 KBSutharsan
#26 interdiff-1273968-24-26.txt1.19 KBSutharsan
#24 the_last_eval-1273968-24.patch8.22 KBSutharsan
#24 interdiff-1273968-23-24.txt6.58 KBSutharsan
#23 the_last_eval-1273968-23.patch5.57 KBSutharsan
#21 the_last_eval-1273968-21.patch124.83 KBSutharsan
the_last_eval.patch4.85 KBchx
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Status: Needs review » Needs work

The last submitted patch, the_last_eval.patch, failed testing.

chx’s picture

Status: Needs work » Needs review

Because Gabor asked (although a comment in there says so) the way we get rid of the eval is to pre-compute plurals (re-using the already existing stack validation code structure). How many? As far as I can see, only the last two digits used, ever. So we could precompute 0-99 only and use that? No, because the first few are typically exceptions. So we precompute 0-199 instead.

Later we can do various optimizations: recognizing the default, recognizing 1, recognizing if formula(N + 100) equals to formula(N) and store / retrieve only formula(N) (is 123 stored? no? let's use 23). Once all that's done, we only store about 100 numbers and even that for a relatively few languages.

Gábor Hojtsy’s picture

A short summary of some things we talked about with @chx earlier:

- eval() is evil
- locale module is one of the very few places in core still using eval(), PHP.module is another one (so unlike the patch name, we are not eliminating the last one)
- to avoid using eval, @chx proposes we precompute a lookup table of results based on the formula
- @chx looked at the formulas in l10n_pconfig module and the plural form wiki at http://translate.sourceforge.net/wiki/l10n/pluralforms and concluded that we can keep most lookup tables pretty small (if you look at the formulas they repeat at most after 100, but most repeat patterns below that)
- therefore we can apply a lookup table and if we don't find the value in there, we can mod the number and try again, which should overall be much simpler than eval()-ing the formula all the time
- no performance tests so far to prove it actually performs faster

(Note I just summarized our discussion, was not involved in the making of the algorithm/patch).

Gábor Hojtsy’s picture

Issue tags: +D8MI, +hiphop

This is being looked at the extended D8MI sprint in Denver in context of making it simple to provide examples for a UI for plural translation. (#1452188: New UI for string translation). Also discussed yesterday with some people vs. making Drupal 8 better support hiphop.

Sutharsan’s picture

The hard coded plural processing as in Symphonys PluralizationRules (https://github.com/symfony/Translation/blob/master/PluralizationRules.php) can be partial solution for known languages. However for unknown languages we (may) need the flexibility a populated plural table can provide.

Gábor Hojtsy’s picture

I have not tested the suggested patch but if we can (a) cover all operators supported (b) use a prepopulated table for lookup, then we can avoid both hardcoding some formulas and needing to compute for others, since we can do it all dynamic but still pretty fast. That was the conclusion of our discussion about the above with chx.

andypost’s picture

Suppose we should provide a predefined formula|rule for languages in standard_language_list() and use serialized tables as chx suggested for other languages.

Gábor Hojtsy’s picture

Why maintain two ways in parallel?

clemens.tolboom’s picture

In #1570346: Let Symfony Translation component handle language messages we discuss symfony Translation component which has PluralizationRules.

I like the idea of hardcoded rules but PluralizationRules allows for custom rules.

Can't we use that instead?

clemens.tolboom’s picture

Added some links from here and other places into Issue Summary.

andypost’s picture

We need to re-roll this anyway, so I suggests to add serialized values for standard_language_list() and inherit them in locale_translation_plurals variable, also it's good question about how to convert it's value to CMI?

chx’s picture

Status: Needs review » Closed (won't fix)

Oh of course we can use Symfony! Heaven forbid we use our own code.

clemens.tolboom’s picture

Status: Closed (won't fix) » Needs work

This is definitely not closed :)

1. Symfony has hard coded pluralization rules with support for custom.
2. We do have a PluralForms parser and Symfony does not have yet.

When we switch to Symfony Translation component we need implement a callable when we use https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Tra...

On http://localize.drupal.org/translate/languages we support for 104 languages.

Symfony has 95 langcodes in PluralizationRules.php

YesCT’s picture

chx: I know you are pretty busy. Do you have any intermediate work on this that might help someone else help you? Or maybe this is ok for someone else to pick up?

chx’s picture

Assigned: chx » Unassigned
YesCT’s picture

OK. :)

This looks challenging, and next step might just be a reroll.

chx’s picture

A simple way of compression would be: while evaluating the stack for 0-199 (btw the patch leaves out 199), count the ocurrences of values. Store the highest occurence count as default, store everything else with their indexes. So if you have [1, 2, 3, 3, 3, ... 100 => 1, 101 => 2, 3, 3.....] then the array to be stored is only [default => 3, 0 => 1, 1 => 2, 100 => 1, 101 => 2]. array_filter($plural, function ($n) use ($default) { return $n != $default; } will neatly throw away the defaults from the $plural array.
and then:

      $index = $count > 199 ? 100 + ($count % 100) : $count;
      $plurals[$langcode][$count] = isset($locale_formula[$langcode][$index]) ? $locale_formula[$langcode][$index] : $locale_formula[$langcode]['default'];
Gábor Hojtsy’s picture

Priority: Normal » Major

We should not have eval() in Drupal core like this.

Sutharsan’s picture

Assigned: Unassigned » Sutharsan

Porting ...

Gábor Hojtsy’s picture

Issue tags: +sprint, +language-ui

Also put on sprint then.

Sutharsan’s picture

FileSize
124.83 KB

This is an port of the #0 patch. It needs more work to get evaluatePlural() functioning.

chx’s picture

Get foo functioning is not a bug report.

Sutharsan’s picture

Bad patch in #21. Corrected version here, still needs work.

Sutharsan’s picture

Status: Needs work » Needs review
FileSize
6.58 KB
8.22 KB

Working patch without the compression suggested in #17.
Needs tests.

Status: Needs review » Needs work

The last submitted patch, the_last_eval-1273968-24.patch, failed testing.

Sutharsan’s picture

Status: Needs work » Needs review
FileSize
1.19 KB
8.22 KB

This should pass the test. Additional tests are not required, the existing tests have sufficient coverage IMHO.

clemens.tolboom’s picture

+++ b/core/lib/Drupal/Component/Gettext/PoHeader.php
@@ -417,4 +417,102 @@ private function tokenizeFormula($formula) {
+  /**
+   * Evaluate the plural element stack using a plural value.
+   *
+   * Using an element stack, which represents a plural formula, we calculate
+   * which plural string should be used for a given plural value.

Can we have an example evaluation. The code is now hard to grasp.

+++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php
@@ -178,7 +178,7 @@ function setHeader(PoHeader $header) {
+          'formula' =>  $formula,

whitespace (2 spaces)

chx’s picture

FileSize
1.65 KB

This is what I used yesterday to find out what's wrong (see the comment on switch(TRUE); that was wrong). I am not sure we want something like this in an inline comment, it's way too long.

chx’s picture

Also, I looked at the patch and it's not exactly "the top elements of the stack are evaluated." -- what we evaluate is the first operator from the top which can be pretty deep for example step 13 has true 0 false true 1 20 >= as the stack top. We pick the >= to evaluate because it's the first symbol and it uses 1 and 20 as arguments and evaluates to false. After evaluating we remove the arguments plus the operator itself -- typically 2 arguments and 1 element for the operator, except ?: which is 3 arguments and 2 elements for the operator and then put back the result. This is why the evaluation starts at 2 to boot -- every operator has at least two arguments.

chx’s picture

One more thing, we do not have any infinite loop counters for broken plural formulas and we need one. Anything as crude as adding a $counter = 50 before the while and then if (!$counter--) throw exception('broken plural formula') works.

Also, if this passes we should implement the compression algorithm from above.

Thanks for all the work!

Status: Needs review » Needs work

The last submitted patch, the_last_eval-1273968-26.patch, failed testing.

Sutharsan’s picture

Status: Needs work » Needs review
FileSize
8.21 KB
11.7 KB

Added more comments (#27, #29), added a limiter to prevent endless loops (#30), added the compression logic (#17) and a bit of extra test cases.
Finally I've added code for the upgrade. But this still fails and I did not manage to find the cause of it:

@@ -475,10 +476,14 @@ function update_prepare_d8_language() {
     $javascript = array();
     $prefixes = array();
     $domains = array();
+    $header = new PoHeader;
     foreach ($languages as $language) {
+      $plural_forms = 'nplurals=' . $language->plurals . '; plural=' . $language->formula . ';';
+      $parsed = $header->parsePluralForms($plural_forms);
+      list($nplurals, $formula) = $parsed;
       $plurals[$language->language] = array(
-        'plurals' => $language->plurals,
-        'formula' => $language->formula,
+        'plurals' => $nplurals,
+        'formula' => $formula,
       );

Status: Needs review » Needs work

The last submitted patch, the_last_eval-1273968-32.patch, failed testing.

Sutharsan’s picture

Removed the surplus type casting.
Upgrade test still fails with:

Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'drupal8.simpletest12875language' doesn't exist: SELECT * FROM {language} ORDER BY weight ASC, name ASC; Array ( ) in Drupal\Core\Database\Connection->query() (line 571 of /Users/erik/www/drupal8/core/lib/Drupal/Core/Database/Connection.php).
Drupal\Core\Database\Connection->query('SELECT * FROM {language} ORDER BY weight ASC, name ASC', Array, Array)
db_query('SELECT * FROM {language} ORDER BY weight ASC, name ASC')
language_list()
Drupal\language\HttpKernel\PathProcessorLanguage->__construct(Object, Object, Object)
ReflectionClass->newInstanceArgs(Array)
Symfony\Component\DependencyInjection\ContainerBuilder->createService(Object, 'path_processor_language')
Symfony\Component\DependencyInjection\ContainerBuilder->get('path_processor_language', 1)
Symfony\Component\DependencyInjection\ContainerBuilder->resolveServices(Object)
Symfony\Component\DependencyInjection\ContainerBuilder->resolveServices(Array)
Symfony\Component\DependencyInjection\ContainerBuilder->callMethod(Object, Array)
Symfony\Component\DependencyInjection\ContainerBuilder->createService(Object, 'path_processor_manager')
Symfony\Component\DependencyInjection\ContainerBuilder->get('path_processor_manager', 1)
Symfony\Component\DependencyInjection\ContainerBuilder->resolveServices(Object)
Symfony\Component\DependencyInjection\ContainerBuilder->resolveServices(Array)
Symfony\Component\DependencyInjection\ContainerBuilder->createService(Object, 'url_generator')
Symfony\Component\DependencyInjection\ContainerBuilder->get('url_generator')
Drupal\simpletest\WebTestBase->prepareRequestForGenerator()
Drupal\simpletest\WebTestBase->rebuildContainer()
Drupal\system\Tests\Upgrade\UpgradePathTestBase->getUpdatePhp()
Drupal\system\Tests\Upgrade\UpgradePathTestBase->performUpgrade()
Drupal\contact\Tests\ContactUpgradePathTest->testContactUpgrade()
Drupal\simpletest\TestBase->run()
_simpletest_batch_operation(Array, '21', Array)
call_user_func_array('_simpletest_batch_operation', Array)
_batch_process()
_batch_do()
_batch_page()
system_batch_page()
call_user_func_array('system_batch_page', Array)
Drupal\Core\EventSubscriber\{closure}()
call_user_func_array(Object, Array)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1)
Drupal\Core\HttpKernel->handle(Object, 1, 1)
Symfony\Component\HttpKernel\Kernel->handle(Object)
drupal_handle_request()
 

This seems *not* to be caused by the changes in core/includes/update.inc.

Sutharsan’s picture

Status: Needs work » Needs review

After full reboot and running an upgrade test with this patch applied to HEAD the the same upgrade test passes. I guess it's too late for humans, lets see what the bot thinks.

Status: Needs review » Needs work

The last submitted patch, the_last_eval-1273968-34.patch, failed testing.

Sutharsan’s picture

Status: Needs work » Needs review

Tests fail in a different manner locally. Not nice, give it another spin.

Sutharsan’s picture

Issue tags: -D8MI, -sprint, -language-ui, -hiphop, -challenging, -Needs remaining tasks

#34: the_last_eval-1273968-34.patch queued for re-testing.

Status: Needs review » Needs work
Issue tags: +D8MI, +sprint, +language-ui, +hiphop, +challenging, +Needs remaining tasks

The last submitted patch, the_last_eval-1273968-34.patch, failed testing.

Sutharsan’s picture

Status: Needs work » Needs review
FileSize
1.93 KB
11.75 KB

This brings us one step closer. The test now (only) fails with The plural formula could not be parsed. .

Status: Needs review » Needs work

The last submitted patch, the_last_eval-1273968-40.patch, failed testing.

penyaskito’s picture

Status: Needs work » Needs review
FileSize
12.41 KB
927 bytes

I think that the problem is that we have 'nplurals=2; plural=($n>1);' with $n instead of n in the UpgradeTest.
I am having problems with testing this locally, but could be green.

Gábor Hojtsy’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Component/Gettext/PoHeader.phpundefined
@@ -302,6 +301,9 @@ private function parseArithmetic($string) {
+      elseif ($current_token == "$") {
+        // Do nothing, we will process next n (we have found $n).
+      }

No! .po files will not have a $ in the number. The *database* data has it. The **upgrade** should remove the $ and then parse it.

penyaskito’s picture

Assigned: Sutharsan » penyaskito

Oops... working on that then.

penyaskito’s picture

Oops... working on that then.

penyaskito’s picture

Status: Needs work » Needs review
FileSize
11.82 KB
1.5 KB

This should address the issue, but I'm having some issues with Upgrade tests in my system. Let's wait for testbot.

Sutharsan’s picture

chx noted in irc that we could filter out more values from the more $plurals array. This is what I came up with for parsePluralForms():

  $element_stack = $this->parseArithmetic($plural);
  $default = 0;
  if ($element_stack !== FALSE) {
    for ($i = 0; $i <= 199; $i++) {
      $plurals[$i] = $this->evaluatePlural($element_stack, $i);
    }
    $default = $plurals[$i-1];
    $plurals = array_filter($plurals, function ($value) use ($default) {
      return ($value != $default);
    });
    $plurals['default'] = $default;

    return array($nplurals, $plurals);
  }

Instead of truncating values from the end of the array, this uses array_filter to remove all occurrences of $default from the array.

Sutharsan’s picture

Status: Needs review » Needs work
penyaskito’s picture

Status: Needs work » Needs review
FileSize
11.7 KB
1.16 KB

Let's if chx + Sutharsan suggestion works...

Sutharsan’s picture

Yes it works :)
Out of curiosity I tried to filter on a few other positions ($plurals[0], $plurals[1], $plurals[189] .. $plurals[198]) but none gave a better result. On average the compression is 90%. Because I have used many exotic plural formula's for the test, in practice the compression may go up to 99%.

chx’s picture

We do not need to have the absolute possible best compression, anything sane is good enough -- I think this is good, not sure who could review it now though :)

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

@webflo and I looked at this and we found no problems. I'm also very happy with the set of people who looked at this and worked together :) Yay!

chx’s picture

Fixed some formatting.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Committed to 8.x. Thanks!

Sutharsan’s picture

Assigned: penyaskito » Unassigned

Thanks!
Upon suggestion of chx, I have create a follow-up issue for additional unit test.

Sutharsan’s picture

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

Gábor Hojtsy’s picture

Issue tags: -sprint

Removing sprint tag.

Gábor Hojtsy’s picture

Attempt to remove tag again.

Gábor Hojtsy’s picture

This did not cover JavaScript unfortunately. See #2043953: Singular/plural strings don't work in JavaScript anymore.

catch’s picture

Status: Closed (fixed) » Needs work

I've rolled this back and am closing the other issue as duplicate. We can commit this again along with the js fix.

larowlan’s picture

Priority: Major » Critical

Because #2043953: Singular/plural strings don't work in JavaScript anymore was critical
Feel free to bump back.

chx’s picture

Title: locale.module uses eval » remove eval from locale.module
Version: 8.x-dev » 9.x-dev
Category: bug » feature
Priority: Critical » Normal
Issue tags: -hiphop

It was critical because it was broken but since this is rolled back, it's not a critical any more, by far. Now a) you can't disable eval easily in Zend Engine (because it is not, in fact, a function) b) Regarding HipHop, HPHP is no more and HHVM can run eval just fine. So with the original reasons gone, I am degrading this to a 9.x feature request. Nothing is fundamentally broken here that would call to focus our very limited JS resources in pursuing this dream further. Sad :(

Gábor Hojtsy’s picture

Title: remove eval from locale.module » locale.module uses eval
Version: 9.x-dev » 8.x-dev
Category: feature » bug
Priority: Normal » Major
Issue tags: +hiphop

I'm not sure @catch considers having an eval() in Drupal's codebase critical, he rolled this patch back to manage the critical queue by trading a critical bug for a major one. That is rather keep an eval() in vs. having a critical about a non-complete implementation of it. I don't necessarily agree that the rollback was the best technique forward to get the eval removed and JS properly fixed, but I don't need to :)

Gábor Hojtsy’s picture

Title: locale.module uses eval » removal eval from locale.module
Version: 8.x-dev » 9.x-dev
Category: bug » feature
Issue tags: -hiphop

Cross-post.

Gábor Hojtsy’s picture

Title: removal eval from locale.module » remove eval from locale.module
catch’s picture

Title: remove eval from locale.module » removal eval from locale.module

afaik there's no API change here, so I'd commit a patch to 8.x, I'm not going to play status pong though.

I hate eval() but I don't consider it a release blocker no (especially since we could backport that change anyway).

chx’s picture

Title: removal eval from locale.module » remove eval from locale.module
Version: 9.x-dev » 8.x-dev
Category: feature » task
Priority: Major » Normal

Let's agree on a 8.x task, then. Doing this in JS couldn't be hard when the heavy lifting is assembling the array, just pass it to the JS settings and port the, what, two lines of PHP code to JS.

Gábor Hojtsy’s picture

@catch: it is an API change in terms of how the language object is structured, which having no methods exposes its stuff via public properties. That is why the JS code broke.

catch’s picture

@Gabor: Ok technically that's an API change but do you know anywhere other than the js that uses formula?

Gábor Hojtsy’s picture

At least the l10n_server, potx and l10n_pconfig modules rely on the formula in some way (needing the string itself in l10n_pconfig and potx and the num of plurals in l10n_server). Which in fact points to a problem in the above patch that it does not keep storing that information additionally to the computed array.

nod_’s picture

Issue tags: +JavaScript

tag

nod_’s picture

Issue summary: View changes

Updated issue summary.

Sutharsan’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
0 bytes

Rerolled the #53 patch.

Sutharsan’s picture

Nevermind :/ A proper reroll, this time.

Sutharsan’s picture

This patch makes JS use the plural "formula" array instead of the old formula. In the translated js file the formula gets replaced by an array of indexes. Drupal.formatPlural() used this array to determine the right index.

I have seen it working with English plural strings in JS, but could not get translated strings. It probably needs the right JS to to demonstrate plural translation.

Gábor Hojtsy’s picture

@Sutharsan: @Sweetchuck wanted to take this on on Drupal sprint weekend and found several other bugs in JS translation to make JS translation work in the first place... Those are now fixed in #2182265: Drupal.t() placeholder substitution doesn't always work correctly (backport part of the Javascript file translation fixes from Drupal 8), so this can now be adapted to work with JS too from a prior working JS state (as opposed to several bugs there :)

Sweetchuck’s picture

Status: Needs review » Needs work
FileSize
21.49 KB

This patch is not finished.
The JS Drupal.t() is not works with the english language, but works with every other language.

Sweetchuck’s picture

Status: Needs work » Needs review
FileSize
22.74 KB
Sutharsan’s picture

Status: Needs review » Needs work

@sweetchuck, I tried to review the issue but found it very hard to check your work. Your patch combines too many things in one: (unrelated) changes due to the reroll, code style changes and finally the actual functional code changes. As you did not provide an interdiff, I can not judge the patch. Could you create a new patch, with interdiff. Ideally I would like to see 1. (first) a patch of the #75 rerolled; 2. a patch with only functional changes including an interdiff and lastly a separate follow-up issue with the code style changes and the non-essential comment changes.
I do realise that this is a lot of extra work, but the #78 patch is not in a state that I can review it and therefore holds the issue up.

Sweetchuck’s picture

the_last_eval-1273968-80-75-reroll.patch
Rerolled version of patch #75.

the_last_eval-1273968-80-78-clean.patch
Simplified version of patch #78. The code comments and coding standards modifications has been removed.

the_last_eval-1273968-80-interdiff-75-78.txt
This is the interdiff between the the_last_eval-1273968-80-75-reroll and the_last_eval-1273968-80-78-clean.patch

Sutharsan’s picture

I've reviewed the interdiff:

  1. +++ b/core/includes/common.inc
    @@ -2073,7 +2073,12 @@ function _drupal_add_js($data = NULL, $options = NULL) {
    -          $javascript['settings']['data'][] = array('path' => $path);
    +          $javascript['settings']['data'][] = array(
    +            'path' => $path,
    +            'locale' => array(
    +              'pluralDelimiter' => LOCALE_PLURAL_DELIMITER,
    +            ),
    +          );
    

    This duplicates #2224691: JavaScript Drupal.t and Drupal.formatPlural only works for non-english languages.

  2. +++ b/core/includes/update.inc
    @@ -15,7 +15,6 @@
    -use Drupal\Component\Gettext\PoHeader;
    

    This change seems not to be related to this patch, it is probably code clean-up. If so, remove it from the patch.

  3. +++ b/core/misc/drupal.js
    @@ -363,26 +363,20 @@ if (window.jQuery) {
       Drupal.formatPlural = function (count, singular, plural, args, options) {
         args = args || {};
         args['@count'] = count;
    +
    +    var pluralDelimiter = drupalSettings.locale.pluralDelimiter,
    +      translations = Drupal.t(singular + pluralDelimiter + plural, args, options).split(pluralDelimiter),
    

    This is duplicate code of #2224691: JavaScript Drupal.t and Drupal.formatPlural only works for non-english languages. Remove it.

  4. +++ b/core/misc/drupal.js
    @@ -363,26 +363,20 @@ if (window.jQuery) {
    -    else {
    -      index = (args['@count'] === 1) ? 0 : 1;
    +    else if (args['@count'] !== 1) {
    +      index = 1;
    

    This is not a functional change, but a personal preference in code style. Please remove to avoid clutter in the patch.

  5. +++ b/core/modules/locale/locale.module
    @@ -340,7 +340,6 @@ function locale_get_plural($count, $langcode = NULL) {
           // find exceptions (1, 2, etc).
           $index = $count > 199 ? 100 + ($count % 100) : $count;
           $plural_indexes[$langcode][$count] = isset($plural_formulas[$langcode]['formula'][$index]) ? $plural_formulas[$langcode]['formula'][$index] : $plural_formulas[$langcode]['formula']['default'];
    -
         }
    

    More code clean-up.

  6. +++ b/core/modules/locale/locale.module
    @@ -340,7 +340,6 @@ function locale_get_plural($count, $langcode = NULL) {
    @@ -1299,19 +1298,16 @@ function _locale_rebuild_js($langcode = NULL) {
    
    @@ -1299,19 +1298,16 @@ function _locale_rebuild_js($langcode = NULL) {
       $data_hash = NULL;
       $data = $status = '';
       if (!empty($translations)) {
    -    $data = "Drupal.locale = { ";
    +    $data = array(
    +      'strings' => $translations,
    +    );
    +
    

    This is duplicate code of #2224691: JavaScript Drupal.t and Drupal.formatPlural only works for non-english languages. Remove it, or drop that issue.

  7. +++ b/core/modules/locale/locale.module
    @@ -1372,6 +1368,7 @@ function _locale_rebuild_js($langcode = NULL) {
    +
         case 'rebuilt':
    

    More code clean-up. Please remove.

+++ b/core/misc/drupal.js
@@ -363,26 +363,20 @@ if (window.jQuery) {
+      translations = Drupal.t(singular + pluralDelimiter + plural, args, options).split(pluralDelimiter),

+++ b/core/modules/locale/locale.module
@@ -1299,19 +1298,16 @@ function _locale_rebuild_js($langcode = NULL) {
+    $data = 'Drupal.locale = ' . Json::encode($data) . ';';

Am I correct to understand that this line is the only functional change in this patch? The other changes are 1. covered by #2224691: JavaScript Drupal.t and Drupal.formatPlural only works for non-english languages, 2. usage of Json::encode() instead of custom encoding and 3. the above commented non-essential changes.

We need to decide what to do with #2224691: JavaScript Drupal.t and Drupal.formatPlural only works for non-english languages. Those changes are completely included in the #80 patch. We either drop the first issue or remove its code from the patch. Dropping #2224691: JavaScript Drupal.t and Drupal.formatPlural only works for non-english languages simplifies the workflow and we need the combined code to fix the (plural) translation in English. And that is what we try to achieve after this issue got reverted.

Sweetchuck’s picture

Issue summary: View changes
Sutharsan’s picture

Issue tags: +sprint

Jay, #2224691: JavaScript Drupal.t and Drupal.formatPlural only works for non-english languages got committed.
@Sweetchuck, Will you re-roll and update the patch as we discussed yesterday?

Tagging for D8MI sprint board.

Sweetchuck’s picture

Assigned: Unassigned » Sweetchuck

Yes. I will reroll this patch today. After the "D8 Field API - Fields reborn" session

Sweetchuck’s picture

Assigned: Sweetchuck » Unassigned
Status: Needs work » Needs review
FileSize
12.81 KB

Reroll the patch #80 the_last_eval-1273968-80-78-clean.patch

Sutharsan’s picture

Issue tags: +needs manual test

The code looks Ok to me. Since javascript is not tested by the testbot, this patch now needs a manual test. Alternatively we could use a simple javascript script and us use that in a manual test.

Sweetchuck’s picture

How to test manually

The Drupal.formatPlural() is called only twice in Drupal core.
core/modules/comment/js/node-new-comments-link.js
core/modules/contextual/js/contextual.toolbar.js
Not easy to trigger it manually. Therefore I wrote a small script to call theDrupal.formatPlural() as way as I want.

  1. Enable the interface translation module
  2. Add the Polish language on admin/config/regional/language page
  3. We need some translated text, so download the translation of the Date module http://ftp.drupal.org/files/translations/7.x/date/date-7.x-2.7.pl.po
  4. Import the downloaded PO file on admin/config/regional/translate/import page.
  5. Add this two line to the core/themes/bartik/bartik.info.yml file
    scripts:
      - js/bartik.format_plural_tester.js
    
  6. Create the file "core/themes/bartik/js/bartik.format_plural_tester.js" with the following content:
    Drupal.behaviors.bartikFormatPluralTester = {
      attach: function (context, settings) {
        var
          args = {},
          options = {},
          t = '';
    
        t = Drupal.formatPlural(1, 'Do not translate this text.', 'Do not translate this texts.', {}, {});
        console.log('Untranslated singular -> ' + t);
    
        t = Drupal.formatPlural(2, 'Do not translate this text.', 'Do not translate this texts.', {}, {});
        console.log('Untranslated plural -> ' + t);
    
        for (var i = 1; i < 120; i++) {
          t = Drupal.formatPlural(i, 'every week', 'every @count weeks', args, options);
          console.log(i + ' -> ' + t);
        }
      }
    };
    
  7. Open the console window of your browser
  8. Visit the front page with English interface language
  9. Visit the front page with Polish interface language

Of course you can use any other language not just the Polish language.

Sweetchuck’s picture

Issue summary: View changes
Sutharsan’s picture

Issue summary: View changes

Updated the issue summary with a general problem description and added details to the manual test.

Sutharsan’s picture

Issue summary: View changes
Status: Needs review » Needs work

I have ran the manual test,but it fails. This is the output:

Untranslated singular -> Some untranslated singular.
Untranslated plural -> Some untranslated plural text.
1 -> every week
2 -> every 2 weeks
3 -> every 3 weeks
4 -> every 4 weeks
5 -> undefined
6 -> undefined
...
21 -> undefined
22 -> every 22 weeks
23 -> every 23 weeks
24 -> every 24 weeks
25 -> undefined
...

It looks like the default is not being applied.

Sutharsan’s picture

Issue summary: View changes
Sutharsan’s picture

Issue summary: View changes
Status: Needs work » Needs review

The problem was in the test. It used a translated string, but the string was not in the JavaScript translation file. The source string is now include in the code and used in the Drupal.t() function, further and a more commonly used source string is being used in the test.

The last submitted patch, 80: the_last_eval-1273968-80-78-clean.patch, failed testing.

The last submitted patch, 80: the_last_eval-1273968-80-75-reroll.patch, failed testing.

The last submitted patch, 77: the_last_eval-1273968-76.patch, failed testing.

Sutharsan’s picture

Ignore #93 - #95 comments, they are about old patches. The patch in #85 is the latest and still applies.

botanic_spark’s picture

Status: Needs review » Reviewed & tested by the community

I did the manual test, and I got all strings translated.

Some untranslated singular text.
Some untranslated plural text.
1 час 
2 часа
3 часа
4 часа
5[2] часова
6[2] часова
7[2] часова
8[2] часова
9[2] часова
10[2] часова
11[2] часова
12[2] часова
13[2] часова
14[2] часова
15[2] часова 
Sutharsan’s picture

Issue tags: -needs manual test

@botanic_spark, thanks for testing.
+1 for RTBC

alexpott’s picture

Committed 0ac4b91 and pushed to 8.x. Thanks!

diff --git a/core/lib/Drupal/Component/Gettext/PoHeader.php b/core/lib/Drupal/Component/Gettext/PoHeader.php
index 5088104..3136f2e 100644
--- a/core/lib/Drupal/Component/Gettext/PoHeader.php
+++ b/core/lib/Drupal/Component/Gettext/PoHeader.php
@@ -228,7 +228,6 @@ function parsePluralForms($pluralforms) {
     // For data compression we store the last position the array value
     // changes and store it as default.
     $element_stack = $this->parseArithmetic($plural);
-    $default = 0;
     if ($element_stack !== FALSE) {
       for ($i = 0; $i <= 199; $i++) {
         $plurals[$i] = $this->evaluatePlural($element_stack, $i);

Removed unused local variable on commit.

  • Commit 0ac4b91 on 8.x by alexpott:
    Issue #1273968 by Sutharsan, Sweetchuck, penyaskito, chx: Remove eval...
alexpott’s picture

Status: Reviewed & tested by the community » Fixed
Gábor Hojtsy’s picture

Issue tags: -sprint

Yay, thanks all!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

idebr’s picture

Since the arithmetic plural formula is now evaluated and then discarded, the original plural formula is no longer available. This is a problem when creating interface translation exports in .po files. This currently blocks #2611298: Port potx to Drupal 8

Can you have a look at the following issue and help determine the proposed resolution? #2882617: String version of plural formula is not available, exported .po files contain an incorrect default