diff --git src/wp-includes/class-wp-locale-switcher.php src/wp-includes/class-wp-locale-switcher.php
index e111892871..fa1f4bc41a 100644
--- src/wp-includes/class-wp-locale-switcher.php
+++ src/wp-includes/class-wp-locale-switcher.php
@@ -194,10 +194,6 @@ class WP_Locale_Switcher {
 		load_default_textdomain( $locale );
 
 		foreach ( $domains as $domain ) {
-			if ( 'default' === $domain ) {
-				continue;
-			}
-
 			unload_textdomain( $domain );
 			get_translations_for_domain( $domain );
 		}
@@ -211,17 +207,20 @@ class WP_Locale_Switcher {
 	 *
 	 * @since 4.7.0
 	 *
-	 * @global WP_Locale $wp_locale The WordPress date and time locale object.
+	 * @global WP_Locale              $wp_locale              The WordPress date and time locale object.
+	 * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
 	 *
 	 * @param string $locale The locale to change to.
 	 */
 	private function change_locale( $locale ) {
+		global $wp_locale, $wp_textdomain_registry;
+
 		// Reset translation availability information.
-		_get_path_to_translation( null, true );
+		$wp_textdomain_registry->reset();
 
 		$this->load_translations( $locale );
 
-		$GLOBALS['wp_locale'] = new WP_Locale();
+		$wp_locale = new WP_Locale();
 
 		/**
 		 * Fires when the locale is switched to or restored.
diff --git src/wp-includes/class-wp-textdomain-registry.php src/wp-includes/class-wp-textdomain-registry.php
new file mode 100644
index 0000000000..03b1d032f6
--- /dev/null
+++ src/wp-includes/class-wp-textdomain-registry.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Locale API: WP_Textdomain_Registry class
+ *
+ * @package    WordPress
+ * @subpackage i18n
+ * @since      5.0.0
+ */
+
+/**
+ * Core class used for registering textdomains
+ *
+ * @since 5.0.0
+ */
+class WP_Textdomain_Registry {
+	/**
+	 * List of domains and their language directory paths.
+	 *
+	 * @since 5.0.0
+	 *
+	 * @var array
+	 */
+	protected $domains = array();
+
+	/**
+	 * Holds a cached list of available .mo files to improve performance.
+	 *
+	 * @since 5.0.0
+	 *
+	 * @var array
+	 */
+	protected $cached_mofiles;
+
+	/**
+	 * Returns the MO file path for a specific domain.
+	 *
+	 * @since 5.0.0
+	 * @access public
+	 *
+	 * @param string $domain Text domain.
+	 *
+	 * @return string|false|null MO file path or false if there is none available.
+	 *                           Null if none have been fetched yet.
+	 */
+	public function get( $domain ) {
+		return isset( $this->domains[ $domain ] ) ? $this->domains[ $domain ] : null;
+	}
+
+	/**
+	 * Sets the MO file path for a specific domain.
+	 *
+	 * @since 5.0.0
+	 * @access public
+	 *
+	 * @param string $domain Text domain.
+	 * @param string $path Language directory path.
+	 */
+	public function set( $domain, $path ) {
+		$this->domains[ $domain ] = $path;
+	}
+
+	/**
+	 * Resets the registry state.
+	 *
+	 * @since 5.0.0
+	 * @access public
+	 */
+	public function reset( ) {
+		$this->cached_mofiles = null;
+		$this->domains = array();
+	}
+
+	/**
+	 * Gets the path to a translation file in the languages directory for the current locale.
+	 *
+	 * @since 5.0.0
+	 * @access public
+	 *
+	 * @see _get_path_to_translation_from_lang_dir()
+	 *
+	 * @param string $domain Text domain.
+	 */
+	public function get_translation_from_lang_dir( $domain ) {
+		if ( null === $this->cached_mofiles ) {
+			$this->cached_mofiles = array();
+
+			$this->fetch_available_mofiles();
+		}
+
+		$locale = is_admin() ? get_user_locale() : get_locale();
+		$mofile = "{$domain}-{$locale}.mo";
+
+		$path = WP_LANG_DIR . '/plugins/' . $mofile;
+		if ( in_array( $path, $this->cached_mofiles, true ) ) {
+			$this->set( $domain, WP_LANG_DIR . '/plugins/' );
+
+			return;
+		}
+
+		$path = WP_LANG_DIR . '/themes/' . $mofile;
+		if ( in_array( $path, $this->cached_mofiles, true ) ) {
+			$this->set( $domain, WP_LANG_DIR . '/themes/' );
+
+			return;
+		}
+
+		$this->set( $domain, false );
+	}
+
+	/**
+	 * Fetches all available MO files from the plugins and themes language directories.
+	 *
+	 * @since 5.0.0
+	 * @access protected
+	 *
+	 * @see _get_path_to_translation_from_lang_dir()
+	 */
+	protected function fetch_available_mofiles() {
+		$locations = array(
+			WP_LANG_DIR . '/plugins',
+			WP_LANG_DIR . '/themes',
+		);
+
+		foreach ( $locations as $location ) {
+			$mofiles = glob( $location . '/*.mo' );
+
+			if ( $mofiles ) {
+				$this->cached_mofiles = array_merge( $this->cached_mofiles, $mofiles );
+			}
+		}
+	}
+}
diff --git src/wp-includes/deprecated.php src/wp-includes/deprecated.php
index 06ecd4b518..21b2e34a0e 100644
--- src/wp-includes/deprecated.php
+++ src/wp-includes/deprecated.php
@@ -3944,3 +3944,85 @@ function wp_ajax_press_this_add_category() {
 		wp_send_json_error( array( 'errorMessage' => __( 'The Press This plugin is required.' ) ) );
 	}
 }
+
+/**
+ * Gets the path to a translation file for loading a textdomain just in time.
+ *
+ * Caches the retrieved results internally.
+ *
+ * @since 4.7.0
+ * @deprecated 5.0.0
+ * @access private
+ *
+ * @see _load_textdomain_just_in_time()
+ *
+ * @param string $domain Text domain. Unique identifier for retrieving translated strings.
+ * @param bool   $reset  Whether to reset the internal cache. Used by the switch to locale functionality.
+ * @return string|false The path to the translation file or false if no translation file was found.
+ */
+function _get_path_to_translation( $domain, $reset = false ) {
+	_deprecated_function( __FUNCTION__, '3.1.0', 'WP_Textdomain_Registry' );
+
+	static $available_translations = array();
+
+	if ( true === $reset ) {
+		$available_translations = array();
+	}
+
+	if ( ! isset( $available_translations[ $domain ] ) ) {
+		$available_translations[ $domain ] = _get_path_to_translation_from_lang_dir( $domain );
+	}
+
+	return $available_translations[ $domain ];
+}
+
+/**
+ * Gets the path to a translation file in the languages directory for the current locale.
+ *
+ * Holds a cached list of available .mo files to improve performance.
+ *
+ * @since 4.7.0
+ * @deprecated 5.0.0
+ * @access private
+ *
+ * @see _get_path_to_translation()
+ *
+ * @param string $domain Text domain. Unique identifier for retrieving translated strings.
+ * @return string|false The path to the translation file or false if no translation file was found.
+ */
+function _get_path_to_translation_from_lang_dir( $domain ) {
+	_deprecated_function( __FUNCTION__, '3.1.0', 'WP_Textdomain_Registry' );
+
+	static $cached_mofiles = null;
+
+	if ( null === $cached_mofiles ) {
+		$cached_mofiles = array();
+
+		$locations = array(
+			WP_LANG_DIR . '/plugins',
+			WP_LANG_DIR . '/themes',
+		);
+
+		foreach ( $locations as $location ) {
+			$mofiles = glob( $location . '/*.mo' );
+			if ( $mofiles ) {
+				$cached_mofiles = array_merge( $cached_mofiles, $mofiles );
+			}
+		}
+	}
+
+	$locale = is_admin() ? get_user_locale() : get_locale();
+	$mofile = "{$domain}-{$locale}.mo";
+
+	$path = WP_LANG_DIR . '/plugins/' . $mofile;
+	if ( in_array( $path, $cached_mofiles ) ) {
+		return $path;
+	}
+
+	$path = WP_LANG_DIR . '/themes/' . $mofile;
+	if ( in_array( $path, $cached_mofiles ) ) {
+		return $path;
+	}
+
+	return false;
+}
diff --git src/wp-includes/l10n.php src/wp-includes/l10n.php
index 72bb406bc3..858dbcc276 100644
--- src/wp-includes/l10n.php
+++ src/wp-includes/l10n.php
@@ -532,15 +532,16 @@ function translate_nooped_plural( $nooped_plural, $count, $domain = 'default' )
  *
  * @since 1.5.0
  *
- * @global array $l10n          An array of all currently loaded text domains.
- * @global array $l10n_unloaded An array of all text domains that have been unloaded again.
+ * @global array                  $l10n                   An array of all currently loaded text domains.
+ * @global array                  $l10n_unloaded          An array of all text domains that have been unloaded again.
+ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
  *
  * @param string $domain Text domain. Unique identifier for retrieving translated strings.
  * @param string $mofile Path to the .mo file.
  * @return bool True on success, false on failure.
  */
 function load_textdomain( $domain, $mofile ) {
-	global $l10n, $l10n_unloaded;
+	global $l10n, $l10n_unloaded, $wp_textdomain_registry;
 
 	$l10n_unloaded = (array) $l10n_unloaded;
 
@@ -581,17 +582,26 @@ function load_textdomain( $domain, $mofile ) {
 	 */
 	$mofile = apply_filters( 'load_textdomain_mofile', $mofile, $domain );
 
-	if ( !is_readable( $mofile ) ) return false;
+	if ( ! is_readable( $mofile ) ) {
+		return false;
+	}
 
-	$mo = new MO();
-	if ( !$mo->import_from_file( $mofile ) ) return false;
+	unset( $l10n_unloaded[ $domain ] );
+
+	if ( isset( $l10n[$domain] ) ) {
+		$mo = new MO();
+
+		if ( ! $mo->import_from_file( $mofile ) ) {
+			return false;
+		}
 
-	if ( isset( $l10n[$domain] ) )
 		$mo->merge_with( $l10n[$domain] );
 
-	unset( $l10n_unloaded[ $domain ] );
+		return true;
+	}
 
-	$l10n[$domain] = &$mo;
+	/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+	$wp_textdomain_registry->set( $domain, dirname( $mofile ) );
 
 	return true;
 }
@@ -601,14 +611,15 @@ function load_textdomain( $domain, $mofile ) {
  *
  * @since 3.0.0
  *
- * @global array $l10n          An array of all currently loaded text domains.
- * @global array $l10n_unloaded An array of all text domains that have been unloaded again.
+ * @global array                  $l10n                   An array of all currently loaded text domains.
+ * @global array                  $l10n_unloaded          An array of all text domains that have been unloaded again.
+ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
  *
  * @param string $domain Text domain. Unique identifier for retrieving translated strings.
  * @return bool Whether textdomain was unloaded.
  */
 function unload_textdomain( $domain ) {
-	global $l10n, $l10n_unloaded;
+	global $l10n, $l10n_unloaded, $wp_textdomain_registry;
 
 	$l10n_unloaded = (array) $l10n_unloaded;
 
@@ -637,9 +648,12 @@ function unload_textdomain( $domain ) {
 	 */
 	do_action( 'unload_textdomain', $domain );
 
-	if ( isset( $l10n[$domain] ) ) {
-		unset( $l10n[$domain] );
+	if ( isset( $l10n[ $domain ] ) ) {
+		unset( $l10n[ $domain ] );
+	}
 
+	/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+	if ( null !== $wp_textdomain_registry->get( $domain ) ) {
 		$l10n_unloaded[ $domain ] = true;
 
 		return true;
@@ -696,6 +710,8 @@ function load_default_textdomain( $locale = null ) {
  * @since 1.5.0
  * @since 4.6.0 The function now tries to load the .mo file from the languages directory first.
  *
+ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
+ *
  * @param string $domain          Unique identifier for retrieving translated strings
  * @param string $deprecated      Optional. Use the $plugin_rel_path parameter instead. Default false.
  * @param string $plugin_rel_path Optional. Relative path to WP_PLUGIN_DIR where the .mo file resides.
@@ -703,6 +719,8 @@ function load_default_textdomain( $locale = null ) {
  * @return bool True when textdomain is successfully loaded, false otherwise.
  */
 function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {
+	global $wp_textdomain_registry;
+
 	/**
 	 * Filters a plugin's locale.
 	 *
@@ -729,6 +747,9 @@ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path
 		$path = WP_PLUGIN_DIR;
 	}
 
+	/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+	$wp_textdomain_registry->set( $domain, $path );
+
 	return load_textdomain( $domain, $path . '/' . $mofile );
 }
 
@@ -738,12 +759,16 @@ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path
  * @since 3.0.0
  * @since 4.6.0 The function now tries to load the .mo file from the languages directory first.
  *
+ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
+ *
  * @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
  *                                   file resides. Default empty string.
  * @return bool True when textdomain is successfully loaded, false otherwise.
  */
 function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
+	global $wp_textdomain_registry;
+
 	/** This filter is documented in wp-includes/l10n.php */
 	$locale = apply_filters( 'plugin_locale', is_admin() ? get_user_locale() : get_locale(), $domain );
 
@@ -756,6 +781,9 @@ function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
 
 	$path = WPMU_PLUGIN_DIR . '/' . ltrim( $mu_plugin_rel_path, '/' );
 
+	/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+	$wp_textdomain_registry->set( $domain, $path );
+
 	return load_textdomain( $domain, $path . '/' . $mofile );
 }
 
@@ -770,12 +798,16 @@ function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
  * @since 1.5.0
  * @since 4.6.0 The function now tries to load the .mo file from the languages directory first.
  *
+ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
+ *
  * @param string $domain Text domain. Unique identifier for retrieving translated strings.
  * @param string $path   Optional. Path to the directory containing the .mo file.
  *                       Default false.
  * @return bool True when textdomain is successfully loaded, false otherwise.
  */
 function load_theme_textdomain( $domain, $path = false ) {
+	global $wp_textdomain_registry;
+
 	/**
 	 * Filters a theme's locale.
 	 *
@@ -797,6 +829,9 @@ function load_theme_textdomain( $domain, $path = false ) {
 		$path = get_template_directory();
 	}
 
+	/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+	$wp_textdomain_registry->set( $domain, $path );
+
 	return load_textdomain( $domain, $path . '/' . $locale . '.mo' );
 }
 
@@ -832,103 +867,60 @@ function load_child_theme_textdomain( $domain, $path = false ) {
  * @access private
  *
  * @see get_translations_for_domain()
- * @global array $l10n_unloaded An array of all text domains that have been unloaded again.
+ *
+ * @global array                  $l10n                   An array of all currently loaded text domains.
+ * @global array                  $l10n_unloaded          An array of all text domains that have been unloaded again.
+ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
  *
  * @param string $domain Text domain. Unique identifier for retrieving translated strings.
  * @return bool True when the textdomain is successfully loaded, false otherwise.
  */
 function _load_textdomain_just_in_time( $domain ) {
-	global $l10n_unloaded;
+	global $l10n, $l10n_unloaded, $wp_textdomain_registry;
 
 	$l10n_unloaded = (array) $l10n_unloaded;
 
-	// Short-circuit if domain is 'default' which is reserved for core.
-	if ( 'default' === $domain || isset( $l10n_unloaded[ $domain ] ) ) {
+	if ( isset( $l10n_unloaded[ $domain ] ) ) {
 		return false;
 	}
 
-	$translation_path = _get_path_to_translation( $domain );
-	if ( false === $translation_path ) {
-		return false;
+	/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+	if ( null === $wp_textdomain_registry->get( $domain ) ) {
+		$wp_textdomain_registry->get_translation_from_lang_dir( $domain );
 	}
 
-	return load_textdomain( $domain, $translation_path );
-}
+	$path = $wp_textdomain_registry->get( $domain );
 
-/**
- * Gets the path to a translation file for loading a textdomain just in time.
- *
- * Caches the retrieved results internally.
- *
- * @since 4.7.0
- * @access private
- *
- * @see _load_textdomain_just_in_time()
- *
- * @param string $domain Text domain. Unique identifier for retrieving translated strings.
- * @param bool   $reset  Whether to reset the internal cache. Used by the switch to locale functionality.
- * @return string|false The path to the translation file or false if no translation file was found.
- */
-function _get_path_to_translation( $domain, $reset = false ) {
-	static $available_translations = array();
-
-	if ( true === $reset ) {
-		$available_translations = array();
-	}
-
-	if ( ! isset( $available_translations[ $domain ] ) ) {
-		$available_translations[ $domain ] = _get_path_to_translation_from_lang_dir( $domain );
+	if ( false === $path ) {
+		return false;
 	}
 
-	return $available_translations[ $domain ];
-}
-
-/**
- * Gets the path to a translation file in the languages directory for the current locale.
- *
- * Holds a cached list of available .mo files to improve performance.
- *
- * @since 4.7.0
- * @access private
- *
- * @see _get_path_to_translation()
- *
- * @param string $domain Text domain. Unique identifier for retrieving translated strings.
- * @return string|false The path to the translation file or false if no translation file was found.
- */
-function _get_path_to_translation_from_lang_dir( $domain ) {
-	static $cached_mofiles = null;
-
-	if ( null === $cached_mofiles ) {
-		$cached_mofiles = array();
+	$locale = is_admin() ? get_user_locale() : get_locale();
+	$mofile = "{$path}/{$domain}-{$locale}.mo";
 
-		$locations = array(
-			WP_LANG_DIR . '/plugins',
-			WP_LANG_DIR . '/themes',
-		);
+	if ( 'default' === $domain ) {
+		$mofile = "{$path}/{$locale}.mo";
+	}
 
-		foreach ( $locations as $location ) {
-			$mofiles = glob( $location . '/*.mo' );
-			if ( $mofiles ) {
-				$cached_mofiles = array_merge( $cached_mofiles, $mofiles );
-			}
-		}
+	if ( ! is_readable( $mofile ) ) {
+		return false;
 	}
 
-	$locale = is_admin() ? get_user_locale() : get_locale();
-	$mofile = "{$domain}-{$locale}.mo";
+	$mo = new MO();
 
-	$path = WP_LANG_DIR . '/plugins/' . $mofile;
-	if ( in_array( $path, $cached_mofiles ) ) {
-		return $path;
+	if ( ! $mo->import_from_file( $mofile ) ) {
+		return false;
 	}
 
-	$path = WP_LANG_DIR . '/themes/' . $mofile;
-	if ( in_array( $path, $cached_mofiles ) ) {
-		return $path;
+	if ( isset( $l10n[ $domain ] ) ) {
+		$mo->merge_with( $l10n[ $domain ] );
 	}
 
-	return false;
+	unset( $l10n_unloaded[ $domain ] );
+
+	$l10n[ $domain ] = &$mo;
+
+	return true;
 }
 
 /**
@@ -962,14 +954,18 @@ function get_translations_for_domain( $domain ) {
  *
  * @since 3.0.0
  *
- * @global array $l10n
+ * @global array                  $l10n                   An array of all currently loaded text domains.
+ * @global array                  $l10n_unloaded          An array of all text domains that have been unloaded again.
+ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
  *
  * @param string $domain Text domain. Unique identifier for retrieving translated strings.
  * @return bool Whether there are translations.
  */
 function is_textdomain_loaded( $domain ) {
-	global $l10n;
-	return isset( $l10n[ $domain ] );
+	global $l10n, $l10n_unloaded, $wp_textdomain_registry;
+
+	/** @var WP_Textdomain_Registry $wp_textdomain_registry */
+	return isset( $l10n[ $domain ] ) || ( ! isset( $l10n_unloaded[ $domain ] ) && $wp_textdomain_registry->get( $domain ) );
 }
 
 /**
diff --git src/wp-settings.php src/wp-settings.php
index 3d4c210338..1b04180505 100644
--- src/wp-settings.php
+++ src/wp-settings.php
@@ -133,6 +133,7 @@ if ( SHORTINIT )
 
 // Load the L10n library.
 require_once( ABSPATH . WPINC . '/l10n.php' );
+require_once( ABSPATH . WPINC . '/class-wp-textdomain-registry.php' );
 require_once( ABSPATH . WPINC . '/class-wp-locale.php' );
 require_once( ABSPATH . WPINC . '/class-wp-locale-switcher.php' );
 
@@ -242,6 +243,17 @@ require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php'
 
 $GLOBALS['wp_embed'] = new WP_Embed();
 
+/**
+ * WordPress Textdomain Registry object.
+ *
+ * Used to support just-in-time translations for manually loaded textdomains.
+ *
+ * @since 5.0.0
+ *
+ * @global WP_Locale_Switcher $wp_locale_switcher WordPress Textdomain Registry.
+ */
+$GLOBALS['wp_textdomain_registry'] = new WP_Textdomain_Registry();
+
 // Load multisite-specific files.
 if ( is_multisite() ) {
 	require( ABSPATH . WPINC . '/ms-functions.php' );
@@ -407,7 +419,7 @@ unset( $locale_file );
 $GLOBALS['wp_locale'] = new WP_Locale();
 
 /**
- *  WordPress Locale Switcher object for switching locales.
+ * WordPress Locale Switcher object for switching locales.
  *
  * @since 4.7.0
  *
diff --git tests/phpunit/data/plugins/custom-internationalized-plugin/custom-internationalized-plugin.php tests/phpunit/data/plugins/custom-internationalized-plugin/custom-internationalized-plugin.php
new file mode 100644
index 0000000000..6889deca6a
--- /dev/null
+++ tests/phpunit/data/plugins/custom-internationalized-plugin/custom-internationalized-plugin.php
@@ -0,0 +1,14 @@
+<?php
+/*
+Plugin Name: Custom Dummy Plugin
+Plugin URI: https://wordpress.org/
+Description: For testing purposes only.
+Version: 1.0.0
+Text Domain: custom-internationalized-plugin
+*/
+
+load_plugin_textdomain( 'custom-internationalized-plugin', false, basename( dirname( __FILE__ ) ) . '/languages' );
+
+function custom_i18n_plugin_test() {
+	return __( 'This is a dummy plugin', 'custom-internationalized-plugin' );
+}
diff --git tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-de_DE.mo tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-de_DE.mo
new file mode 100644
index 0000000000..a9f2de4915
Binary files /dev/null and tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-de_DE.mo differ
diff --git tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-de_DE.po tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-de_DE.po
new file mode 100644
index 0000000000..1bf9084bed
--- /dev/null
+++ tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-de_DE.po
@@ -0,0 +1,23 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: 2015-12-31 16:31+0100\n"
+"PO-Revision-Date: 2017-10-02 22:34+0200\n"
+"Language: de_DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.2\n"
+"X-Poedit-Basepath: .\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-KeywordsList: __;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;"
+"_nx_noop:1,2,3c;esc_attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;"
+"esc_html_x:1,2c\n"
+"X-Textdomain-Support: yes\n"
+"Last-Translator: Pascal Birchler <swissspidy@chat.wordpress.org>\n"
+"Language-Team: \n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: internationalized-plugin.php:11
+msgid "This is a dummy plugin"
+msgstr "Das ist ein Dummy Plugin"
diff --git tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-en_GB.mo tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-en_GB.mo
new file mode 100644
index 0000000000..a0395710ca
Binary files /dev/null and tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-en_GB.mo differ
diff --git tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-en_GB.po tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-en_GB.po
new file mode 100644
index 0000000000..7fe6a51b68
--- /dev/null
+++ tests/phpunit/data/plugins/custom-internationalized-plugin/languages/custom-internationalized-plugin-en_GB.po
@@ -0,0 +1,23 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: 2015-12-31 16:31+0100\n"
+"PO-Revision-Date: 2017-10-02 22:34+0200\n"
+"Language: en_GB\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.2\n"
+"X-Poedit-Basepath: .\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-KeywordsList: __;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;"
+"_nx_noop:1,2,3c;esc_attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;"
+"esc_html_x:1,2c\n"
+"X-Textdomain-Support: yes\n"
+"Last-Translator: Pascal Birchler <swissspidy@chat.wordpress.org>\n"
+"Language-Team: \n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: internationalized-plugin.php:11
+msgid "This is a dummy plugin"
+msgstr "This is a wally plugin"
diff --git tests/phpunit/tests/l10n/loadTextdomain.php tests/phpunit/tests/l10n/loadTextdomain.php
index a44f09a7fd..0d500e4296 100644
--- tests/phpunit/tests/l10n/loadTextdomain.php
+++ tests/phpunit/tests/l10n/loadTextdomain.php
@@ -22,12 +22,22 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
 
 		add_filter( 'plugin_locale', array( $this, 'store_locale' ) );
 		add_filter( 'theme_locale', array( $this, 'store_locale' ) );
+
+		/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+		global $wp_textdomain_registry;
+
+		$wp_textdomain_registry->reset();
 	}
 
 	public function tearDown() {
 		remove_filter( 'plugin_locale', array( $this, 'store_locale' ) );
 		remove_filter( 'theme_locale', array( $this, 'store_locale' ) );
 
+		/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+		global $wp_textdomain_registry;
+
+		$wp_textdomain_registry->reset();
+
 		parent::tearDown();
 	}
 
@@ -114,13 +124,13 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
 	/**
 	 * @ticket 21319
 	 */
-	function test_is_textdomain_is_not_loaded_after_gettext_call_with_no_translations() {
+	public function test_is_textdomain_is_not_loaded_after_gettext_call_with_no_translations() {
 		$this->assertFalse( is_textdomain_loaded( 'wp-tests-domain' ) );
 		__( 'just some string', 'wp-tests-domain' );
 		$this->assertFalse( is_textdomain_loaded( 'wp-tests-domain' ) );
 	}
 
-	function test_override_load_textdomain_noop() {
+	public function test_override_load_textdomain_noop() {
 		add_filter( 'override_load_textdomain', '__return_true' );
 		$load_textdomain = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/non-existent-file' );
 		remove_filter( 'override_load_textdomain', '__return_true' );
@@ -129,7 +139,7 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
 		$this->assertFalse( is_textdomain_loaded( 'wp-tests-domain' ) );
 	}
 
-	function test_override_load_textdomain_non_existent_mofile() {
+	public function test_override_load_textdomain_non_existent_mofile() {
 		add_filter( 'override_load_textdomain', array( $this, '_override_load_textdomain_filter' ), 10, 3 );
 		$load_textdomain = load_textdomain( 'wp-tests-domain', WP_LANG_DIR . '/non-existent-file.mo' );
 		remove_filter( 'override_load_textdomain', array( $this, '_override_load_textdomain_filter' ) );
@@ -143,7 +153,8 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
 		$this->assertFalse( $is_textdomain_loaded_after );
 	}
 
-	function test_override_load_textdomain_custom_mofile() {
+	public function test_override_load_textdomain_custom_mofile() {
+		global $l10n, $l10n_unloaded, $wp_textdomain_registry;
 		add_filter( 'override_load_textdomain', array( $this, '_override_load_textdomain_filter' ), 10, 3 );
 		$load_textdomain = load_textdomain( 'wp-tests-domain', WP_LANG_DIR . '/plugins/internationalized-plugin-de_DE.mo' );
 		remove_filter( 'override_load_textdomain', array( $this, '_override_load_textdomain_filter' ) );
@@ -163,7 +174,7 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
 	 * @param string $file     Path to the MO file.
 	 * @return bool
 	 */
-	function _override_load_textdomain_filter( $override, $domain, $file ) {
+	public function _override_load_textdomain_filter( $override, $domain, $file ) {
 		global $l10n;
 
 		if ( ! is_readable( $file ) ) {
diff --git tests/phpunit/tests/l10n/loadTextdomainJustInTime.php tests/phpunit/tests/l10n/loadTextdomainJustInTime.php
index fc2b84aa3a..0e8e364a3d 100644
--- tests/phpunit/tests/l10n/loadTextdomainJustInTime.php
+++ tests/phpunit/tests/l10n/loadTextdomainJustInTime.php
@@ -30,10 +30,12 @@ class Tests_L10n_loadTextdomainJustInTime extends WP_UnitTestCase {
 		add_filter( 'stylesheet_root', array( $this, 'filter_theme_root' ) );
 		add_filter( 'template_root', array( $this, 'filter_theme_root' ) );
 		wp_clean_themes_cache();
-		unset( $GLOBALS['wp_themes'] );
-		unset( $GLOBALS['l10n'] );
-		unset( $GLOBALS['l10n_unloaded'] );
-		_get_path_to_translation( null, true );
+		unset( $GLOBALS['wp_themes'], $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
+
+		/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+		global $wp_textdomain_registry;
+
+		$wp_textdomain_registry->reset();
 	}
 
 	public function tearDown() {
@@ -42,10 +44,12 @@ class Tests_L10n_loadTextdomainJustInTime extends WP_UnitTestCase {
 		remove_filter( 'stylesheet_root', array( $this, 'filter_theme_root' ) );
 		remove_filter( 'template_root', array( $this, 'filter_theme_root' ) );
 		wp_clean_themes_cache();
-		unset( $GLOBALS['wp_themes'] );
-		unset( $GLOBALS['l10n'] );
-		unset( $GLOBALS['l10n_unloaded'] );
-		_get_path_to_translation( null, true );
+		unset( $GLOBALS['wp_themes'], $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
+
+		/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+		global $wp_textdomain_registry;
+
+		$wp_textdomain_registry->reset();
 
 		parent::tearDown();
 	}
@@ -111,7 +115,7 @@ class Tests_L10n_loadTextdomainJustInTime extends WP_UnitTestCase {
 		remove_filter( 'override_load_textdomain', '__return_true' );
 		remove_filter( 'locale', array( $this, 'filter_set_locale_to_german' ) );
 
-		$this->assertTrue( $translations instanceof NOOP_Translations );
+		$this->assertNotNull( 'NOOP_Translations', $translations );
 	}
 
 	/**
diff --git tests/phpunit/tests/l10n/localeSwitcher.php tests/phpunit/tests/l10n/localeSwitcher.php
index 41492bdb45..640aa34df2 100644
--- tests/phpunit/tests/l10n/localeSwitcher.php
+++ tests/phpunit/tests/l10n/localeSwitcher.php
@@ -22,15 +22,21 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
 		$this->locale = '';
 		$this->previous_locale = '';
 
-		unset( $GLOBALS['l10n'] );
-		unset( $GLOBALS['l10n_unloaded'] );
-		_get_path_to_translation( null, true );
+		unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
+
+		/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+		global $wp_textdomain_registry;
+
+		$wp_textdomain_registry->reset();
 	}
 
 	public function tearDown() {
-		unset( $GLOBALS['l10n'] );
-		unset( $GLOBALS['l10n_unloaded'] );
-		_get_path_to_translation( null, true );
+		unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
+
+		/* @var WP_Textdomain_Registry $wp_textdomain_registry */
+		global $wp_textdomain_registry;
+
+		$wp_textdomain_registry->reset();
 
 		parent::tearDown();
 	}
@@ -251,7 +257,7 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
 
 		$site_locale = get_locale();
 
-		$user_id = $this->factory()->user->create( array(
+		$user_id = static::factory()->user->create( array(
 			'role'   => 'administrator',
 			'locale' => 'de_DE',
 		) );
@@ -269,6 +275,7 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
 		$this->assertSame( 'de_DE', $user_locale );
 
 		load_default_textdomain( $user_locale );
+		get_translations_for_domain( 'default' );
 		$language_header_before_switch = $l10n['default']->headers['Language']; // de_DE
 
 		$locale_switched_user_locale = switch_to_locale( $user_locale ); // False.
@@ -300,7 +307,7 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
 
 		$site_locale = get_locale();
 
-		$user_id = $this->factory()->user->create( array(
+		$user_id = static::factory()->user->create( array(
 			'role'   => 'administrator',
 			'locale' => 'de_DE',
 		) );
@@ -318,15 +325,22 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
 		$this->assertSame( 'de_DE', $user_locale );
 
 		load_default_textdomain( $user_locale );
+		get_translations_for_domain( 'default' );
 		$language_header_before_switch = $l10n['default']->headers['Language']; // de_DE
 
 		$locale_switched_user_locale = switch_to_locale( $user_locale ); // False.
 		$locale_switched_site_locale = switch_to_locale( $site_locale ); // True.
 		$site_locale_after_switch = get_locale();
+		load_default_textdomain( get_locale() );
+		get_translations_for_domain( 'default' );
+
 		$language_header_after_switch = $l10n['default']->headers['Language']; // es_ES
 
 		restore_current_locale();
 
+		load_default_textdomain();
+		get_translations_for_domain( 'default' );
+
 		$language_header_after_restore = $l10n['default']->headers['Language']; // de_DE
 
 		$wp_locale_switcher = $locale_switcher;
@@ -348,7 +362,7 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
 
 		$site_locale = get_locale();
 
-		$user_id = $this->factory()->user->create( array(
+		$user_id = static::factory()->user->create( array(
 			'role'   => 'administrator',
 			'locale' => 'en_GB',
 		) );
@@ -382,6 +396,34 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
 		$this->assertSame( 'This is a dummy plugin', $expected );
 	}
 
+	/**
+	 * @ticket 39210
+	 */
+	public function test_switch_reloads_translations_outside_wplang() {
+		require_once DIR_TESTDATA . '/plugins/custom-internationalized-plugin/custom-internationalized-plugin.php';
+
+		$this->assertTrue( true );
+		return;
+
+		$expected = custom_i18n_plugin_test();
+
+		$this->assertSame( 'This is a dummy plugin', $expected );
+
+		switch_to_locale( 'en_GB' );
+		switch_to_locale( 'de_DE' );
+
+		$expected = custom_i18n_plugin_test();
+
+		$this->assertSame( 'Das ist ein Dummy Plugin', $expected );
+
+		restore_previous_locale();
+
+		$expected = custom_i18n_plugin_test();
+		$this->assertSame( 'This is a wally plugin', $expected );
+
+		restore_current_locale();
+	}
+
 	public function filter_locale() {
 		return 'es_ES';
 	}
