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:
- Install Drupal 8.0.x, standard profile.
- Enable Language and Interface translations modules.
- Add a language with translations, e.g. Spanish.
- Go to
http://yoursite/es
- 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
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
Quick-fix: Add scope footer inlocale_js_alter()
or change the default indrupal_js_defaults
. I think its fine to change the default becausedrupal_js_defaults
is only used by locale module.
This solution cannot work, see #7.More AdvancedProper solution: Add a new librarylocale/translation
with proper dependencies and replace the path to the generated JS file at runtime inhook_js_alter()
. (Bonus: this allows us to removedrupal_js_defaults()
from core, which serves no purpose anymore, andlocale.module
is the last caller).The translation library is a conditional dependency of
core/drupal
, i.e.core/drupal
must depend onlocale/translations
,locale/translations
must not depend oncore/drupal
! After all, it'sdrupal.js
that uses the translations, the translations don't usedrupal.js
. We can usehook_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.
Comment | File | Size | Author |
---|---|---|---|
#76 | js_translations-2419897-76.patch | 11.19 KB | penyaskito |
Comments
Comment #1
webflo CreditAttribution: webflo commentedComment #2
penyaskitoComment #3
Gábor HojtsyBetter title if I understand the problem right.
Comment #4
penyaskitoComment #5
penyaskitoComment #6
Wim LeersWith test coverage for this (its absence is totally understandable), this would have been prevented. Will need tests to prevent regressions like this.
Comment #7
Wim LeersThis 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 ondrupal.js
, thendrupal.js
will be in the footer.This is the correct solution :)
Comment #8
penyaskitoThis does not work yet, but I need feedback if it is the right direction.
Comment #9
penyaskitoRe: #8: Obvious error: if this is cached, the library is generated only for one language. Anyway it does not work even for one language.
Comment #10
penyaskitoVia @webflo:
hook_library_alter
looks a better place for this. See #2378263: hook_library_alter() must be manually invoked by users of LibraryDiscovery, and has no test coverage too.Comment #11
webflo CreditAttribution: webflo commentedComment #14
webflo CreditAttribution: webflo commentedNahh hacked the previous patch file because of subtree split.
Comment #15
webflo CreditAttribution: webflo commentedComment #16
Gábor HojtsyLooks very clever. Still needs tests :)
Comment #17
penyaskito@Wim: This will add js to all pages when visiting in a non-English language, even if anonymous. Is that acceptable?
Comment #18
Wim LeersI'm formulating a response. I have at least one viable solution already. Assigning to me to prevent duplicate work.
Comment #19
penyaskitoWIP of the test. I will wait for @Wim feedback before continuing on this.
Comment #21
Wim LeersWhile replying to this, I ended up with a very rambling reply. So… added some titles to group things :)
Patch review
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 thelocale
module to implementhook_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 ofcore/drupal
, i.e.core/drupal
must depend onlocale/translations
,locale/translations
must not depend oncore/drupal
! After all, it'sdrupal.js
that uses the translations, the translations don't usedrupal.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 ofcore/drupal
onlocale/translations
should be enough, butdrupal.js
's custom weight messes with that. So for now, until #1945262 is fixed, we have to add one LoC to adopt the weight ofdrupal.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: https://www.drupal.org/node/2379475 (as of that moment, Drupal 8 used asset libraries solely).
. This was indeed necessary in all prior versions of Drupal, and in Drupal 8… untilSo… 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.
Comment #22
Wim LeersComment #23
Wim LeersComment #24
Gábor HojtsyWim: 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.
Comment #26
Wim LeersFixed 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!).
Comment #27
Gábor HojtsyFix looks good. Needs tests merged.
Comment #28
penyaskitoHA! 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.
Comment #31
penyaskitofacepalm, this is green locally.
Comment #32
penyaskitoOK, let's remove xpath and simplify the assertion. This is more in line with
AttachedAssetsTest
.Comment #33
Wim LeersActually, it was removed on purpose; it's not necessary :) Could you remove it again?
Test looks great! Nitpicks:
s/translations js/translation JS/
Also: the first cited line contradicts the last cited line.
"translate to langcode" -> typo? Not sure what that means.
s/js/JS/
Comment #34
penyaskitoRemoved 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.
Comment #35
Wim LeersOh, 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.Comment #36
nod_Since the dependencies are kind of backwards if we use
Drupal.locale
, let's get rid ofDrupal.locale
and usedrupalTranslations
everywhere. Needs to be added in .eslintrc as a global too.Add drupalTranslations to the closure like it's done with drupalSettings.
Comment #37
nod_Comment #38
penyaskitoI'm going to work on that.-
Comment #39
penyaskitoComment #40
penyaskitoComment #41
penyaskitoArgh, forgot to rebase my branch against 8.0.x
Comment #48
penyaskitoForgotten
.eslintrc
as requested in #36.Comment #49
Wim LeersShould
LOCALE_PLURAL_DELIMITER
anddrupalSettings.locale.pluralDelimiter
also be moved tolocale
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).
Comment #50
Mark_L6n CreditAttribution: Mark_L6n commentedI'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!
Comment #51
Wim LeersAs the title indicates: the order in which files are loaded in HEAD is wrong, hence
Drupal.t()
doesn't find the translated strings.Comment #52
Mark_L6n CreditAttribution: Mark_L6n commentedComment #53
Mark_L6n CreditAttribution: Mark_L6n commentedComment #54
Wim LeersMark: thanks! :)
Comment #55
YesCT CreditAttribution: YesCT commentedI 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."
Comment #56
Mark_L6n CreditAttribution: Mark_L6n commentedNo 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.
Comment #57
Mark_L6n CreditAttribution: Mark_L6n commentedComment #58
penyaskito#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.
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.
Comment #59
Mark_L6n CreditAttribution: Mark_L6n commentedComment #60
Mark_L6n CreditAttribution: Mark_L6n commentedComment #61
Mark_L6n CreditAttribution: Mark_L6n commentedComment #62
penyaskitoComment #63
Wim LeersSo #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:
Comment #64
nod_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.
Need to add a couple of
typeof drupalTranslations !== 'undefined'
in drupal.js.Once that's fixed, RTBC for me.
Comment #65
Gábor Hojtsywhen there are no translations, the whole thing crash sounds bad :/ in what way?
Comment #66
nod_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.
Comment #67
nod_Comment #68
penyaskitoThanks @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.
Comment #71
Gábor Hojtsy@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.
Comment #72
penyaskitorgrep 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 atdrupal.js
(withoutlocale
namespace).Comment #73
Wim Leers#72: only in
core/lib/Drupal/Core/StringTranslation/TranslationManager.php
, really. The single occurrence insystem.module
is precisely the one that should be moved tolocale.module
. But indeed, because of English sites, that's easier said than done.Comment #74
penyaskitoThere 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.
Comment #75
Wim LeersSorry for not being clear: I agree, we should not block this issue on the ideal place for the plural delimiter.
Comment #76
penyaskitoSo attached a patch that moves
drupalSettings.locale.pluralDelimiter
todrupalSettings.pluralDelimiter
now that we removedDrupal.locale
.For the backend part we will create a different issue, as it shouldn't block a major.
Comment #77
Wim Leers#76 interdiff looks good. Patch looks good. I'll leave RTBC'ing this to Gábor — I did too much work on this issue.
Comment #78
Gábor HojtsyLooks good to me.
Comment #79
alexpottThis 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!
Comment #81
Gábor HojtsySuperb, thanks @WimLeers, @penyaskito, @nod_, @webflo!
Comment #82
YesCT CreditAttribution: YesCT commenteddid the issue #76 mentions we need get created?
Comment #84
penyaskito@YesCT: No, AFAIR it wasn't :(
I'll try to do that tomorrow if noone beats me to do it.
Comment #85
penyaskitoTomorrow is always too late. This already happened on #2570107: Make format_plural() return a PluralTranslatableString object to remove reliance on a static, unpredictable safe list