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