Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blocks: Add support for variations in block.json file #1499

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/wp-includes/block-i18n.json
Original file line number Diff line number Diff line change
@@ -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" ]
}
]
}
71 changes: 27 additions & 44 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
Expand Down Expand Up @@ -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',
Expand All @@ -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 );
}
}
}
Expand Down
157 changes: 11 additions & 146 deletions src/wp-includes/class-wp-theme-json-resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -59,121 +59,25 @@ 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;
}
}
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();
gziolo marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -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' ) {
Copy link
Member Author

@gziolo gziolo Jul 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR replaces the existing logic to translate theme.json so it would be great to confirm that everything works as before. All existing unit tests pass for processing theme.json are still green. New tests were added to cover new cases in the extracted general-purpose translate_settings_using_i18n_schema method (it works with block.json, too) that no longer use the intermediate step to run get_fields_to_translate which collects an alist of fields to translate.

@jorgefilipecosta and @nosolosw, I would appreciate your feedback.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested theme.json translation and things are working well 👍 Nice work! We should probably also update plugin code right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could update the plugin, although it works exactly the same so I consider it a low priority. I'll have to explore what is necessary to bring variations handling to block.json for older versions of WordPress supported by the Gutenberg plugin. If we need to polyfill translate_settings_using_i18n_schema then we can refactor the logic for theme.json as well.

$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 );
}

/**
Expand Down Expand Up @@ -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;
}

}
48 changes: 48 additions & 0 deletions src/wp-includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() ) {
gziolo marked this conversation as resolved.
Show resolved Hide resolved
$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.
*
Expand Down
Loading