I18N: Introduce `WP_Textdomain_Registry` to store text domains and their language directory paths.

Previously, when using `switch_to_locale()` all current loaded text domains were unloaded and added to the `$l10n_unloaded` global. This prevented the just-in-time loading for text domains after a switch. The just-in-time loading was also only possible if the translations were stored in `WP_LANG_DIR`. Both issues have been fixed.

* Adds `WP_Textdomain_Registry` to keep track of the language directory paths for all plugins and themes.
* Updates all `load_*_textdomain()`  functions to store the path in `WP_Textdomain_Registry`.
* Adds `$reloadable` parameter to `unload_textdomain()` to define whether a text domain can be loaded just-in-time again. This is used by `WP_Locale_Switcher::load_translations()`.
* Extends `_load_textdomain_just_in_time()` to also support text domains of plugins and themes with custom language directories.
* Fixes the incorrect `test_plugin_translation_after_switching_locale_twice()` test which should have catch this issue earlier.
* Adds a new test plugin/theme to test the loading of translations with a custom language directory.
* Deprecates the now unused and private `_get_path_to_translation()` and `_get_path_to_translation_from_lang_dir()` functions.

Props yoavf, swissspidy, dd32, ocean90.
See #26511.
Fixes #39210.
Built from https://develop.svn.wordpress.org/trunk@49236


git-svn-id: http://core.svn.wordpress.org/trunk@48998 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Dominik Schilling 2020-10-20 16:05:07 +00:00
parent e2e3c8ec47
commit 173b9aa122
6 changed files with 290 additions and 97 deletions

View File

@ -196,11 +196,12 @@ class WP_Locale_Switcher {
load_default_textdomain( $locale ); load_default_textdomain( $locale );
foreach ( $domains as $domain ) { foreach ( $domains as $domain ) {
// The default text domain is handled by `load_default_textdomain()`.
if ( 'default' === $domain ) { if ( 'default' === $domain ) {
continue; continue;
} }
unload_textdomain( $domain ); unload_textdomain( $domain, true );
get_translations_for_domain( $domain ); get_translations_for_domain( $domain );
} }
} }
@ -218,12 +219,11 @@ class WP_Locale_Switcher {
* @param string $locale The locale to change to. * @param string $locale The locale to change to.
*/ */
private function change_locale( $locale ) { private function change_locale( $locale ) {
// Reset translation availability information. global $wp_locale;
_get_path_to_translation( null, true );
$this->load_translations( $locale ); $this->load_translations( $locale );
$GLOBALS['wp_locale'] = new WP_Locale(); $wp_locale = new WP_Locale();
/** /**
* Fires when the locale is switched to or restored. * Fires when the locale is switched to or restored.

View File

@ -0,0 +1,130 @@
<?php
/**
* Locale API: WP_Textdomain_Registry class
*
* @package WordPress
* @subpackage i18n
* @since 5.6.0
*/
/**
* Core class used for registering text domains.
*
* @since 5.6.0
*/
class WP_Textdomain_Registry {
/**
* List of domains and their language directory paths.
*
* @since 5.6.0
*
* @var array
*/
protected $domains = array();
/**
* Holds a cached list of available .mo files to improve performance.
*
* @since 5.6.0
*
* @var array
*/
protected $cached_mo_files;
/**
* Returns the MO file path for a specific domain.
*
* @since 5.6.0
*
* @param string $domain Text domain.
* @return string|false MO file path or false if there is none available.
*/
public function get( $domain ) {
if ( isset( $this->domains[ $domain ] ) ) {
return $this->domains[ $domain ];
}
return $this->get_path_from_lang_dir( $domain );
}
/**
* Sets the MO file path for a specific domain.
*
* @since 5.6.0
*
* @param string $domain Text domain.
* @param string|false $path Language directory path or false if there is none available.
*/
public function set( $domain, $path ) {
$this->domains[ $domain ] = $path ? trailingslashit( $path ) : false;
}
/**
* Resets the registry state.
*
* @since 5.6.0
*/
public function reset() {
$this->cached_mo_files = null;
$this->domains = array();
}
/**
* Gets the path to a translation file in the languages directory for the current locale.
*
* @since 5.6.0
*
* @param string $domain Text domain.
* @return string|false MO file path or false if there is none available.
*/
private function get_path_from_lang_dir( $domain ) {
if ( null === $this->cached_mo_files ) {
$this->cached_mo_files = array();
$this->set_cached_mo_files();
}
$locale = determine_locale();
$mofile = "{$domain}-{$locale}.mo";
$path = WP_LANG_DIR . '/plugins/' . $mofile;
if ( in_array( $path, $this->cached_mo_files, true ) ) {
$path = WP_LANG_DIR . '/plugins/';
$this->set( $domain, $path );
return $path;
}
$path = WP_LANG_DIR . '/themes/' . $mofile;
if ( in_array( $path, $this->cached_mo_files, true ) ) {
$path = WP_LANG_DIR . '/themes/';
$this->set( $domain, $path );
return $path;
}
$this->set( $domain, false );
return false;
}
/**
* Reads and caches all available MO files from the plugins and themes language directories.
*
* @since 5.6.0
*/
protected function set_cached_mo_files() {
$locations = array(
WP_LANG_DIR . '/plugins',
WP_LANG_DIR . '/themes',
);
foreach ( $locations as $location ) {
$mo_files = glob( $location . '/*.mo' );
if ( $mo_files ) {
$this->cached_mo_files = array_merge( $this->cached_mo_files, $mo_files );
}
}
}
}

View File

@ -4134,3 +4134,85 @@ function wp_slash_strings_only( $value ) {
function addslashes_strings_only( $value ) { function addslashes_strings_only( $value ) {
return is_string( $value ) ? addslashes( $value ) : $value; return is_string( $value ) ? addslashes( $value ) : $value;
} }
/**
* Gets the path to a translation file for loading a textdomain just in time.
*
* Caches the retrieved results internally.
*
* @since 4.7.0
* @deprecated 5.6.0
* @access private
*
* @see _load_textdomain_just_in_time()
*
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reset Whether to reset the internal cache. Used by the switch to locale functionality.
* @return string|false The path to the translation file or false if no translation file was found.
*/
function _get_path_to_translation( $domain, $reset = false ) {
_deprecated_function( __FUNCTION__, '5.6.0', 'WP_Textdomain_Registry' );
static $available_translations = array();
if ( true === $reset ) {
$available_translations = array();
}
if ( ! isset( $available_translations[ $domain ] ) ) {
$available_translations[ $domain ] = _get_path_to_translation_from_lang_dir( $domain );
}
return $available_translations[ $domain ];
}
/**
* Gets the path to a translation file in the languages directory for the current locale.
*
* Holds a cached list of available .mo files to improve performance.
*
* @since 4.7.0
* @deprecated 5.6.0
* @access private
*
* @see _get_path_to_translation()
*
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @return string|false The path to the translation file or false if no translation file was found.
*/
function _get_path_to_translation_from_lang_dir( $domain ) {
_deprecated_function( __FUNCTION__, '5.6.0', 'WP_Textdomain_Registry' );
static $cached_mofiles = null;
if ( null === $cached_mofiles ) {
$cached_mofiles = array();
$locations = array(
WP_LANG_DIR . '/plugins',
WP_LANG_DIR . '/themes',
);
foreach ( $locations as $location ) {
$mofiles = glob( $location . '/*.mo' );
if ( $mofiles ) {
$cached_mofiles = array_merge( $cached_mofiles, $mofiles );
}
}
}
$locale = determine_locale();
$mofile = "{$domain}-{$locale}.mo";
$path = WP_LANG_DIR . '/plugins/' . $mofile;
if ( in_array( $path, $cached_mofiles, true ) ) {
return $path;
}
$path = WP_LANG_DIR . '/themes/' . $mofile;
if ( in_array( $path, $cached_mofiles, true ) ) {
return $path;
}
return false;
}

View File

@ -691,13 +691,14 @@ function translate_nooped_plural( $nooped_plural, $count, $domain = 'default' )
* *
* @global MO[] $l10n An array of all currently loaded text domains. * @global MO[] $l10n An array of all currently loaded text domains.
* @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again. * @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again.
* @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $mofile Path to the .mo file. * @param string $mofile Path to the .mo file.
* @return bool True on success, false on failure. * @return bool True on success, false on failure.
*/ */
function load_textdomain( $domain, $mofile ) { function load_textdomain( $domain, $mofile ) {
global $l10n, $l10n_unloaded; global $l10n, $l10n_unloaded, $wp_textdomain_registry;
$l10n_unloaded = (array) $l10n_unloaded; $l10n_unloaded = (array) $l10n_unloaded;
@ -755,6 +756,9 @@ function load_textdomain( $domain, $mofile ) {
$l10n[ $domain ] = &$mo; $l10n[ $domain ] = &$mo;
/** @var WP_Textdomain_Registry $wp_textdomain_registry */
$wp_textdomain_registry->set( $domain, dirname( $mofile ) );
return true; return true;
} }
@ -762,14 +766,16 @@ function load_textdomain( $domain, $mofile ) {
* Unload translations for a text domain. * Unload translations for a text domain.
* *
* @since 3.0.0 * @since 3.0.0
* @since 5.6.0 Added the `$reloadable` parameter.
* *
* @global MO[] $l10n An array of all currently loaded text domains. * @global MO[] $l10n An array of all currently loaded text domains.
* @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again. * @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reloadable Whether the text domain can be loaded just-in-time again.
* @return bool Whether textdomain was unloaded. * @return bool Whether textdomain was unloaded.
*/ */
function unload_textdomain( $domain ) { function unload_textdomain( $domain, $reloadable = false ) {
global $l10n, $l10n_unloaded; global $l10n, $l10n_unloaded;
$l10n_unloaded = (array) $l10n_unloaded; $l10n_unloaded = (array) $l10n_unloaded;
@ -778,14 +784,18 @@ function unload_textdomain( $domain ) {
* Filters whether to override the text domain unloading. * Filters whether to override the text domain unloading.
* *
* @since 3.0.0 * @since 3.0.0
* @since 5.6.0 Added the `$reloadable` parameter.
* *
* @param bool $override Whether to override the text domain unloading. Default false. * @param bool $override Whether to override the text domain unloading. Default false.
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reloadable Whether the text domain can be loaded just-in-time again.
*/ */
$plugin_override = apply_filters( 'override_unload_textdomain', false, $domain ); $plugin_override = apply_filters( 'override_unload_textdomain', false, $domain, $reloadable );
if ( $plugin_override ) { if ( $plugin_override ) {
if ( ! $reloadable ) {
$l10n_unloaded[ $domain ] = true; $l10n_unloaded[ $domain ] = true;
}
return true; return true;
} }
@ -794,15 +804,19 @@ function unload_textdomain( $domain ) {
* Fires before the text domain is unloaded. * Fires before the text domain is unloaded.
* *
* @since 3.0.0 * @since 3.0.0
* @since 5.6.0 Added the `$reloadable` parameter.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reloadable Whether the text domain can be loaded just-in-time again.
*/ */
do_action( 'unload_textdomain', $domain ); do_action( 'unload_textdomain', $domain, $reloadable );
if ( isset( $l10n[ $domain ] ) ) { if ( isset( $l10n[ $domain ] ) ) {
unset( $l10n[ $domain ] ); unset( $l10n[ $domain ] );
if ( ! $reloadable ) {
$l10n_unloaded[ $domain ] = true; $l10n_unloaded[ $domain ] = true;
}
return true; return true;
} }
@ -867,6 +881,8 @@ function load_default_textdomain( $locale = null ) {
* @return bool True when textdomain is successfully loaded, false otherwise. * @return bool True when textdomain is successfully loaded, false otherwise.
*/ */
function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) { function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {
global $wp_textdomain_registry;
/** /**
* Filters a plugin's locale. * Filters a plugin's locale.
* *
@ -893,6 +909,9 @@ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path
$path = WP_PLUGIN_DIR; $path = WP_PLUGIN_DIR;
} }
/* @var WP_Textdomain_Registry $wp_textdomain_registry */
$wp_textdomain_registry->set( $domain, $path );
return load_textdomain( $domain, $path . '/' . $mofile ); return load_textdomain( $domain, $path . '/' . $mofile );
} }
@ -902,12 +921,16 @@ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path
* @since 3.0.0 * @since 3.0.0
* @since 4.6.0 The function now tries to load the .mo file from the languages directory first. * @since 4.6.0 The function now tries to load the .mo file from the languages directory first.
* *
* @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
*
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $mu_plugin_rel_path Optional. Relative to `WPMU_PLUGIN_DIR` directory in which the .mo * @param string $mu_plugin_rel_path Optional. Relative to `WPMU_PLUGIN_DIR` directory in which the .mo
* file resides. Default empty string. * file resides. Default empty string.
* @return bool True when textdomain is successfully loaded, false otherwise. * @return bool True when textdomain is successfully loaded, false otherwise.
*/ */
function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) { function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
global $wp_textdomain_registry;
/** This filter is documented in wp-includes/l10n.php */ /** This filter is documented in wp-includes/l10n.php */
$locale = apply_filters( 'plugin_locale', determine_locale(), $domain ); $locale = apply_filters( 'plugin_locale', determine_locale(), $domain );
@ -920,6 +943,9 @@ function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
$path = WPMU_PLUGIN_DIR . '/' . ltrim( $mu_plugin_rel_path, '/' ); $path = WPMU_PLUGIN_DIR . '/' . ltrim( $mu_plugin_rel_path, '/' );
/* @var WP_Textdomain_Registry $wp_textdomain_registry */
$wp_textdomain_registry->set( $domain, $path );
return load_textdomain( $domain, $path . '/' . $mofile ); return load_textdomain( $domain, $path . '/' . $mofile );
} }
@ -934,12 +960,16 @@ function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
* @since 1.5.0 * @since 1.5.0
* @since 4.6.0 The function now tries to load the .mo file from the languages directory first. * @since 4.6.0 The function now tries to load the .mo file from the languages directory first.
* *
* @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
*
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $path Optional. Path to the directory containing the .mo file. * @param string $path Optional. Path to the directory containing the .mo file.
* Default false. * Default false.
* @return bool True when textdomain is successfully loaded, false otherwise. * @return bool True when textdomain is successfully loaded, false otherwise.
*/ */
function load_theme_textdomain( $domain, $path = false ) { function load_theme_textdomain( $domain, $path = false ) {
global $wp_textdomain_registry;
/** /**
* Filters a theme's locale. * Filters a theme's locale.
* *
@ -961,6 +991,9 @@ function load_theme_textdomain( $domain, $path = false ) {
$path = get_template_directory(); $path = get_template_directory();
} }
/* @var WP_Textdomain_Registry $wp_textdomain_registry */
$wp_textdomain_registry->set( $domain, $path );
return load_textdomain( $domain, $path . '/' . $locale . '.mo' ); return load_textdomain( $domain, $path . '/' . $locale . '.mo' );
} }
@ -1190,14 +1223,14 @@ function load_script_translations( $file, $handle, $domain ) {
* @since 4.6.0 * @since 4.6.0
* @access private * @access private
* *
* @see get_translations_for_domain()
* @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again. * @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again.
* @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @return bool True when the textdomain is successfully loaded, false otherwise. * @return bool True when the textdomain is successfully loaded, false otherwise.
*/ */
function _load_textdomain_just_in_time( $domain ) { function _load_textdomain_just_in_time( $domain ) {
global $l10n_unloaded; global $l10n_unloaded, $wp_textdomain_registry;
$l10n_unloaded = (array) $l10n_unloaded; $l10n_unloaded = (array) $l10n_unloaded;
@ -1206,88 +1239,24 @@ function _load_textdomain_just_in_time( $domain ) {
return false; return false;
} }
$translation_path = _get_path_to_translation( $domain ); /** @var WP_Textdomain_Registry $wp_textdomain_registry */
if ( false === $translation_path ) { $path = $wp_textdomain_registry->get( $domain );
if ( ! $path ) {
return false; return false;
} }
return load_textdomain( $domain, $translation_path );
}
/**
* Gets the path to a translation file for loading a textdomain just in time.
*
* Caches the retrieved results internally.
*
* @since 4.7.0
* @access private
*
* @see _load_textdomain_just_in_time()
*
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reset Whether to reset the internal cache. Used by the switch to locale functionality.
* @return string|false The path to the translation file or false if no translation file was found.
*/
function _get_path_to_translation( $domain, $reset = false ) {
static $available_translations = array();
if ( true === $reset ) {
$available_translations = array();
}
if ( ! isset( $available_translations[ $domain ] ) ) {
$available_translations[ $domain ] = _get_path_to_translation_from_lang_dir( $domain );
}
return $available_translations[ $domain ];
}
/**
* Gets the path to a translation file in the languages directory for the current locale.
*
* Holds a cached list of available .mo files to improve performance.
*
* @since 4.7.0
* @access private
*
* @see _get_path_to_translation()
*
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @return string|false The path to the translation file or false if no translation file was found.
*/
function _get_path_to_translation_from_lang_dir( $domain ) {
static $cached_mofiles = null;
if ( null === $cached_mofiles ) {
$cached_mofiles = array();
$locations = array(
WP_LANG_DIR . '/plugins',
WP_LANG_DIR . '/themes',
);
foreach ( $locations as $location ) {
$mofiles = glob( $location . '/*.mo' );
if ( $mofiles ) {
$cached_mofiles = array_merge( $cached_mofiles, $mofiles );
}
}
}
$locale = determine_locale(); $locale = determine_locale();
$mofile = "{$domain}-{$locale}.mo";
$path = WP_LANG_DIR . '/plugins/' . $mofile; // Themes with their language directory outside of WP_LANG_DIR have a different file name.
if ( in_array( $path, $cached_mofiles, true ) ) { $template_directory = trailingslashit( get_template_directory() );
return $path; $stylesheet_directory = trailingslashit( get_stylesheet_directory() );
if ( 0 === strpos( $path, $template_directory ) || 0 === strpos( $path, $stylesheet_directory ) ) {
$mofile = "{$path}{$locale}.mo";
} else {
$mofile = "{$path}{$domain}-{$locale}.mo";
} }
$path = WP_LANG_DIR . '/themes/' . $mofile; return load_textdomain( $domain, $mofile );
if ( in_array( $path, $cached_mofiles, true ) ) {
return $path;
}
return false;
} }
/** /**
@ -1297,7 +1266,7 @@ function _get_path_to_translation_from_lang_dir( $domain ) {
* *
* @since 2.8.0 * @since 2.8.0
* *
* @global MO[] $l10n * @global MO[] $l10n An array of all currently loaded text domains.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @return Translations|NOOP_Translations A Translations instance. * @return Translations|NOOP_Translations A Translations instance.
@ -1321,7 +1290,7 @@ function get_translations_for_domain( $domain ) {
* *
* @since 3.0.0 * @since 3.0.0
* *
* @global MO[] $l10n * @global MO[] $l10n An array of all currently loaded text domains.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @return bool Whether there are translations. * @return bool Whether there are translations.

View File

@ -13,7 +13,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '5.6-alpha-49235'; $wp_version = '5.6-alpha-49236';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@ -152,6 +152,7 @@ if ( SHORTINIT ) {
// Load the L10n library. // Load the L10n library.
require_once ABSPATH . WPINC . '/l10n.php'; require_once ABSPATH . WPINC . '/l10n.php';
require_once ABSPATH . WPINC . '/class-wp-textdomain-registry.php';
require_once ABSPATH . WPINC . '/class-wp-locale.php'; require_once ABSPATH . WPINC . '/class-wp-locale.php';
require_once ABSPATH . WPINC . '/class-wp-locale-switcher.php'; require_once ABSPATH . WPINC . '/class-wp-locale-switcher.php';
@ -301,6 +302,17 @@ require ABSPATH . WPINC . '/block-supports/typography.php';
$GLOBALS['wp_embed'] = new WP_Embed(); $GLOBALS['wp_embed'] = new WP_Embed();
/**
* WordPress Textdomain Registry object.
*
* Used to support just-in-time translations for manually loaded textdomains.
*
* @since 5.6.0
*
* @global WP_Locale_Switcher $wp_locale_switcher WordPress Textdomain Registry.
*/
$GLOBALS['wp_textdomain_registry'] = new WP_Textdomain_Registry();
// Load multisite-specific files. // Load multisite-specific files.
if ( is_multisite() ) { if ( is_multisite() ) {
require ABSPATH . WPINC . '/ms-functions.php'; require ABSPATH . WPINC . '/ms-functions.php';