From f46fbae857efbb941d7901f322627eec2c3e370e Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Fri, 14 Dec 2018 05:52:52 +0000 Subject: [PATCH] I18N: Add JavaScript translation support. Adds the `wp_set_script_translations()` function which registers translations for a JavaScript file. This function takes a handle, domain and optionally a path and ensures JavaScript translation files are loaded if they exist. Merges [43825,43828,43859,43898] from the 5.0 branch to trunk. Props herregroen, atimmer, omarreiss, nerrad, swissspidy, ocean90, georgestephanis. Fixes #45103, #45256. Built from https://develop.svn.wordpress.org/trunk@44169 git-svn-id: http://core.svn.wordpress.org/trunk@43999 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/class-wp-dependency.php | 24 ++++++++ wp-includes/class.wp-scripts.php | 68 +++++++++++++++++++++- wp-includes/functions.wp-scripts.php | 26 +++++++++ wp-includes/l10n.php | 85 ++++++++++++++++++++++++++++ wp-includes/version.php | 2 +- 5 files changed, 203 insertions(+), 2 deletions(-) diff --git a/wp-includes/class-wp-dependency.php b/wp-includes/class-wp-dependency.php index d14928f60b..cea4de5507 100644 --- a/wp-includes/class-wp-dependency.php +++ b/wp-includes/class-wp-dependency.php @@ -67,6 +67,22 @@ class _WP_Dependency { */ public $extra = array(); + /** + * Translation textdomain set for this dependency. + * + * @since 5.0.0 + * @var string + */ + public $textdomain; + + /** + * Translation path set for this dependency. + * + * @since 5.0.0 + * @var string + */ + public $translations_path; + /** * Setup dependencies. * @@ -96,4 +112,12 @@ class _WP_Dependency { return true; } + public function set_translations( $domain, $path = null ) { + if ( ! is_string( $domain ) ) { + return false; + } + $this->textdomain = $domain; + $this->translations_path = $path; + return true; + } } diff --git a/wp-includes/class.wp-scripts.php b/wp-includes/class.wp-scripts.php index 6f8e4f9a8c..42374815d0 100644 --- a/wp-includes/class.wp-scripts.php +++ b/wp-includes/class.wp-scripts.php @@ -330,6 +330,11 @@ class WP_Scripts extends WP_Dependencies { return true; } + $translations = $this->print_translations( $handle, false ); + if ( $translations ) { + $translations = sprintf( "\n", $translations ); + } + if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $this->content_url && 0 === strpos( $src, $this->content_url ) ) ) { $src = $this->base_url . $src; } @@ -345,7 +350,7 @@ class WP_Scripts extends WP_Dependencies { return true; } - $tag = "{$cond_before}{$before_handle}\n{$after_handle}{$cond_after}"; + $tag = "{$translations}{$cond_before}{$before_handle}\n{$after_handle}{$cond_after}"; /** * Filters the HTML script tag of an enqueued script. @@ -490,6 +495,67 @@ class WP_Scripts extends WP_Dependencies { return parent::set_group( $handle, $recursion, $grp ); } + /** + * Sets a translation textdomain. + * + * @since 5.0.0 + * + * @param string $handle Name of the script to register a translation domain to. + * @param string $domain The textdomain. + * @param string $path Optional. The full file path to the directory containing translation files. + * + * @return bool True if the textdomain was registered, false if not. + */ + public function set_translations( $handle, $domain, $path = null ) { + if ( ! isset( $this->registered[ $handle ] ) ) { + return false; + } + + /** @var \_WP_Dependency $obj */ + $obj = $this->registered[ $handle ]; + + if ( ! in_array( 'wp-i18n', $obj->deps, true ) ) { + $obj->deps[] = 'wp-i18n'; + } + return $obj->set_translations( $domain, $path ); + } + + /** + * Prints translations set for a specific handle. + * + * @since 5.0.0 + * + * @param string $handle Name of the script to add the inline script to. Must be lowercase. + * @param bool $echo Optional. Whether to echo the script instead of just returning it. + * Default true. + * @return string|false Script on success, false otherwise. + */ + public function print_translations( $handle, $echo = true ) { + if ( ! isset( $this->registered[ $handle ] ) || empty( $this->registered[ $handle ]->textdomain ) ) { + return false; + } + + $domain = $this->registered[ $handle ]->textdomain; + $path = $this->registered[ $handle ]->translations_path; + + $json_translations = load_script_textdomain( $handle, $domain, $path ); + + if ( ! $json_translations ) { + // Register empty locale data object to ensure the domain still exists. + $json_translations = '{ "locale_data": { "messages": { "": {} } } }'; + } + + $output = '(function( translations ){' . + 'wp.i18n.setLocaleData( translations.locale_data, "' . $domain . '" );' . + '})(' . $json_translations . ');'; + + if ( $echo ) { + printf( "\n", $output ); + } + + return $output; + } + /** * Determines script dependencies. * diff --git a/wp-includes/functions.wp-scripts.php b/wp-includes/functions.wp-scripts.php index 3fdf33974c..af86eddb96 100644 --- a/wp-includes/functions.wp-scripts.php +++ b/wp-includes/functions.wp-scripts.php @@ -200,6 +200,32 @@ function wp_localize_script( $handle, $object_name, $l10n ) { return $wp_scripts->localize( $handle, $object_name, $l10n ); } +/** + * Sets translated strings for a script. + * + * Works only if the script has already been added. + * + * @see WP_Scripts::set_translations() + * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. + * + * @since 5.0.0 + * + * @param string $handle Script handle the textdomain will be attached to. + * @param string $domain The textdomain. + * @param string $path Optional. The full file path to the directory containing translation files. + * + * @return bool True if the textdomain was successfully localized, false otherwise. + */ +function wp_set_script_translations( $handle, $domain, $path = null ) { + global $wp_scripts; + if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { + _wp_scripts_maybe_doing_it_wrong( __FUNCTION__ ); + return false; + } + + return $wp_scripts->set_translations( $handle, $domain, $path ); +} + /** * Remove a registered script. * diff --git a/wp-includes/l10n.php b/wp-includes/l10n.php index 312ae31f90..3705f46f23 100644 --- a/wp-includes/l10n.php +++ b/wp-includes/l10n.php @@ -893,6 +893,91 @@ function load_child_theme_textdomain( $domain, $path = false ) { return load_theme_textdomain( $domain, $path ); } +/** + * Load the script translated strings. + * + * @see WP_Scripts::set_translations() + * @link https://core.trac.wordpress.org/ticket/45103 + * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. + * + * @since 5.0.0 + * + * @param string $handle Name of the script to register a translation domain to. + * @param string $domain The textdomain. + * @param string $path Optional. The full file path to the directory containing translation files. + * + * @return false|string False if the script textdomain could not be loaded, the translated strings + * in JSON encoding otherwise. + */ +function load_script_textdomain( $handle, $domain, $path = null ) { + global $wp_scripts; + + $path = untrailingslashit( $path ); + $locale = is_admin() ? get_locale() : get_user_locale(); + + // If a path was given and the handle file exists simply return it. + $file_base = $domain === 'default' ? $locale : $domain . '-' . $locale; + $handle_filename = $file_base . '-' . $handle . '.json'; + if ( $path && file_exists( $path . '/' . $handle_filename ) ) { + return file_get_contents( $path . '/' . $handle_filename ); + } + + $obj = $wp_scripts->registered[ $handle ]; + + /** This filter is documented in wp-includes/class.wp-scripts.php */ + $src = esc_url( apply_filters( 'script_loader_src', $obj->src, $handle ) ); + + $relative = false; + $languages_path = WP_LANG_DIR; + + $src_url = wp_parse_url( $src ); + $content_url = wp_parse_url( content_url() ); + $site_url = wp_parse_url( site_url() ); + + // If the host is the same or it's a relative URL. + if ( + strpos( $src_url['path'], $content_url['path'] ) === 0 && + ( ! isset( $src_url['host'] ) || $src_url['host'] !== $content_url['host'] ) + ) { + // Make the src relative the specific plugin or theme. + $relative = trim( substr( $src, strlen( $content_url['path'] ) ), '/' ); + $relative = explode( '/', $relative ); + + $languages_path = WP_LANG_DIR . '/' . $relative[0]; + + $relative = array_slice( $relative, 2 ); + $relative = implode( '/', $relative ); + } elseif ( ! isset( $src_url['host'] ) || $src_url['host'] !== $site_url['host'] ) { + if ( ! isset( $site_url['path'] ) ) { + $relative = trim( $src_url['path'], '/' ); + } elseif ( ( strpos( $src_url['path'], $site_url['path'] ) === 0 ) ) { + // Make the src relative to the WP root. + $relative = substr( $src, strlen( $site_url['path'] ) ); + $relative = trim( $relative, '/' ); + } + } + + // If the source is not from WP. + if ( false === $relative ) { + return false; + } + + // Translations are always based on the unminified filename. + if ( substr( $relative, -7 ) === '.min.js' ) { + $relative = substr( $relative, 0, -7 ) . '.js'; + } + + $md5_filename = $file_base . '-' . md5( $relative ) . '.json'; + if ( $path && file_exists( $path . '/' . $md5_filename ) ) { + return file_get_contents( $path . '/' . $md5_filename ); + } + if ( file_exists( $languages_path . '/' . $md5_filename ) ) { + return file_get_contents( $languages_path . '/' . $md5_filename ); + } + + return false; +} + /** * Loads plugin and theme textdomains just-in-time. * diff --git a/wp-includes/version.php b/wp-includes/version.php index cf8842ff7e..a68d0c917e 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @global string $wp_version */ -$wp_version = '5.1-alpha-44168'; +$wp_version = '5.1-alpha-44169'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.