From fba12b6eedd73ae41d795d4401e98e4649ceffc9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 20 Dec 2022 15:10:35 +0000 Subject: [PATCH] I18N: Change how `WP_Textdomain_Registry` caches translation information. `WP_Textdomain_Registry` was introduced in [53874] and later adjusted in [54682] to store text domains and their language directory paths, addressing issues with just-in-time loading of textdomains when using locale switching and `load_*_textdomain()` functions. This change improves how the class stores information about all existing MO files on the site, addressing an issue where translations are not loaded after calling `switch_to_locale()`. Props johnbillion, ocean90, SergeyBiryukov. Fixes #57116. git-svn-id: https://develop.svn.wordpress.org/trunk@55010 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-locale-switcher.php | 2 +- .../class-wp-textdomain-registry.php | 78 +++++++++--- tests/phpunit/tests/l10n/wpLocaleSwitcher.php | 115 ++++++++++++------ .../tests/l10n/wpTextdomainRegistry.php | 24 +--- 4 files changed, 140 insertions(+), 79 deletions(-) diff --git a/src/wp-includes/class-wp-locale-switcher.php b/src/wp-includes/class-wp-locale-switcher.php index 2e498eb3f8..c95c8ada03 100644 --- a/src/wp-includes/class-wp-locale-switcher.php +++ b/src/wp-includes/class-wp-locale-switcher.php @@ -36,7 +36,7 @@ class WP_Locale_Switcher { * @since 4.7.0 * @var string[] An array of language codes (file names without the .mo extension). */ - private $available_languages = array(); + private $available_languages; /** * Constructor. diff --git a/src/wp-includes/class-wp-textdomain-registry.php b/src/wp-includes/class-wp-textdomain-registry.php index d06d87c4db..e13cd76e62 100644 --- a/src/wp-includes/class-wp-textdomain-registry.php +++ b/src/wp-includes/class-wp-textdomain-registry.php @@ -51,7 +51,16 @@ class WP_Textdomain_Registry { * * @var array */ - protected $cached_mo_files; + protected $cached_mo_files = array(); + + /** + * Holds a cached list of domains with translations to improve performance. + * + * @since 6.1.2 + * + * @var string[] + */ + protected $domains_with_translations = array(); /** * Returns the languages directory path for a specific domain and locale. @@ -84,7 +93,11 @@ class WP_Textdomain_Registry { * @return bool Whether any MO file paths are available for the domain. */ public function has( $domain ) { - return ! empty( $this->current[ $domain ] ) || empty( $this->all[ $domain ] ); + return ( + ! empty( $this->current[ $domain ] ) || + empty( $this->all[ $domain ] ) || + in_array( $domain, $this->domains_with_translations, true ) + ); } /** @@ -109,6 +122,8 @@ class WP_Textdomain_Registry { * * Used by {@see load_plugin_textdomain()} and {@see load_theme_textdomain()}. * + * @since 6.1.0 + * * @param string $domain Text domain. * @param string $path Language directory path. */ @@ -116,6 +131,27 @@ class WP_Textdomain_Registry { $this->custom_paths[ $domain ] = untrailingslashit( $path ); } + /** + * Returns possible language directory paths for a given text domain. + * + * @since 6.1.2 + * + * @param string $domain Text domain. + * @return string[] Array of language directory paths. + */ + private function get_paths_for_domain( $domain ) { + $locations = array( + WP_LANG_DIR . '/plugins', + WP_LANG_DIR . '/themes', + ); + + if ( isset( $this->custom_paths[ $domain ] ) ) { + $locations[] = $this->custom_paths[ $domain ]; + } + + return $locations; + } + /** * Gets the path to the language directory for the current locale. * @@ -131,37 +167,43 @@ class WP_Textdomain_Registry { * @return string|false Language directory path or false if there is none available. */ private function get_path_from_lang_dir( $domain, $locale ) { - $locations = array( - WP_LANG_DIR . '/plugins', - WP_LANG_DIR . '/themes', - ); + $locations = $this->get_paths_for_domain( $domain ); - if ( isset( $this->custom_paths[ $domain ] ) ) { - $locations[] = $this->custom_paths[ $domain ]; - } - - $mofile = "$domain-$locale.mo"; + $found_location = false; foreach ( $locations as $location ) { if ( ! isset( $this->cached_mo_files[ $location ] ) ) { $this->set_cached_mo_files( $location ); } - $path = $location . '/' . $mofile; + $path = "$location/$domain-$locale.mo"; - if ( in_array( $path, $this->cached_mo_files[ $location ], true ) ) { - $this->set( $domain, $locale, $location ); + foreach ( $this->cached_mo_files[ $location ] as $mo_path ) { + if ( + ! in_array( $domain, $this->domains_with_translations, true ) && + str_starts_with( str_replace( "$location/", '', $mo_path ), "$domain-" ) + ) { + $this->domains_with_translations[] = $domain; + } - return trailingslashit( $location ); + if ( $mo_path === $path ) { + $found_location = trailingslashit( $location ); + } } } + if ( $found_location ) { + $this->set( $domain, $locale, $found_location ); + + return $found_location; + } + // If no path is found for the given locale and a custom path has been set // using load_plugin_textdomain/load_theme_textdomain, use that one. if ( 'en_US' !== $locale && isset( $this->custom_paths[ $domain ] ) ) { - $path = trailingslashit( $this->custom_paths[ $domain ] ); - $this->set( $domain, $locale, $path ); - return $path; + $fallback_location = trailingslashit( $this->custom_paths[ $domain ] ); + $this->set( $domain, $locale, $fallback_location ); + return $fallback_location; } $this->set( $domain, $locale, false ); diff --git a/tests/phpunit/tests/l10n/wpLocaleSwitcher.php b/tests/phpunit/tests/l10n/wpLocaleSwitcher.php index 3fbea448c7..b160f24833 100644 --- a/tests/phpunit/tests/l10n/wpLocaleSwitcher.php +++ b/tests/phpunit/tests/l10n/wpLocaleSwitcher.php @@ -24,20 +24,26 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] ); - /** @var WP_Textdomain_Registry $wp_textdomain_registry */ - global $wp_textdomain_registry; + global $wp_textdomain_registry, $wp_locale_switcher; $wp_textdomain_registry = new WP_Textdomain_Registry(); + + remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) ); + $wp_locale_switcher = new WP_Locale_Switcher(); + $wp_locale_switcher->init(); } public function tear_down() { unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] ); - /** @var WP_Textdomain_Registry $wp_textdomain_registry */ - global $wp_textdomain_registry; + global $wp_textdomain_registry, $wp_locale_switcher; $wp_textdomain_registry = new WP_Textdomain_Registry(); + remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) ); + $wp_locale_switcher = new WP_Locale_Switcher(); + $wp_locale_switcher->init(); + parent::tear_down(); } @@ -336,8 +342,8 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { wp_set_current_user( $user_id ); set_current_screen( 'dashboard' ); - $locale_switcher = clone $wp_locale_switcher; - + // Reset $wp_locale_switcher so it thinks es_ES is the original locale. + remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) ); $wp_locale_switcher = new WP_Locale_Switcher(); $wp_locale_switcher->init(); @@ -357,8 +363,6 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { $language_header_after_restore = $l10n['default']->headers['Language']; // de_DE - $wp_locale_switcher = $locale_switcher; - $this->assertFalse( $locale_switched_user_locale ); $this->assertTrue( $locale_switched_site_locale ); $this->assertSame( $site_locale, $site_locale_after_switch ); @@ -388,8 +392,8 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { wp_set_current_user( $user_id ); set_current_screen( 'dashboard' ); - $locale_switcher = clone $wp_locale_switcher; - + // Reset $wp_locale_switcher so it thinks es_ES is the original locale. + remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) ); $wp_locale_switcher = new WP_Locale_Switcher(); $wp_locale_switcher->init(); @@ -409,8 +413,6 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { $language_header_after_restore = $l10n['default']->headers['Language']; // de_DE - $wp_locale_switcher = $locale_switcher; - remove_filter( 'locale', array( $this, 'filter_locale' ) ); $this->assertFalse( $locale_switched_user_locale ); @@ -426,8 +428,6 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { * @covers ::load_default_textdomain */ public function test_multiple_switches_to_site_locale_and_user_locale() { - global $wp_locale_switcher; - $site_locale = get_locale(); $user_id = self::factory()->user->create( @@ -440,11 +440,6 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { wp_set_current_user( $user_id ); set_current_screen( 'dashboard' ); - $locale_switcher = clone $wp_locale_switcher; - - $wp_locale_switcher = new WP_Locale_Switcher(); - $wp_locale_switcher->init(); - $user_locale = get_user_locale(); load_default_textdomain( $user_locale ); @@ -458,8 +453,6 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { restore_current_locale(); - $wp_locale_switcher = $locale_switcher; - $this->assertSame( 'en_US', get_locale() ); $this->assertSame( 'This is a dummy plugin', $actual ); } @@ -469,12 +462,7 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { */ public function test_switch_reloads_plugin_translations_outside_wp_lang_dir() { /** @var WP_Textdomain_Registry $wp_textdomain_registry */ - global $wp_locale_switcher, $wp_textdomain_registry; - - $locale_switcher = clone $wp_locale_switcher; - - $wp_locale_switcher = new WP_Locale_Switcher(); - $wp_locale_switcher->init(); + global $wp_textdomain_registry; require_once DIR_TESTDATA . '/plugins/custom-internationalized-plugin/custom-internationalized-plugin.php'; @@ -494,25 +482,58 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { restore_current_locale(); - $wp_locale_switcher = $locale_switcher; - $this->assertSame( 'This is a dummy plugin', $actual ); $this->assertSame( WP_PLUGIN_DIR . '/custom-internationalized-plugin/languages/', $registry_value ); $this->assertSame( 'Das ist ein Dummy Plugin', $actual_de_de ); $this->assertSame( 'Este es un plugin dummy', $actual_es_es ); } + /** + * @ticket 57116 + */ + public function test_switch_reloads_plugin_translations() { + /** @var WP_Textdomain_Registry $wp_textdomain_registry */ + global $wp_textdomain_registry; + + $has_translations_1 = $wp_textdomain_registry->has( 'internationalized-plugin' ); + + require_once DIR_TESTDATA . '/plugins/internationalized-plugin.php'; + + $actual = i18n_plugin_test(); + + switch_to_locale( 'es_ES' ); + + $lang_path_es_es = $wp_textdomain_registry->get( 'internationalized-plugin', determine_locale() ); + + switch_to_locale( 'de_DE' ); + + $actual_de_de = i18n_plugin_test(); + + $has_translations_3 = $wp_textdomain_registry->has( 'internationalized-plugin' ); + + restore_previous_locale(); + + $actual_es_es = i18n_plugin_test(); + + restore_current_locale(); + + $lang_path_en_us = $wp_textdomain_registry->get( 'internationalized-plugin', determine_locale() ); + + $this->assertSame( 'This is a dummy plugin', $actual ); + $this->assertSame( 'Das ist ein Dummy Plugin', $actual_de_de ); + $this->assertSame( 'Este es un plugin dummy', $actual_es_es ); + $this->assertTrue( $has_translations_1 ); + $this->assertTrue( $has_translations_3 ); + $this->assertSame( WP_LANG_DIR . '/plugins/', $lang_path_es_es ); + $this->assertFalse( $lang_path_en_us ); + } + /** * @ticket 39210 */ public function test_switch_reloads_theme_translations_outside_wp_lang_dir() { /** @var WP_Textdomain_Registry $wp_textdomain_registry */ - global $wp_locale_switcher, $wp_textdomain_registry; - - $locale_switcher = clone $wp_locale_switcher; - - $wp_locale_switcher = new WP_Locale_Switcher(); - $wp_locale_switcher->init(); + global $wp_textdomain_registry; switch_theme( 'custom-internationalized-theme' ); @@ -534,14 +555,34 @@ class Tests_L10n_wpLocaleSwitcher extends WP_UnitTestCase { restore_current_locale(); - $wp_locale_switcher = $locale_switcher; - $this->assertSame( get_template_directory() . '/languages/', $registry_value ); $this->assertSame( 'This is a dummy theme', $actual ); $this->assertSame( 'Das ist ein Dummy Theme', $actual_de_de ); $this->assertSame( 'Este es un tema dummy', $actual_es_es ); } + /** + * @ticket 57116 + */ + public function test_switch_to_locale_should_work() { + global $wp_textdomain_registry; + require_once DIR_TESTDATA . '/plugins/internationalized-plugin.php'; + + $has_translations = $wp_textdomain_registry->has( 'internationalized-plugin' ); + $path = $wp_textdomain_registry->get( 'internationalized-plugin', 'es_ES' ); + + $actual = i18n_plugin_test(); + + switch_to_locale( 'es_ES' ); + + $actual_es_es = i18n_plugin_test(); + + $this->assertTrue( $has_translations ); + $this->assertNotEmpty( $path ); + $this->assertSame( 'This is a dummy plugin', $actual ); + $this->assertSame( 'Este es un plugin dummy', $actual_es_es ); + } + public function filter_locale() { return 'es_ES'; } diff --git a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php index 4186d8555a..fc53dd9c56 100644 --- a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php +++ b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php @@ -28,7 +28,7 @@ class Tests_L10n_wpTextdomainRegistry extends WP_UnitTestCase { $reflection_property = $reflection->getProperty( 'cached_mo_files' ); $reflection_property->setAccessible( true ); - $this->assertNull( + $this->assertEmpty( $reflection_property->getValue( $this->instance ), 'Cache not empty by default' ); @@ -78,28 +78,6 @@ class Tests_L10n_wpTextdomainRegistry extends WP_UnitTestCase { ); } - /** - * @covers ::get_path_from_lang_dir - */ - public function test_get_does_not_check_themes_directory_for_plugin() { - $reflection = new ReflectionClass( $this->instance ); - $reflection_property = $reflection->getProperty( 'cached_mo_files' ); - $reflection_property->setAccessible( true ); - - $this->instance->get( 'internationalized-plugin', 'de_DE' ); - - $this->assertArrayHasKey( - WP_LANG_DIR . '/plugins', - $reflection_property->getValue( $this->instance ), - 'Default plugins path missing from cache' - ); - $this->assertArrayNotHasKey( - WP_LANG_DIR . '/themes', - $reflection_property->getValue( $this->instance ), - 'Default themes path should not be in cache' - ); - } - /** * @covers ::set * @covers ::get