wp/wp-includes/l10n.php
changeset 22 8c2e4d02f4ef
parent 21 48c4eec2b7e6
--- 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<string, WP_Translations|NOOP_Translations> $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<string, WP_Translations|NOOP_Translations> $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<string, WP_Translations|NOOP_Translations> $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<string, WP_Translations|NOOP_Translations> $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<string, WP_Translations|NOOP_Translations> $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<string, WP_Translations|NOOP_Translations> $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/<plugin name> or themes/<theme name>.
 		$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.' ),
+				'<code>' . $domain . '</code>',
+				'<code>init</code>'
+			),
+			'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 );
+}