diff --git a/bin/plugin/commands/common.js b/bin/plugin/commands/common.js index 57326be7205bf..4f3e0bb065243 100644 --- a/bin/plugin/commands/common.js +++ b/bin/plugin/commands/common.js @@ -116,6 +116,7 @@ function calculateVersionBumpFromChangelog( if ( lineNormalized.startsWith( '### deprecation' ) || lineNormalized.startsWith( '### enhancement' ) || + lineNormalized.startsWith( '### new api' ) || lineNormalized.startsWith( '### new feature' ) ) { versionBump = 'minor'; diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index 35e014595b0c8..6a4160906ba3b 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -438,7 +438,7 @@ return array( ## Internationalization -WordPress string discovery automatically will translate fields marked in the documentation as translatable using the `textdomain` property when specified in the `block.json` file. In that case, localized properties will be automatically wrapped in `_x` function calls on the backend of WordPress when executing `register_block_type_from_metadata`. These translations are added as an inline script to the `wp-block-library` script handle in WordPress core or to the plugin's script handle. +WordPress string discovery system can automatically translate fields marked in this document as translatable. First, you need to set the `textdomain` property in the `block.json` file that provides block metadata. **Example:** @@ -451,19 +451,40 @@ WordPress string discovery automatically will translate fields marked in the doc } ``` -The way `register_block_type_from_metadata` processes translatable values is roughly equivalent to: +### PHP + +In PHP, localized properties will be automatically wrapped in `_x` function calls on the backend of WordPress when executing `register_block_type_from_metadata`. These translations get added as an inline script to the plugin's script handle or to the `wp-block-library` script handle in WordPress core. + +The way `register_block_type_from_metadata` processes translatable values is roughly equivalent to the following code snippet: ```php _x( 'My block', 'block title', 'my-plugin' ), 'description' => _x( 'My block is fantastic!', 'block description', 'my-plugin' ), - 'keywords' => array( _x( 'fantastic', 'block keywords', 'my-plugin' ) ), + 'keywords' => array( _x( 'fantastic', 'block keyword', 'my-plugin' ) ), ); ``` Implementation follows the existing [get_plugin_data](https://codex.wordpress.org/Function_Reference/get_plugin_data) function which parses the plugin contents to retrieve the plugin’s metadata, and it applies translations dynamically. +### JavaScript + +In JavaScript, you need to use `registerBlockTypeFromMetadata` method from `@wordpress/blocks` package to process loaded block metadata. All localized properties get automatically wrapped in `_x` (from `@wordpress/i18n` package) function calls similar to how it works in PHP. + +**Example:** + +```js +import { registerBlockTypeFromMetadata } from '@wordpress/blocks'; +import Edit from './edit'; +import metadata from './block.json'; + +registerBlockTypeFromMetadata( metadata, { + edit: Edit, + // ...other client-side settings +} ); +``` + ## Backward Compatibility The existing registration mechanism (both server side and frontend) will continue to work, it will serve as low-level implementation detail for the `block.json` based registration. diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index e1b6d279acaad..22b4385b9a806 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -4,12 +4,11 @@ import '@wordpress/core-data'; import '@wordpress/block-editor'; import { - registerBlockType, + registerBlockTypeFromMetadata, setDefaultBlockName, setFreeformContentHandlerName, setUnregisteredTypeHandlerName, setGroupingBlockName, - unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase } from '@wordpress/blocks'; /** @@ -106,10 +105,7 @@ const registerBlock = ( block ) => { return; } const { metadata, settings, name } = block; - if ( metadata ) { - unstable__bootstrapServerSideBlockDefinitions( { [ name ]: metadata } ); - } - registerBlockType( name, settings ); + registerBlockTypeFromMetadata( { name, ...metadata }, settings ); }; /** diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index 7a39f06a8ef3e..0dffcb66269c1 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -10,6 +10,7 @@ import { sortBy } from 'lodash'; import { hasBlockSupport, registerBlockType, + registerBlockTypeFromMetadata, setDefaultBlockName, setFreeformContentHandlerName, setUnregisteredTypeHandlerName, @@ -127,10 +128,13 @@ const registerBlock = ( block ) => { return; } const { metadata, settings, name } = block; - registerBlockType( name, { - ...metadata, - ...settings, - } ); + registerBlockTypeFromMetadata( + { + name, + ...metadata, + }, + settings + ); }; /** diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 8a45220bdf44a..498eb1bd41825 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New API + +- `registerBlockTypeFromMetadata` method can be used to register a block type using the metadata loaded from `block.json` file ([#30293](https://github.com/WordPress/gutenberg/pull/30293)). + ## 8.0.0 (2021-03-17) ### Breaking Change diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 8adbe8d9dee9d..3d39a3651ae25 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -713,6 +713,26 @@ _Returns_ - `?WPBlock`: The block, if it has been successfully registered; otherwise `undefined`. +# **registerBlockTypeFromMetadata** + +Registers a new block provided from metadata stored in `block.json` file. +It uses `registerBlockType` internally. + +_Related_ + +- registerBlockType + +_Parameters_ + +- _metadata_ `Object`: Block metadata loaded from `block.json`. +- _metadata.name_ `string`: Block name. +- _metadata.textdomain_ `string`: Textdomain to use with translations. +- _additionalSettings_ `Object`: Additional block settings. + +_Returns_ + +- `?WPBlock`: The block, if it has been successfully registered; otherwise `undefined`. + # **registerBlockVariation** Registers a new block variation for the given block type. diff --git a/packages/blocks/src/api/i18n-block.json b/packages/blocks/src/api/i18n-block.json new file mode 100644 index 0000000000000..3d31f78592eaa --- /dev/null +++ b/packages/blocks/src/api/i18n-block.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/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 5fdd8aad97bdf..4d535589b09ab 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -108,6 +108,7 @@ export { getCategories, setCategories, updateCategory } from './categories'; // children of another block. export { registerBlockType, + registerBlockTypeFromMetadata, registerBlockCollection, unregisterBlockType, setFreeformContentHandlerName, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 81e27faab26f2..b1f62fe4ee9cb 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -5,9 +5,13 @@ */ import { camelCase, + isArray, + isEmpty, isFunction, isNil, + isObject, isPlainObject, + isString, mapKeys, omit, pick, @@ -20,11 +24,13 @@ import { */ import { applyFilters } from '@wordpress/hooks'; import { select, dispatch } from '@wordpress/data'; +import { _x } from '@wordpress/i18n'; import { blockDefault } from '@wordpress/icons'; /** * Internal dependencies */ +import i18nBlockSchema from './i18n-block.json'; import { isValidIcon, normalizeIconObject } from './utils'; import { DEPRECATED_ENTRY_KEYS } from './constants'; import { store as blocksStore } from '../store'; @@ -310,6 +316,115 @@ export function registerBlockType( name, settings ) { return settings; } +/** + * Translates block settings provided with metadata using the i18n schema. + * + * @param {string|string[]|Object[]} i18nSchema I18n schema for the block setting. + * @param {string|string[]|Object[]} settingValue Value for the block setting. + * @param {string} textdomain Textdomain to use with translations. + * + * @return {string|string[]|Object[]} Translated setting. + */ +function translateBlockSettingUsingI18nSchema( + i18nSchema, + settingValue, + textdomain +) { + if ( isString( i18nSchema ) && isString( settingValue ) ) { + // eslint-disable-next-line @wordpress/i18n-no-variables, @wordpress/i18n-text-domain + return _x( settingValue, i18nSchema, textdomain ); + } + if ( + isArray( i18nSchema ) && + ! isEmpty( i18nSchema ) && + isArray( settingValue ) + ) { + return settingValue.map( ( value ) => + translateBlockSettingUsingI18nSchema( + i18nSchema[ 0 ], + value, + textdomain + ) + ); + } + if ( + isObject( i18nSchema ) && + ! isEmpty( i18nSchema ) && + isObject( settingValue ) + ) { + return Object.keys( settingValue ).reduce( ( accumulator, key ) => { + if ( ! i18nSchema[ key ] ) { + accumulator[ key ] = settingValue[ key ]; + return accumulator; + } + accumulator[ key ] = translateBlockSettingUsingI18nSchema( + i18nSchema[ key ], + settingValue[ key ], + textdomain + ); + return accumulator; + }, {} ); + } + return settingValue; +} + +/** + * Registers a new block provided from metadata stored in `block.json` file. + * It uses `registerBlockType` internally. + * + * @see registerBlockType + * + * @param {Object} metadata Block metadata loaded from `block.json`. + * @param {string} metadata.name Block name. + * @param {string} metadata.textdomain Textdomain to use with translations. + * @param {Object} additionalSettings Additional block settings. + * + * @return {?WPBlock} The block, if it has been successfully registered; + * otherwise `undefined`. + */ +export function registerBlockTypeFromMetadata( + { name, textdomain, ...metadata }, + additionalSettings +) { + const allowedFields = [ + 'apiVersion', + 'title', + 'category', + 'parent', + 'icon', + 'description', + 'keywords', + 'attributes', + 'providesContext', + 'usesContext', + 'supports', + 'styles', + 'example', + 'variations', + ]; + + const settings = pick( metadata, allowedFields ); + + if ( textdomain ) { + Object.keys( i18nBlockSchema ).forEach( ( key ) => { + if ( ! settings[ key ] ) { + return; + } + settings[ key ] = translateBlockSettingUsingI18nSchema( + i18nBlockSchema[ key ], + settings[ key ], + textdomain + ); + } ); + } + + unstable__bootstrapServerSideBlockDefinitions( { + [ name ]: settings, + } ); + + return registerBlockType( name, additionalSettings ); +} + /** * Registers a new block collection to group blocks in the same namespace in the inserter. * diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 3ef1c4713f814..b8d7c9c413754 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -8,7 +8,7 @@ import { noop, get, omit, pick } from 'lodash'; /** * WordPress dependencies */ -import { addFilter, removeAllFilters } from '@wordpress/hooks'; +import { addFilter, removeAllFilters, removeFilter } from '@wordpress/hooks'; import { select } from '@wordpress/data'; import { blockDefault as blockIcon } from '@wordpress/icons'; @@ -17,6 +17,7 @@ import { blockDefault as blockIcon } from '@wordpress/icons'; */ import { registerBlockType, + registerBlockTypeFromMetadata, registerBlockCollection, unregisterBlockCollection, unregisterBlockType, @@ -802,6 +803,127 @@ describe( 'blocks', () => { } ); } ); + describe( 'registerBlockTypeFromMetadata', () => { + test( 'registers block from metadata', () => { + const Edit = () => 'test'; + const block = registerBlockTypeFromMetadata( + { + name: 'test/block-from-metadata', + title: 'Block from metadata', + category: 'text', + icon: 'palmtree', + variations: [ + { + name: 'variation', + title: 'Variation Title', + description: 'Variation description', + keywords: [ 'variation' ], + }, + ], + }, + { + edit: Edit, + save: noop, + } + ); + expect( block ).toEqual( { + name: 'test/block-from-metadata', + title: 'Block from metadata', + category: 'text', + icon: { + src: 'palmtree', + }, + keywords: [], + attributes: {}, + providesContext: {}, + usesContext: [], + supports: {}, + styles: [], + variations: [ + { + name: 'variation', + title: 'Variation Title', + description: 'Variation description', + keywords: [ 'variation' ], + }, + ], + edit: Edit, + save: noop, + } ); + } ); + test( 'registers block from metadata with translation', () => { + addFilter( + 'i18n.gettext_with_context_test', + 'test/mark-as-translated', + ( value ) => value + ' (translated)' + ); + + const Edit = () => 'test'; + const block = registerBlockTypeFromMetadata( + { + name: 'test/block-from-metadata-i18n', + title: 'I18n title from metadata', + description: 'I18n description from metadata', + keywords: [ 'i18n', 'metadata' ], + styles: [ + { + name: 'i18n-style', + label: 'I18n Style Label', + }, + ], + variations: [ + { + name: 'i18n-variation', + title: 'I18n Variation Title', + description: 'I18n variation description', + keywords: [ 'variation' ], + }, + ], + textdomain: 'test', + icon: 'palmtree', + }, + { + edit: Edit, + save: noop, + } + ); + removeFilter( + 'i18n.gettext_with_context_test', + 'test/mark-as-translated' + ); + + expect( block ).toEqual( { + name: 'test/block-from-metadata-i18n', + title: 'I18n title from metadata (translated)', + description: 'I18n description from metadata (translated)', + icon: { + src: 'palmtree', + }, + keywords: [ 'i18n (translated)', 'metadata (translated)' ], + attributes: {}, + providesContext: {}, + usesContext: [], + supports: {}, + styles: [ + { + name: 'i18n-style', + label: 'I18n Style Label (translated)', + }, + ], + variations: [ + { + name: 'i18n-variation', + title: 'I18n Variation Title (translated)', + description: 'I18n variation description (translated)', + keywords: [ 'variation (translated)' ], + }, + ], + edit: Edit, + save: noop, + } ); + } ); + } ); + describe( 'registerBlockCollection()', () => { it( 'creates a new block collection', () => { registerBlockCollection( 'core', { title: 'Core' } );