Skip to content

I18N: Print translations for the script modules that classic scripts depend on - #14260

Draft
jorgefilipecosta wants to merge 2 commits into
WordPress:trunkfrom
jorgefilipecosta:fix/classic-script-module-translations
Draft

jorgefilipecosta wants to merge 2 commits into
WordPress:trunkfrom
jorgefilipecosta:fix/classic-script-module-translations

Conversation

@jorgefilipecosta

@jorgefilipecosta jorgefilipecosta commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Right now WP_Script_Modules::print_script_module_translations() only prints the translations of the enqueued script modules and their dependencies. Since WordPress 7.0, a classic script can also declare the script modules it imports in its module_dependencies data, and get_import_map() adds those script modules and their dependencies to the import map. So a script module that only a classic script imports loads fine, but its translations are never printed, and its strings show in English even when the language pack translates them.

This PR:

  • Moves the walk over the classic scripts' module_dependencies out of get_import_map() into a private get_classic_script_module_dependencies() method, so the import map and the translations always cover the same script modules.
  • Makes print_script_module_translations() also print the translations of the script modules that classic scripts depend on, and of their dependencies. A script module that is both enqueued and a classic script's dependency is printed once.
  • Makes the _doing_it_wrong() notice for missing script modules fire once per classic script. The walk already runs twice per page, from print_import_map() and print_script_module_data(), so the notice fired twice, and the new caller would make it three times.

Some notes:

Before

The test page of the e2e test in the Gutenberg backport, where one line comes from an enqueued script module and the other from a script module that only a classic script imports:

The test page on trunk: the line written by the enqueued script module ends with "(translated)", the line written by the script module that a classic script imports does not.

After

The test page with this PR: both lines end with "(translated)".

Testing Instructions

  1. If Gutenberg is active, deactivate it, since Script Modules: Fix missing translations of script modules gutenberg#84651 fixes the same issue in the plugin.

  2. Save this snippet as wp-content/mu-plugins/module-translations-test.php (src/wp-content/mu-plugins/ in a wordpress-develop checkout). It registers a test/hello script module that only a classic script imports, gives it a translation, and makes it show __( 'Hello' ) in an alert on the Dashboard:

    <?php
    add_action( 'load-index.php', function () {
    	wp_register_script_module( 'test/hello', 'data:text/javascript,alert(wp.i18n.__("Hello"))', array(), null );
    	wp_register_script( 'test-classic', false, array( 'wp-i18n' ), false, array( 'in_footer' => true, 'module_dependencies' => array( 'test/hello' ) ) );
    	wp_add_inline_script( 'test-classic', 'addEventListener("DOMContentLoaded",()=>import("test/hello"))' );
    	wp_enqueue_script( 'test-classic' );
    } );
    add_filter( 'pre_load_script_translations', function ( $translations, $file, $handle ) {
    	return 'test/hello' === $handle ? '{"locale_data":{"messages":{"":{},"Hello":["Hello (translated)"]}}}' : $translations;
    }, 10, 3 );
    
  3. Open the Dashboard. On trunk, the alert says "Hello". With this PR, it says "Hello (translated)".

  4. Delete the snippet and run npm run test:php -- --group script-modules.

Gutenberg backport: WordPress/gutenberg#84651.

Trac ticket: TBD

Use of AI Tools

AI assistance: Yes
Used for: drafting the fix, the tests and this description; reviewed and edited by me.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

…ies into a helper.

Moves the code that collects the script module dependencies of classic scripts out of `WP_Script_Modules::get_import_map()` into a private `get_classic_script_module_dependencies()` method, so other methods can use the same list. There is no change in behavior.
…depend on.

`WP_Script_Modules::print_script_module_translations()` only covered the enqueued script modules and their dependencies. A script module that only a classic script imports, declared in the classic script's `module_dependencies`, is added to the import map and loads, but its translations were never printed, so its strings showed untranslated.

The method now also covers the script module dependencies of classic scripts and their dependencies: the same script modules the import map adds for classic scripts, collected by the same helper. A script module that is both enqueued and imported by a classic script gets its translations printed once, and a missing script module dependency still triggers a single `_doing_it_wrong()` notice.
@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@manzoorwanijk manzoorwanijk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me.

Comment on lines +399 to +411
/*
* Collect the IDs of the script modules that can load on the page: the enqueued ones, the ones that classic
* scripts depend on, and the dependencies of both.
*/
$classic_script_module_dependencies = $this->get_classic_script_module_dependencies();

$module_ids = array_unique(
array_merge(
$this->get_sorted_dependencies( $this->queue ),
$classic_script_module_dependencies,
array_keys( $this->get_dependencies( $classic_script_module_dependencies ) )
)
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print_script_module_data() already builds "every module that can load" from the queue plus the import map keys:

public function print_script_module_data(): void {
$modules = array();
foreach ( array_unique( $this->queue ) as $id ) {
if ( '@wordpress/a11y' === $id ) {
$this->a11y_available = true;
}
$modules[ $id ] = true;
}
foreach ( array_keys( $this->get_import_map()['imports'] ) as $id ) {
if ( '@wordpress/a11y' === $id ) {
$this->a11y_available = true;
}
$modules[ $id ] = true;
}

The same works here and gives the same set: the import map is the classic script module dependencies plus get_dependencies() of the queue and of those, so only the queue itself needs adding. The one difference is a module whose filtered src is '', which cannot load anyway. This keeps the import map, data and translations in lockstep by construction, and the get_classic_script_module_dependencies() extraction is no longer needed (the once-per-handle notice guard still is, since get_import_map() would then run three times).

Suggested change
/*
* Collect the IDs of the script modules that can load on the page: the enqueued ones, the ones that classic
* scripts depend on, and the dependencies of both.
*/
$classic_script_module_dependencies = $this->get_classic_script_module_dependencies();
$module_ids = array_unique(
array_merge(
$this->get_sorted_dependencies( $this->queue ),
$classic_script_module_dependencies,
array_keys( $this->get_dependencies( $classic_script_module_dependencies ) )
)
);
/*
* Collect the IDs of the script modules that can load on the page: the enqueued ones and their dependencies,
* and the ones in the import map, which includes the script modules that classic scripts depend on.
*/
$module_ids = array_unique(
array_merge(
$this->get_sorted_dependencies( $this->queue ),
array_keys( $this->get_import_map()['imports'] )
)
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think the biggest issue after we have this PR is that there are two loops over all reachable modules, print_script_module_translations and print_script_module_data, which should loop exactly the same way, but they don't. get_sorted_dependencies is used inconsistently: one loop has, the other doesn't. And the classic dependencies are not sorted at all, although they should be.

get_import_data loops differently, but not differently enough to have a completely custom code. It subtracts the already queued modules and it also skips modules with src=''. The additional src='' check, maybe it should be a validity check that applies to all modules? I don't think we support inline modules.

Unifying the translations and data behaviors in important, even a blocker, get_import_data can remain custom.

* @since 7.2.0
* @var string[]
*/
private $classic_scripts_with_missing_module_dependencies = array();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nit: let's call these just "scripts", not "classic scripts". Everything else in the codebase calls them "scripts". The WP_Scripts class, functions like wp_register_script, actions like admin_print_scripts, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants