I18N: Introduce `WP_Textdomain_Registry` to store text domains and their language directory paths.

Previously, when using `switch_to_locale()` all current loaded text domains were unloaded and added to the `$l10n_unloaded` global. This prevented the just-in-time loading for text domains after a switch. The just-in-time loading was also only possible if the translations were stored in `WP_LANG_DIR`. Both issues have been fixed.

* Adds `WP_Textdomain_Registry` to keep track of the language directory paths for all plugins and themes.
* Updates all `load_*_textdomain()`  functions to store the path in `WP_Textdomain_Registry`.
* Adds `$reloadable` parameter to `unload_textdomain()` to define whether a text domain can be loaded just-in-time again. This is used by `WP_Locale_Switcher::load_translations()`.
* Extends `_load_textdomain_just_in_time()` to also support text domains of plugins and themes with custom language directories.
* Fixes the incorrect `test_plugin_translation_after_switching_locale_twice()` test which should have catch this issue earlier.
* Adds a new test plugin/theme to test the loading of translations with a custom language directory.
* Deprecates the now unused and private `_get_path_to_translation()` and `_get_path_to_translation_from_lang_dir()` functions.

Props yoavf, swissspidy, dd32, ocean90.
See #26511.
Fixes #39210.

git-svn-id: https://develop.svn.wordpress.org/trunk@49236 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Dominik Schilling 2020-10-20 16:03:58 +00:00
parent e3f28a9a11
commit 9e83d04f84
30 changed files with 564 additions and 126 deletions

View File

@ -196,11 +196,12 @@ class WP_Locale_Switcher {
load_default_textdomain( $locale ); load_default_textdomain( $locale );
foreach ( $domains as $domain ) { foreach ( $domains as $domain ) {
// The default text domain is handled by `load_default_textdomain()`.
if ( 'default' === $domain ) { if ( 'default' === $domain ) {
continue; continue;
} }
unload_textdomain( $domain ); unload_textdomain( $domain, true );
get_translations_for_domain( $domain ); get_translations_for_domain( $domain );
} }
} }
@ -218,12 +219,11 @@ class WP_Locale_Switcher {
* @param string $locale The locale to change to. * @param string $locale The locale to change to.
*/ */
private function change_locale( $locale ) { private function change_locale( $locale ) {
// Reset translation availability information. global $wp_locale;
_get_path_to_translation( null, true );
$this->load_translations( $locale ); $this->load_translations( $locale );
$GLOBALS['wp_locale'] = new WP_Locale(); $wp_locale = new WP_Locale();
/** /**
* Fires when the locale is switched to or restored. * Fires when the locale is switched to or restored.

View File

@ -0,0 +1,130 @@
<?php
/**
* Locale API: WP_Textdomain_Registry class
*
* @package WordPress
* @subpackage i18n
* @since 5.6.0
*/
/**
* Core class used for registering text domains.
*
* @since 5.6.0
*/
class WP_Textdomain_Registry {
/**
* List of domains and their language directory paths.
*
* @since 5.6.0
*
* @var array
*/
protected $domains = array();
/**
* Holds a cached list of available .mo files to improve performance.
*
* @since 5.6.0
*
* @var array
*/
protected $cached_mo_files;
/**
* Returns the MO file path for a specific domain.
*
* @since 5.6.0
*
* @param string $domain Text domain.
* @return string|false MO file path or false if there is none available.
*/
public function get( $domain ) {
if ( isset( $this->domains[ $domain ] ) ) {
return $this->domains[ $domain ];
}
return $this->get_path_from_lang_dir( $domain );
}
/**
* Sets the MO file path for a specific domain.
*
* @since 5.6.0
*
* @param string $domain Text domain.
* @param string|false $path Language directory path or false if there is none available.
*/
public function set( $domain, $path ) {
$this->domains[ $domain ] = $path ? trailingslashit( $path ) : false;
}
/**
* Resets the registry state.
*
* @since 5.6.0
*/
public function reset() {
$this->cached_mo_files = null;
$this->domains = array();
}
/**
* Gets the path to a translation file in the languages directory for the current locale.
*
* @since 5.6.0
*
* @param string $domain Text domain.
* @return string|false MO file path or false if there is none available.
*/
private function get_path_from_lang_dir( $domain ) {
if ( null === $this->cached_mo_files ) {
$this->cached_mo_files = array();
$this->set_cached_mo_files();
}
$locale = determine_locale();
$mofile = "{$domain}-{$locale}.mo";
$path = WP_LANG_DIR . '/plugins/' . $mofile;
if ( in_array( $path, $this->cached_mo_files, true ) ) {
$path = WP_LANG_DIR . '/plugins/';
$this->set( $domain, $path );
return $path;
}
$path = WP_LANG_DIR . '/themes/' . $mofile;
if ( in_array( $path, $this->cached_mo_files, true ) ) {
$path = WP_LANG_DIR . '/themes/';
$this->set( $domain, $path );
return $path;
}
$this->set( $domain, false );
return false;
}
/**
* Reads and caches all available MO files from the plugins and themes language directories.
*
* @since 5.6.0
*/
protected function set_cached_mo_files() {
$locations = array(
WP_LANG_DIR . '/plugins',
WP_LANG_DIR . '/themes',
);
foreach ( $locations as $location ) {
$mo_files = glob( $location . '/*.mo' );
if ( $mo_files ) {
$this->cached_mo_files = array_merge( $this->cached_mo_files, $mo_files );
}
}
}
}

View File

@ -4134,3 +4134,85 @@ function wp_slash_strings_only( $value ) {
function addslashes_strings_only( $value ) { function addslashes_strings_only( $value ) {
return is_string( $value ) ? addslashes( $value ) : $value; return is_string( $value ) ? addslashes( $value ) : $value;
} }
/**
* 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.6.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__, '5.6.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.6.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__, '5.6.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 = determine_locale();
$mofile = "{$domain}-{$locale}.mo";
$path = WP_LANG_DIR . '/plugins/' . $mofile;
if ( in_array( $path, $cached_mofiles, true ) ) {
return $path;
}
$path = WP_LANG_DIR . '/themes/' . $mofile;
if ( in_array( $path, $cached_mofiles, true ) ) {
return $path;
}
return false;
}

View File

@ -689,15 +689,16 @@ function translate_nooped_plural( $nooped_plural, $count, $domain = 'default' )
* *
* @since 1.5.0 * @since 1.5.0
* *
* @global MO[] $l10n An array of all currently loaded text domains. * @global MO[] $l10n An array of all currently loaded text domains.
* @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again. * @global MO[] $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 $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $mofile Path to the .mo file. * @param string $mofile Path to the .mo file.
* @return bool True on success, false on failure. * @return bool True on success, false on failure.
*/ */
function load_textdomain( $domain, $mofile ) { function load_textdomain( $domain, $mofile ) {
global $l10n, $l10n_unloaded; global $l10n, $l10n_unloaded, $wp_textdomain_registry;
$l10n_unloaded = (array) $l10n_unloaded; $l10n_unloaded = (array) $l10n_unloaded;
@ -755,6 +756,9 @@ function load_textdomain( $domain, $mofile ) {
$l10n[ $domain ] = &$mo; $l10n[ $domain ] = &$mo;
/** @var WP_Textdomain_Registry $wp_textdomain_registry */
$wp_textdomain_registry->set( $domain, dirname( $mofile ) );
return true; return true;
} }
@ -762,14 +766,16 @@ function load_textdomain( $domain, $mofile ) {
* Unload translations for a text domain. * Unload translations for a text domain.
* *
* @since 3.0.0 * @since 3.0.0
* @since 5.6.0 Added the `$reloadable` parameter.
* *
* @global MO[] $l10n An array of all currently loaded text domains. * @global MO[] $l10n An array of all currently loaded text domains.
* @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again. * @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reloadable Whether the text domain can be loaded just-in-time again.
* @return bool Whether textdomain was unloaded. * @return bool Whether textdomain was unloaded.
*/ */
function unload_textdomain( $domain ) { function unload_textdomain( $domain, $reloadable = false ) {
global $l10n, $l10n_unloaded; global $l10n, $l10n_unloaded;
$l10n_unloaded = (array) $l10n_unloaded; $l10n_unloaded = (array) $l10n_unloaded;
@ -778,14 +784,18 @@ function unload_textdomain( $domain ) {
* Filters whether to override the text domain unloading. * Filters whether to override the text domain unloading.
* *
* @since 3.0.0 * @since 3.0.0
* @since 5.6.0 Added the `$reloadable` parameter.
* *
* @param bool $override Whether to override the text domain unloading. Default false. * @param bool $override Whether to override the text domain unloading. Default false.
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reloadable Whether the text domain can be loaded just-in-time again.
*/ */
$plugin_override = apply_filters( 'override_unload_textdomain', false, $domain ); $plugin_override = apply_filters( 'override_unload_textdomain', false, $domain, $reloadable );
if ( $plugin_override ) { if ( $plugin_override ) {
$l10n_unloaded[ $domain ] = true; if ( ! $reloadable ) {
$l10n_unloaded[ $domain ] = true;
}
return true; return true;
} }
@ -794,15 +804,19 @@ function unload_textdomain( $domain ) {
* Fires before the text domain is unloaded. * Fires before the text domain is unloaded.
* *
* @since 3.0.0 * @since 3.0.0
* @since 5.6.0 Added the `$reloadable` parameter.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param bool $reloadable Whether the text domain can be loaded just-in-time again.
*/ */
do_action( 'unload_textdomain', $domain ); do_action( 'unload_textdomain', $domain, $reloadable );
if ( isset( $l10n[ $domain ] ) ) { if ( isset( $l10n[ $domain ] ) ) {
unset( $l10n[ $domain ] ); unset( $l10n[ $domain ] );
$l10n_unloaded[ $domain ] = true; if ( ! $reloadable ) {
$l10n_unloaded[ $domain ] = true;
}
return true; return true;
} }
@ -867,6 +881,8 @@ function load_default_textdomain( $locale = null ) {
* @return bool True when textdomain is successfully loaded, false otherwise. * @return bool True when textdomain is successfully loaded, false otherwise.
*/ */
function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) { function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {
global $wp_textdomain_registry;
/** /**
* Filters a plugin's locale. * Filters a plugin's locale.
* *
@ -893,6 +909,9 @@ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path
$path = WP_PLUGIN_DIR; $path = WP_PLUGIN_DIR;
} }
/* @var WP_Textdomain_Registry $wp_textdomain_registry */
$wp_textdomain_registry->set( $domain, $path );
return load_textdomain( $domain, $path . '/' . $mofile ); return load_textdomain( $domain, $path . '/' . $mofile );
} }
@ -902,12 +921,16 @@ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path
* @since 3.0.0 * @since 3.0.0
* @since 4.6.0 The function now tries to load the .mo file from the languages directory first. * @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 $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 * @param string $mu_plugin_rel_path Optional. Relative to `WPMU_PLUGIN_DIR` directory in which the .mo
* file resides. Default empty string. * file resides. Default empty string.
* @return bool True when textdomain is successfully loaded, false otherwise. * @return bool True when textdomain is successfully loaded, false otherwise.
*/ */
function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) { function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
global $wp_textdomain_registry;
/** This filter is documented in wp-includes/l10n.php */ /** This filter is documented in wp-includes/l10n.php */
$locale = apply_filters( 'plugin_locale', determine_locale(), $domain ); $locale = apply_filters( 'plugin_locale', determine_locale(), $domain );
@ -920,6 +943,9 @@ function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
$path = WPMU_PLUGIN_DIR . '/' . ltrim( $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 ); return load_textdomain( $domain, $path . '/' . $mofile );
} }
@ -934,12 +960,16 @@ function load_muplugin_textdomain( $domain, $mu_plugin_rel_path = '' ) {
* @since 1.5.0 * @since 1.5.0
* @since 4.6.0 The function now tries to load the .mo file from the languages directory first. * @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 $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $path Optional. Path to the directory containing the .mo file. * @param string $path Optional. Path to the directory containing the .mo file.
* Default false. * Default false.
* @return bool True when textdomain is successfully loaded, false otherwise. * @return bool True when textdomain is successfully loaded, false otherwise.
*/ */
function load_theme_textdomain( $domain, $path = false ) { function load_theme_textdomain( $domain, $path = false ) {
global $wp_textdomain_registry;
/** /**
* Filters a theme's locale. * Filters a theme's locale.
* *
@ -961,6 +991,9 @@ function load_theme_textdomain( $domain, $path = false ) {
$path = get_template_directory(); $path = get_template_directory();
} }
/* @var WP_Textdomain_Registry $wp_textdomain_registry */
$wp_textdomain_registry->set( $domain, $path );
return load_textdomain( $domain, $path . '/' . $locale . '.mo' ); return load_textdomain( $domain, $path . '/' . $locale . '.mo' );
} }
@ -1190,14 +1223,14 @@ function load_script_translations( $file, $handle, $domain ) {
* @since 4.6.0 * @since 4.6.0
* @access private * @access private
* *
* @see get_translations_for_domain() * @global MO[] $l10n_unloaded An array of all text domains that have been unloaded again.
* @global MO[] $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 $domain Text domain. Unique identifier for retrieving translated strings.
* @return bool True when the textdomain is successfully loaded, false otherwise. * @return bool True when the textdomain is successfully loaded, false otherwise.
*/ */
function _load_textdomain_just_in_time( $domain ) { function _load_textdomain_just_in_time( $domain ) {
global $l10n_unloaded; global $l10n_unloaded, $wp_textdomain_registry;
$l10n_unloaded = (array) $l10n_unloaded; $l10n_unloaded = (array) $l10n_unloaded;
@ -1206,88 +1239,24 @@ function _load_textdomain_just_in_time( $domain ) {
return false; return false;
} }
$translation_path = _get_path_to_translation( $domain ); /** @var WP_Textdomain_Registry $wp_textdomain_registry */
if ( false === $translation_path ) { $path = $wp_textdomain_registry->get( $domain );
if ( ! $path ) {
return false; return false;
} }
return load_textdomain( $domain, $translation_path );
}
/**
* 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 );
}
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();
$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 = determine_locale(); $locale = determine_locale();
$mofile = "{$domain}-{$locale}.mo";
$path = WP_LANG_DIR . '/plugins/' . $mofile; // Themes with their language directory outside of WP_LANG_DIR have a different file name.
if ( in_array( $path, $cached_mofiles, true ) ) { $template_directory = trailingslashit( get_template_directory() );
return $path; $stylesheet_directory = trailingslashit( get_stylesheet_directory() );
if ( 0 === strpos( $path, $template_directory ) || 0 === strpos( $path, $stylesheet_directory ) ) {
$mofile = "{$path}{$locale}.mo";
} else {
$mofile = "{$path}{$domain}-{$locale}.mo";
} }
$path = WP_LANG_DIR . '/themes/' . $mofile; return load_textdomain( $domain, $mofile );
if ( in_array( $path, $cached_mofiles, true ) ) {
return $path;
}
return false;
} }
/** /**
@ -1297,7 +1266,7 @@ function _get_path_to_translation_from_lang_dir( $domain ) {
* *
* @since 2.8.0 * @since 2.8.0
* *
* @global MO[] $l10n * @global MO[] $l10n An array of all currently loaded text domains.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @return Translations|NOOP_Translations A Translations instance. * @return Translations|NOOP_Translations A Translations instance.
@ -1321,7 +1290,7 @@ function get_translations_for_domain( $domain ) {
* *
* @since 3.0.0 * @since 3.0.0
* *
* @global MO[] $l10n * @global MO[] $l10n An array of all currently loaded text domains.
* *
* @param string $domain Text domain. Unique identifier for retrieving translated strings. * @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @return bool Whether there are translations. * @return bool Whether there are translations.

View File

@ -152,6 +152,7 @@ if ( SHORTINIT ) {
// Load the L10n library. // Load the L10n library.
require_once ABSPATH . WPINC . '/l10n.php'; 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.php';
require_once ABSPATH . WPINC . '/class-wp-locale-switcher.php'; require_once ABSPATH . WPINC . '/class-wp-locale-switcher.php';
@ -301,6 +302,17 @@ require ABSPATH . WPINC . '/block-supports/typography.php';
$GLOBALS['wp_embed'] = new WP_Embed(); $GLOBALS['wp_embed'] = new WP_Embed();
/**
* WordPress Textdomain Registry object.
*
* Used to support just-in-time translations for manually loaded textdomains.
*
* @since 5.6.0
*
* @global WP_Locale_Switcher $wp_locale_switcher WordPress Textdomain Registry.
*/
$GLOBALS['wp_textdomain_registry'] = new WP_Textdomain_Registry();
// Load multisite-specific files. // Load multisite-specific files.
if ( is_multisite() ) { if ( is_multisite() ) {
require ABSPATH . WPINC . '/ms-functions.php'; require ABSPATH . WPINC . '/ms-functions.php';

View File

@ -2,18 +2,20 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2015-12-31 16:31+0100\n" "POT-Creation-Date: 2015-12-31 16:31+0100\n"
"PO-Revision-Date: 2016-10-26 00:02+0200\n" "PO-Revision-Date: 2020-10-20 17:11+0200\n"
"Language: de_DE\n" "Language: de_DE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.10\n" "X-Generator: Poedit 2.4.1\n"
"X-Poedit-Basepath: .\n" "X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\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;" "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;" "_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" "esc_html_x:1,2c\n"
"X-Textdomain-Support: yes\n" "X-Textdomain-Support: yes\n"
"Language-Team: \n"
"Last-Translator: \n"
"X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-0: .\n"
#: internationalized-plugin.php:11 #: internationalized-plugin.php:11

View File

@ -0,0 +1,23 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2015-12-31 16:31+0100\n"
"PO-Revision-Date: 2020-10-20 17:12+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.4.1\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"
"Language-Team: \n"
"Last-Translator: \n"
"X-Poedit-SearchPath-0: .\n"
#: internationalized-plugin.php:11
msgid "This is a dummy plugin"
msgstr "Este es un plugin dummy"

View File

@ -2,18 +2,20 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2015-12-31 16:38+0100\n" "POT-Creation-Date: 2015-12-31 16:38+0100\n"
"PO-Revision-Date: 2016-10-26 00:02+0200\n" "PO-Revision-Date: 2020-10-20 17:09+0200\n"
"Language: de_DE\n" "Language: de_DE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.10\n" "X-Generator: Poedit 2.4.1\n"
"X-Poedit-Basepath: .\n" "X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\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;" "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;" "_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" "esc_html_x:1,2c\n"
"X-Textdomain-Support: yes\n" "X-Textdomain-Support: yes\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-0: .\n"
#: functions.php:7 #: functions.php:7

View File

@ -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, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
function custom_i18n_plugin_test() {
return __( 'This is a dummy plugin', 'custom-internationalized-plugin' );
}

View File

@ -0,0 +1,23 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2015-12-31 16:31+0100\n"
"PO-Revision-Date: 2020-10-20 17:09+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.4.1\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: Dominik Schilling\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"

View File

@ -0,0 +1,23 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2015-12-31 16:31+0100\n"
"PO-Revision-Date: 2020-10-20 17:09+0200\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.4.1\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: Dominik Schilling\n"
"Language-Team: \n"
"X-Poedit-SearchPath-0: .\n"
#: internationalized-plugin.php:11
msgid "This is a dummy plugin"
msgstr "Este es un plugin dummy"

View File

@ -4,11 +4,8 @@ Plugin Name: Hello Dolly
Plugin URI: http://wordpress.org/# Plugin URI: http://wordpress.org/#
Description: This is not just a plugin, it symbolizes the hope and enthusiasm of an entire generation summed up in two words sung most famously by Louis Armstrong: Hello, Dolly. When activated you will randomly see a lyric from <cite>Hello, Dolly</cite> in the upper right of your admin screen on every page. Description: This is not just a plugin, it symbolizes the hope and enthusiasm of an entire generation summed up in two words sung most famously by Louis Armstrong: Hello, Dolly. When activated you will randomly see a lyric from <cite>Hello, Dolly</cite> in the upper right of your admin screen on every page.
Author: Matt Mullenweg Author: Matt Mullenweg
Version: 1.5.1 Version: 1.7.2
Author URI: http://ma.tt/ Author URI: http://ma.tt/
Text Domain: hello-dolly Text Domain: hello-dolly
*/ */
// Test for
?>

View File

@ -0,0 +1,10 @@
<?php
/**
* Dummy theme.
*/
load_theme_textdomain( 'custom-internationalized-theme', get_template_directory() . '/languages' );
function custom_i18n_theme_test() {
return __( 'This is a dummy theme', 'custom-internationalized-theme' );
}

View File

@ -0,0 +1,4 @@
<?php
/**
* Dummy theme.
*/

View File

@ -0,0 +1,23 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2015-12-31 16:38+0100\n"
"PO-Revision-Date: 2020-10-20 17:11+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.4.1\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: \n"
"Language-Team: \n"
"X-Poedit-SearchPath-0: .\n"
#: functions.php:7
msgid "This is a dummy theme"
msgstr "Das ist ein Dummy Theme"

View File

@ -0,0 +1,23 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2015-12-31 16:38+0100\n"
"PO-Revision-Date: 2020-10-20 17:10+0200\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.4.1\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: \n"
"Language-Team: \n"
"X-Poedit-SearchPath-0: .\n"
#: functions.php:7
msgid "This is a dummy theme"
msgstr "Este es un tema dummy"

View File

@ -0,0 +1,7 @@
/*
Theme Name: Custom Internationalized Theme
Theme URI: https://wordpress.org/
Description: For testing purposes only.
Version: 1.0.0
Text Domain: custom-internationalized-theme
*/

View File

@ -76,7 +76,8 @@ define( 'WP_TESTS_TABLE_PREFIX', $table_prefix );
define( 'DIR_TESTDATA', __DIR__ . '/../data' ); define( 'DIR_TESTDATA', __DIR__ . '/../data' );
define( 'DIR_TESTROOT', realpath( dirname( __DIR__ ) ) ); define( 'DIR_TESTROOT', realpath( dirname( __DIR__ ) ) );
define( 'WP_LANG_DIR', DIR_TESTDATA . '/languages' ); define( 'WP_LANG_DIR', realpath( DIR_TESTDATA . '/languages' ) );
define( 'WP_PLUGIN_DIR', realpath( DIR_TESTDATA . '/plugins' ) );
if ( ! defined( 'WP_TESTS_FORCE_KNOWN_BUGS' ) ) { if ( ! defined( 'WP_TESTS_FORCE_KNOWN_BUGS' ) ) {
define( 'WP_TESTS_FORCE_KNOWN_BUGS', false ); define( 'WP_TESTS_FORCE_KNOWN_BUGS', false );

View File

@ -14,7 +14,7 @@ class Tests_Admin_includesPlugin extends WP_UnitTestCase {
'Description' => 'This is not just a plugin, it symbolizes the hope and enthusiasm of an entire generation summed up in two words sung most famously by Louis Armstrong: Hello, Dolly. When activated you will randomly see a lyric from Hello, Dolly in the upper right of your admin screen on every page. <cite>By <a href="http://ma.tt/">Matt Mullenweg</a>.</cite>', 'Description' => 'This is not just a plugin, it symbolizes the hope and enthusiasm of an entire generation summed up in two words sung most famously by Louis Armstrong: Hello, Dolly. When activated you will randomly see a lyric from Hello, Dolly in the upper right of your admin screen on every page. <cite>By <a href="http://ma.tt/">Matt Mullenweg</a>.</cite>',
'Author' => '<a href="http://ma.tt/">Matt Mullenweg</a>', 'Author' => '<a href="http://ma.tt/">Matt Mullenweg</a>',
'AuthorURI' => 'http://ma.tt/', 'AuthorURI' => 'http://ma.tt/',
'Version' => '1.5.1', 'Version' => '1.7.2',
'TextDomain' => 'hello-dolly', 'TextDomain' => 'hello-dolly',
'DomainPath' => '', 'DomainPath' => '',
); );

View File

@ -24,12 +24,22 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
add_filter( 'plugin_locale', array( $this, 'store_locale' ) ); add_filter( 'plugin_locale', array( $this, 'store_locale' ) );
add_filter( 'theme_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() { public function tearDown() {
remove_filter( 'plugin_locale', array( $this, 'store_locale' ) ); remove_filter( 'plugin_locale', array( $this, 'store_locale' ) );
remove_filter( 'theme_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(); parent::tearDown();
} }
@ -116,13 +126,13 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
/** /**
* @ticket 21319 * @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' ) ); $this->assertFalse( is_textdomain_loaded( 'wp-tests-domain' ) );
__( 'just some string', 'wp-tests-domain' ); __( 'just some string', 'wp-tests-domain' );
$this->assertFalse( is_textdomain_loaded( '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' ); add_filter( 'override_load_textdomain', '__return_true' );
$load_textdomain = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/non-existent-file' ); $load_textdomain = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/non-existent-file' );
remove_filter( 'override_load_textdomain', '__return_true' ); remove_filter( 'override_load_textdomain', '__return_true' );
@ -131,7 +141,7 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
$this->assertFalse( is_textdomain_loaded( 'wp-tests-domain' ) ); $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 ); 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' ); $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' ) ); remove_filter( 'override_load_textdomain', array( $this, '_override_load_textdomain_filter' ) );
@ -145,7 +155,7 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
$this->assertFalse( $is_textdomain_loaded_after ); $this->assertFalse( $is_textdomain_loaded_after );
} }
function test_override_load_textdomain_custom_mofile() { public function test_override_load_textdomain_custom_mofile() {
add_filter( 'override_load_textdomain', array( $this, '_override_load_textdomain_filter' ), 10, 3 ); 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' ); $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' ) ); remove_filter( 'override_load_textdomain', array( $this, '_override_load_textdomain_filter' ) );
@ -165,7 +175,7 @@ class Tests_L10n_loadTextdomain extends WP_UnitTestCase {
* @param string $file Path to the MO file. * @param string $file Path to the MO file.
* @return bool * @return bool
*/ */
function _override_load_textdomain_filter( $override, $domain, $file ) { public function _override_load_textdomain_filter( $override, $domain, $file ) {
global $l10n; global $l10n;
if ( ! is_readable( $file ) ) { if ( ! is_readable( $file ) ) {

View File

@ -32,10 +32,12 @@ class Tests_L10n_loadTextdomainJustInTime extends WP_UnitTestCase {
add_filter( 'stylesheet_root', array( $this, 'filter_theme_root' ) ); add_filter( 'stylesheet_root', array( $this, 'filter_theme_root' ) );
add_filter( 'template_root', array( $this, 'filter_theme_root' ) ); add_filter( 'template_root', array( $this, 'filter_theme_root' ) );
wp_clean_themes_cache(); wp_clean_themes_cache();
unset( $GLOBALS['wp_themes'] ); unset( $GLOBALS['wp_themes'], $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
unset( $GLOBALS['l10n'] );
unset( $GLOBALS['l10n_unloaded'] ); /** @var WP_Textdomain_Registry $wp_textdomain_registry */
_get_path_to_translation( null, true ); global $wp_textdomain_registry;
$wp_textdomain_registry->reset();
} }
public function tearDown() { public function tearDown() {
@ -44,10 +46,12 @@ class Tests_L10n_loadTextdomainJustInTime extends WP_UnitTestCase {
remove_filter( 'stylesheet_root', array( $this, 'filter_theme_root' ) ); remove_filter( 'stylesheet_root', array( $this, 'filter_theme_root' ) );
remove_filter( 'template_root', array( $this, 'filter_theme_root' ) ); remove_filter( 'template_root', array( $this, 'filter_theme_root' ) );
wp_clean_themes_cache(); wp_clean_themes_cache();
unset( $GLOBALS['wp_themes'] ); unset( $GLOBALS['wp_themes'], $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
unset( $GLOBALS['l10n'] );
unset( $GLOBALS['l10n_unloaded'] ); /** @var WP_Textdomain_Registry $wp_textdomain_registry */
_get_path_to_translation( null, true ); global $wp_textdomain_registry;
$wp_textdomain_registry->reset();
parent::tearDown(); parent::tearDown();
} }
@ -170,6 +174,7 @@ class Tests_L10n_loadTextdomainJustInTime extends WP_UnitTestCase {
/** /**
* @ticket 37997 * @ticket 37997
* @ticket 39210
*/ */
public function test_plugin_translation_after_switching_locale_twice() { public function test_plugin_translation_after_switching_locale_twice() {
require_once DIR_TESTDATA . '/plugins/internationalized-plugin.php'; require_once DIR_TESTDATA . '/plugins/internationalized-plugin.php';
@ -183,7 +188,7 @@ class Tests_L10n_loadTextdomainJustInTime extends WP_UnitTestCase {
restore_current_locale(); restore_current_locale();
$this->assertSame( 'Das ist ein Dummy Plugin', $expected_de_de ); $this->assertSame( 'Das ist ein Dummy Plugin', $expected_de_de );
$this->assertSame( 'This is a dummy plugin', $expected_es_es ); $this->assertSame( 'Este es un plugin dummy', $expected_es_es );
} }
/** /**

View File

@ -22,15 +22,21 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
$this->locale = ''; $this->locale = '';
$this->previous_locale = ''; $this->previous_locale = '';
unset( $GLOBALS['l10n'] ); unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
unset( $GLOBALS['l10n_unloaded'] );
_get_path_to_translation( null, true ); /** @var WP_Textdomain_Registry $wp_textdomain_registry */
global $wp_textdomain_registry;
$wp_textdomain_registry->reset();
} }
public function tearDown() { public function tearDown() {
unset( $GLOBALS['l10n'] ); unset( $GLOBALS['l10n'], $GLOBALS['l10n_unloaded'] );
unset( $GLOBALS['l10n_unloaded'] );
_get_path_to_translation( null, true ); /** @var WP_Textdomain_Registry $wp_textdomain_registry */
global $wp_textdomain_registry;
$wp_textdomain_registry->reset();
parent::tearDown(); parent::tearDown();
} }
@ -388,6 +394,77 @@ class Tests_Locale_Switcher extends WP_UnitTestCase {
$this->assertSame( 'This is a dummy plugin', $expected ); $this->assertSame( 'This is a dummy plugin', $expected );
} }
/**
* @ticket 39210
*/
public function test_switch_reloads_plugin_translations_outside_wp_lang_dir() {
global $wp_locale_switcher, $wp_textdomain_registry;
$locale_switcher = clone $wp_locale_switcher;
$wp_locale_switcher = new WP_Locale_Switcher();
$wp_locale_switcher->init();
require_once DIR_TESTDATA . '/plugins/custom-internationalized-plugin/custom-internationalized-plugin.php';
$this->assertSame( WP_PLUGIN_DIR . '/custom-internationalized-plugin/languages/', $wp_textdomain_registry->get( 'custom-internationalized-plugin' ) );
$expected = custom_i18n_plugin_test();
$this->assertSame( 'This is a dummy plugin', $expected );
switch_to_locale( 'es_ES' );
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( 'Este es un plugin dummy', $expected );
restore_current_locale();
$wp_locale_switcher = $locale_switcher;
}
/**
* @ticket 39210
*/
public function test_switch_reloads_theme_translations_outside_wp_lang_dir() {
global $wp_locale_switcher, $wp_textdomain_registry;
$locale_switcher = clone $wp_locale_switcher;
$wp_locale_switcher = new WP_Locale_Switcher();
$wp_locale_switcher->init();
switch_theme( 'custom-internationalized-theme' );
require_once get_stylesheet_directory() . '/functions.php';
$this->assertSame( get_template_directory() . '/languages/', $wp_textdomain_registry->get( 'custom-internationalized-theme' ) );
$expected = custom_i18n_theme_test();
$this->assertSame( 'This is a dummy theme', $expected );
switch_to_locale( 'es_ES' );
switch_to_locale( 'de_DE' );
$expected = custom_i18n_theme_test();
$this->assertSame( 'Das ist ein Dummy Theme', $expected );
restore_previous_locale();
$expected = custom_i18n_theme_test();
$this->assertSame( 'Este es un tema dummy', $expected );
restore_current_locale();
$wp_locale_switcher = $locale_switcher;
}
public function filter_locale() { public function filter_locale() {
return 'es_ES'; return 'es_ES';
} }

View File

@ -162,6 +162,7 @@ class Tests_Theme_ThemeDir extends WP_UnitTestCase {
'Page Template Theme', // Theme with page templates for other test code. 'Page Template Theme', // Theme with page templates for other test code.
'Theme with Spaces in the Directory', 'Theme with Spaces in the Directory',
'Internationalized Theme', 'Internationalized Theme',
'Custom Internationalized Theme',
'camelCase', 'camelCase',
'REST Theme', 'REST Theme',
); );