1212 * Core class used to register script modules.
1313 *
1414 * @since 6.5.0
15+ *
16+ * @phpstan-type ScriptModule array{
17+ * src: string,
18+ * version: string|false|null,
19+ * dependencies: array<int, array{ id: string, import: 'static'|'dynamic' }>,
20+ * in_footer: bool,
21+ * fetchpriority: 'auto'|'low'|'high',
22+ * textdomain?: string,
23+ * translations_path?: string,
24+ * }
1525 */
1626class WP_Script_Modules {
1727 /**
1828 * Holds the registered script modules, keyed by script module identifier.
1929 *
2030 * @since 6.5.0
2131 * @var array<string, array<string, mixed>>
32+ * @phpstan-var array<string, ScriptModule>
2233 */
2334 private $ registered = array ();
2435
@@ -328,6 +339,87 @@ public function deregister( string $id ) {
328339 unset( $ this ->registered [ $ id ] );
329340 }
330341
342+ /**
343+ * Overrides the text domain and path used to load translations for a script module.
344+ *
345+ * This is only needed for modules whose text domain differs from 'default'
346+ * or whose translation files live outside the standard locations, for
347+ * example plugin modules that register their own text domain. Translations
348+ * for modules that use the default domain are loaded automatically by
349+ * {@see WP_Script_Modules::print_script_module_translations()}.
350+ *
351+ * @since 7.0.0
352+ *
353+ * @param string $id The identifier of the script module.
354+ * @param string $domain Optional. Text domain. Default 'default'.
355+ * @param string $path Optional. The full file path to the directory containing translation files.
356+ * @return bool True if the text domain was registered, false if the module is not registered.
357+ */
358+ public function set_translations ( string $ id , string $ domain = 'default ' , string $ path = '' ): bool {
359+ if ( ! isset ( $ this ->registered [ $ id ] ) ) {
360+ return false ;
361+ }
362+
363+ $ this ->registered [ $ id ]['textdomain ' ] = $ domain ;
364+ $ this ->registered [ $ id ]['translations_path ' ] = $ path ;
365+
366+ return true ;
367+ }
368+
369+ /**
370+ * Prints translations for all enqueued script modules.
371+ *
372+ * Outputs inline `<script>` tags that call `wp.i18n.setLocaleData()` with
373+ * the translated strings for each script module. This must run before
374+ * the script modules execute.
375+ *
376+ * Auto-detects the text domain and translation path for each module from
377+ * its source URL. Modules whose text domain or path differs from the
378+ * defaults can opt into a specific domain/path via
379+ * {@see WP_Script_Modules::set_translations()}.
380+ *
381+ * @since 7.0.0
382+ */
383+ public function print_script_module_translations (): void {
384+ // Collect all module IDs that will be on the page (enqueued + their dependencies).
385+ $ module_ids = $ this ->get_sorted_dependencies ( $ this ->queue );
386+
387+ $ set_locale_data_js_function = <<<'JS'
388+ ( domain, translations ) => {
389+ const localeData = translations.locale_data[ domain ] || translations.locale_data.messages;
390+ localeData[""].domain = domain;
391+ wp.i18n.setLocaleData( localeData, domain );
392+ }
393+ JS;
394+
395+ foreach ( $ module_ids as $ id ) {
396+ $ domain = $ this ->registered [ $ id ]['textdomain ' ] ?? 'default ' ;
397+ $ path = $ this ->registered [ $ id ]['translations_path ' ] ?? '' ;
398+
399+ $ json_translations = load_script_module_textdomain ( $ id , $ domain , $ path );
400+
401+ if ( ! $ json_translations ) {
402+ continue ;
403+ }
404+
405+ $ output = sprintf (
406+ '( %s )( %s, %s ); ' ,
407+ $ set_locale_data_js_function ,
408+ wp_json_encode ( $ domain , JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
409+ $ json_translations
410+ );
411+ $ script_id = "wp-script-module-translation-data- {$ id }" ;
412+ $ output .= "\n//# sourceURL= " . rawurlencode ( $ script_id );
413+
414+ // Ensure wp-i18n is printed; the inline script below relies on wp.i18n.setLocaleData().
415+ if ( ! wp_script_is ( 'wp-i18n ' , 'done ' ) ) {
416+ wp_scripts ()->do_items ( array ( 'wp-i18n ' ) );
417+ }
418+
419+ wp_print_inline_script_tag ( $ output , array ( 'id ' => $ script_id ) );
420+ }
421+ }
422+
331423 /**
332424 * Adds the hooks to print the import map, enqueued script modules and script
333425 * module preloads.
@@ -359,6 +451,15 @@ public function add_hooks() {
359451 add_action ( 'admin_print_footer_scripts ' , array ( $ this , 'print_enqueued_script_modules ' ) );
360452 add_action ( 'admin_print_footer_scripts ' , array ( $ this , 'print_script_module_preloads ' ) );
361453
454+ /*
455+ * Print translations after classic scripts like wp-i18n are loaded (at
456+ * priority 10 via _wp_footer_scripts), but before the script modules
457+ * execute. Script modules with type="module" are deferred by default,
458+ * so inline translation scripts at priority 11 will execute before them.
459+ */
460+ add_action ( 'wp_footer ' , array ( $ this , 'print_script_module_translations ' ), 21 );
461+ add_action ( 'admin_print_footer_scripts ' , array ( $ this , 'print_script_module_translations ' ), 11 );
462+
362463 add_action ( 'wp_footer ' , array ( $ this , 'print_script_module_data ' ) );
363464 add_action ( 'admin_print_footer_scripts ' , array ( $ this , 'print_script_module_data ' ) );
364465 add_action ( 'wp_footer ' , array ( $ this , 'print_a11y_script_module_html ' ), 20 );
@@ -631,6 +732,7 @@ private function get_import_map(): array {
631732 * @since 6.5.0
632733 *
633734 * @return array<string, array<string, mixed>> Script modules marked for enqueue, keyed by script module identifier.
735+ * @phpstan-return array<string, ScriptModule>
634736 */
635737 private function get_marked_for_enqueue (): array {
636738 return wp_array_slice_assoc (
@@ -652,6 +754,7 @@ private function get_marked_for_enqueue(): array {
652754 * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
653755 * Default is both.
654756 * @return array<string, array<string, mixed>> List of dependencies, keyed by script module identifier.
757+ * @phpstan-return array<string, ScriptModule>
655758 */
656759 private function get_dependencies ( array $ ids , array $ import_types = array ( 'static ' , 'dynamic ' ) ): array {
657760 $ all_dependencies = array ();
@@ -840,6 +943,19 @@ private function sort_item_dependencies( string $id, array $import_types, array
840943 return true ;
841944 }
842945
946+ /**
947+ * Gets the data for a registered script module.
948+ *
949+ * @since 7.0.0
950+ *
951+ * @param string $id The script module identifier.
952+ * @return array|null The script module data, or null if not registered.
953+ * @phpstan-return ScriptModule|null
954+ */
955+ public function get_registered ( string $ id ): ?array {
956+ return $ this ->registered [ $ id ] ?? null ;
957+ }
958+
843959 /**
844960 * Gets the versioned URL for a script module src.
845961 *
0 commit comments