diff --git a/src/wp-includes/block-i18n.json b/src/wp-includes/block-i18n.json new file mode 100644 index 0000000000000..3d31f78592eaa --- /dev/null +++ b/src/wp-includes/block-i18n.json @@ -0,0 +1,17 @@ +{ + "title": "block title", + "description": "block description", + "keywords": [ "block keyword" ], + "styles": [ + { + "label": "block style label" + } + ], + "variations": [ + { + "title": "block variation title", + "description": "block variation description", + "keywords": [ "block variation keyword" ] + } + ] +} diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 96b897aeec5f8..5351ab94aaa4c 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -187,11 +187,29 @@ function register_block_style_handle( $metadata, $field_name ) { return $result ? $style_handle : false; } +/** + * Gets i18n schema for block's metadata read from `block.json` file. + * + * @since 5.9.0 + * + * @return array The schema for block's metadata. + */ +function get_block_metadata_i18n_schema() { + static $i18n_block_schema; + + if ( ! isset( $i18n_block_schema ) ) { + $i18n_block_schema = wp_json_file_decode( __DIR__ . '/block-i18n.json' ); + } + + return $i18n_block_schema; +} + /** * Registers a block type from the metadata stored in the `block.json` file. * * @since 5.5.0 - * @since 5.9.0 Added support for the `viewScript` field. + * @since 5.7.0 Added support for `textdomain` field and i18n handling for all translatable fields. + * @since 5.9.0 Added support for `variations` and `viewScript` fields. * * @param string $file_or_folder Path to the JSON file with metadata definition for * the block or path to the folder where the `block.json` file is located. @@ -209,7 +227,7 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { return false; } - $metadata = json_decode( file_get_contents( $metadata_file ), true ); + $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) ); if ( ! is_array( $metadata ) || empty( $metadata['name'] ) ) { return false; } @@ -238,6 +256,7 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { $settings = array(); $property_mappings = array( + 'apiVersion' => 'api_version', 'title' => 'title', 'category' => 'category', 'parent' => 'parent', @@ -249,53 +268,17 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { 'usesContext' => 'uses_context', 'supports' => 'supports', 'styles' => 'styles', + 'variations' => 'variations', 'example' => 'example', - 'apiVersion' => 'api_version', ); + $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : null; + $i18n_schema = get_block_metadata_i18n_schema(); foreach ( $property_mappings as $key => $mapped_key ) { if ( isset( $metadata[ $key ] ) ) { - $value = $metadata[ $key ]; - if ( empty( $metadata['textdomain'] ) ) { - $settings[ $mapped_key ] = $value; - continue; - } - $textdomain = $metadata['textdomain']; - switch ( $key ) { - case 'title': - case 'description': - // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain - $settings[ $mapped_key ] = translate_with_gettext_context( $value, sprintf( 'block %s', $key ), $textdomain ); - break; - case 'keywords': - $settings[ $mapped_key ] = array(); - if ( ! is_array( $value ) ) { - continue 2; - } - - foreach ( $value as $keyword ) { - // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain - $settings[ $mapped_key ][] = translate_with_gettext_context( $keyword, 'block keyword', $textdomain ); - } - - break; - case 'styles': - $settings[ $mapped_key ] = array(); - if ( ! is_array( $value ) ) { - continue 2; - } - - foreach ( $value as $style ) { - if ( ! empty( $style['label'] ) ) { - // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain - $style['label'] = translate_with_gettext_context( $style['label'], 'block style label', $textdomain ); - } - $settings[ $mapped_key ][] = $style; - } - - break; - default: - $settings[ $mapped_key ] = $value; + $settings[ $mapped_key ] = $metadata[ $key ]; + if ( $textdomain && isset( $i18n_schema->$key ) ) { + $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain ); } } } diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 39354106f2506..0e5a92698a728 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -40,12 +40,12 @@ class WP_Theme_JSON_Resolver { private static $theme_has_support = null; /** - * Structure to hold i18n metadata. + * Container to keep loaded i18n schema for `theme.json`. * - * @since 5.8.0 + * @since 5.9.0 * @var array */ - private static $theme_json_i18n = null; + private static $i18n_schema = null; /** * Processes a file that adheres to the theme.json schema @@ -59,17 +59,7 @@ class WP_Theme_JSON_Resolver { private static function read_json_file( $file_path ) { $config = array(); if ( $file_path ) { - $decoded_file = json_decode( - file_get_contents( $file_path ), - true - ); - - $json_decoding_error = json_last_error(); - if ( JSON_ERROR_NONE !== $json_decoding_error ) { - trigger_error( "Error when decoding a theme.json schema at path $file_path " . json_last_error_msg() ); - return $config; - } - + $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); if ( is_array( $decoded_file ) ) { $config = $decoded_file; } @@ -77,103 +67,17 @@ private static function read_json_file( $file_path ) { return $config; } - /** - * Converts a tree as in i18n-theme.json into a linear array - * containing metadata to translate a theme.json file. - * - * For example, given this input: - * - * { - * "settings": { - * "*": { - * "typography": { - * "fontSizes": [ { "name": "Font size name" } ], - * "fontStyles": [ { "name": "Font size name" } ] - * } - * } - * } - * } - * - * will return this output: - * - * [ - * 0 => [ - * 'path' => [ 'settings', '*', 'typography', 'fontSizes' ], - * 'key' => 'name', - * 'context' => 'Font size name' - * ], - * 1 => [ - * 'path' => [ 'settings', '*', 'typography', 'fontStyles' ], - * 'key' => 'name', - * 'context' => 'Font style name' - * ] - * ] - * - * @since 5.8.0 - * - * @param array $i18n_partial A tree that follows the format of i18n-theme.json. - * @param array $current_path Optional. Keeps track of the path as we walk down the given tree. - * Default empty array. - * @return array A linear array containing the paths to translate. - */ - private static function extract_paths_to_translate( $i18n_partial, $current_path = array() ) { - $result = array(); - foreach ( $i18n_partial as $property => $partial_child ) { - if ( is_numeric( $property ) ) { - foreach ( $partial_child as $key => $context ) { - $result[] = array( - 'path' => $current_path, - 'key' => $key, - 'context' => $context, - ); - } - return $result; - } - $result = array_merge( - $result, - self::extract_paths_to_translate( $partial_child, array_merge( $current_path, array( $property ) ) ) - ); - } - return $result; - } - /** * Returns a data structure used in theme.json translation. * * @since 5.8.0 + * @deprecated 5.9.0 * * @return array An array of theme.json fields that are translatable and the keys that are translatable. */ public static function get_fields_to_translate() { - if ( null === self::$theme_json_i18n ) { - $file_structure = self::read_json_file( __DIR__ . '/theme-i18n.json' ); - self::$theme_json_i18n = self::extract_paths_to_translate( $file_structure ); - } - return self::$theme_json_i18n; - } - - /** - * Translates a chunk of the loaded theme.json structure. - * - * @since 5.8.0 - * - * @param array $array_to_translate The chunk of theme.json to translate. - * @param string $key The key of the field that contains the string to translate. - * @param string $context The context to apply in the translation call. - * @param string $domain Text domain. Unique identifier for retrieving translated strings. - * @return array Returns the modified $theme_json chunk. - */ - private static function translate_theme_json_chunk( array $array_to_translate, $key, $context, $domain ) { - foreach ( $array_to_translate as $item_key => $item_to_translate ) { - if ( empty( $item_to_translate[ $key ] ) ) { - continue; - } - - // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain - $array_to_translate[ $item_key ][ $key ] = translate_with_gettext_context( $array_to_translate[ $item_key ][ $key ], $context, $domain ); - } - - return $array_to_translate; + _deprecated_function( __METHOD__, '5.9.0' ); + return array(); } /** @@ -188,50 +92,12 @@ private static function translate_theme_json_chunk( array $array_to_translate, $ * @return array Returns the modified $theme_json_structure. */ private static function translate( $theme_json, $domain = 'default' ) { - $fields = self::get_fields_to_translate(); - foreach ( $fields as $field ) { - $path = $field['path']; - $key = $field['key']; - $context = $field['context']; - - /* - * We need to process the paths that include '*' separately. - * One example of such a path would be: - * [ 'settings', 'blocks', '*', 'color', 'palette' ] - */ - $nodes_to_iterate = array_keys( $path, '*', true ); - if ( ! empty( $nodes_to_iterate ) ) { - /* - * At the moment, we only need to support one '*' in the path, so take it directly. - * - base will be [ 'settings', 'blocks' ] - * - data will be [ 'color', 'palette' ] - */ - $base_path = array_slice( $path, 0, $nodes_to_iterate[0] ); - $data_path = array_slice( $path, $nodes_to_iterate[0] + 1 ); - $base_tree = _wp_array_get( $theme_json, $base_path, array() ); - foreach ( $base_tree as $node_name => $node_data ) { - $array_to_translate = _wp_array_get( $node_data, $data_path, null ); - if ( is_null( $array_to_translate ) ) { - continue; - } - - // Whole path will be [ 'settings', 'blocks', 'core/paragraph', 'color', 'palette' ]. - $whole_path = array_merge( $base_path, array( $node_name ), $data_path ); - $translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain ); - _wp_array_set( $theme_json, $whole_path, $translated_array ); - } - } else { - $array_to_translate = _wp_array_get( $theme_json, $path, null ); - if ( is_null( $array_to_translate ) ) { - continue; - } - - $translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain ); - _wp_array_set( $theme_json, $path, $translated_array ); - } + if ( null === self::$i18n_schema ) { + $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); + self::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; } - return $theme_json; + return translate_settings_using_i18n_schema( self::$i18n_schema, $theme_json, $domain ); } /** @@ -365,7 +231,6 @@ public static function clean_cached_data() { self::$core = null; self::$theme = null; self::$theme_has_support = null; - self::$theme_json_i18n = null; } } diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 2b75cfea034cd..33344f31532d3 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -4267,6 +4267,54 @@ function wp_check_jsonp_callback( $callback ) { return 0 === $illegal_char_count; } +/** + * Reads and decodes a JSON file. + * + * @since 5.9.0 + * + * @param string $filename Path to the JSON file. + * @param array $options { + * Optional. Options to be used with `json_decode()`. + * + * @type bool associative Optional. When `true`, JSON objects will be returned as associative arrays. + * When `false`, JSON objects will be returned as objects. + * } + * + * @return mixed Returns the value encoded in JSON in appropriate PHP type. + * `null` is returned if the file is not found, or its content can't be decoded. + */ +function wp_json_file_decode( $filename, $options = array() ) { + $result = null; + $filename = wp_normalize_path( realpath( $filename ) ); + if ( ! file_exists( $filename ) ) { + trigger_error( + sprintf( + /* translators: %s: Path to the JSON file. */ + __( "File %s doesn't exist!" ), + $filename + ) + ); + return $result; + } + + $options = wp_parse_args( $options, array( 'associative' => false ) ); + $decoded_file = json_decode( file_get_contents( $filename ), $options['associative'] ); + + if ( JSON_ERROR_NONE !== json_last_error() ) { + trigger_error( + sprintf( + /* translators: 1: Path to the JSON file, 2: Error message. */ + __( 'Error when decoding a JSON file at path %1$s: %2$s' ), + $filename, + json_last_error_msg() + ) + ); + return $result; + } + + return $decoded_file; +} + /** * Retrieve the WordPress home page URL. * diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index f69a24ffd838a..7699d92b1efc9 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -1712,3 +1712,47 @@ function is_locale_switched() { return $wp_locale_switcher->is_switched(); } + +/** + * Translates the provided settings value using its i18n schema. + * + * @since 5.9.0 + * @access private + * + * @param string|string[]|array[]|object $i18n_schema I18n schema for the setting. + * @param string|string[]|array[] $settings Value for the settings. + * @param string $textdomain Textdomain to use with translations. + * + * @return string|string[]|array[] Translated settings. + */ +function translate_settings_using_i18n_schema( $i18n_schema, $settings, $textdomain ) { + if ( empty( $i18n_schema ) || empty( $settings ) || empty( $textdomain ) ) { + return $settings; + } + + if ( is_string( $i18n_schema ) && is_string( $settings ) ) { + return translate_with_gettext_context( $settings, $i18n_schema, $textdomain ); + } + if ( is_array( $i18n_schema ) && is_array( $settings ) ) { + $translated_settings = array(); + foreach ( $settings as $value ) { + $translated_settings[] = translate_settings_using_i18n_schema( $i18n_schema[0], $value, $textdomain ); + } + return $translated_settings; + } + if ( is_object( $i18n_schema ) && is_array( $settings ) ) { + $group_key = '*'; + $translated_settings = array(); + foreach ( $settings as $key => $value ) { + if ( isset( $i18n_schema->$key ) ) { + $translated_settings[ $key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $value, $textdomain ); + } elseif ( isset( $i18n_schema->$group_key ) ) { + $translated_settings[ $key ] = translate_settings_using_i18n_schema( $i18n_schema->$group_key, $value, $textdomain ); + } else { + $translated_settings[ $key ] = $value; + } + } + return $translated_settings; + } + return $settings; +} diff --git a/tests/phpunit/data/blocks/notice/block.json b/tests/phpunit/data/blocks/notice/block.json index 610a96099f90a..d448a49c5c8dd 100644 --- a/tests/phpunit/data/blocks/notice/block.json +++ b/tests/phpunit/data/blocks/notice/block.json @@ -41,6 +41,14 @@ "label": "Other" } ], + "variations": [ + { + "name": "error", + "title": "Error", + "description": "Shows error.", + "keywords": [ "failure" ] + } + ], "example": { "attributes": { "message": "This is a notice!" diff --git a/tests/phpunit/data/languages/plugins/notice-pl_PL.mo b/tests/phpunit/data/languages/plugins/notice-pl_PL.mo index 284872cf6f88a..439c6786a4c6e 100644 Binary files a/tests/phpunit/data/languages/plugins/notice-pl_PL.mo and b/tests/phpunit/data/languages/plugins/notice-pl_PL.mo differ diff --git a/tests/phpunit/data/languages/plugins/notice-pl_PL.po b/tests/phpunit/data/languages/plugins/notice-pl_PL.po index 22ca6ef12914f..2e7448b75853a 100644 --- a/tests/phpunit/data/languages/plugins/notice-pl_PL.po +++ b/tests/phpunit/data/languages/plugins/notice-pl_PL.po @@ -2,12 +2,12 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2015-12-31 16:31+0100\n" -"PO-Revision-Date: 2021-01-14 18:26+0100\n" +"PO-Revision-Date: 2021-07-15 13:36+0200\n" "Language: pl_PL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.4.2\n" +"X-Generator: Poedit 3.0\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;" @@ -41,3 +41,15 @@ msgstr "Domyślny" msgctxt "block style label" msgid "Other" msgstr "Inny" + +msgctxt "block variation title" +msgid "Error" +msgstr "Błąd" + +msgctxt "block variation description" +msgid "Shows error." +msgstr "Wyświetla błąd." + +msgctxt "block variation keyword" +msgid "failure" +msgstr "niepowodzenie" diff --git a/tests/phpunit/tests/blocks/register.php b/tests/phpunit/tests/blocks/register.php index 8778c95f991f1..1f3104d09b3a6 100644 --- a/tests/phpunit/tests/blocks/register.php +++ b/tests/phpunit/tests/blocks/register.php @@ -69,6 +69,15 @@ function tear_down() { parent::tear_down(); } + /** + * Returns Polish locale string. + * + * @return string + */ + function filter_set_locale_to_polish() { + return 'pl_PL'; + } + /** * @ticket 45109 */ @@ -370,6 +379,17 @@ function test_block_registers_with_metadata_fixture() { ), $result->styles ); + $this->assertSame( + array( + array( + 'name' => 'error', + 'title' => 'Error', + 'description' => 'Shows error.', + 'keywords' => array( 'failure' ), + ), + ), + $result->variations + ); $this->assertSame( array( 'attributes' => array( @@ -407,10 +427,7 @@ function test_block_register_block_type_proxy_for_metadata() { * @ticket 52301 */ function test_block_registers_with_metadata_i18n_support() { - function filter_set_locale_to_polish() { - return 'pl_PL'; - } - add_filter( 'locale', 'filter_set_locale_to_polish' ); + add_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); load_textdomain( 'notice', WP_LANG_DIR . '/plugins/notice-pl_PL.mo' ); $result = register_block_type_from_metadata( @@ -418,7 +435,7 @@ function filter_set_locale_to_polish() { ); unload_textdomain( 'notice' ); - remove_filter( 'locale', 'filter_set_locale_to_polish' ); + remove_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); $this->assertInstanceOf( 'WP_Block_Type', $result ); $this->assertSame( 'tests/notice', $result->name ); @@ -439,6 +456,17 @@ function filter_set_locale_to_polish() { ), $result->styles ); + $this->assertSame( + array( + array( + 'name' => 'error', + 'title' => 'Błąd', + 'description' => 'Wyświetla błąd.', + 'keywords' => array( 'niepowodzenie' ), + ), + ), + $result->variations + ); } /** diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index d85ad1af4008c..4ef24d22a1991 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -1037,6 +1037,31 @@ function test_wp_json_encode_depth() { $this->assertFalse( $json ); } + /** + * @ticket 53238 + */ + function test_wp_json_file_decode() { + $result = wp_json_file_decode( + DIR_TESTDATA . '/blocks/notice/block.json' + ); + + $this->assertIsObject( $result ); + $this->assertSame( 'tests/notice', $result->name ); + } + + /** + * @ticket 53238 + */ + function test_wp_json_file_decode_associative_array() { + $result = wp_json_file_decode( + DIR_TESTDATA . '/blocks/notice/block.json', + array( 'associative' => true ) + ); + + $this->assertIsArray( $result ); + $this->assertSame( 'tests/notice', $result['name'] ); + } + /** * @ticket 36054 * @dataProvider datetime_provider diff --git a/tests/phpunit/tests/l10n/translateSettingsUsingI18nSchema.php b/tests/phpunit/tests/l10n/translateSettingsUsingI18nSchema.php new file mode 100644 index 0000000000000..e555e6bb386ba --- /dev/null +++ b/tests/phpunit/tests/l10n/translateSettingsUsingI18nSchema.php @@ -0,0 +1,99 @@ + 'block title', + 'keywords' => array( 'block keyword' ), + 'styles' => array( + (object) array( 'label' => 'block style label' ), + ), + 'context' => (object) array( + '*' => (object) array( + 'variations' => array( + (object) array( + 'title' => 'block variation title', + 'description' => 'block variation description', + 'keywords' => array( 'block variation keyword' ), + ), + ), + ), + ), + ); + $settings = array( + 'title' => 'Notice', + 'keywords' => array( + 'alert', + 'message', + ), + 'styles' => array( + array( 'label' => 'Default' ), + array( 'label' => 'Other' ), + ), + 'context' => array( + 'namespace' => array( + 'variations' => array( + array( + 'title' => 'Error', + 'description' => 'Shows error.', + 'keywords' => array( 'failure' ), + ), + ), + ), + ), + ); + $result = translate_settings_using_i18n_schema( + $i18n_schema, + $settings, + $textdomain + ); + + unload_textdomain( $textdomain ); + remove_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); + + $this->assertSame( 'Powiadomienie', $result['title'] ); + $this->assertSameSets( array( 'ostrzeżenie', 'wiadomość' ), $result['keywords'] ); + $this->assertSame( + array( + array( + 'label' => 'Domyślny', + ), + array( + 'label' => 'Inny', + ), + ), + $result['styles'] + ); + $this->assertSame( + array( + array( + 'title' => 'Błąd', + 'description' => 'Wyświetla błąd.', + 'keywords' => array( 'niepowodzenie' ), + ), + ), + $result['context']['namespace']['variations'] + ); + } +} diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index 081cb26234bf8..7e4bd37d54d0a 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -44,53 +44,6 @@ public function filter_set_locale_to_polish() { return 'pl_PL'; } - /** - * @ticket 52991 - */ - public function test_fields_are_extracted() { - $actual = WP_Theme_JSON_Resolver::get_fields_to_translate(); - - $expected = array( - array( - 'path' => array( 'settings', 'typography', 'fontSizes' ), - 'key' => 'name', - 'context' => 'Font size name', - ), - array( - 'path' => array( 'settings', 'color', 'palette' ), - 'key' => 'name', - 'context' => 'Color name', - ), - array( - 'path' => array( 'settings', 'color', 'gradients' ), - 'key' => 'name', - 'context' => 'Gradient name', - ), - array( - 'path' => array( 'settings', 'color', 'duotone' ), - 'key' => 'name', - 'context' => 'Duotone name', - ), - array( - 'path' => array( 'settings', 'blocks', '*', 'typography', 'fontSizes' ), - 'key' => 'name', - 'context' => 'Font size name', - ), - array( - 'path' => array( 'settings', 'blocks', '*', 'color', 'palette' ), - 'key' => 'name', - 'context' => 'Color name', - ), - array( - 'path' => array( 'settings', 'blocks', '*', 'color', 'gradients' ), - 'key' => 'name', - 'context' => 'Gradient name', - ), - ); - - $this->assertSame( $expected, $actual ); - } - /** * @ticket 52991 */