Make WordPress Core

Changeset 55010


Ignore:
Timestamp:
12/20/2022 03:10:35 PM (18 months ago)
Author:
swissspidy
Message:

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.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-locale-switcher.php

    r54318 r55010  
    3737     * @var string[] An array of language codes (file names without the .mo extension).
    3838     */
    39     private $available_languages = array();
     39    private $available_languages;
    4040
    4141    /**
  • trunk/src/wp-includes/class-wp-textdomain-registry.php

    r54669 r55010  
    5252     * @var array
    5353     */
    54     protected $cached_mo_files;
     54    protected $cached_mo_files = array();
     55
     56    /**
     57     * Holds a cached list of domains with translations to improve performance.
     58     *
     59     * @since 6.1.2
     60     *
     61     * @var string[]
     62     */
     63    protected $domains_with_translations = array();
    5564
    5665    /**
     
    8594     */
    8695    public function has( $domain ) {
    87         return ! empty( $this->current[ $domain ] ) || empty( $this->all[ $domain ] );
     96        return (
     97            ! empty( $this->current[ $domain ] ) ||
     98            empty( $this->all[ $domain ] ) ||
     99            in_array( $domain, $this->domains_with_translations, true )
     100        );
    88101    }
    89102
     
    110123     * Used by {@see load_plugin_textdomain()} and {@see load_theme_textdomain()}.
    111124     *
     125     * @since 6.1.0
     126     *
    112127     * @param string $domain Text domain.
    113128     * @param string $path   Language directory path.
     
    118133
    119134    /**
    120      * Gets the path to the language directory for the current locale.
    121      *
    122      * Checks the plugins and themes language directories as well as any
    123      * custom directory set via {@see load_plugin_textdomain()} or {@see load_theme_textdomain()}.
    124      *
    125      * @since 6.1.0
    126      *
    127      * @see _get_path_to_translation_from_lang_dir()
    128      *
    129      * @param string $domain Text domain.
    130      * @param string $locale Locale.
    131      * @return string|false Language directory path or false if there is none available.
    132      */
    133     private function get_path_from_lang_dir( $domain, $locale ) {
     135     * Returns possible language directory paths for a given text domain.
     136     *
     137     * @since 6.1.2
     138     *
     139     * @param string $domain Text domain.
     140     * @return string[] Array of language directory paths.
     141     */
     142    private function get_paths_for_domain( $domain ) {
    134143        $locations = array(
    135144            WP_LANG_DIR . '/plugins',
     
    141150        }
    142151
    143         $mofile = "$domain-$locale.mo";
     152        return $locations;
     153    }
     154
     155    /**
     156     * Gets the path to the language directory for the current locale.
     157     *
     158     * Checks the plugins and themes language directories as well as any
     159     * custom directory set via {@see load_plugin_textdomain()} or {@see load_theme_textdomain()}.
     160     *
     161     * @since 6.1.0
     162     *
     163     * @see _get_path_to_translation_from_lang_dir()
     164     *
     165     * @param string $domain Text domain.
     166     * @param string $locale Locale.
     167     * @return string|false Language directory path or false if there is none available.
     168     */
     169    private function get_path_from_lang_dir( $domain, $locale ) {
     170        $locations = $this->get_paths_for_domain( $domain );
     171
     172        $found_location = false;
    144173
    145174        foreach ( $locations as $location ) {
     
    148177            }
    149178
    150             $path = $location . '/' . $mofile;
    151 
    152             if ( in_array( $path, $this->cached_mo_files[ $location ], true ) ) {
    153                 $this->set( $domain, $locale, $location );
    154 
    155                 return trailingslashit( $location );
     179            $path = "$location/$domain-$locale.mo";
     180
     181            foreach ( $this->cached_mo_files[ $location ] as $mo_path ) {
     182                if (
     183                    ! in_array( $domain, $this->domains_with_translations, true ) &&
     184                    str_starts_with( str_replace( "$location/", '', $mo_path ), "$domain-" )
     185                ) {
     186                    $this->domains_with_translations[] = $domain;
     187                }
     188
     189                if ( $mo_path === $path ) {
     190                    $found_location = trailingslashit( $location );
     191                }
    156192            }
     193        }
     194
     195        if ( $found_location ) {
     196            $this->set( $domain, $locale, $found_location );
     197
     198            return $found_location;
    157199        }
    158200
     
    160202        // using load_plugin_textdomain/load_theme_textdomain, use that one.
    161203        if ( 'en_US' !== $locale && isset( $this->custom_paths[ $domain ] ) ) {
    162             $path = trailingslashit( $this->custom_paths[ $domain ] );
    163             $this->set( $domain, $locale, $path );
    164             return $path;
     204            $fallback_location = trailingslashit( $this->custom_paths[ $domain ] );
     205            $this->set( $domain, $locale, $fallback_location );
     206            return $fallback_location;
    165207        }
    166208
  • trunk/tests/phpunit/tests/l10n/wpLocaleSwitcher.php

    r54669 r55010  
    2525        unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
    2626
    27         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    28         global $wp_textdomain_registry;
     27        global $wp_textdomain_registry, $wp_locale_switcher;
    2928
    3029        $wp_textdomain_registry = new WP_Textdomain_Registry();
     30
     31        remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
     32        $wp_locale_switcher = new WP_Locale_Switcher();
     33        $wp_locale_switcher->init();
    3134    }
    3235
     
    3437        unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
    3538
    36         /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    37         global $wp_textdomain_registry;
     39        global $wp_textdomain_registry, $wp_locale_switcher;
    3840
    3941        $wp_textdomain_registry = new WP_Textdomain_Registry();
     42
     43        remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
     44        $wp_locale_switcher = new WP_Locale_Switcher();
     45        $wp_locale_switcher->init();
    4046
    4147        parent::tear_down();
     
    337343        set_current_screen( 'dashboard' );
    338344
    339         $locale_switcher = clone $wp_locale_switcher;
    340 
     345        // Reset $wp_locale_switcher so it thinks es_ES is the original locale.
     346        remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
    341347        $wp_locale_switcher = new WP_Locale_Switcher();
    342348        $wp_locale_switcher->init();
     
    357363
    358364        $language_header_after_restore = $l10n['default']->headers['Language']; // de_DE
    359 
    360         $wp_locale_switcher = $locale_switcher;
    361365
    362366        $this->assertFalse( $locale_switched_user_locale );
     
    389393        set_current_screen( 'dashboard' );
    390394
    391         $locale_switcher = clone $wp_locale_switcher;
    392 
     395        // Reset $wp_locale_switcher so it thinks es_ES is the original locale.
     396        remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
    393397        $wp_locale_switcher = new WP_Locale_Switcher();
    394398        $wp_locale_switcher->init();
     
    409413
    410414        $language_header_after_restore = $l10n['default']->headers['Language']; // de_DE
    411 
    412         $wp_locale_switcher = $locale_switcher;
    413415
    414416        remove_filter( 'locale', array( $this, 'filter_locale' ) );
     
    427429     */
    428430    public function test_multiple_switches_to_site_locale_and_user_locale() {
    429         global $wp_locale_switcher;
    430 
    431431        $site_locale = get_locale();
    432432
     
    441441        set_current_screen( 'dashboard' );
    442442
    443         $locale_switcher = clone $wp_locale_switcher;
    444 
    445         $wp_locale_switcher = new WP_Locale_Switcher();
    446         $wp_locale_switcher->init();
    447 
    448443        $user_locale = get_user_locale();
    449444
     
    458453
    459454        restore_current_locale();
    460 
    461         $wp_locale_switcher = $locale_switcher;
    462455
    463456        $this->assertSame( 'en_US', get_locale() );
     
    470463    public function test_switch_reloads_plugin_translations_outside_wp_lang_dir() {
    471464        /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    472         global $wp_locale_switcher, $wp_textdomain_registry;
    473 
    474         $locale_switcher = clone $wp_locale_switcher;
    475 
    476         $wp_locale_switcher = new WP_Locale_Switcher();
    477         $wp_locale_switcher->init();
     465        global $wp_textdomain_registry;
    478466
    479467        require_once DIR_TESTDATA . '/plugins/custom-internationalized-plugin/custom-internationalized-plugin.php';
     
    494482
    495483        restore_current_locale();
    496 
    497         $wp_locale_switcher = $locale_switcher;
    498484
    499485        $this->assertSame( 'This is a dummy plugin', $actual );
     
    504490
    505491    /**
     492     * @ticket 57116
     493     */
     494    public function test_switch_reloads_plugin_translations() {
     495        /** @var WP_Textdomain_Registry $wp_textdomain_registry */
     496        global $wp_textdomain_registry;
     497
     498        $has_translations_1 = $wp_textdomain_registry->has( 'internationalized-plugin' );
     499
     500        require_once DIR_TESTDATA . '/plugins/internationalized-plugin.php';
     501
     502        $actual = i18n_plugin_test();
     503
     504        switch_to_locale( 'es_ES' );
     505
     506        $lang_path_es_es = $wp_textdomain_registry->get( 'internationalized-plugin', determine_locale() );
     507
     508        switch_to_locale( 'de_DE' );
     509
     510        $actual_de_de = i18n_plugin_test();
     511
     512        $has_translations_3 = $wp_textdomain_registry->has( 'internationalized-plugin' );
     513
     514        restore_previous_locale();
     515
     516        $actual_es_es = i18n_plugin_test();
     517
     518        restore_current_locale();
     519
     520        $lang_path_en_us = $wp_textdomain_registry->get( 'internationalized-plugin', determine_locale() );
     521
     522        $this->assertSame( 'This is a dummy plugin', $actual );
     523        $this->assertSame( 'Das ist ein Dummy Plugin', $actual_de_de );
     524        $this->assertSame( 'Este es un plugin dummy', $actual_es_es );
     525        $this->assertTrue( $has_translations_1 );
     526        $this->assertTrue( $has_translations_3 );
     527        $this->assertSame( WP_LANG_DIR . '/plugins/', $lang_path_es_es );
     528        $this->assertFalse( $lang_path_en_us );
     529    }
     530
     531    /**
    506532     * @ticket 39210
    507533     */
    508534    public function test_switch_reloads_theme_translations_outside_wp_lang_dir() {
    509535        /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    510         global $wp_locale_switcher, $wp_textdomain_registry;
    511 
    512         $locale_switcher = clone $wp_locale_switcher;
    513 
    514         $wp_locale_switcher = new WP_Locale_Switcher();
    515         $wp_locale_switcher->init();
     536        global $wp_textdomain_registry;
    516537
    517538        switch_theme( 'custom-internationalized-theme' );
     
    534555
    535556        restore_current_locale();
    536 
    537         $wp_locale_switcher = $locale_switcher;
    538557
    539558        $this->assertSame( get_template_directory() . '/languages/', $registry_value );
     
    543562    }
    544563
     564    /**
     565     * @ticket 57116
     566     */
     567    public function test_switch_to_locale_should_work() {
     568        global $wp_textdomain_registry;
     569        require_once DIR_TESTDATA . '/plugins/internationalized-plugin.php';
     570
     571        $has_translations = $wp_textdomain_registry->has( 'internationalized-plugin' );
     572        $path             = $wp_textdomain_registry->get( 'internationalized-plugin', 'es_ES' );
     573
     574        $actual = i18n_plugin_test();
     575
     576        switch_to_locale( 'es_ES' );
     577
     578        $actual_es_es = i18n_plugin_test();
     579
     580        $this->assertTrue( $has_translations );
     581        $this->assertNotEmpty( $path );
     582        $this->assertSame( 'This is a dummy plugin', $actual );
     583        $this->assertSame( 'Este es un plugin dummy', $actual_es_es );
     584    }
     585
    545586    public function filter_locale() {
    546587        return 'es_ES';
  • trunk/tests/phpunit/tests/l10n/wpTextdomainRegistry.php

    r54669 r55010  
    2929        $reflection_property->setAccessible( true );
    3030
    31         $this->assertNull(
     31        $this->assertEmpty(
    3232            $reflection_property->getValue( $this->instance ),
    3333            'Cache not empty by default'
     
    7676            $reflection_property->getValue( $this->instance ),
    7777            'Default plugins path missing from cache'
    78         );
    79     }
    80 
    81     /**
    82      * @covers ::get_path_from_lang_dir
    83      */
    84     public function test_get_does_not_check_themes_directory_for_plugin() {
    85         $reflection          = new ReflectionClass( $this->instance );
    86         $reflection_property = $reflection->getProperty( 'cached_mo_files' );
    87         $reflection_property->setAccessible( true );
    88 
    89         $this->instance->get( 'internationalized-plugin', 'de_DE' );
    90 
    91         $this->assertArrayHasKey(
    92             WP_LANG_DIR . '/plugins',
    93             $reflection_property->getValue( $this->instance ),
    94             'Default plugins path missing from cache'
    95         );
    96         $this->assertArrayNotHasKey(
    97             WP_LANG_DIR . '/themes',
    98             $reflection_property->getValue( $this->instance ),
    99             'Default themes path should not be in cache'
    10078        );
    10179    }
Note: See TracChangeset for help on using the changeset viewer.