From c344148315693d6592283b67e232e8609169c2f9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 26 Nov 2024 10:14:07 +0000 Subject: [PATCH] I18N: Load translations just-in-time for custom themes and plugins. In #34114, just-in-time (JIT) translation loading was implemented for projects hosted on WordPress.org. This is now expanded to all other plugins/themes. Projects with a custom `Text Domain` and `Domain Path` header no longer need to call `load_plugin_textdomain()` or `load_theme_textdomain()`. This reduces the risk of calling them too late, after some translation calls already happened, and generally makes it easier to properly internationalize a plugin or theme. This moves the `get_plugin_data()` from `wp-admin/includes/plugin.php` to `wp-includes/functions.php` so it's available during the plugin loading process. Props swissspidy. Fixes #62244. git-svn-id: https://develop.svn.wordpress.org/trunk@59461 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/plugin.php | 215 ------------------ src/wp-includes/functions.php | 215 ++++++++++++++++++ .../class-wp-rest-templates-controller.php | 2 +- src/wp-settings.php | 16 +- 4 files changed, 231 insertions(+), 217 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index de1468352b3d9..0969f956577ed 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -6,221 +6,6 @@ * @subpackage Administration */ -/** - * Parses the plugin contents to retrieve plugin's metadata. - * - * All plugin headers must be on their own line. Plugin description must not have - * any newlines, otherwise only parts of the description will be displayed. - * The below is formatted for printing. - * - * /* - * Plugin Name: Name of the plugin. - * Plugin URI: The home page of the plugin. - * Description: Plugin description. - * Author: Plugin author's name. - * Author URI: Link to the author's website. - * Version: Plugin version. - * Text Domain: Optional. Unique identifier, should be same as the one used in - * load_plugin_textdomain(). - * Domain Path: Optional. Only useful if the translations are located in a - * folder above the plugin's base path. For example, if .mo files are - * located in the locale folder then Domain Path will be "/locale/" and - * must have the first slash. Defaults to the base folder the plugin is - * located in. - * Network: Optional. Specify "Network: true" to require that a plugin is activated - * across all sites in an installation. This will prevent a plugin from being - * activated on a single site when Multisite is enabled. - * Requires at least: Optional. Specify the minimum required WordPress version. - * Requires PHP: Optional. Specify the minimum required PHP version. - * * / # Remove the space to close comment. - * - * The first 8 KB of the file will be pulled in and if the plugin data is not - * within that first 8 KB, then the plugin author should correct their plugin - * and move the plugin data headers to the top. - * - * The plugin file is assumed to have permissions to allow for scripts to read - * the file. This is not checked however and the file is only opened for - * reading. - * - * @since 1.5.0 - * @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers. - * @since 5.8.0 Added support for `Update URI` header. - * @since 6.5.0 Added support for `Requires Plugins` header. - * - * @param string $plugin_file Absolute path to the main plugin file. - * @param bool $markup Optional. If the returned data should have HTML markup applied. - * Default true. - * @param bool $translate Optional. If the returned data should be translated. Default true. - * @return array { - * Plugin data. Values will be empty if not supplied by the plugin. - * - * @type string $Name Name of the plugin. Should be unique. - * @type string $PluginURI Plugin URI. - * @type string $Version Plugin version. - * @type string $Description Plugin description. - * @type string $Author Plugin author's name. - * @type string $AuthorURI Plugin author's website address (if set). - * @type string $TextDomain Plugin textdomain. - * @type string $DomainPath Plugin's relative directory path to .mo files. - * @type bool $Network Whether the plugin can only be activated network-wide. - * @type string $RequiresWP Minimum required version of WordPress. - * @type string $RequiresPHP Minimum required version of PHP. - * @type string $UpdateURI ID of the plugin for update purposes, should be a URI. - * @type string $RequiresPlugins Comma separated list of dot org plugin slugs. - * @type string $Title Title of the plugin and link to the plugin's site (if set). - * @type string $AuthorName Plugin author's name. - * } - */ -function get_plugin_data( $plugin_file, $markup = true, $translate = true ) { - - $default_headers = array( - 'Name' => 'Plugin Name', - 'PluginURI' => 'Plugin URI', - 'Version' => 'Version', - 'Description' => 'Description', - 'Author' => 'Author', - 'AuthorURI' => 'Author URI', - 'TextDomain' => 'Text Domain', - 'DomainPath' => 'Domain Path', - 'Network' => 'Network', - 'RequiresWP' => 'Requires at least', - 'RequiresPHP' => 'Requires PHP', - 'UpdateURI' => 'Update URI', - 'RequiresPlugins' => 'Requires Plugins', - // Site Wide Only is deprecated in favor of Network. - '_sitewide' => 'Site Wide Only', - ); - - $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' ); - - // Site Wide Only is the old header for Network. - if ( ! $plugin_data['Network'] && $plugin_data['_sitewide'] ) { - /* translators: 1: Site Wide Only: true, 2: Network: true */ - _deprecated_argument( __FUNCTION__, '3.0.0', sprintf( __( 'The %1$s plugin header is deprecated. Use %2$s instead.' ), 'Site Wide Only: true', 'Network: true' ) ); - $plugin_data['Network'] = $plugin_data['_sitewide']; - } - $plugin_data['Network'] = ( 'true' === strtolower( $plugin_data['Network'] ) ); - unset( $plugin_data['_sitewide'] ); - - // If no text domain is defined fall back to the plugin slug. - if ( ! $plugin_data['TextDomain'] ) { - $plugin_slug = dirname( plugin_basename( $plugin_file ) ); - if ( '.' !== $plugin_slug && ! str_contains( $plugin_slug, '/' ) ) { - $plugin_data['TextDomain'] = $plugin_slug; - } - } - - if ( $markup || $translate ) { - $plugin_data = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup, $translate ); - } else { - $plugin_data['Title'] = $plugin_data['Name']; - $plugin_data['AuthorName'] = $plugin_data['Author']; - } - - return $plugin_data; -} - -/** - * Sanitizes plugin data, optionally adds markup, optionally translates. - * - * @since 2.7.0 - * - * @see get_plugin_data() - * - * @access private - * - * @param string $plugin_file Path to the main plugin file. - * @param array $plugin_data An array of plugin data. See get_plugin_data(). - * @param bool $markup Optional. If the returned data should have HTML markup applied. - * Default true. - * @param bool $translate Optional. If the returned data should be translated. Default true. - * @return array Plugin data. Values will be empty if not supplied by the plugin. - * See get_plugin_data() for the list of possible values. - */ -function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup = true, $translate = true ) { - - // Sanitize the plugin filename to a WP_PLUGIN_DIR relative path. - $plugin_file = plugin_basename( $plugin_file ); - - // Translate fields. - if ( $translate ) { - $textdomain = $plugin_data['TextDomain']; - if ( $textdomain ) { - if ( ! is_textdomain_loaded( $textdomain ) ) { - if ( $plugin_data['DomainPath'] ) { - load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) . $plugin_data['DomainPath'] ); - } else { - load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) ); - } - } - } elseif ( 'hello.php' === basename( $plugin_file ) ) { - $textdomain = 'default'; - } - if ( $textdomain ) { - foreach ( array( 'Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version' ) as $field ) { - if ( ! empty( $plugin_data[ $field ] ) ) { - // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain - $plugin_data[ $field ] = translate( $plugin_data[ $field ], $textdomain ); - } - } - } - } - - // Sanitize fields. - $allowed_tags_in_links = array( - 'abbr' => array( 'title' => true ), - 'acronym' => array( 'title' => true ), - 'code' => true, - 'em' => true, - 'strong' => true, - ); - - $allowed_tags = $allowed_tags_in_links; - $allowed_tags['a'] = array( - 'href' => true, - 'title' => true, - ); - - /* - * Name is marked up inside tags. Don't allow these. - * Author is too, but some plugins have used here (omitting Author URI). - */ - $plugin_data['Name'] = wp_kses( $plugin_data['Name'], $allowed_tags_in_links ); - $plugin_data['Author'] = wp_kses( $plugin_data['Author'], $allowed_tags ); - - $plugin_data['Description'] = wp_kses( $plugin_data['Description'], $allowed_tags ); - $plugin_data['Version'] = wp_kses( $plugin_data['Version'], $allowed_tags ); - - $plugin_data['PluginURI'] = esc_url( $plugin_data['PluginURI'] ); - $plugin_data['AuthorURI'] = esc_url( $plugin_data['AuthorURI'] ); - - $plugin_data['Title'] = $plugin_data['Name']; - $plugin_data['AuthorName'] = $plugin_data['Author']; - - // Apply markup. - if ( $markup ) { - if ( $plugin_data['PluginURI'] && $plugin_data['Name'] ) { - $plugin_data['Title'] = '' . $plugin_data['Name'] . ''; - } - - if ( $plugin_data['AuthorURI'] && $plugin_data['Author'] ) { - $plugin_data['Author'] = '' . $plugin_data['Author'] . ''; - } - - $plugin_data['Description'] = wptexturize( $plugin_data['Description'] ); - - if ( $plugin_data['Author'] ) { - $plugin_data['Description'] .= sprintf( - /* translators: %s: Plugin author. */ - ' ' . __( 'By %s.' ) . '', - $plugin_data['Author'] - ); - } - } - - return $plugin_data; -} - /** * Gets a list of a plugin's files. * diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index d9303083bfaa9..a4cfa13cd20dc 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -6929,6 +6929,221 @@ function get_file_data( $file, $default_headers, $context = '' ) { return $all_headers; } +/** + * Parses the plugin contents to retrieve plugin's metadata. + * + * All plugin headers must be on their own line. Plugin description must not have + * any newlines, otherwise only parts of the description will be displayed. + * The below is formatted for printing. + * + * /* + * Plugin Name: Name of the plugin. + * Plugin URI: The home page of the plugin. + * Description: Plugin description. + * Author: Plugin author's name. + * Author URI: Link to the author's website. + * Version: Plugin version. + * Text Domain: Optional. Unique identifier, should be same as the one used in + * load_plugin_textdomain(). + * Domain Path: Optional. Only useful if the translations are located in a + * folder above the plugin's base path. For example, if .mo files are + * located in the locale folder then Domain Path will be "/locale/" and + * must have the first slash. Defaults to the base folder the plugin is + * located in. + * Network: Optional. Specify "Network: true" to require that a plugin is activated + * across all sites in an installation. This will prevent a plugin from being + * activated on a single site when Multisite is enabled. + * Requires at least: Optional. Specify the minimum required WordPress version. + * Requires PHP: Optional. Specify the minimum required PHP version. + * * / # Remove the space to close comment. + * + * The first 8 KB of the file will be pulled in and if the plugin data is not + * within that first 8 KB, then the plugin author should correct their plugin + * and move the plugin data headers to the top. + * + * The plugin file is assumed to have permissions to allow for scripts to read + * the file. This is not checked however and the file is only opened for + * reading. + * + * @since 1.5.0 + * @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers. + * @since 5.8.0 Added support for `Update URI` header. + * @since 6.5.0 Added support for `Requires Plugins` header. + * + * @param string $plugin_file Absolute path to the main plugin file. + * @param bool $markup Optional. If the returned data should have HTML markup applied. + * Default true. + * @param bool $translate Optional. If the returned data should be translated. Default true. + * @return array { + * Plugin data. Values will be empty if not supplied by the plugin. + * + * @type string $Name Name of the plugin. Should be unique. + * @type string $PluginURI Plugin URI. + * @type string $Version Plugin version. + * @type string $Description Plugin description. + * @type string $Author Plugin author's name. + * @type string $AuthorURI Plugin author's website address (if set). + * @type string $TextDomain Plugin textdomain. + * @type string $DomainPath Plugin's relative directory path to .mo files. + * @type bool $Network Whether the plugin can only be activated network-wide. + * @type string $RequiresWP Minimum required version of WordPress. + * @type string $RequiresPHP Minimum required version of PHP. + * @type string $UpdateURI ID of the plugin for update purposes, should be a URI. + * @type string $RequiresPlugins Comma separated list of dot org plugin slugs. + * @type string $Title Title of the plugin and link to the plugin's site (if set). + * @type string $AuthorName Plugin author's name. + * } + */ +function get_plugin_data( $plugin_file, $markup = true, $translate = true ) { + + $default_headers = array( + 'Name' => 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Description' => 'Description', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'UpdateURI' => 'Update URI', + 'RequiresPlugins' => 'Requires Plugins', + // Site Wide Only is deprecated in favor of Network. + '_sitewide' => 'Site Wide Only', + ); + + $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' ); + + // Site Wide Only is the old header for Network. + if ( ! $plugin_data['Network'] && $plugin_data['_sitewide'] ) { + /* translators: 1: Site Wide Only: true, 2: Network: true */ + _deprecated_argument( __FUNCTION__, '3.0.0', sprintf( __( 'The %1$s plugin header is deprecated. Use %2$s instead.' ), 'Site Wide Only: true', 'Network: true' ) ); + $plugin_data['Network'] = $plugin_data['_sitewide']; + } + $plugin_data['Network'] = ( 'true' === strtolower( $plugin_data['Network'] ) ); + unset( $plugin_data['_sitewide'] ); + + // If no text domain is defined fall back to the plugin slug. + if ( ! $plugin_data['TextDomain'] ) { + $plugin_slug = dirname( plugin_basename( $plugin_file ) ); + if ( '.' !== $plugin_slug && ! str_contains( $plugin_slug, '/' ) ) { + $plugin_data['TextDomain'] = $plugin_slug; + } + } + + if ( $markup || $translate ) { + $plugin_data = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup, $translate ); + } else { + $plugin_data['Title'] = $plugin_data['Name']; + $plugin_data['AuthorName'] = $plugin_data['Author']; + } + + return $plugin_data; +} + +/** + * Sanitizes plugin data, optionally adds markup, optionally translates. + * + * @since 2.7.0 + * + * @see get_plugin_data() + * + * @access private + * + * @param string $plugin_file Path to the main plugin file. + * @param array $plugin_data An array of plugin data. See get_plugin_data(). + * @param bool $markup Optional. If the returned data should have HTML markup applied. + * Default true. + * @param bool $translate Optional. If the returned data should be translated. Default true. + * @return array Plugin data. Values will be empty if not supplied by the plugin. + * See get_plugin_data() for the list of possible values. + */ +function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup = true, $translate = true ) { + + // Sanitize the plugin filename to a WP_PLUGIN_DIR relative path. + $plugin_file = plugin_basename( $plugin_file ); + + // Translate fields. + if ( $translate ) { + $textdomain = $plugin_data['TextDomain']; + if ( $textdomain ) { + if ( ! is_textdomain_loaded( $textdomain ) ) { + if ( $plugin_data['DomainPath'] ) { + load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) . $plugin_data['DomainPath'] ); + } else { + load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) ); + } + } + } elseif ( 'hello.php' === basename( $plugin_file ) ) { + $textdomain = 'default'; + } + if ( $textdomain ) { + foreach ( array( 'Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version' ) as $field ) { + if ( ! empty( $plugin_data[ $field ] ) ) { + // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain + $plugin_data[ $field ] = translate( $plugin_data[ $field ], $textdomain ); + } + } + } + } + + // Sanitize fields. + $allowed_tags_in_links = array( + 'abbr' => array( 'title' => true ), + 'acronym' => array( 'title' => true ), + 'code' => true, + 'em' => true, + 'strong' => true, + ); + + $allowed_tags = $allowed_tags_in_links; + $allowed_tags['a'] = array( + 'href' => true, + 'title' => true, + ); + + /* + * Name is marked up inside tags. Don't allow these. + * Author is too, but some plugins have used here (omitting Author URI). + */ + $plugin_data['Name'] = wp_kses( $plugin_data['Name'], $allowed_tags_in_links ); + $plugin_data['Author'] = wp_kses( $plugin_data['Author'], $allowed_tags ); + + $plugin_data['Description'] = wp_kses( $plugin_data['Description'], $allowed_tags ); + $plugin_data['Version'] = wp_kses( $plugin_data['Version'], $allowed_tags ); + + $plugin_data['PluginURI'] = esc_url( $plugin_data['PluginURI'] ); + $plugin_data['AuthorURI'] = esc_url( $plugin_data['AuthorURI'] ); + + $plugin_data['Title'] = $plugin_data['Name']; + $plugin_data['AuthorName'] = $plugin_data['Author']; + + // Apply markup. + if ( $markup ) { + if ( $plugin_data['PluginURI'] && $plugin_data['Name'] ) { + $plugin_data['Title'] = '' . $plugin_data['Name'] . ''; + } + + if ( $plugin_data['AuthorURI'] && $plugin_data['Author'] ) { + $plugin_data['Author'] = '' . $plugin_data['Author'] . ''; + } + + $plugin_data['Description'] = wptexturize( $plugin_data['Description'] ); + + if ( $plugin_data['Author'] ) { + $plugin_data['Description'] .= sprintf( + /* translators: %s: Plugin author. */ + ' ' . __( 'By %s.' ) . '', + $plugin_data['Author'] + ); + } + } + + return $plugin_data; +} + /** * Returns true. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 43780fb4e677b..267f40e77fd0f 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -872,7 +872,7 @@ private static function get_wp_templates_author_text_field( $template_object ) { $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); return empty( $theme_name ) ? $template_object->theme : $theme_name; case 'plugin': - if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'get_plugin_data' ) ) { + if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( isset( $template_object->plugin ) ) { diff --git a/src/wp-settings.php b/src/wp-settings.php index 62ba4d5dee6ca..98ab4d68d0efc 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -534,8 +534,19 @@ * @param string $plugin Full path to the plugin's main file. */ do_action( 'plugin_loaded', $plugin ); + + $plugin_data = get_plugin_data( $plugin, false, false ); + + $textdomain = $plugin_data['TextDomain']; + if ( $textdomain ) { + if ( $plugin_data['DomainPath'] ) { + $GLOBALS['wp_textdomain_registry']->set_custom_path( $textdomain, dirname( $plugin ) . $plugin_data['DomainPath'] ); + } else { + $GLOBALS['wp_textdomain_registry']->set_custom_path( $textdomain, dirname( $plugin ) ); + } + } } -unset( $plugin, $_wp_plugin_file ); +unset( $plugin, $_wp_plugin_file, $plugin_data, $textdomain ); // Load pluggable functions. require ABSPATH . WPINC . '/pluggable.php'; @@ -671,6 +682,9 @@ if ( file_exists( $theme . '/functions.php' ) ) { include $theme . '/functions.php'; } + + $theme = wp_get_theme( basename( $theme ) ); + $theme->load_textdomain(); } unset( $theme );