Extending
ExtendingTranslating additional Gutenberg blocks

Translating additional Gutenberg blocks

Gato AI Translations for Polylang can translate block-based posts.

The plugin ships with support for many blocks out of the box. For anything beyond that — your own custom blocks, or blocks from 3rd-party plugins that don't ship a wpml-config.xml — you can extend support via PHP hooks.

Translating strings

To register additional translatable attributes for a block, use the gatompl:gutenberg_block_type_translatable_attribute_regexes filter.

Why regexes?

A Gutenberg block is persisted to post_content as an HTML comment that carries the block's JSON attributes, followed by the block's rendered HTML, e.g.:

<!-- wp:my-plugin/my-block {"title":"Hello"} -->
<div class="wp-block-my-plugin-my-block">Hello</div>
<!-- /wp:my-plugin/my-block -->

Translating a block means finding the specific substring to translate inside that markup, swapping it for its translation, and leaving everything else untouched (block name, other attributes, HTML structure, surrounding blocks). Regexes are how the plugin pinpoints exactly which substring to replace: the boilerplate before and after the value is captured in groups, the value itself is the part that gets swapped.

Standard string attributes (stored in the block's JSON)

If the property is a normal string stored in the block's JSON attributes, pass true and the plugin will use its default regex.

For instance, to translate the daysLabel, hoursLabel, minutesLabel and secondsLabel attributes of the kadence/countdown block — whose markup looks like this:

<!-- wp:kadence/countdown {"uniqueID":"_abc123","date":"2026-12-31 00:00:00","daysLabel":"Days","hoursLabel":"Hours","minutesLabel":"Minutes","secondsLabel":"Seconds"} -->
<div class="wp-block-kadence-countdown">…</div>
<!-- /wp:kadence/countdown -->

…register the attributes via:

add_filter(
    'gatompl:gutenberg_block_type_translatable_attribute_regexes',
    static function (array $regexes): array {
        $regexes['kadence/countdown'] = [
            'daysLabel'    => true,
            'hoursLabel'   => true,
            'minutesLabel' => true,
            'secondsLabel' => true,
        ];
        return $regexes;
    }
);

The true value is expanded internally into this default regex:

#(<!-- wp:%3$s \{.*?\"%2$s\":\")%1$s(\".*?\}/?-->)#

…where the placeholders are:

  1. %1$s → the attribute value
  2. %2$s → the attribute name
  3. %3$s → the block name

For the daysLabel attribute on kadence/countdown, the placeholders are substituted as %3$skadence/countdown, %2$sdaysLabel, %1$sDays, producing:

#(<!-- wp:kadence/countdown \{.*?\"daysLabel\":\")Days(\".*?\}/?-->)#

Only Days is replaced; the block name, the other attributes, and the closing comment are preserved by the capture groups.

The shape of the regex is:

#(everything before)attribute value(everything after)#

Strings stored inside the block's HTML

If the value isn't stored in the JSON attributes but inside the rendered HTML, provide your own regex. You can use %s (instead of %1$s) where the attribute value goes, and have the block name and attribute name hardcoded in the regex.

Example — translating the content attribute of the generateblocks/text block. Its markup looks like this — note that Hello world is not inside the JSON, it sits between the rendered tags:

<!-- wp:generateblocks/text {"uniqueId":"abc123","tagName":"p"} -->
<p class="gb-text">Hello world</p>
<!-- /wp:generateblocks/text -->

The default regex would never find that substring, so you supply your own:

add_filter(
    'gatompl:gutenberg_block_type_translatable_attribute_regexes',
    static function (array $regexes): array {
        $regexes['generateblocks/text'] = [
            'content' => '#(<!-- wp:generateblocks/text [^>]*?-->\n?<[a-z0-9]+ ?[^>]*?>)%s(</[a-z0-9]+>\n?<!-- /wp:generateblocks/text -->)#',
        ];
        return $regexes;
    }
);

When the same value appears in multiple places

If the same attribute appears both in the JSON attributes and in the HTML (or in two different spots), pass an array of regexes — each one needs to fire so every copy of the string is translated.

For example, on the generateblocks/media block, alt and title are stored both inside htmlAttributes in the JSON, and as HTML attributes on the rendered <img>:

<!-- wp:generateblocks/media {"mediaId":42,"htmlAttributes":{"alt":"Cat sitting","title":"My cat"}} -->
<figure class="gb-media"><img src="…" alt="Cat sitting" title="My cat"></figure>
<!-- /wp:generateblocks/media -->

Two regexes per attribute — one targeting the JSON, one targeting the <img> — make sure both copies stay in sync after translation:

add_filter(
    'gatompl:gutenberg_block_type_translatable_attribute_regexes',
    static function (array $regexes): array {
        $regexes['generateblocks/media'] = [
            'htmlAttributes.alt' => [
                '#(<!-- wp:generateblocks/media \{.*?\"htmlAttributes\":\{.*?\"alt\":\")%s(\".*?\}.*?\} -->)#',
                '#(<!-- wp:generateblocks/media [^>]*?-->\n?.*<img [^>]*alt=\")%s(\"[^>]*?>.*\n?<!-- /wp:generateblocks/media -->)#',
            ],
            'htmlAttributes.title' => [
                '#(<!-- wp:generateblocks/media \{.*?\"htmlAttributes\":\{.*?\"title\":\")%s(\".*?\}.*?\} -->)#',
                '#(<!-- wp:generateblocks/media [^>]*?-->\n?.*<img [^>]*title=\")%s(\"[^>]*?>.*\n?<!-- /wp:generateblocks/media -->)#',
            ],
        ];
        return $regexes;
    }
);

If the attribute value is a JSON object, you can target a specific sub-property using a . (dot) in the attribute name, as shown above with htmlAttributes.alt and htmlAttributes.title on generateblocks/media.

Disabling translation for an automatically-translated attribute

Passing false or null removes translation for an attribute that the plugin would otherwise translate automatically. This is useful, for example, to opt a specific string attribute out of automatic translation in PHP-only blocks, or for blocks pulled in from a wpml-config.xml whose declared attributes you don't want translated:

add_filter(
    'gatompl:gutenberg_block_type_translatable_attribute_regexes',
    static function (array $regexes): array {
        // Disable translation of the `header` attribute on the
        // `my-plugin/duplicate-alert` block (either form works)
        unset($regexes['my-plugin/duplicate-alert']['header']);
        $regexes['my-plugin/duplicate-alert']['implications'] = false;
        return $regexes;
    }
);

Translating entity references

Entity references (a post/media/term/menu ID stored in a block attribute) can be remapped to the corresponding target-language entity at translation time. Use one of the following filters depending on the kind of reference:

Reference kindFilter
Custom posts and mediagatompl:gutenberg_block_type_custompost_and_media_reference_attribute_regexes
Taxonomy termsgatompl:gutenberg_block_type_taxonomy_term_reference_attribute_regexes
Menus by IDgatompl:gutenberg_block_type_menu_reference_by_id_attribute_regexes
Menus by sluggatompl:gutenberg_block_type_menu_reference_by_slug_attribute_regexes

Each filter receives the same structure as the translatable-attributes filter (true for the default regex, a string/array for custom regexes).

For example, the woocommerce/single-product block stores the linked product as a numeric productId:

<!-- wp:woocommerce/single-product {"productId":42} /-->

When the post is translated, that 42 (the source-language product) needs to be remapped to its target-language counterpart (say 87). Mark productId as a custom-post reference so the plugin captures and swaps the ID at translation time:

add_filter(
    'gatompl:gutenberg_block_type_custompost_and_media_reference_attribute_regexes',
    static function (array $regexes): array {
        $regexes['woocommerce/single-product'] = [
            'productId' => true,
            // …or a custom regex if `productId` is not stored in the standard JSON shape:
            // 'productId' => '#(<!-- wp:woocommerce/single-product \{.*?\"productId\":)%s([,\}].*? /?-->)#',
        ];
        return $regexes;
    }
);

Use the same pattern for the other reference kinds. Each kind looks the same in the block markup — a numeric ID or a slug embedded in the JSON — what differs is how the plugin resolves it to the target language:

<!-- wp:my-plugin/related-category {"categoryId":17} /-->
<!-- wp:my-plugin/menu-picker {"menuId":5} /-->
<!-- wp:my-plugin/menu-picker {"menuSlug":"main-nav"} /-->
// Taxonomy term reference
add_filter(
    'gatompl:gutenberg_block_type_taxonomy_term_reference_attribute_regexes',
    static function (array $regexes): array {
        $regexes['my-plugin/related-category'] = [
            'categoryId' => true,
        ];
        return $regexes;
    }
);
 
// Menu reference by ID
add_filter(
    'gatompl:gutenberg_block_type_menu_reference_by_id_attribute_regexes',
    static function (array $regexes): array {
        $regexes['my-plugin/menu-picker'] = [
            'menuId' => true,
        ];
        return $regexes;
    }
);
 
// Menu reference by slug
add_filter(
    'gatompl:gutenberg_block_type_menu_reference_by_slug_attribute_regexes',
    static function (array $regexes): array {
        $regexes['my-plugin/menu-picker'] = [
            'menuSlug' => true,
        ];
        return $regexes;
    }
);

Discovering the attribute names

The fastest way to find the attribute names and how they are stored is to run the Translate custom posts GraphQL query and look at the blockFlattenedDataItems response field for the block in question.

See the Retrieving page builder data to translate guide for how to run that query and read its output.

Working around blocks whose attributes need processing

The hooks above assume the attribute value exposed via blockFlattenedDataItems is already the value to translate (a scalar or array).

If the value is wrapped — for example, the attribute stores <li>Some text</li> and you want only Some text translated — you need to extract it first via the gatompl:gutenberg_block_flattened_data_item_attributes filter.

The generateblocks/image block is a real-world example: its alt and title are not exposed as standalone attributes, they live inside the innerContent HTML and have to be extracted with a regex.

add_filter(
    'gatompl:gutenberg_block_flattened_data_item_attributes',
    static function (?\stdClass $attributes, string $blockTypeName, \stdClass $blockDataItem): ?\stdClass {
        if ($attributes === null || $blockTypeName !== 'generateblocks/image') {
            return $attributes;
        }
 
        $innerContent = $blockDataItem->innerContent ?? null;
        if (!is_array($innerContent) || !isset($innerContent[0]) || !is_string($innerContent[0])) {
            return $attributes;
        }
        $html = $innerContent[0];
 
        if (preg_match('#<img [^>]*alt="([^"]*)"[^>]*?>#', $html, $matches) === 1 && $matches[1] !== '') {
            $attributes->alt = $matches[1];
        }
        if (preg_match('#<img [^>]*title="([^"]*)"[^>]*?>#', $html, $matches) === 1 && $matches[1] !== '') {
            $attributes->title = $matches[1];
        }
        return $attributes;
    },
    10,
    3
);

Once alt and title exist on the attributes object, the regex-based hooks above can target them like any other attribute.

Where to find examples

The plugin's own integrations are good real-world references. Explore these files inside the plugin you installed:

  • Block attribute regexes: wp-content/plugins/gato-ai-translations-for-polylang/src/Constants/BlockTypeAttributeValues.php
  • Pre-processing block attributes: wp-content/plugins/gato-ai-translations-for-polylang/src/ConditionalOnContext/LicenseIsActive/Hooks/CoreBlockFlattenedDataItemAttributesHookSet.php