-
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
base: trunk
Are you sure you want to change the base?
Changes from all commits
e23d4f2
7057417
40bbbf2
e23c08a
1a0d9ca
05c6016
de8090f
44a1f56
eb78a18
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 | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -81,6 +81,17 @@ class WP_Script_Modules { | |||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| private $modules_with_missing_dependencies = array(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Holds translation data for script modules, keyed by script module identifier. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * Each entry contains 'domain' and 'path' keys for the text domain | ||||||||||||||||||||||||||||||||||||||||||||
| * and the path to translation files respectively. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * @since 7.0.0 | ||||||||||||||||||||||||||||||||||||||||||||
| * @var array<string, array{domain: string, path: string}> | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| private $translations = array(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Registers the script module if no script module with that script module | ||||||||||||||||||||||||||||||||||||||||||||
| * identifier has already been registered. | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -328,6 +339,74 @@ public function deregister( string $id ) { | |||||||||||||||||||||||||||||||||||||||||||
| unset( $this->registered[ $id ] ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Sets translated strings for a script module. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * Works similar to {@see WP_Scripts::set_translations()} but for script modules. | ||||||||||||||||||||||||||||||||||||||||||||
| * The translations will be loaded and output as inline scripts before | ||||||||||||||||||||||||||||||||||||||||||||
| * the script modules are printed, calling `wp.i18n.setLocaleData()`. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * @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 ] ) ) { | ||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| $this->translations[ $id ] = array( | ||||||||||||||||||||||||||||||||||||||||||||
| 'domain' => $domain, | ||||||||||||||||||||||||||||||||||||||||||||
| 'path' => $path, | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Prints translations for all enqueued script modules that have translations set. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * 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. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * @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 ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| foreach ( $module_ids as $id ) { | ||||||||||||||||||||||||||||||||||||||||||||
| if ( ! isset( $this->translations[ $id ] ) ) { | ||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| $domain = $this->translations[ $id ]['domain']; | ||||||||||||||||||||||||||||||||||||||||||||
| $path = $this->translations[ $id ]['path']; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| $json_translations = load_script_module_textdomain( $id, $domain, $path ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if ( ! $json_translations ) { | ||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| $output = <<<JS | ||||||||||||||||||||||||||||||||||||||||||||
| ( ( domain, translations ) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const localeData = translations.locale_data[ domain ] || translations.locale_data.messages; | ||||||||||||||||||||||||||||||||||||||||||||
| localeData[""].domain = domain; | ||||||||||||||||||||||||||||||||||||||||||||
| wp.i18n.setLocaleData( localeData, domain ); | ||||||||||||||||||||||||||||||||||||||||||||
| } )( "{$domain}", {$json_translations} ); | ||||||||||||||||||||||||||||||||||||||||||||
| JS; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+396
to
+403
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. Here's an alternative I tested which fixes the issue with indenting the closing HEREDOC/NOWDOC identifier. The issue is that the contents need to be indented the same, and PHP strips out the indentation from the string:
Suggested change
This is also improved to use a NOWDOC instead of HEREDOC, which is better since it avoids accidental interpolation. Plugin Check, for example, disallows NOWDOC but not HEREDOC: WordPress/plugin-check#1036 This also addresses various static analysis issues flagged by PHPStorm, and it's much easier to read. Before:
After:
|
||||||||||||||||||||||||||||||||||||||||||||
| $source_url = rawurlencode( "{$id}-js-module-translations" ); | ||||||||||||||||||||||||||||||||||||||||||||
| $output .= "\n//# sourceURL={$source_url}"; | ||||||||||||||||||||||||||||||||||||||||||||
| wp_print_inline_script_tag( $output, array( 'id' => "{$id}-js-module-translations" ) ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Adds the hooks to print the import map, enqueued script modules and script | ||||||||||||||||||||||||||||||||||||||||||||
| * module preloads. | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -359,6 +438,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 ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -840,6 +928,22 @@ private function sort_item_dependencies( string $id, array $import_types, array | |||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Gets the raw source URL for a registered script module. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * Returns the source URL without version query string appended. | ||||||||||||||||||||||||||||||||||||||||||||
| * This is used by {@see load_script_module_textdomain()} to determine | ||||||||||||||||||||||||||||||||||||||||||||
| * the relative path for loading translation files. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * @since 7.0.0 | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * @param string $id The script module identifier. | ||||||||||||||||||||||||||||||||||||||||||||
| * @return string|null The script module source URL, or null if not registered. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| public function get_registered_src( string $id ): ?string { | ||||||||||||||||||||||||||||||||||||||||||||
| return $this->registered[ $id ]['src'] ?? null; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Gets the versioned URL for a script module src. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1280,6 +1280,151 @@ function load_script_textdomain( $handle, $domain = 'default', $path = '' ) { | |||||||||
| return load_script_translations( false, $handle, $domain ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Loads the translation data for a given script module ID and text domain. | ||||||||||
| * | ||||||||||
| * Works like {@see load_script_textdomain()} but for script modules registered | ||||||||||
| * via {@see wp_register_script_module()}. | ||||||||||
| * | ||||||||||
| * @since 7.0.0 | ||||||||||
| * | ||||||||||
|
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.
Suggested change
|
||||||||||
| * @param string $id The script module identifier. | ||||||||||
| * @param string $domain Optional. Text domain. Default 'default'. | ||||||||||
| * @param string $path Optional. The full file path to the directory containing translation files. | ||||||||||
| * @return string|false The JSON-encoded translated strings for the given script module and text domain. | ||||||||||
| * False if there are none. | ||||||||||
| */ | ||||||||||
| function load_script_module_textdomain( $id, $domain = 'default', $path = '' ) { | ||||||||||
|
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.
Suggested change
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. Since this function is almost identical with |
||||||||||
| /** @var WP_Textdomain_Registry $wp_textdomain_registry */ | ||||||||||
|
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. PhpStorm is flagging this as being redundant:
Suggested change
|
||||||||||
| global $wp_textdomain_registry; | ||||||||||
|
|
||||||||||
| $src = wp_script_modules()->get_registered_src( $id ); | ||||||||||
|
|
||||||||||
| if ( null === $src ) { | ||||||||||
| return false; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| $locale = determine_locale(); | ||||||||||
|
|
||||||||||
| if ( ! $path ) { | ||||||||||
| $path = $wp_textdomain_registry->get( $domain, $locale ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| $path = untrailingslashit( $path ); | ||||||||||
|
|
||||||||||
| // If a path was given and the handle file exists simply return it. | ||||||||||
| $file_base = 'default' === $domain ? $locale : $domain . '-' . $locale; | ||||||||||
| $handle_filename = $file_base . '-' . $id . '.json'; | ||||||||||
|
|
||||||||||
| if ( $path ) { | ||||||||||
| $translations = load_script_translations( $path . '/' . $handle_filename, $id, $domain ); | ||||||||||
|
|
||||||||||
| if ( $translations ) { | ||||||||||
| return $translations; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Ensure src is an absolute URL for path resolution. | ||||||||||
| if ( ! preg_match( '|^(https?:)?//|', $src ) ) { | ||||||||||
| $src = site_url( $src ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| $relative = false; | ||||||||||
| $languages_path = WP_LANG_DIR; | ||||||||||
|
|
||||||||||
| $src_url = wp_parse_url( $src ); | ||||||||||
| $content_url = wp_parse_url( content_url() ); | ||||||||||
| $plugins_url = wp_parse_url( plugins_url() ); | ||||||||||
| $site_url = wp_parse_url( site_url() ); | ||||||||||
| $theme_root = get_theme_root(); | ||||||||||
|
|
||||||||||
| // If the host is the same or it's a relative URL. | ||||||||||
| if ( | ||||||||||
| ( ! isset( $content_url['path'] ) || str_starts_with( $src_url['path'], $content_url['path'] ) ) && | ||||||||||
| ( ! isset( $src_url['host'] ) || ! isset( $content_url['host'] ) || $src_url['host'] === $content_url['host'] ) | ||||||||||
| ) { | ||||||||||
| // Make the src relative the specific plugin or theme. | ||||||||||
| if ( isset( $content_url['path'] ) ) { | ||||||||||
| $relative = substr( $src_url['path'], strlen( $content_url['path'] ) ); | ||||||||||
| } else { | ||||||||||
| $relative = $src_url['path']; | ||||||||||
| } | ||||||||||
| $relative = trim( $relative, '/' ); | ||||||||||
| $relative = explode( '/', $relative ); | ||||||||||
|
|
||||||||||
| $theme_dir = array_slice( explode( '/', $theme_root ), -1 ); | ||||||||||
| $dirname = $theme_dir[0] === $relative[0] ? 'themes' : 'plugins'; | ||||||||||
|
|
||||||||||
| $languages_path = WP_LANG_DIR . '/' . $dirname; | ||||||||||
|
|
||||||||||
| $relative = array_slice( $relative, 2 ); // Remove plugins/<plugin name> or themes/<theme name>. | ||||||||||
| $relative = implode( '/', $relative ); | ||||||||||
| } elseif ( | ||||||||||
| ( ! isset( $plugins_url['path'] ) || str_starts_with( $src_url['path'], $plugins_url['path'] ) ) && | ||||||||||
| ( ! isset( $src_url['host'] ) || ! isset( $plugins_url['host'] ) || $src_url['host'] === $plugins_url['host'] ) | ||||||||||
| ) { | ||||||||||
| // Make the src relative the specific plugin. | ||||||||||
| if ( isset( $plugins_url['path'] ) ) { | ||||||||||
| $relative = substr( $src_url['path'], strlen( $plugins_url['path'] ) ); | ||||||||||
| } else { | ||||||||||
| $relative = $src_url['path']; | ||||||||||
| } | ||||||||||
| $relative = trim( $relative, '/' ); | ||||||||||
| $relative = explode( '/', $relative ); | ||||||||||
|
|
||||||||||
| $languages_path = WP_LANG_DIR . '/plugins'; | ||||||||||
|
|
||||||||||
| $relative = array_slice( $relative, 1 ); // Remove <plugin name>. | ||||||||||
| $relative = implode( '/', $relative ); | ||||||||||
| } elseif ( ! isset( $src_url['host'] ) || ! isset( $site_url['host'] ) || $src_url['host'] === $site_url['host'] ) { | ||||||||||
| if ( ! isset( $site_url['path'] ) ) { | ||||||||||
| $relative = trim( $src_url['path'], '/' ); | ||||||||||
| } elseif ( str_starts_with( $src_url['path'], trailingslashit( $site_url['path'] ) ) ) { | ||||||||||
| // Make the src relative to the WP root. | ||||||||||
| $relative = substr( $src_url['path'], strlen( $site_url['path'] ) ); | ||||||||||
| $relative = trim( $relative, '/' ); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Filters the relative path of script module source used for finding translation files. | ||||||||||
| * | ||||||||||
| * @since 7.0.0 | ||||||||||
| * | ||||||||||
| * @param string|false $relative The relative path of the script module source. False if it could not be determined. | ||||||||||
| * @param string $src The full source URL of the script module. | ||||||||||
| */ | ||||||||||
| $relative = apply_filters( 'load_script_module_textdomain_relative_path', $relative, $src ); | ||||||||||
|
|
||||||||||
| // If the source is not from WP. | ||||||||||
| if ( false === $relative ) { | ||||||||||
| return load_script_translations( false, $id, $domain ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Translations are always based on the unminified filename. | ||||||||||
| if ( str_ends_with( $relative, '.min.js' ) ) { | ||||||||||
| $relative = substr( $relative, 0, -7 ) . '.js'; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| $md5_filename = $file_base . '-' . md5( $relative ) . '.json'; | ||||||||||
|
|
||||||||||
| if ( $path ) { | ||||||||||
| $translations = load_script_translations( $path . '/' . $md5_filename, $id, $domain ); | ||||||||||
|
|
||||||||||
| if ( $translations ) { | ||||||||||
| return $translations; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| $translations = load_script_translations( $languages_path . '/' . $md5_filename, $id, $domain ); | ||||||||||
|
|
||||||||||
| if ( $translations ) { | ||||||||||
| return $translations; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return load_script_translations( false, $id, $domain ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Loads the translation data for the given script handle and text domain. | ||||||||||
| * | ||||||||||
|
|
||||||||||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A PHP 7.4 feature we can use.