Issue description generated with the help of an LLM. Debug and solution is brain-made.

Problem/Motivation

If a JavaScript translation file (assets://languages/[langcode]_[hash].js) is deleted from disk, it is not regenerated. locale_js_translate() only rebuilds it when new JS files are parsed or when caches are rebuilt. Until then, every page that references it tries to aggregate a file that no longer exists.

In a basic/standard setup, this produces a warning:

file_get_contents(assets://languages/es_bujkrUn1sDCUHrPFyAbdcHT8QVedCwVJoD_ufmvGZSo.js): Failed to open stream...

But after #3592731: JavaScript translation files are now stored in the assets:// path location it causes real problems on multi-head setups that do not share the assets directory between nodes:

  • The translation file only exists on the node that generated it.
  • The other nodes still have the file name in state (locale.translation.javascript), so they try to include it in the aggregate. That fails with a file_get_contents() warning and produces an aggregate with a different hash.
  • Each node then 301-redirects requests for the other node's aggregate hash to its own. Behind a CDN/Varnish with consistent hashing, this can turn into a redirect loop.

Steps to reproduce

  1. Enable JS aggregation on a site with a non-English interface language (Spanish in this example).
  2. Remove the generated files:
    rm -rf web/sites/default/files/languages/* web/sites/default/files/js/*
  3. In a new browser tab, open DevTools > Network, filter by js_ and load a page.
  4. The listed js_* files match the ones on disk, and the translation file is created:
    $ ls -lh web/sites/default/files/languages/ web/sites/default/files/js/*js
    -rw-rw-r-- 1 jonhattan jonhattan 209K nov  5 09:47 web/sites/default/files/js/js_dpq2jaQYyc0E0lAkfz8VMOHuUcWV1ydluMqj3E2H0oc.js
    -rw-rw-r-- 1 jonhattan jonhattan  61K nov  5 09:47 web/sites/default/files/js/js_rYjaONBsQxBUG-dL6VjVZhQrnQsmAZCFrdznvaSIH34.js
    -rw-rw-r-- 1 jonhattan jonhattan  233 nov  5 09:47 web/sites/default/files/js/js_U-381Wzwm6zvcOob9_ZT5wCYdBaZQ581ih9PoHQPHGk.js
    -rw-rw-r-- 1 jonhattan jonhattan  34K nov  5 09:47 web/sites/default/files/js/js_vxuQTvLZNYQJJ9dttg2FoukK8aPaYWNnLFJmFOBl-8E.js
    
    web/sites/default/files/languages/:
    -rw-rw-r-- 1 jonhattan jonhattan 19K nov  5 09:47 es_bujkrUn1sDCUHrPFyAbdcHT8QVedCwVJoD_ufmvGZSo.js

    Watchdog:

    locale  Warning  JavaScript translation file bujkrUn1sDCUHrPFyAbdcHT8QVedCwVJoD_ufmvGZSo.js was lost.
    locale  Notice   Created JavaScript translation file for the language Español.
  5. Remove the files again and repeat step 3 in another new tab, so the first result stays visible. This simulates a request hitting another node where neither the aggregates nor the translation file exist. Results:
    • The translation file is not regenerated.
    • A new aggregate with a different hash is created: js/js_cRxt1dBfhSiG3lzYl97YgGAq0DPWCQoY_2FBiVuVuGY.js
    • Watchdog:
      Warning: file_get_contents(assets://languages/es_bujkrUn1sDCUHrPFyAbdcHT8QVedCwVJoD_ufmvGZSo.js): Failed to open stream: "Drupal\Core\StreamWrapper\AssetsStream::stream_open" call failed in Drupal\Core\Asset\JsOptimizer->optimize() (line 41 of core/lib/Drupal/Core/Asset/JsOptimizer.php).
  6. Request the URL of the first aggregate. Drupal redirects it to the new hash:
    $ curl -I "https://example.com/sites/default/files/js/js_dpq2jaQYyc0E0lAkfz8VMOHuUcWV1ydluMqj3E2H0oc.js?scope=footer&delta=1&language=es&theme=eada&include=eJxl..."
    HTTP/2 301
    location: /sites/default/files/js/js_cRxt1dBfhSiG3lzYl97YgGAq0DPWCQoY_2FBiVuVuGY.js?scope=footer&delta=1&language=es&theme=eada&include=eJxl...

    On a multi-head setup, every node does this with its own hash.

Proposed resolution

In locale_js_translate() regenerate the file is it is supposed to exist and is not present on disk

Remaining tasks

  • Open an MR with the fix.
  • Review.

User interface changes

None.

API changes

None.

Data model changes

None.

Release notes snippet

N/A

Issue fork drupal-3635501

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

jonhattan created an issue. See original summary.

jonhattan’s picture

Issue summary: View changes

jonhattan’s picture

Assigned: jonhattan » Unassigned
Status: Active » Needs review
StatusFileSize
new1.31 KB

Patch for 11.4

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new1.26 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

jonhattan’s picture

Status: Needs work » Needs review
pfrenssen’s picture

Tested on main with a throwaway functional test. It translates a string in locale.admin.js, loads the page, deletes the translation file and loads the page again.

  1. With a warm JS asset cache, the file is not recreated, with or without the MR. jsTranslate() runs from hook_js_alter(), only on a cache miss in AssetResolver::getJsAssets(). A second web server that shares the cache gets a cache hit.
  2. With a cold asset cache, the MR recreates the file. Without the MR, it does not.
  3. LocaleJs.php:87 invalidates all languages when the current language's file is missing.
  4. The MR has no test. LocaleTranslationUiTest::testJavaScriptTranslation() deletes the file but calls rebuild() directly.
  5. On main, the locale/translations library aggregate returns 400 "Invalid filename.", also when the file exists. JsAssetController::getGroups() passes only the placeholder to jsAlter(), so jsTranslate() returns NULL.

The Nightwatch failure is a logout timeout in machineNameTransliterationTest.js. Earlier pipelines passed Nightwatch.

AI disclosure: Used AI to search for edge cases that are not covered by the patch, and to write this comment.