Skip to content

WPML

WPML translates the attributes of a custom block only as far as a language configuration file declares them. Blockstudio stores every field value in the block comment under blockstudio.attributes, so each block needs an entry that points WPML at its text fields and its attachment IDs. The wp bs wpml config command writes these entries for every registered block.

Why blocks need a configuration

WPML reads the configuration from a file named wpml-config.xml in the root folder of the active theme and of each active plugin. For a must-use plugin that is a single file, the folder has the name of the file without .php. See Language Configuration Files and Make Custom Gutenberg Blocks Translatable.

A block without an entry is left to WPML’s automatic string detection, which offers every string attribute of the block for translation. For a Blockstudio block that includes select values, file references, URLs, and CSS classes. Translators see these technical values next to the real text, and a translated select value or class can break the block.

Generate the configuration

Print the configuration and save it in the root folder of the theme or plugin that ships the blocks:

Shell
wp bs wpml config > wp-content/themes/my-theme/wpml-config.xml

Or let the command write the file:

Shell
wp bs wpml config --file=wp-content/themes/my-theme/wpml-config.xml

A block with a text field, an image, a link, and a repeater:

JSON
{
  "name": "my-theme/cards",
  "title": "Cards",
  "blockstudio": {
    "attributes": [
      { "id": "heading", "type": "text", "label": "Heading" },
      { "id": "gallery", "type": "files", "label": "Gallery", "multiple": true },
      {
        "id": "style",
        "type": "select",
        "label": "Style",
        "options": [
          { "value": "light", "label": "Light" },
          { "value": "dark", "label": "Dark" }
        ]
      },
      {
        "id": "cards",
        "type": "repeater",
        "label": "Cards",
        "attributes": [
          { "id": "title", "type": "text", "label": "Title" },
          { "id": "image", "type": "files", "label": "Image" },
          { "id": "link", "type": "link", "label": "Button" }
        ]
      }
    ]
  }
}

becomes:

XML
<wpml-config>
	<gutenberg-blocks>
		<!-- blockstudio:start -->
		<!-- Generated by wp bs wpml config. Run it again after changing block fields; edits between the markers are replaced. -->
		<gutenberg-block type="my-theme/cards" translate="1" label="Cards">
			<key name="blockstudio">
				<key name="attributes">
					<key name="heading" label="Heading" />
					<key name="gallery">
						<key name="*" type="post-ids" sub-type="attachment" />
					</key>
					<key name="cards">
						<key name="*">
							<key name="title" label="Cards: Title" />
							<key name="image" type="post-ids" sub-type="attachment" />
							<key name="link">
								<key name="title" label="Cards: Button: Link text" />
								<key name="url" type="link" label="Cards: Button: URL" />
							</key>
						</key>
					</key>
				</key>
			</key>
		</gutenberg-block>
		<!-- blockstudio:end -->
	</gutenberg-blocks>
</wpml-config>

The style value is not declared, so WPML copies it into the translation unchanged. The gallery and image IDs are copied as well, and WPML replaces each one with the attachment of the page language when the block is rendered, so a translated image can have its own alt text. See “Automatically Converting IDs in Blocks” in Make Custom Gutenberg Blocks Translatable. When an attachment has no translation, WPML keeps the original ID.

What is declared

The command reads the registered blocks, after custom/ fields and block fields are expanded and overrides and attribute filters are applied. Fields that extensions add to a Blockstudio block are included. Fields that extensions add to core blocks or to blocks of other plugins are not, because those blocks get their configuration from WPML or from their own plugin.

Field Declaration
text, textarea, richtext, wysiwyg Translatable text.
link title as text and url with type="link", so WPML replaces a link with its translation when one is available.
files The stored attachment ID with type="post-ids" and sub-type="attachment". With multiple, a level named * for the list, which also matches the single ID that the field stores when one file is selected.
repeater A level named * that matches every row, with the declarations of the row fields. Nested repeaters work the same way.
group with an id The fields of the group, with the group ID as key prefix (cta_text for the field text in the group cta).
group without an id, tabs The fields, without a level of their own.
Custom field type registered with translate set to true Translatable text, like a text field.
Every other built-in type Not declared. WPML copies the value unchanged.

A block without a declared text field gets translate="0". WPML then copies all of its values and offers nothing from it for translation. Its files fields are still declared inside the entry, because WPML converts attachment IDs whatever translate says. Inner blocks are translated as usual.

Not declared:

  • Fields with hidden, because their values are not written by editors.
  • Fields with "translate": false, see below.
  • Fields of custom field types that are not registered with translate set to true. The command prints a warning for each, because only the field type knows whether its value holds text. An explicit "translate": false on a field also suppresses this warning for that field. See below.
  • Attributes that a block declares in its native attributes instead of as Blockstudio fields.
  • Storage keys whose fields have different types, see below.

Every text field is declared, including one that holds a technical value such as a CSS class, unless it sets "translate": false.

Leave a field out

A text field can hold a value that must be the same in every language: an account handle, an email address, an anchor, a CSS class. WPML offers it for translation like any other text, and a translated value can break the block. Set translate to false to leave the field out of the configuration, so WPML copies its value into the translation unchanged:

JSON
{
  "id": "account",
  "type": "text",
  "label": "Account handle",
  "translate": false
}

On a group, tabs, or repeater, "translate": false applies to every field inside, including nested containers. A child cannot override an opted-out container with "translate": true. On a files field, false leaves out the attachment IDs as well, so the translation keeps the original attachments.

Omitting translate or setting it to true preserves the existing behavior: the field type decides. It does not make hidden fields or value types such as select translatable. The option applies to block and extension fields, does not hide or disable their editor controls, and does not change their stored values. Run wp bs wpml config again after changing the setting and update your wpml-config.xml with the generated configuration.

The label of each key is the field label, preceded by the labels of the enclosing repeaters and groups with an ID. WPML shows it as the field name in the translation editor. The two keys of a link field add Link text and URL to the label. These suffixes are translated into the site language, so run the command with the locale your translators work in.

Custom field types

A field type that stores a plain string of text says so when it is registered. Set translate to true in the definition, which requires attribute to be string:

PHP
bs_register_field_type('acme/headline', [
  'attribute' => 'string',
  'translate' => true,
]);

The command then declares every field of that type as translatable text. A field can still set "translate": false to leave itself out. Without the option, the fields of the type are left out and reported, as before.

Change the labels

Field labels are written for the block editor and can carry hints such as “Image (optional)” that mean nothing in the translation editor. The blockstudio/wpml/config/label filter receives each label before it is written, with the context of the key:

  • block: the block name.
  • path: the storage path of the field below blockstudio.attributes, with a level * for the rows of each repeater, for example cards/*/link.
  • field: the field definition.
  • parents: the labels of the enclosing repeaters and groups with an ID.
  • part: text for a text field, title or url for the two keys of a link field.
PHP
add_filter('blockstudio/wpml/config/label', function ($label, $context) {
  return str_replace(' (optional)', '', $label);
}, 10, 2);

Return an empty string to leave out the label of a key, so WPML shows the label of the block instead. A return value that is not a string keeps the original label. Run the command again after changing the filter.

Fields with the same storage key

Two fields can end up with the same storage key, for example a block field and an extension field with the same id, or the field text in a group with the ID cta and a field with the ID cta_text. The command declares such a key only when it is clear what is stored there:

  • Within the fields of a block, outside repeater rows, Blockstudio registers one attribute per storage key, so a later field replaces an earlier one, whatever its type. The fields of the extensions of a block work the same way: a later field, or the field of an extension registered later, replaces an earlier one.
  • A block field and an extension field with the same key both stay in effect. The block template receives the value as the block’s field defines it, while the extension is applied with its own field. The key is declared once when both fields need the same declaration: both are text fields (text, textarea, richtext, or wysiwyg), both are links, both are files fields (with multiple on either, the key gets the * level), or neither is declared, such as a select and a number. Two repeaters are compared field by field in their rows. The label comes from the block’s field.
  • Two fields with the same id in the rows of one repeater both stay in effect as well, and are compared the same way.

When the types differ, for example a text field in the block and a select in an extension, the stored value is text or a select option, depending on the control that wrote it. The command does not declare the key and prints a warning:

Shell
Warning: Not declared, the fields that store this key have different types: my-theme/cards: tone (text in the block, select in an extension)

Rename one of the fields, or give both the same type, to have the key declared.

Keep the configuration up to date

Run the command again after adding, removing, or renaming fields, and ship the updated file with the theme or plugin. Regenerate configurations made before 7.7.5 to include attachment-ID declarations. Content that existed before shows new fields for translation once its original is updated, as described in the WPML guide above.

With --file, the command creates a missing file. In an existing file, it replaces only the text between <!-- blockstudio:start --> and <!-- blockstudio:end -->, so other sections and entries for other blocks stay as they are:

XML
<wpml-config>
	<custom-types>
		<custom-type translate="1">book</custom-type>
	</custom-types>
	<gutenberg-blocks>
		<gutenberg-block type="my-plugin/map" translate="1">
			<key name="caption" />
		</gutenberg-block>
		<!-- blockstudio:start -->
		<!-- blockstudio:end -->
	</gutenberg-blocks>
</wpml-config>

To use an existing file that has no markers yet, add the two marker lines once inside its gutenberg-blocks element, or add a gutenberg-blocks element with the markers if the file has none. Without the markers, or when the result would not be a valid configuration, the command leaves the file unchanged and exits with an error.

Entries for Blockstudio blocks that were written by hand, such as translate="0" entries that hide technical values, are no longer needed once the generated section covers the block. The command warns about every block that is declared more than once, so these entries can be removed.

Image texts in translation jobs

WPML puts the texts of the images that a page uses into the translation job of the page: title, alt text, caption, and description. When the job is completed, WPML writes the translated texts to the attachment of the target language and creates that attachment if there is none yet. See How to Translate Media. WPML finds the images in <img> tags in the content, in galleries, in the featured image, and in the blocks it knows. A Blockstudio block stores attachment IDs in the block comment, so WPML finds none of them.

When WPML is active, Blockstudio adds the attachments that its fields store in the blocks of a page to the images WPML found:

  • files fields, including hidden ones, and attributes fields with media, the same fields the block editor loads as media.
  • Fields in groups, tabs, repeater rows, and inner blocks.
  • Fields of extensions, also on core blocks and blocks of other plugins, because their values are stored in the attributes of the extended block.

Values that are not the ID of an attachment are left out. Nothing needs to be configured, and the wpml-config.xml is not involved. Without WPML, nothing is scanned for translation jobs and the media integration class is not loaded.

This is separate from attachment-ID conversion: the generated XML tells WPML which translated attachment to render, while this integration tells it which attachment texts to include in a translation job. String references and IDs of non-attachment posts are not added to media jobs. Saved nested field values take precedence over top-level values on Blockstudio blocks, as on render.

The texts are part of the job only when one of the image text options under WPML > Settings > Media Translation is enabled, “Automatically detect best options for translating image texts” or “Translate Media Library texts when translating content”, as for images in core blocks.

Blockstudio reports the images through the filter wpml_ids_of_media_used_in_post, which WPML applies to the media of a post in PostWithMediaFiles. To leave an image out of the jobs, remove its ID in a callback on the same filter with a priority above 10:

PHP
add_filter(
	'wpml_ids_of_media_used_in_post',
	function ( $ids ) {
		return array_values( array_diff( $ids, array( 42 ) ) );
	},
	20
);