Make WordPress Core

Changeset 57337


Ignore:
Timestamp:
01/23/2024 01:32:34 PM (9 months ago)
Author:
swissspidy
Message:

I18N: Introduce a more performant localization library.

This introduces a more lightweight library for loading .mo translation files which offers increased speed and lower memory usage.
It also supports loading multiple locales at the same time, which makes locale switching faster too.

For plugins interacting with the $l10n global variable in core, a shim is added to retain backward compatibility with the existing pomo library.

In addition to that, this library supports translations contained in PHP files, avoiding a binary file format and leveraging OPCache if available.
If an .mo translation file has a corresponding .l10n.php file, the latter will be loaded instead.
This behavior can be adjusted using the new translation_file_format and load_translation_file filters.

PHP translation files will be typically created by downloading language packs, but can also be generated by plugins.
See https://make.wordpress.org/core/2023/11/08/merging-performant-translations-into-core/ for more context.

Props dd32, swissspidy, flixos90, joemcgill, westonruter, akirk, SergeyBiryukov.
Fixes #59656.

Location:
trunk
Files:
17 added
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/plugin.php

    r56571 r57337  
    10101010                $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.po' );
    10111011                $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.mo' );
     1012                $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.l10n.php' );
    10121013
    10131014                $json_translation_files = glob( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '-*.json' );
  • trunk/src/wp-includes/class-wp-locale-switcher.php

    r56178 r57337  
    284284        $wp_locale = new WP_Locale();
    285285
     286        WP_Translation_Controller::instance()->set_locale( $locale );
     287
    286288        /**
    287289         * Fires when the locale is switched to or restored.
  • trunk/src/wp-includes/compat.php

    r56549 r57337  
    421421}
    422422
     423if ( ! function_exists( 'array_is_list' ) ) {
     424    /**
     425     * Polyfill for `array_is_list()` function added in PHP 8.1.
     426     *
     427     * Determines if the given array is a list.
     428     *
     429     * An array is considered a list if its keys consist of consecutive numbers from 0 to count($array)-1.
     430     *
     431     * @see https://github.com/symfony/polyfill-php81/tree/main
     432     *
     433     * @since 6.5.0
     434     *
     435     * @param array<mixed> $arr The array being evaluated.
     436     * @return bool True if array is a list, false otherwise.
     437     */
     438    function array_is_list( $arr ) {
     439        if ( ( array() === $arr ) || ( array_values( $arr ) === $arr ) ) {
     440            return true;
     441        }
     442
     443        $next_key = -1;
     444
     445        foreach ( $arr as $k => $v ) {
     446            if ( ++$next_key !== $k ) {
     447                return false;
     448            }
     449        }
     450
     451        return true;
     452    }
     453}
     454
    423455if ( ! function_exists( 'str_contains' ) ) {
    424456    /**
  • trunk/src/wp-includes/functions.php

    r57312 r57337  
    65516551        $locale_loaded = $locale ? $locale : get_locale();
    65526552        $mofile        = WP_LANG_DIR . '/continents-cities-' . $locale_loaded . '.mo';
    6553         unload_textdomain( 'continents-cities' );
     6553        unload_textdomain( 'continents-cities', true );
    65546554        load_textdomain( 'continents-cities', $mofile, $locale_loaded );
    65556555        $mo_loaded = true;
  • trunk/src/wp-includes/l10n.php

    r57287 r57337  
    798798    }
    799799
    800     $mo = new MO();
    801     if ( ! $mo->import_from_file( $mofile ) ) {
    802         $wp_textdomain_registry->set( $domain, $locale, false );
    803 
    804         return false;
    805     }
    806 
    807     if ( isset( $l10n[ $domain ] ) ) {
    808         $mo->merge_with( $l10n[ $domain ] );
    809     }
    810 
    811     unset( $l10n_unloaded[ $domain ] );
    812 
    813     $l10n[ $domain ] = &$mo;
    814 
    815     $wp_textdomain_registry->set( $domain, $locale, dirname( $mofile ) );
     800    $i18n_controller = WP_Translation_Controller::instance();
     801
     802    // Ensures the correct locale is set as the current one, in case it was filtered.
     803    $i18n_controller->set_locale( $locale );
     804
     805    /**
     806     * Filters the preferred file format for translation files.
     807     *
     808     * Can be used to disable the use of PHP files for translations.
     809     *
     810     * @since 6.5.0
     811     *
     812     * @param string $preferred_format Preferred file format. Possible values: 'php', 'mo'. Default: 'php'.
     813     * @param string $domain           The text domain.
     814     */
     815    $preferred_format = apply_filters( 'translation_file_format', 'php', $domain );
     816    if ( ! in_array( $preferred_format, array( 'php', 'mo' ), true ) ) {
     817        $preferred_format = 'php';
     818    }
     819
     820    $translation_files = array( $mofile );
     821    if ( 'mo' !== $preferred_format ) {
     822        array_unshift(
     823            $translation_files,
     824            substr_replace( $mofile, '.l10n.', - strlen( $preferred_format ) )
     825        );
     826    }
     827
     828    foreach ( $translation_files as $file ) {
     829        /**
     830         * Filters the file path for loading translations for the given text domain.
     831         *
     832         * Similar to the {@see 'load_textdomain_mofile'} filter with the difference that
     833         * the file path could be for an MO or PHP file.
     834         *
     835         * @since 6.5.0
     836         *
     837         * @param string $file   Path to the translation file to load.
     838         * @param string $domain The text domain.
     839         */
     840        $file = (string) apply_filters( 'load_translation_file', $file, $domain );
     841
     842        $success = $i18n_controller->load_file( $file, $domain, $locale );
     843
     844        if ( $success ) {
     845            if ( isset( $l10n[ $domain ] ) && $l10n[ $domain ] instanceof MO ) {
     846                $i18n_controller->load_file( $l10n[ $domain ]->get_filename(), $domain, $locale );
     847            }
     848
     849            // Unset NOOP_Translations reference in get_translations_for_domain().
     850            unset( $l10n[ $domain ] );
     851
     852            $l10n[ $domain ] = new WP_Translations( $i18n_controller, $domain );
     853
     854            $wp_textdomain_registry->set( $domain, $locale, dirname( $file ) );
     855
     856            return true;
     857        }
     858    }
    816859
    817860    return true;
     
    867910    do_action( 'unload_textdomain', $domain, $reloadable );
    868911
     912    // Since multiple locales are supported, reloadable text domains don't actually need to be unloaded.
     913    if ( ! $reloadable ) {
     914        WP_Translation_Controller::instance()->unload_textdomain( $domain );
     915    }
     916
    869917    if ( isset( $l10n[ $domain ] ) ) {
    870918        if ( $l10n[ $domain ] instanceof NOOP_Translations ) {
     
    905953
    906954    // Unload previously loaded strings so we can switch translations.
    907     unload_textdomain( 'default' );
     955    unload_textdomain( 'default', true );
    908956
    909957    $return = load_textdomain( 'default', WP_LANG_DIR . "/$locale.mo", $locale );
  • trunk/src/wp-settings.php

    r57320 r57337  
    116116require ABSPATH . WPINC . '/class-wp-error.php';
    117117require ABSPATH . WPINC . '/pomo/mo.php';
     118require ABSPATH . WPINC . '/l10n/class-wp-translation-controller.php';
     119require ABSPATH . WPINC . '/l10n/class-wp-translations.php';
     120require ABSPATH . WPINC . '/l10n/class-wp-translation-file.php';
     121require ABSPATH . WPINC . '/l10n/class-wp-translation-file-mo.php';
     122require ABSPATH . WPINC . '/l10n/class-wp-translation-file-php.php';
    118123
    119124/**
     
    618623$GLOBALS['wp_locale_switcher']->init();
    619624
     625WP_Translation_Controller::instance()->set_locale( $locale );
     626
    620627// Load the functions for the active theme, for both parent and child theme if applicable.
    621628foreach ( wp_get_active_and_valid_themes() as $theme ) {
  • trunk/tests/phpunit/tests/l10n/loadTextdomainJustInTime.php

    r55865 r57337  
    4949        $wp_textdomain_registry = new WP_Textdomain_Registry();
    5050
     51        unload_textdomain( 'internationalized-plugin' );
     52        unload_textdomain( 'internationalized-theme' );
     53
    5154        parent::tear_down();
    5255    }
  • trunk/tests/phpunit/tests/l10n/wpLocaleSwitcher.php

    r55865 r57337  
    2121     */
    2222    protected static $user_id;
     23
     24    /**
     25     * @var WP_Locale_Switcher
     26     */
     27    protected $orig_instance;
    2328
    2429    public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
     
    4348        $wp_textdomain_registry = new WP_Textdomain_Registry();
    4449
    45         remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
     50        $this->orig_instance = $wp_locale_switcher;
     51
     52        remove_all_filters( 'locale' );
     53        remove_all_filters( 'determine_locale' );
     54
    4655        $wp_locale_switcher = new WP_Locale_Switcher();
    4756        $wp_locale_switcher->init();
     
    5968        restore_current_locale();
    6069
    61         remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
    62         $wp_locale_switcher = new WP_Locale_Switcher();
    63         $wp_locale_switcher->init();
     70        remove_all_filters( 'locale' );
     71        remove_all_filters( 'determine_locale' );
     72
     73        $wp_locale_switcher = $this->orig_instance;
     74
     75        unload_textdomain( 'internationalized-plugin' );
     76        unload_textdomain( 'custom-internationalized-theme' );
    6477
    6578        parent::tear_down();
Note: See TracChangeset for help on using the changeset viewer.