diff --git a/wp-includes/block-i18n.json b/wp-includes/block-i18n.json new file mode 100644 index 0000000000..3d31f78592 --- /dev/null +++ b/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/wp-includes/blocks.php b/wp-includes/blocks.php index 96b897aeec..5351ab94aa 100644 --- a/wp-includes/blocks.php +++ b/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/wp-includes/class-wp-theme-json-resolver.php b/wp-includes/class-wp-theme-json-resolver.php index 39354106f2..0e5a92698a 100644 --- a/wp-includes/class-wp-theme-json-resolver.php +++ b/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 @@ class WP_Theme_JSON_Resolver { 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 @@ class WP_Theme_JSON_Resolver { * @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 @@ class WP_Theme_JSON_Resolver { self::$core = null; self::$theme = null; self::$theme_has_support = null; - self::$theme_json_i18n = null; } } diff --git a/wp-includes/functions.php b/wp-includes/functions.php index 2b75cfea03..33344f3153 100644 --- a/wp-includes/functions.php +++ b/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/wp-includes/l10n.php b/wp-includes/l10n.php index f69a24ffd8..7699d92b1e 100644 --- a/wp-includes/l10n.php +++ b/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/wp-includes/version.php b/wp-includes/version.php index e80b508a38..51cd8a9a20 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @global string $wp_version */ -$wp_version = '5.9-alpha-51598'; +$wp_version = '5.9-alpha-51599'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.