Problem/Motivation

The javascript translations file that enables translation of javascript strings is not loading, which may result in mixed languages on a page, as translation of server-side strings is working.

Previously, JS libraries can be loaded in core in 3 ways: by file weight, by scope (header, footer, etc.) and by the library dependency system. Currently, the file-weight method is planned to be removed (see #1945262: Replace custom weights with dependencies in library declarations; introduce "before" and "after" for conditional ordering) and many JS libraries have had their scope changed, being loaded in the footer (see #784626: Default all JS to the footer, allow asset libraries to force their JS to the header).

Previously, the library loaded OK because a library it depends on, drupal/drupal, loaded in the same scope (header) and had a weight of -18, which meant it loaded first. Currently, the translation JS library is included first and tries to use the Drupal Javascript object, which causes an error. Plus, it's possible for drupal.js (and hence the Drupal JS object) to be loaded in either the header or the footer, depending on where it's needed. Which means that even regardless of the weight, the translations currently are easily loaded in the wrong scope too.

Steps to reproduce:

  1. Install Drupal 8.0.x, standard profile.
  2. Enable Language and Interface translations modules.
  3. Add a language with translations, e.g. Spanish.
  4. Go to http://yoursite/es
  5. See JS error "Uncaught reference error: Drupal is not defined" in the console in a file named es_very-long-hash.js

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug: code is not loading properly, resulting in a JS error.
Issue priority Major: page can be left with mixed languages, as server/php translations work, but translations of javascript do not work.
Prioritized changes Prioritized: is a bug fix.
Disruption None.

Proposed resolution

  1. Quick-fix: Add scope footer in locale_js_alter() or change the default in drupal_js_defaults. I think its fine to change the default because drupal_js_defaults is only used by locale module.
    This solution cannot work, see #7.
  2. More Advanced Proper solution: Add a new library locale/translation with proper dependencies and replace the path to the generated JS file at runtime in hook_js_alter(). (Bonus: this allows us to remove drupal_js_defaults() from core, which serves no purpose anymore, and locale.module is the last caller).

    The translation library is a conditional dependency of core/drupal, i.e. core/drupal must depend on locale/translations, locale/translations must not depend on core/drupal! After all, it's drupal.js that uses the translations, the translations don't use drupal.js. We can use hook_js_alter() to ensure that is the case.

Remaining tasks

Feedback on #49

User interface changes

None.

API changes

No change record is needed, as there does not appear to be any backwards-compatibility-breaking API changes, based on looking at https://www.drupal.org/files/issues/js_translations-2419897-48.patch and not seeing any changes to function signatures.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

webflo’s picture

Issue summary: View changes
penyaskito’s picture

Issue tags: +JavaScript
Gábor Hojtsy’s picture

Title: Fix Javascript translation » Javascript translations are added before drupal.js but have dependency on drupal.js
Issue tags: +sprint

Better title if I understand the problem right.

penyaskito’s picture

Issue summary: View changes
penyaskito’s picture

Issue summary: View changes
Wim Leers’s picture

Issue tags: +Needs tests

With test coverage for this (its absence is totally understandable), this would have been prevented. Will need tests to prevent regressions like this.

Wim Leers’s picture

Quick-fix: Add scope footer in locale_js_alter() or change the default in drupal_js_defaults. I think its fine to change the default because drupal_js_defaults is only used by locale module.

This is not a solution; it will only fix it in what has become the typical case. drupal.js could be either in the header or the footer, depending on where it's needed. If some JS declares it must live in the header, and it depends on drupal.js, then drupal.js will be in the footer.

More Advanced solution: Add a new library locale/drupal.locale.translation with proper dependencies and replace the path to the generated js file during runtime in hook_js_alter or hook_library_info_alter. Remove drupal_js_defaults from core.

This is the correct solution :)

penyaskito’s picture

Status: Active » Needs review
FileSize
1.89 KB

This does not work yet, but I need feedback if it is the right direction.

penyaskito’s picture

Re: #8: Obvious error: if this is cached, the library is generated only for one language. Anyway it does not work even for one language.

penyaskito’s picture

webflo’s picture

The last submitted patch, 8: 2419897-translations-js-8.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 11: 2419897-translations-js-10.patch, failed testing.

webflo’s picture

FileSize
2.09 KB

Nahh hacked the previous patch file because of subtree split.

webflo’s picture

Status: Needs work » Needs review
Gábor Hojtsy’s picture

Issue tags: +language-ui

Looks very clever. Still needs tests :)

penyaskito’s picture

@Wim: This will add js to all pages when visiting in a non-English language, even if anonymous. Is that acceptable?

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

I'm formulating a response. I have at least one viable solution already. Assigning to me to prevent duplicate work.

penyaskito’s picture

FileSize
2.83 KB

WIP of the test. I will wait for @Wim feedback before continuing on this.

Status: Needs review » Needs work

The last submitted patch, 19: 2419897-test-wip.diff, failed testing.

Wim Leers’s picture

While replying to this, I ended up with a very rambling reply. So… added some titles to group things :)

Patch review

+++ b/modules/locale/locale.module
@@ -1304,3 +1307,10 @@ function locale_translation_language_table($form_element) {
+function locale_page_attachments(array &$attachments) {
+  $attachments['#attached']['library'][] = 'locale/drupal.locale.translation';
+}

This is wrong; even on pages where no JS is necessary, this will still load the JS translation library, plus core/drupal. (EDIT: as penyaskito remarked in #17 — I'd already started writing this comment before that :P)

Hence I think this approach won't work after all.

Idea #1: each translation file injected into the library that contains the strings to be translated

But… I have a much better idea … I think — it's late here and I'm hungry :P. Here it is. All assets are in libraries now. Including JS assets. Therefore, it is now possible for the locale module to implement hook_library_alter()… and just inject its translation file in there! Except that it breaks down completely when you realize libraries are cached globally, but doing this would require them to be cached per language. That could be solvable.

It would be very elegant though.

That won't work, because the locale module is designed to generate a single JS translation file.

Idea #2: fix the patch in #14 by fixing wrong assumptions from the past

The root cause of the patch in #14 not working is that the library containing the translations declares a dependency on core/drupal. But that dependency is actually a lie; what really is going on is that the translation library is a conditional dependency of core/drupal, i.e. core/drupal must depend on locale/translations, locale/translations must not depend on core/drupal! After all, it's drupal.js that uses the translations, the translations don't use drupal.js.

Basically, that is all that we need to do.

There is one silly complication: weights. We've been working to get rid of them, but it's not yet finished. See #1945262: Replace custom weights with dependencies in library declarations; introduce "before" and "after" for conditional ordering. Sadly, drupal.js is one of the few JS assets still using weights. The dependency of core/drupal on locale/translations should be enough, but drupal.js's custom weight messes with that. So for now, until #1945262 is fixed, we have to add one LoC to adopt the weight of drupal.js and decrease it a little bit, to ensure the translation file is loaded first.

This is what the attached patch does!

Simplification/performance enhancement: no more need to track JS being discovered, we can generate it all at once, because we can list all JS assets with certainty

I realized that the way JS translation is currently designed, is basically: detect all JS files on the fly, track which ones we've already seen, if we encounter one we haven't seen yet, rebuild the translation file. This was indeed necessary in all prior versions of Drupal, and in Drupal 8… until https://www.drupal.org/node/2379475 (as of that moment, Drupal 8 used asset libraries solely).
So… now it's possible to generate the translation files all at once, without having to do that iterative building! That will mean fewer I/O after a cache clear or a deployment.

But that is out of scope for this issue. Opened #2421323: Parse js files from library definitions during rebuild to minimize variable_set() calls for that.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Wim Leers’s picture

Title: Javascript translations are added before drupal.js but have dependency on drupal.js » Javascript translations are broken
Gábor Hojtsy’s picture

Wim: was the prior title incorrect? "JS translations are broken" could be for a whole set of reasons. I tried to spell out the reason so that its more evident from the commit message and its easier to search for it later. Reasons for why JS translation was broken before include: (1) the JS translation file was not added to the page at all (2) the source JS files were not properly parsed (3) Drupal was parsing non-Drupal JS files from 3rd party libs that it should not, etc.

Status: Needs review » Needs work

The last submitted patch, 21: js_translations-2419897-21.patch, failed testing.

Wim Leers’s picture

Title: Javascript translations are broken » Javascript translations are loaded in the wrong order due to missing dependencies
Status: Needs work » Needs review
FileSize
3.91 KB
698 bytes

Fixed test failures and improved title per #24. (EDIT: and yes, the title was arguably incorrect, but only because of my patch: it was correct from HEAD's POV, but since my patch flips the order around, the previous issue title contradicted what the patch does. Hopefully the new title is sufficiently specific?)

The tests written in #19 should be merged into this patch (great job, penyaskito!).

Gábor Hojtsy’s picture

Status: Needs review » Needs work

Fix looks good. Needs tests merged.

penyaskito’s picture

HA! So the problem with the test was that the translations were created via API in our test, and the refresh of the locale translations are done in translate form submit handler... Changed the test for translating via drupalPostForm turns it green.

Also re-added the locale JS placeholder file, I guess @Wim forgot it and wasn't removed on purpose.

The last submitted patch, 28: js_translations-2419897-28.only-test.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 28: js_translations-2419897-28.patch, failed testing.

penyaskito’s picture

facepalm, this is green locally.

penyaskito’s picture

OK, let's remove xpath and simplify the assertion. This is more in line with AttachedAssetsTest.

Wim Leers’s picture

Status: Needs review » Needs work

Also re-added the locale JS placeholder file, I guess @Wim forgot it and wasn't removed on purpose.

Actually, it was removed on purpose; it's not necessary :) Could you remove it again?

Test looks great! Nitpicks:

  1. +++ b/core/modules/locale/src/Tests/LocaleJavascriptTranslationTest.php
    @@ -93,4 +94,62 @@ public function testFileParsing() {
    +   * Ensure that the translations js is added after its dependencies.
    ...
    +    // Loading the Translate page should refresh the translations js and import
    ...
    +    // Assert translations js is included before drupal.js.
    

    s/translations js/translation JS/

    Also: the first cited line contradicts the last cited line.

  2. +++ b/core/modules/locale/src/Tests/LocaleJavascriptTranslationTest.php
    @@ -93,4 +94,62 @@ public function testFileParsing() {
    +    // Translate a string in locale.admin.js to langcode.
    

    "translate to langcode" -> typo? Not sure what that means.

  3. +++ b/core/modules/locale/src/Tests/LocaleJavascriptTranslationTest.php
    @@ -93,4 +94,62 @@ public function testFileParsing() {
    +    // We need the generated hash in the js filename.
    

    s/js/JS/

penyaskito’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
6.54 KB
2.45 KB

Removed the placeholder file and attended other #33 remarks.
Also removed an unnecessary request in the test. I think this is as simple as it can be right now.

Having a only-test patch was a no-sense. The JS files were in the correct order, the problem was that translations assumed that Drupal JS object already existed. Now we are storing in the window object.

Wim Leers’s picture

Assigned: Unassigned » nod_
+++ b/core/modules/locale/src/Tests/LocaleJavascriptTranslationTest.php
@@ -135,20 +135,15 @@ public function testLocaleTranslationJsDependencies() {
-    // Loading the Translate page should refresh the translations js and import
-    // the sources.
-    $this->drupalGet($prefix . '/admin/config/regional/translate');

Oh, nice, one less GET :)

Assigning to nod_ for review, since he oversees how drupal.js works and this is closely related to that, it feels like he's almost the only person who can RTBC this.

nod_’s picture

Status: Needs review » Needs work

Since the dependencies are kind of backwards if we use Drupal.locale, let's get rid of Drupal.locale and use drupalTranslations everywhere. Needs to be added in .eslintrc as a global too.

Add drupalTranslations to the closure like it's done with drupalSettings.

nod_’s picture

Assigned: nod_ » Unassigned
penyaskito’s picture

Assigned: Unassigned » penyaskito

I'm going to work on that.-

penyaskito’s picture

Status: Needs work » Needs review
FileSize
40.05 KB
2.04 KB
penyaskito’s picture

Assigned: penyaskito » Unassigned
penyaskito’s picture

FileSize
8.08 KB

Argh, forgot to rebase my branch against 8.0.x

Status: Needs review » Needs work

The last submitted patch, 41: js_translations-2419897-40.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 41: js_translations-2419897-40.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 41: js_translations-2419897-40.patch, failed testing.

Status: Needs work » Needs review
penyaskito’s picture

Forgotten .eslintrc as requested in #36.

Wim Leers’s picture

Assigned: Unassigned » nod_

Should LOCALE_PLURAL_DELIMITER and drupalSettings.locale.pluralDelimiter also be moved to locale module?

Other than that, this looks great IMO.

Assigning to nod_ again, since I can't RTBC this (I've worked too much on this patch).

Mark_L6n’s picture

I'm at the sprint before DrupalCon Latino, doing 'beta evaluation' work, and would like to edit the 'Problem/Motivation' section to be more comprehensible to those not involved with the issue. Could you provide some more clarification about 'Javascript translation is currently broken'? Is the issue that a) certain JS files/functions are not loading properly, or b) translation of javascript strings doesn't function correctly or c) something else? Thanks!

Wim Leers’s picture

As the title indicates: the order in which files are loaded in HEAD is wrong, hence Drupal.t() doesn't find the translated strings.

Mark_L6n’s picture

Issue summary: View changes
Mark_L6n’s picture

Issue summary: View changes
Wim Leers’s picture

Issue summary: View changes

Mark: thanks! :)

YesCT’s picture

I dont understand this in the issue summary:
"This worked fine because both js files where is the same scope and drupal/drupal has a weight of -18."

Mark_L6n’s picture

No change record is needed, as there does not appear to be any backwards-compatibility-breaking API changes, based on looking at https://www.drupal.org/files/issues/js_translations-2419897-48.patch and not seeing any changes to function signatures.

Mark_L6n’s picture

Issue summary: View changes
penyaskito’s picture

#55 @YesCT: I'm far from being an expert in the area but will try to clarify.

We have three different concepts in JS inclusion: JS file weights, scope (header, footer...) and the library dependency system.
First one of them is going to be removed on #1945262: Replace custom weights with dependencies in library declarations; introduce "before" and "after" for conditional ordering as we should use components with explicit dependencies.

"This worked fine because both js files where is the same scope and drupal/drupal has a weight of -18."

Same scope: both are in header/footer/wherever. Weights: drupal.js has a weight of -18 specified on core.libraries.yml, so it was included before the translations, which has nothing specified so its weight 0. Lower weight comes first.

+++ b/core/modules/locale/locale.module
@@ -537,6 +555,15 @@ function locale_library_info_alter(array &$libraries, $module) {
+  if ($module === 'core' && isset($libraries['drupal'])) {
+    $libraries['drupal']['dependencies'][] = 'locale/translations';
+  }

This makes the precedence of locale/translation to be calculated with the new library dependency system instead of the weight one, which hopefully will be removed before 8.0.0. We do this dinamically instead of in core.libraries.yml because it should only be done if locale is enabled.

Hope this helps.

Mark_L6n’s picture

Issue summary: View changes
Mark_L6n’s picture

Issue summary: View changes
Mark_L6n’s picture

Issue summary: View changes
penyaskito’s picture

Issue summary: View changes
Wim Leers’s picture

Issue summary: View changes

So #49–#62 was a big distraction for the IS. I've clarified the IS further.

Let's get back on topic now. Repeating the last on-topic comment, #49:

Should LOCALE_PLURAL_DELIMITER and drupalSettings.locale.pluralDelimiter also be moved to locale module?

Other than that, this looks great IMO.

Assigning to nod_ again, since I can't RTBC this (I've worked too much on this patch).

nod_’s picture

Status: Needs review » Needs work

Hate to fail a patch on that but there is an eslint error, extra space. Also when there are no translations, the whole thing crash.

if (drupalTranslations.strings && drupalTranslations.strings[options.context] && drupalTranslations.strings[options.context][str]) {
    

…


    if (drupalTranslations.pluralFormula) {

Need to add a couple of typeof drupalTranslations !== 'undefined' in drupal.js.

Once that's fixed, RTBC for me.

Gábor Hojtsy’s picture

when there are no translations, the whole thing crash sounds bad :/ in what way?

nod_’s picture

drupal.js has a js error because drupalTranslations is not defined and we're trying to access a member of that object directly, without checking if it exist. Very good way to test no-js version of a page.

nod_’s picture

Assigned: nod_ » Unassigned
penyaskito’s picture

Status: Needs work » Needs review
FileSize
1.36 KB
8.44 KB

Thanks @nod_, I learnt something new with that :-)

Fixed @nod_ comments at #64. Double-checked with eslint that everything is OK.
I implicitly understand that #63 can be discussed in a follow-up issue.

Status: Needs review » Needs work

The last submitted patch, 68: js_translations-2419897-68.patch, failed testing.

Status: Needs work » Needs review
Gábor Hojtsy’s picture

Status: Needs review » Needs work

@penyaskito: The updates look good. I don't think that moving half of the things out of Drupal.locale and leaving some things there is a good idea. Should move the plural delimiter too here AFAIS.

penyaskito’s picture

rgrep LOCALE_PLURAL_DELIMITER . and there are several references in core libs and system module. So I don't think we should block this bugfix on that.

drupalSettings.locale.pluralDelimiter is needed in English sites too. So I guess this needs to stay at drupal.js (without locale namespace).

Wim Leers’s picture

Status: Needs work » Needs review

#72: only in core/lib/Drupal/Core/StringTranslation/TranslationManager.php, really. The single occurrence in system.module is precisely the one that should be moved to locale.module. But indeed, because of English sites, that's easier said than done.

penyaskito’s picture

There is also Drupal\Component\Gettext, which uses this. So if we move the definition, we need to move that to locale too? I am not sure Gábor agrees with that.

Wim Leers’s picture

Sorry for not being clear: I agree, we should not block this issue on the ideal place for the plural delimiter.

penyaskito’s picture

FileSize
2.94 KB
11.19 KB

So attached a patch that moves drupalSettings.locale.pluralDelimiter to drupalSettings.pluralDelimiter now that we removed Drupal.locale.

For the backend part we will create a different issue, as it shouldn't block a major.

Wim Leers’s picture

#76 interdiff looks good. Patch looks good. I'll leave RTBC'ing this to Gábor — I did too much work on this issue.

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a major bug and is allowed per https://www.drupal.org/core/beta-changes. Committed bda2066 and pushed to 8.0.x. Thanks!

  • alexpott committed bda2066 on 8.0.x
    Issue #2419897 by penyaskito, Wim Leers, webflo: Javascript translations...
Gábor Hojtsy’s picture

Issue tags: -sprint

Superb, thanks @WimLeers, @penyaskito, @nod_, @webflo!

YesCT’s picture

did the issue #76 mentions we need get created?

Status: Fixed » Closed (fixed)

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

penyaskito’s picture

@YesCT: No, AFAIR it wasn't :(
I'll try to do that tomorrow if noone beats me to do it.

penyaskito’s picture