-
Notifications
You must be signed in to change notification settings - Fork 3.4k
I18N: Add translation support for script modules #11543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 38 commits
e23d4f2
7057417
40bbbf2
e23c08a
1a0d9ca
05c6016
de8090f
44a1f56
eb78a18
cee8f0c
c3380cc
2be0c57
28cfe1c
1354cb0
918490c
38dd31a
7aad84d
a146cf8
555f309
d360841
db4106a
da7e67a
7d734d9
33b7f3b
cfd6f50
c374a89
1e9d62f
1eaaa51
63e70d4
07fbf49
f8bd260
8c14a86
e9345df
d3f1691
fc8c957
e112a15
e5b32e7
f4d4e69
37a6b84
0cd71d3
5b592e7
3a0a32d
11c37c2
374a234
89a25ea
3d99f00
0185911
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,13 +12,23 @@ | |||||||||||
| * Core class used to register script modules. | ||||||||||||
| * | ||||||||||||
| * @since 6.5.0 | ||||||||||||
| * | ||||||||||||
| * @phpstan-type ScriptModule array{ | ||||||||||||
| * src: string, | ||||||||||||
| * version: string|false|null, | ||||||||||||
| * dependencies: array<int, array{ id: string, import: 'static'|'dynamic' }>, | ||||||||||||
| * in_footer: bool, | ||||||||||||
| * fetchpriority: 'auto'|'low'|'high', | ||||||||||||
| * translations?: array{ textdomain: string, path: string }, | ||||||||||||
| * } | ||||||||||||
| */ | ||||||||||||
| class WP_Script_Modules { | ||||||||||||
| /** | ||||||||||||
| * Holds the registered script modules, keyed by script module identifier. | ||||||||||||
| * | ||||||||||||
| * @since 6.5.0 | ||||||||||||
| * @var array<string, array<string, mixed>> | ||||||||||||
| * @phpstan-var array<string, ScriptModule> | ||||||||||||
| */ | ||||||||||||
| private $registered = array(); | ||||||||||||
|
|
||||||||||||
|
|
@@ -328,6 +338,86 @@ public function deregister( string $id ) { | |||||||||||
| unset( $this->registered[ $id ] ); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Overrides the text domain and path used to load translations for a script module. | ||||||||||||
| * | ||||||||||||
| * This is only needed for modules whose text domain differs from 'default' | ||||||||||||
| * or whose translation files live outside the standard locations, for | ||||||||||||
| * example plugin modules that register their own text domain. Translations | ||||||||||||
| * for modules that use the default domain are loaded automatically by | ||||||||||||
| * {@see WP_Script_Modules::print_script_module_translations()}. | ||||||||||||
| * | ||||||||||||
| * @since 7.0.0 | ||||||||||||
| * | ||||||||||||
| * @param string $id The identifier of the script module. | ||||||||||||
| * @param string $domain Optional. Text domain. Default 'default'. | ||||||||||||
| * @param string $path Optional. The full file path to the directory containing translation files. | ||||||||||||
| * @return bool True if the text domain was registered, false if the module is not registered. | ||||||||||||
| */ | ||||||||||||
| public function set_translations( string $id, string $domain = 'default', string $path = '' ): bool { | ||||||||||||
| if ( ! isset( $this->registered[ $id ] ) ) { | ||||||||||||
|
manzoorwanijk marked this conversation as resolved.
|
||||||||||||
| return false; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| $this->registered[ $id ]['translations'] = array( | ||||||||||||
| 'textdomain' => $domain, | ||||||||||||
| 'path' => $path, | ||||||||||||
| ); | ||||||||||||
|
|
||||||||||||
| return true; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Prints translations for all enqueued script modules. | ||||||||||||
| * | ||||||||||||
| * Outputs inline `<script>` tags that call `wp.i18n.setLocaleData()` with | ||||||||||||
| * the translated strings for each script module. This must run before | ||||||||||||
| * the script modules execute. | ||||||||||||
| * | ||||||||||||
| * Auto-detects the text domain and translation path for each module from | ||||||||||||
| * its source URL. Modules whose text domain or path differs from the | ||||||||||||
| * defaults can opt into a specific domain/path via | ||||||||||||
| * {@see WP_Script_Modules::set_translations()}. | ||||||||||||
| * | ||||||||||||
| * @since 7.0.0 | ||||||||||||
| */ | ||||||||||||
| public function print_script_module_translations(): void { | ||||||||||||
| // Collect all module IDs that will be on the page (enqueued + their dependencies). | ||||||||||||
| $module_ids = $this->get_sorted_dependencies( $this->queue ); | ||||||||||||
|
|
||||||||||||
| $set_locale_data_js_function = <<<'JS' | ||||||||||||
| ( domain, translations ) => { | ||||||||||||
| if ( ! window.wp?.i18n?.setLocaleData ) { | ||||||||||||
| return; | ||||||||||||
| } | ||||||||||||
|
manzoorwanijk marked this conversation as resolved.
Outdated
|
||||||||||||
| const localeData = translations.locale_data[ domain ] || translations.locale_data.messages; | ||||||||||||
| localeData[""].domain = domain; | ||||||||||||
| wp.i18n.setLocaleData( localeData, domain ); | ||||||||||||
| } | ||||||||||||
| JS; | ||||||||||||
|
|
||||||||||||
| foreach ( $module_ids as $id ) { | ||||||||||||
| $domain = $this->registered[ $id ]['translations']['textdomain'] ?? 'default'; | ||||||||||||
| $path = $this->registered[ $id ]['translations']['path'] ?? ''; | ||||||||||||
|
|
||||||||||||
| $json_translations = load_script_module_textdomain( $id, $domain, $path ); | ||||||||||||
|
|
||||||||||||
| if ( ! $json_translations ) { | ||||||||||||
| continue; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| $output = sprintf( | ||||||||||||
| '( %s )( %s, %s );', | ||||||||||||
| $set_locale_data_js_function, | ||||||||||||
| wp_json_encode( $domain ), | ||||||||||||
|
manzoorwanijk marked this conversation as resolved.
Outdated
|
||||||||||||
| $json_translations | ||||||||||||
| ); | ||||||||||||
| $source_url = rawurlencode( "wp-script-module-translation-data-{$id}" ); | ||||||||||||
| $output .= "\n//# sourceURL={$source_url}"; | ||||||||||||
| wp_print_inline_script_tag( $output, array( 'id' => "wp-script-module-translation-data-{$id}" ) ); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible at this point that the
Suggested change
With that, then the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PR tries to prevent this by making sure that the Maybe we should throw an error or print a warning when
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Priority ordering alone isn't sufficient: it only guarantees we run after
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This gets interesting 🙂 The original version of In that original version But should we really call I see two solutions that are less intrusive:
FYI @sirreal who does a lot of work in the Interactivity API and script module area and might have some insights.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I fully agree. The reality is that the script module here does have a dependency on the wp_enqueue_script( 'wp-i18n' );
wp_enqueue_script_module( 'foo' );
wp_set_script_module_translations( 'foo', 'foo' );If they forget to enqueue Also, this seems to have the effect of cementing the dependency in code. If we are able to improve script module i18n to eliminate the need for the Disclaimer: I'm not an expert on how i18n works.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that makes. The dependency is real, and fail-safe just turns a debuggable missing-script into a silent "English on every translated site." The contamination concern mostly dissolves once you trace it: no
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That brings up a more basic question: when there are import { __ } from '@wordpress/i18n';But we don't have that native What I'd like to point out is that the <script type="module" id="wp-script-module-translations-foo">
import { setLocaleData } from '@wordpress/i18n';
setLocaleData( { fooTranslations } );
</script>So, OK, let's auto-enqueue the
Yes, this is a great point. If we auto-enqueue the classic script in Core, we can change that later, again in Core. Instead of having a big number of published plugins that do the same on their own, and updating very slowly or not at all. I'm convinced 🙂
This is another sign how temporary and improvised the classic script dependencies really are.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Glad we converged 🙂 Reverting to the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in 3a0a32d
manzoorwanijk marked this conversation as resolved.
Outdated
|
||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Adds the hooks to print the import map, enqueued script modules and script | ||||||||||||
| * module preloads. | ||||||||||||
|
|
@@ -359,6 +449,15 @@ public function add_hooks() { | |||||||||||
| add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) ); | ||||||||||||
| add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) ); | ||||||||||||
|
|
||||||||||||
| /* | ||||||||||||
| * Print translations after classic scripts like wp-i18n are loaded (at | ||||||||||||
| * priority 10 via _wp_footer_scripts), but before the script modules | ||||||||||||
| * execute. Script modules with type="module" are deferred by default, | ||||||||||||
| * so inline translation scripts at priority 11 will execute before them. | ||||||||||||
| */ | ||||||||||||
| add_action( 'wp_footer', array( $this, 'print_script_module_translations' ), 21 ); | ||||||||||||
| add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_translations' ), 11 ); | ||||||||||||
|
|
||||||||||||
| add_action( 'wp_footer', array( $this, 'print_script_module_data' ) ); | ||||||||||||
| add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) ); | ||||||||||||
| add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 ); | ||||||||||||
|
|
@@ -631,6 +730,7 @@ private function get_import_map(): array { | |||||||||||
| * @since 6.5.0 | ||||||||||||
| * | ||||||||||||
| * @return array<string, array<string, mixed>> Script modules marked for enqueue, keyed by script module identifier. | ||||||||||||
| * @phpstan-return array<string, ScriptModule> | ||||||||||||
| */ | ||||||||||||
| private function get_marked_for_enqueue(): array { | ||||||||||||
| return wp_array_slice_assoc( | ||||||||||||
|
|
@@ -652,6 +752,7 @@ private function get_marked_for_enqueue(): array { | |||||||||||
| * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both. | ||||||||||||
| * Default is both. | ||||||||||||
| * @return array<string, array<string, mixed>> List of dependencies, keyed by script module identifier. | ||||||||||||
| * @phpstan-return array<string, ScriptModule> | ||||||||||||
| */ | ||||||||||||
| private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array { | ||||||||||||
| $all_dependencies = array(); | ||||||||||||
|
|
@@ -840,6 +941,19 @@ private function sort_item_dependencies( string $id, array $import_types, array | |||||||||||
| return true; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Gets the data for a registered script module. | ||||||||||||
| * | ||||||||||||
| * @since 7.0.0 | ||||||||||||
| * | ||||||||||||
| * @param string $id The script module identifier. | ||||||||||||
| * @return array|null The script module data, or null if not registered. | ||||||||||||
| * @phpstan-return ScriptModule|null | ||||||||||||
| */ | ||||||||||||
| public function get_registered( string $id ): ?array { | ||||||||||||
| return $this->registered[ $id ] ?? null; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Gets the versioned URL for a script module src. | ||||||||||||
| * | ||||||||||||
|
|
||||||||||||
Uh oh!
There was an error while loading. Please reload this page.