# 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](https://wpml.org/documentation/support/language-configuration-files/)
and [Make Custom Gutenberg Blocks Translatable](https://wpml.org/documentation/support/language-configuration-files/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:

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

Or let the command write the file:

```bash
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 title="block.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 title="wpml-config.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](https://wpml.org/documentation/support/language-configuration-files/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](/docs/extensions.md) 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](/docs/blocks/attributes/custom-field-types.md)
  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 title="block.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 title="functions.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 title="functions.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:

```bash
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 title="wpml-config.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](https://wpml.org/documentation/translating-your-contents/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](/docs/extensions.md), 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
);
```
