diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-includes/l10n.php --- a/wp/wp-includes/l10n.php Fri Sep 05 18:40:08 2025 +0200 +++ b/wp/wp-includes/l10n.php Fri Sep 05 18:52:52 2025 +0200 @@ -116,7 +116,8 @@ * * @since 5.0.0 * - * @global string $pagenow The filename of the current screen. + * @global string $pagenow The filename of the current screen. + * @global string $wp_local_package Locale code of the package. * * @return string The determined locale. */ @@ -963,7 +964,7 @@ return $return; } - if ( is_admin() || wp_installing() || ( defined( 'WP_REPAIRING' ) && WP_REPAIRING ) ) { + if ( is_admin() || wp_installing() || ( defined( 'WP_REPAIRING' ) && WP_REPAIRING ) || doing_action( 'wp_maybe_auto_update' ) ) { load_textdomain( 'default', WP_LANG_DIR . "/admin-$locale.mo", $locale ); } @@ -983,6 +984,10 @@ * * @since 1.5.0 * @since 4.6.0 The function now tries to load the .mo file from the languages directory first. + * @since 6.7.0 Translations are no longer immediately loaded, but handed off to the just-in-time loading mechanism. + * + * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry. + * @global array $l10n An array of all currently loaded text domains. * * @param string $domain Unique identifier for retrieving translated strings * @param string|false $deprecated Optional. Deprecated. Use the $plugin_rel_path parameter instead. @@ -993,29 +998,13 @@ */ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) { /** @var WP_Textdomain_Registry $wp_textdomain_registry */ - global $wp_textdomain_registry; + /** @var array $l10n */ + global $wp_textdomain_registry, $l10n; if ( ! is_string( $domain ) ) { return false; } - /** - * Filters a plugin's locale. - * - * @since 3.0.0 - * - * @param string $locale The plugin's current locale. - * @param string $domain Text domain. Unique identifier for retrieving translated strings. - */ - $locale = apply_filters( 'plugin_locale', determine_locale(), $domain ); - - $mofile = $domain . '-' . $locale . '.mo'; - - // Try to load from the languages directory first. - if ( load_textdomain( $domain, WP_LANG_DIR . '/plugins/' . $mofile, $locale ) ) { - return true; - } - if ( false !== $plugin_rel_path ) { $path = WP_PLUGIN_DIR . '/' . trim( $plugin_rel_path, '/' ); } elseif ( false !== $deprecated ) { @@ -1027,7 +1016,12 @@ $wp_textdomain_registry->set_custom_path( $domain, $path ); - return load_textdomain( $domain, $path . '/' . $mofile, $locale ); + // If just-in-time loading was triggered before, reset the entry so it can be tried again. + if ( isset( $l10n[ $domain ] ) && $l10n[ $domain ] instanceof NOOP_Translations ) { + unset( $l10n[ $domain ] ); + } + + return true; } /** @@ -1035,8 +1029,10 @@ * * @since 3.0.0 * @since 4.6.0 The function now tries to load the .mo file from the languages directory first. + * @since 6.7.0 Translations are no longer immediately loaded, but handed off to the just-in-time loading mechanism. * * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry. + * @global array $l10n An array of all currently loaded text domains. * * @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 @@ -1045,27 +1041,23 @@ */ function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) { /** @var WP_Textdomain_Registry $wp_textdomain_registry */ - global $wp_textdomain_registry; + /** @var array $l10n */ + global $wp_textdomain_registry, $l10n; if ( ! is_string( $domain ) ) { return false; } - /** This filter is documented in wp-includes/l10n.php */ - $locale = apply_filters( 'plugin_locale', determine_locale(), $domain ); - - $mofile = $domain . '-' . $locale . '.mo'; - - // Try to load from the languages directory first. - if ( load_textdomain( $domain, WP_LANG_DIR . '/plugins/' . $mofile, $locale ) ) { - return true; - } - $path = WPMU_PLUGIN_DIR . '/' . ltrim( $mu_plugin_rel_path, '/' ); $wp_textdomain_registry->set_custom_path( $domain, $path ); - return load_textdomain( $domain, $path . '/' . $mofile, $locale ); + // If just-in-time loading was triggered before, reset the entry so it can be tried again. + if ( isset( $l10n[ $domain ] ) && $l10n[ $domain ] instanceof NOOP_Translations ) { + unset( $l10n[ $domain ] ); + } + + return true; } /** @@ -1078,8 +1070,10 @@ * * @since 1.5.0 * @since 4.6.0 The function now tries to load the .mo file from the languages directory first. + * @since 6.7.0 Translations are no longer immediately loaded, but handed off to the just-in-time loading mechanism. * * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry. + * @global array $l10n An array of all currently loaded text domains. * * @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string|false $path Optional. Path to the directory containing the .mo file. @@ -1088,36 +1082,25 @@ */ function load_theme_textdomain( $domain, $path = false ) { /** @var WP_Textdomain_Registry $wp_textdomain_registry */ - global $wp_textdomain_registry; + /** @var array $l10n */ + global $wp_textdomain_registry, $l10n; if ( ! is_string( $domain ) ) { return false; } - /** - * Filters a theme's locale. - * - * @since 3.0.0 - * - * @param string $locale The theme's current locale. - * @param string $domain Text domain. Unique identifier for retrieving translated strings. - */ - $locale = apply_filters( 'theme_locale', determine_locale(), $domain ); - - $mofile = $domain . '-' . $locale . '.mo'; - - // Try to load from the languages directory first. - if ( load_textdomain( $domain, WP_LANG_DIR . '/themes/' . $mofile, $locale ) ) { - return true; - } - if ( ! $path ) { $path = get_template_directory(); } $wp_textdomain_registry->set_custom_path( $domain, $path ); - return load_textdomain( $domain, $path . '/' . $locale . '.mo', $locale ); + // If just-in-time loading was triggered before, reset the entry so it can be tried again. + if ( isset( $l10n[ $domain ] ) && $l10n[ $domain ] instanceof NOOP_Translations ) { + unset( $l10n[ $domain ] ); + } + + return true; } /** @@ -1192,6 +1175,7 @@ $content_url = wp_parse_url( content_url() ); $plugins_url = wp_parse_url( plugins_url() ); $site_url = wp_parse_url( site_url() ); + $theme_root = get_theme_root(); // If the host is the same or it's a relative URL. if ( @@ -1207,7 +1191,16 @@ $relative = trim( $relative, '/' ); $relative = explode( '/', $relative ); - $languages_path = WP_LANG_DIR . '/plugins'; + /* + * Ensure correct languages path when using a custom `WP_PLUGIN_DIR` / `WP_PLUGIN_URL` configuration, + * a custom theme root, and/or using Multisite with subdirectories. + * See https://core.trac.wordpress.org/ticket/60891 and https://core.trac.wordpress.org/ticket/62016. + */ + + $theme_dir = array_slice( explode( '/', $theme_root ), -1 ); + $dirname = $theme_dir[0] === $relative[0] ? 'themes' : 'plugins'; + + $languages_path = WP_LANG_DIR . '/' . $dirname; $relative = array_slice( $relative, 2 ); // Remove plugins/ or themes/. $relative = implode( '/', $relative ); @@ -1373,6 +1366,20 @@ if ( ! $path ) { return false; } + + if ( ! doing_action( 'after_setup_theme' ) && ! did_action( 'after_setup_theme' ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: 1: The text domain. 2: 'init'. */ + __( 'Translation loading for the %1$s domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the %2$s action or later.' ), + '' . $domain . '', + 'init' + ), + '6.7.0' + ); + } + // Themes with their language directory outside of WP_LANG_DIR have a different file name. $template_directory = trailingslashit( get_template_directory() ); $stylesheet_directory = trailingslashit( get_stylesheet_directory() ); @@ -1988,3 +1995,17 @@ return $wp_locale->get_word_count_type(); } + +/** + * Returns a boolean to indicate whether a translation exists for a given string with optional text domain and locale. + * + * @since 6.7.0 + * + * @param string $singular Singular translation to check. + * @param string $textdomain Optional. Text domain. Default 'default'. + * @param ?string $locale Optional. Locale. Default current locale. + * @return bool True if the translation exists, false otherwise. + */ +function has_translation( string $singular, string $textdomain = 'default', ?string $locale = null ): bool { + return WP_Translation_Controller::get_instance()->has_translation( $singular, $textdomain, $locale ); +}