Skip to content

Commit e83f5a1

Browse files
committed
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. git-svn-id: https://develop.svn.wordpress.org/trunk@57337 602fd350-edb4-49c9-b593-d223f7449a82
1 parent de0c402 commit e83f5a1

23 files changed

Lines changed: 2578 additions & 15 deletions

src/wp-admin/includes/plugin.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,7 @@ function delete_plugins( $plugins, $deprecated = '' ) {
10091009
foreach ( $translations as $translation => $data ) {
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' );
10141015
if ( $json_translation_files ) {

src/wp-includes/class-wp-locale-switcher.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ private function change_locale( $locale ) {
283283

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.
288290
*

src/wp-includes/compat.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,38 @@ function array_key_last( array $array ) { // phpcs:ignore Universal.NamingConven
420420
}
421421
}
422422

423+
if ( ! 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+
423455
if ( ! function_exists( 'str_contains' ) ) {
424456
/**
425457
* Polyfill for `str_contains()` function added in PHP 8.0.

src/wp-includes/functions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6550,7 +6550,7 @@ function wp_timezone_choice( $selected_zone, $locale = null ) {
65506550
if ( ! $mo_loaded || $locale !== $locale_loaded ) {
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;
65566556
}

src/wp-includes/l10n.php

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -797,22 +797,65 @@ function load_textdomain( $domain, $mofile, $locale = null ) {
797797
$locale = determine_locale();
798798
}
799799

800-
$mo = new MO();
801-
if ( ! $mo->import_from_file( $mofile ) ) {
802-
$wp_textdomain_registry->set( $domain, $locale, false );
800+
$i18n_controller = WP_Translation_Controller::instance();
803801

804-
return false;
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';
805818
}
806819

807-
if ( isset( $l10n[ $domain ] ) ) {
808-
$mo->merge_with( $l10n[ $domain ] );
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+
);
809826
}
810827

811-
unset( $l10n_unloaded[ $domain ] );
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 );
812853

813-
$l10n[ $domain ] = &$mo;
854+
$wp_textdomain_registry->set( $domain, $locale, dirname( $file ) );
814855

815-
$wp_textdomain_registry->set( $domain, $locale, dirname( $mofile ) );
856+
return true;
857+
}
858+
}
816859

817860
return true;
818861
}
@@ -866,6 +909,11 @@ function unload_textdomain( $domain, $reloadable = false ) {
866909
*/
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 ) {
871919
unset( $l10n[ $domain ] );
@@ -904,7 +952,7 @@ function load_default_textdomain( $locale = null ) {
904952
}
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 );
910958

0 commit comments

Comments
 (0)