Make WordPress Core

Changeset 51599


Ignore:
Timestamp:
08/11/2021 09:06:31 AM (5 years 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.