WordPress.org

Make WordPress Core

Changeset 51599


Ignore:
Timestamp:
08/11/2021 09:06:31 AM (4 months ago)
Author:
gziolo
Message:

Blocks: Add support for variations in block.json` file

We integrated variations with block types and the corresponding REST API endpoint in #52688. It's a follow-up patch to add missing support to the block.json metadata file when using register_block_type.

Some fields for variations are translatable.Therefore, i18n schema was copied over from Gutenberg: https://github.com/WordPress/gutenberg/blob/trunk/packages/blocks/src/api/i18n-block.json. The accompanying implementation was adapted as translate_settings_using_i18n_schema.

Props: gwwar, swissspidy, schlessera, jorgefilipecosta.
Fixes #53238.

Location:
trunk
Files:
2 added
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/blocks.php

    r51501 r51599  
    189189
    190190/**
     191 * Gets i18n schema for block's metadata read from `block.json` file.
     192 *
     193 * @since 5.9.0
     194 *
     195 * @return array The schema for block's metadata.
     196 */
     197function get_block_metadata_i18n_schema() {
     198    static $i18n_block_schema;
     199
     200    if ( ! isset( $i18n_block_schema ) ) {
     201        $i18n_block_schema = wp_json_file_decode( __DIR__ . '/block-i18n.json' );
     202    }
     203
     204    return $i18n_block_schema;
     205}
     206
     207/**
    191208 * Registers a block type from the metadata stored in the `block.json` file.
    192209 *
    193210 * @since 5.5.0
    194  * @since 5.9.0 Added support for the `viewScript` field.
     211 * @since 5.7.0 Added support for `textdomain` field and i18n handling for all translatable fields.
     212 * @since 5.9.0 Added support for `variations` and `viewScript` fields.
    195213 *
    196214 * @param string $file_or_folder Path to the JSON file with metadata definition for
     
    210228    }
    211229
    212     $metadata = json_decode( file_get_contents( $metadata_file ), true );
     230    $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) );
    213231    if ( ! is_array( $metadata ) || empty( $metadata['name'] ) ) {
    214232        return false;
     
    239257    $settings          = array();
    240258    $property_mappings = array(
     259        'apiVersion'      => 'api_version',
    241260        'title'           => 'title',
    242261        'category'        => 'category',
     
    250269        'supports'        => 'supports',
    251270        'styles'          => 'styles',
     271        'variations'      => 'variations',
    252272        'example'         => 'example',
    253         'apiVersion'      => 'api_version',
    254     );
     273    );
     274    $textdomain        = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : null;
     275    $i18n_schema       = get_block_metadata_i18n_schema();
    255276
    256277    foreach ( $property_mappings as $key => $mapped_key ) {
    257278        if ( isset( $metadata[ $key ] ) ) {
    258             $value = $metadata[ $key ];
    259             if ( empty( $metadata['textdomain'] ) ) {
    260                 $settings[ $mapped_key ] = $value;
    261                 continue;
    262             }
    263             $textdomain = $metadata['textdomain'];
    264             switch ( $key ) {
    265                 case 'title':
    266                 case 'description':
    267                     // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain
    268                     $settings[ $mapped_key ] = translate_with_gettext_context( $value, sprintf( 'block %s', $key ), $textdomain );
    269                     break;
    270                 case 'keywords':
    271                     $settings[ $mapped_key ] = array();
    272                     if ( ! is_array( $value ) ) {
    273                         continue 2;
    274                     }
    275 
    276                     foreach ( $value as $keyword ) {
    277                         // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain
    278                         $settings[ $mapped_key ][] = translate_with_gettext_context( $keyword, 'block keyword', $textdomain );
    279                     }
    280 
    281                     break;
    282                 case 'styles':
    283                     $settings[ $mapped_key ] = array();
    284                     if ( ! is_array( $value ) ) {
    285                         continue 2;
    286                     }
    287 
    288                     foreach ( $value as $style ) {
    289                         if ( ! empty( $style['label'] ) ) {
    290                             // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain
    291                             $style['label'] = translate_with_gettext_context( $style['label'], 'block style label', $textdomain );
    292                         }
    293                         $settings[ $mapped_key ][] = $style;
    294                     }
    295 
    296                     break;
    297                 default:
    298                     $settings[ $mapped_key ] = $value;
     279            $settings[ $mapped_key ] = $metadata[ $key ];
     280            if ( $textdomain && isset( $i18n_schema->$key ) ) {
     281                $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain );
    299282            }
    300283        }
  • trunk/src/wp-includes/class-wp-theme-json-resolver.php

    r51472 r51599  
    4141
    4242    /**
    43      * Structure to hold i18n metadata.
    44      *
    45      * @since 5.8.0
     43     * Container to keep loaded i18n schema for `theme.json`.
     44     *
     45     * @since 5.9.0
    4646     * @var array
    4747     */
    48     private static $theme_json_i18n = null;
     48    private static $i18n_schema = null;
    4949
    5050    /**
     
    6060        $config = array();
    6161        if ( $file_path ) {
    62             $decoded_file = json_decode(
    63                 file_get_contents( $file_path ),
    64                 true
    65             );
    66 
    67             $json_decoding_error = json_last_error();
    68             if ( JSON_ERROR_NONE !== $json_decoding_error ) {
    69                 trigger_error( "Error when decoding a theme.json schema at path $file_path " . json_last_error_msg() );
    70                 return $config;
    71             }
    72 
     62            $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) );
    7363            if ( is_array( $decoded_file ) ) {
    7464                $config = $decoded_file;
     
    7969
    8070    /**
    81      * Converts a tree as in i18n-theme.json into a linear array
    82      * containing metadata to translate a theme.json file.
    83      *
    84      * For example, given this input:
    85      *
    86      *     {
    87      *       "settings": {
    88      *         "*": {
    89      *           "typography": {
    90      *             "fontSizes": [ { "name": "Font size name" } ],
    91      *             "fontStyles": [ { "name": "Font size name" } ]
    92      *           }
    93      *         }
    94      *       }
    95      *     }
    96      *
    97      * will return this output:
    98      *
    99      *     [
    100      *       0 => [
    101      *         'path'    => [ 'settings', '*', 'typography', 'fontSizes' ],
    102      *         'key'     => 'name',
    103      *         'context' => 'Font size name'
    104      *       ],
    105      *       1 => [
    106      *         'path'    => [ 'settings', '*', 'typography', 'fontStyles' ],
    107      *         'key'     => 'name',
    108      *         'context' => 'Font style name'
    109      *       ]
    110      *     ]
    111      *
    112      * @since 5.8.0
    113      *
    114      * @param array $i18n_partial A tree that follows the format of i18n-theme.json.
    115      * @param array $current_path Optional. Keeps track of the path as we walk down the given tree.
    116      *                            Default empty array.
    117      * @return array A linear array containing the paths to translate.
    118      */
    119     private static function extract_paths_to_translate( $i18n_partial, $current_path = array() ) {
    120         $result = array();
    121         foreach ( $i18n_partial as $property => $partial_child ) {
    122             if ( is_numeric( $property ) ) {
    123                 foreach ( $partial_child as $key => $context ) {
    124                     $result[] = array(
    125                         'path'    => $current_path,
    126                         'key'     => $key,
    127                         'context' => $context,
    128                     );
    129                 }
    130                 return $result;
    131             }
    132             $result = array_merge(
    133                 $result,
    134                 self::extract_paths_to_translate( $partial_child, array_merge( $current_path, array( $property ) ) )
    135             );
    136         }
    137         return $result;
    138     }
    139 
    140     /**
    14171     * Returns a data structure used in theme.json translation.
    14272     *
    14373     * @since 5.8.0
     74     * @deprecated 5.9.0
    14475     *
    14576     * @return array An array of theme.json fields that are translatable and the keys that are translatable.
    14677     */
    14778    public static function get_fields_to_translate() {
    148         if ( null === self::$theme_json_i18n ) {
    149             $file_structure        = self::read_json_file( __DIR__ . '/theme-i18n.json' );
    150             self::$theme_json_i18n = self::extract_paths_to_translate( $file_structure );
    151         }
    152         return self::$theme_json_i18n;
    153     }
    154 
    155     /**
    156      * Translates a chunk of the loaded theme.json structure.
    157      *
    158      * @since 5.8.0
    159      *
    160      * @param array  $array_to_translate The chunk of theme.json to translate.
    161      * @param string $key                The key of the field that contains the string to translate.
    162      * @param string $context            The context to apply in the translation call.
    163      * @param string $domain             Text domain. Unique identifier for retrieving translated strings.
    164      * @return array Returns the modified $theme_json chunk.
    165      */
    166     private static function translate_theme_json_chunk( array $array_to_translate, $key, $context, $domain ) {
    167         foreach ( $array_to_translate as $item_key => $item_to_translate ) {
    168             if ( empty( $item_to_translate[ $key ] ) ) {
    169                 continue;
    170             }
    171 
    172             // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain
    173             $array_to_translate[ $item_key ][ $key ] = translate_with_gettext_context( $array_to_translate[ $item_key ][ $key ], $context, $domain );
    174         }
    175 
    176         return $array_to_translate;
     79        _deprecated_function( __METHOD__, '5.9.0' );
     80        return array();
    17781    }
    17882
     
    18993     */
    19094    private static function translate( $theme_json, $domain = 'default' ) {
    191         $fields = self::get_fields_to_translate();
    192         foreach ( $fields as $field ) {
    193             $path    = $field['path'];
    194             $key     = $field['key'];
    195             $context = $field['context'];
    196 
    197             /*
    198              * We need to process the paths that include '*' separately.
    199              * One example of such a path would be:
    200              * [ 'settings', 'blocks', '*', 'color', 'palette' ]
    201              */
    202             $nodes_to_iterate = array_keys( $path, '*', true );
    203             if ( ! empty( $nodes_to_iterate ) ) {
    204                 /*
    205                  * At the moment, we only need to support one '*' in the path, so take it directly.
    206                  * - base will be [ 'settings', 'blocks' ]
    207                  * - data will be [ 'color', 'palette' ]
    208                  */
    209                 $base_path = array_slice( $path, 0, $nodes_to_iterate[0] );
    210                 $data_path = array_slice( $path, $nodes_to_iterate[0] + 1 );
    211                 $base_tree = _wp_array_get( $theme_json, $base_path, array() );
    212                 foreach ( $base_tree as $node_name => $node_data ) {
    213                     $array_to_translate = _wp_array_get( $node_data, $data_path, null );
    214                     if ( is_null( $array_to_translate ) ) {
    215                         continue;
    216                     }
    217 
    218                     // Whole path will be [ 'settings', 'blocks', 'core/paragraph', 'color', 'palette' ].
    219                     $whole_path       = array_merge( $base_path, array( $node_name ), $data_path );
    220                     $translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain );
    221                     _wp_array_set( $theme_json, $whole_path, $translated_array );
    222                 }
    223             } else {
    224                 $array_to_translate = _wp_array_get( $theme_json, $path, null );
    225                 if ( is_null( $array_to_translate ) ) {
    226                     continue;
    227                 }
    228 
    229                 $translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain );
    230                 _wp_array_set( $theme_json, $path, $translated_array );
    231             }
    232         }
    233 
    234         return $theme_json;
     95        if ( null === self::$i18n_schema ) {
     96            $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' );
     97            self::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema;
     98        }
     99
     100        return translate_settings_using_i18n_schema( self::$i18n_schema, $theme_json, $domain );
    235101    }
    236102
     
    366232        self::$theme             = null;
    367233        self::$theme_has_support = null;
    368         self::$theme_json_i18n   = null;
    369234    }
    370235
  • trunk/src/wp-includes/functions.php

    r51557 r51599  
    42694269
    42704270/**
     4271 * Reads and decodes a JSON file.
     4272 *
     4273 * @since 5.9.0
     4274 *
     4275 * @param string $filename Path to the JSON file.
     4276 * @param array  $options  {
     4277 *     Optional. Options to be used with `json_decode()`.
     4278 *
     4279 *     @type bool associative Optional. When `true`, JSON objects will be returned as associative arrays.
     4280 *                            When `false`, JSON objects will be returned as objects.
     4281 * }
     4282 *
     4283 * @return mixed Returns the value encoded in JSON in appropriate PHP type.
     4284 *               `null` is returned if the file is not found, or its content can't be decoded.
     4285 */
     4286function wp_json_file_decode( $filename, $options = array() ) {
     4287    $result   = null;
     4288    $filename = wp_normalize_path( realpath( $filename ) );
     4289    if ( ! file_exists( $filename ) ) {
     4290        trigger_error(
     4291            sprintf(
     4292                /* translators: %s: Path to the JSON file. */
     4293                __( "File %s doesn't exist!" ),
     4294                $filename
     4295            )
     4296        );
     4297        return $result;
     4298    }
     4299
     4300    $options      = wp_parse_args( $options, array( 'associative' => false ) );
     4301    $decoded_file = json_decode( file_get_contents( $filename ), $options['associative'] );
     4302
     4303    if ( JSON_ERROR_NONE !== json_last_error() ) {
     4304        trigger_error(
     4305            sprintf(
     4306                /* translators: 1: Path to the JSON file, 2: Error message. */
     4307                __( 'Error when decoding a JSON file at path %1$s: %2$s' ),
     4308                $filename,
     4309                json_last_error_msg()
     4310            )
     4311        );
     4312        return $result;
     4313    }
     4314
     4315    return $decoded_file;
     4316}
     4317
     4318/**
    42714319 * Retrieve the WordPress home page URL.
    42724320 *
  • trunk/src/wp-includes/l10n.php

    r51298 r51599  
    17131713    return $wp_locale_switcher->is_switched();
    17141714}
     1715
     1716/**
     1717 * Translates the provided settings value using its i18n schema.
     1718 *
     1719 * @since 5.9.0
     1720 * @access private
     1721 *
     1722 * @param string|string[]|array[]|object $i18n_schema I18n schema for the setting.
     1723 * @param string|string[]|array[]        $settings    Value for the settings.
     1724 * @param string                         $textdomain  Textdomain to use with translations.
     1725 *
     1726 * @return string|string[]|array[] Translated settings.
     1727 */
     1728function translate_settings_using_i18n_schema( $i18n_schema, $settings, $textdomain ) {
     1729    if ( empty( $i18n_schema ) || empty( $settings ) || empty( $textdomain ) ) {
     1730        return $settings;
     1731    }
     1732
     1733    if ( is_string( $i18n_schema ) && is_string( $settings ) ) {
     1734        return translate_with_gettext_context( $settings, $i18n_schema, $textdomain );
     1735    }
     1736    if ( is_array( $i18n_schema ) && is_array( $settings ) ) {
     1737        $translated_settings = array();
     1738        foreach ( $settings as $value ) {
     1739            $translated_settings[] = translate_settings_using_i18n_schema( $i18n_schema[0], $value, $textdomain );
     1740        }
     1741        return $translated_settings;
     1742    }
     1743    if ( is_object( $i18n_schema ) && is_array( $settings ) ) {
     1744        $group_key           = '*';
     1745        $translated_settings = array();
     1746        foreach ( $settings as $key => $value ) {
     1747            if ( isset( $i18n_schema->$key ) ) {
     1748                $translated_settings[ $key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $value, $textdomain );
     1749            } elseif ( isset( $i18n_schema->$group_key ) ) {
     1750                $translated_settings[ $key ] = translate_settings_using_i18n_schema( $i18n_schema->$group_key, $value, $textdomain );
     1751            } else {
     1752                $translated_settings[ $key ] = $value;
     1753            }
     1754        }
     1755        return $translated_settings;
     1756    }
     1757    return $settings;
     1758}
  • trunk/tests/phpunit/data/blocks/notice/block.json

    r51501 r51599  
    4242        }
    4343    ],
     44    "variations": [
     45        {
     46            "name": "error",
     47            "title": "Error",
     48            "description": "Shows error.",
     49            "keywords": [ "failure" ]
     50        }
     51    ],
    4452    "example": {
    4553        "attributes": {
  • trunk/tests/phpunit/data/languages/plugins/notice-pl_PL.po

    r49981 r51599  
    33"Project-Id-Version: \n"
    44"POT-Creation-Date: 2015-12-31 16:31+0100\n"
    5 "PO-Revision-Date: 2021-01-14 18:26+0100\n"
     5"PO-Revision-Date: 2021-07-15 13:36+0200\n"
    66"Language: pl_PL\n"
    77"MIME-Version: 1.0\n"
    88"Content-Type: text/plain; charset=UTF-8\n"
    99"Content-Transfer-Encoding: 8bit\n"
    10 "X-Generator: Poedit 2.4.2\n"
     10"X-Generator: Poedit 3.0\n"
    1111"X-Poedit-Basepath: .\n"
    1212"Plural-Forms: nplurals=2; plural=(n != 1);\n"
     
    4242msgid "Other"
    4343msgstr "Inny"
     44
     45msgctxt "block variation title"
     46msgid "Error"
     47msgstr "Błąd"
     48
     49msgctxt "block variation description"
     50msgid "Shows error."
     51msgstr "Wyświetla błąd."
     52
     53msgctxt "block variation keyword"
     54msgid "failure"
     55msgstr "niepowodzenie"
  • trunk/tests/phpunit/tests/blocks/register.php

    r51568 r51599  
    6868
    6969        parent::tear_down();
     70    }
     71
     72    /**
     73     * Returns Polish locale string.
     74     *
     75     * @return string
     76     */
     77    function filter_set_locale_to_polish() {
     78        return 'pl_PL';
    7079    }
    7180
     
    373382        $this->assertSame(
    374383            array(
     384                array(
     385                    'name'        => 'error',
     386                    'title'       => 'Error',
     387                    'description' => 'Shows error.',
     388                    'keywords'    => array( 'failure' ),
     389                ),
     390            ),
     391            $result->variations
     392        );
     393        $this->assertSame(
     394            array(
    375395                'attributes' => array(
    376396                    'message' => 'This is a notice!',
     
    408428     */
    409429    function test_block_registers_with_metadata_i18n_support() {
    410         function filter_set_locale_to_polish() {
    411             return 'pl_PL';
    412         }
    413         add_filter( 'locale', 'filter_set_locale_to_polish' );
     430        add_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) );
    414431        load_textdomain( 'notice', WP_LANG_DIR . '/plugins/notice-pl_PL.mo' );
    415432
     
    419436
    420437        unload_textdomain( 'notice' );
    421         remove_filter( 'locale', 'filter_set_locale_to_polish' );
     438        remove_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) );
    422439
    423440        $this->assertInstanceOf( 'WP_Block_Type', $result );
     
    440457            $result->styles
    441458        );
     459        $this->assertSame(
     460            array(
     461                array(
     462                    'name'        => 'error',
     463                    'title'       => 'Błąd',
     464                    'description' => 'Wyświetla błąd.',
     465                    'keywords'    => array( 'niepowodzenie' ),
     466                ),
     467            ),
     468            $result->variations
     469        );
    442470    }
    443471
  • trunk/tests/phpunit/tests/functions.php

    r51565 r51599  
    10361036        $json = wp_json_encode( $data, 0, 1 );
    10371037        $this->assertFalse( $json );
     1038    }
     1039
     1040    /**
     1041     * @ticket 53238
     1042     */
     1043    function test_wp_json_file_decode() {
     1044        $result = wp_json_file_decode(
     1045            DIR_TESTDATA . '/blocks/notice/block.json'
     1046        );
     1047
     1048        $this->assertIsObject( $result );
     1049        $this->assertSame( 'tests/notice', $result->name );
     1050    }
     1051
     1052    /**
     1053     * @ticket 53238
     1054     */
     1055    function test_wp_json_file_decode_associative_array() {
     1056        $result = wp_json_file_decode(
     1057            DIR_TESTDATA . '/blocks/notice/block.json',
     1058            array( 'associative' => true )
     1059        );
     1060
     1061        $this->assertIsArray( $result );
     1062        $this->assertSame( 'tests/notice', $result['name'] );
    10381063    }
    10391064
  • trunk/tests/phpunit/tests/theme/wpThemeJsonResolver.php

    r51568 r51599  
    4343    public function filter_set_locale_to_polish() {
    4444        return 'pl_PL';
    45     }
    46 
    47     /**
    48      * @ticket 52991
    49      */
    50     public function test_fields_are_extracted() {
    51         $actual = WP_Theme_JSON_Resolver::get_fields_to_translate();
    52 
    53         $expected = array(
    54             array(
    55                 'path'    => array( 'settings', 'typography', 'fontSizes' ),
    56                 'key'     => 'name',
    57                 'context' => 'Font size name',
    58             ),
    59             array(
    60                 'path'    => array( 'settings', 'color', 'palette' ),
    61                 'key'     => 'name',
    62                 'context' => 'Color name',
    63             ),
    64             array(
    65                 'path'    => array( 'settings', 'color', 'gradients' ),
    66                 'key'     => 'name',
    67                 'context' => 'Gradient name',
    68             ),
    69             array(
    70                 'path'    => array( 'settings', 'color', 'duotone' ),
    71                 'key'     => 'name',
    72                 'context' => 'Duotone name',
    73             ),
    74             array(
    75                 'path'    => array( 'settings', 'blocks', '*', 'typography', 'fontSizes' ),
    76                 'key'     => 'name',
    77                 'context' => 'Font size name',
    78             ),
    79             array(
    80                 'path'    => array( 'settings', 'blocks', '*', 'color', 'palette' ),
    81                 'key'     => 'name',
    82                 'context' => 'Color name',
    83             ),
    84             array(
    85                 'path'    => array( 'settings', 'blocks', '*', 'color', 'gradients' ),
    86                 'key'     => 'name',
    87                 'context' => 'Gradient name',
    88             ),
    89         );
    90 
    91         $this->assertSame( $expected, $actual );
    9245    }
    9346
Note: See TracChangeset for help on using the changeset viewer.