Extending
ExtendingTranslating additional blocks

Translating additional blocks

Gato Multilingual for Polylang can translate block-based posts.

It does this by extracting all the properties inside a block, translating those, and then injecting the translated properties back into the block.

In order to extract the properties from a block, the plugin needs to know:

  • Which properties are translatable
  • How to retrieve them from within the block
  • How to replace the property with its translation

The plugin ships with support for WordPress core blocks, and provides the ability for users to support additional blocks.

Supported blocks

The following WordPress core blocks are already supported:

  • core/heading
  • core/paragraph
  • core/image
  • core/button
  • core/table
  • core/list-item
  • core/cover
  • core/media-text
  • core/verse
  • core/quote
  • core/pullquote
  • core/audio
  • core/video
  • core/preformatted
  • core/embed

Supporting additional blocks

You can also translate custom blocks from your application, or block from 3rd-party plugins.

For instance, let's say we are using the Kadence's testimonials block:

Editing a post with a testimonials block
Editing a post with a testimonials block

When executing the translation, that block will remain in its original language:

Editing the translated post with a testimonials block
Editing the translated post with a testimonials block

To support translating this block, follow the steps below.

1. Enable the Advanced Mode

Click on Enable the Advanced Mode in the plugin Settings > Plugin Configuration > Advanced Use section:

Enabling the advanced mode
Enabling the advanced mode

This will make available the plugin's Queries CPT:

Queries CPT enabled
Queries CPT enabled

2. Edit the Gato GraphQL query

On the Queries list page, edit the Translate custom posts entry:

Translate custom posts entry
Translate custom posts entry

3. Identify the block properties to translate

The Translate custom posts entry contains a GraphQL query (based on Gato GraphQL) with the logic to execute the translation.

You can manually execute the GraphQL query right from that Query CPT entry, by pressing the Run button in the GraphiQL client.

Editing the Translate custom posts entry
Editing the Translate custom posts entry

To execute the query, you will need to provide GraphQL variables (with the ID of the post to translate, and other information), under input Query Variables in the GraphiQL client.

When logs are enabled, Gato Multilingual for Polylang prints the variables used for each execution in the log. You can conveniently copy these from the log file, and paste them in the GraphiQL client.

Access the logs, and then:

  1. Execute some automatic translation (eg: publish a new post)
  2. Copy the variables from the last log entry with [Persisted Query with Slug: "translate-customposts"][Variables: "{

For instance, this log entry:

[2025-02-05 03:11:06] ✅ [Persisted Query with Slug: "translate-customposts"][Variables: "{"statusToUpdate":"draft","updateSlug":true,"translateDefaultLanguageOnly":false,"translateFromLanguage":"en","excludeLanguagesToTranslate":[],"languageTranslationProviders":{},"defaultTranslationProvider":"deepl","providerLanguageMapping":{"google_translate":{"nb":"no"}},"customPostIds":[1021],"customPostType":"post"}"] Execution successful: {"data":{"isGutenbergEditorEnabled":true,"useGutenbergEditorWithCustomPostType":true,"useGutenbergEditor":true,...}

...prints this JSON with variables, that you can copy to execute the query:

{"statusToUpdate":"draft","updateSlug":true,"translateDefaultLanguageOnly":false,"translateFromLanguage":"en","excludeLanguagesToTranslate":[],"languageTranslationProviders":{},"defaultTranslationProvider":"deepl","providerLanguageMapping":{"google_translate":{"nb":"no"}},"customPostIds":[1021],"customPostType":"post"}

(Notice the Query Variables input in the image below, with the JSON copied from the log file.)

After executing the query, the response will contain the data for all blocks under key blockFlattenedDataItems. Explore that entry to identify the block properties that must be translated.

In our case, we find the "kadence/testimonial" block, containing properties title, content and occupation (all under attributes) that need to be translated.

Identifying the block properties to translate
Identifying the block properties to translate

We must also identify how those strings are stored in the block HTML, printed under the first rawContent entry:

Identifying the block properties to translate
Identifying the block properties to translate

In our case, the block HTML is:

<!-- wp:kadence/testimonial {\"uniqueID\":\"1021_4fe748-f5\",\"url\":\"https://gatomultilingual.local/wp-content/uploads/2025/01/Luciano_Pavarotti_2004.jpg\",\"id\":184,\"subtype\":\"jpeg\",\"title\":\"Here is my secret\",\"content\":\"My voice needs to be strong and clear. That's why I drink green tea every morning. Stay away from fried food!\",\"name\":\"Luciano Pavarotti\",\"occupation\":\"Opera singer\",\"sizes\":{\"thumbnail\":{\"height\":150,\"width\":150,\"url\":\"https://gatomultilingual.local/wp-content/uploads/2025/01/Luciano_Pavarotti_2004-150x150.jpg\",\"orientation\":\"landscape\"},\"medium\":{\"height\":300,\"width\":210,\"url\":\"https://gatomultilingual.local/wp-content/uploads/2025/01/Luciano_Pavarotti_2004-210x300.jpg\",\"orientation\":\"portrait\"},\"full\":{\"url\":\"https://gatomultilingual.local/wp-content/uploads/2025/01/Luciano_Pavarotti_2004.jpg\",\"height\":629,\"width\":440,\"orientation\":\"portrait\"}}} /-->

The query will execute a regular expression (regex) search and replace on that HTML, to replace a string with its translation.

Later on, from this HTML, we will identify how to reach each property that needs to be translated (title, content and occupation) and create the corresponding regex.

4. Customize the Gato GraphQL query

We can conveniently check which of the already-supported core blocks has a similar structure, and copy/paste/adapt that code for our block.

In our case, "kadence/testimonial" has a similar structure to "core/paragraph" (which contains the content property to translate), then we can copy and adapt its code.

The logic to translate a block is spread across 7 sections in the GraphQL query. They are marked with a GraphQL comment Insert code for custom blocks ({1-7}), like this one:

#############################################
##### Insert code for custom blocks (1) #####
#############################################

Search for them, copy the logic from the already-supported block ("core/paragraph") in that section, and adapt it accordingly for your block ("kadence/testimonial") by renaming variables, adapting the name of the property, and identifying the regex to replace the value of property in the block HTML.

The 7 sections for "core/paragraph" are:

Section 1 (defines a set of dynamic variables):

      @export(
        as: "originCoreParagraphContentItems"
        type: DICTIONARY
      )
      @export(
        as: "originCoreParagraphContentReplacementsFrom"
        type: DICTIONARY
      )
      @export(
        as: "originCoreParagraphContentReplacementsTo"
        type: DICTIONARY
      )

Section 2 (extracts a property from within the block, and exports it under a dynamic variable):

      originCoreParagraph: blockFlattenedDataItems(
        filterBy: { include: "core/paragraph" }
      )
        @underEachArrayItem
          @underJSONObjectProperty(
            by: { path: "attributes.content" }
            failIfNonExistingKeyOrPath: false
          )
            @export(
              as: "originCoreParagraphContentItems"
              type: DICTIONARY
            )

Section 3 (defines another set of dynamic variables):

        @export(
          as: "coreParagraphContentItems"
          type: DICTIONARY
        )
        @export(
          as: "coreParagraphContentReplacementsFrom"
          type: DICTIONARY
        )
        @export(
          as: "coreParagraphContentReplacementsTo"
          type: DICTIONARY
        )

Section 4 (extracts the strings to translate for a property and assigns them to a dynamic variable, and prepares storing the translations in another dynamic variable):

      originCoreParagraphContentItems: _objectProperty(
        object: $originCoreParagraphContentItems
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      coreParagraphContentItems: _echo(value: $__originCoreParagraphContentItems)
        @export(
          as: "coreParagraphContentItems"
          type: DICTIONARY
        )
        @remove
        
      originCoreParagraphContentReplacementsFrom: _objectProperty(
        object: $originCoreParagraphContentReplacementsFrom
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      coreParagraphContentReplacementsFrom: _echo(value: $__originCoreParagraphContentReplacementsFrom)
        @export(
          as: "coreParagraphContentReplacementsFrom"
          type: DICTIONARY
        )
        @remove
        
      originCoreParagraphContentReplacementsTo: _objectProperty(
        object: $originCoreParagraphContentReplacementsTo
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      coreParagraphContentReplacementsTo: _echo(value: $__originCoreParagraphContentReplacementsTo)
        @export(
          as: "coreParagraphContentReplacementsTo"
          type: DICTIONARY
        )
        @remove

Section 5 (defines a set of replacements to perform):

    coreParagraphContent: {
      from: $coreParagraphContentItems,
      to: $coreParagraphContentItems,
    },

Section 6 (defines the regex that will match the property value within the block's HTML):

    @underJSONObjectProperty(
      by: { key: "coreParagraphContent" }
      affectDirectivesUnderPos: [1, 6]
    )
      @underJSONObjectProperty(
        by: { key: "from" }
        affectDirectivesUnderPos: [1, 4],
      )
        @underEachJSONObjectProperty
          @underEachArrayItem(
            passValueOnwardsAs: "value"
          )
            @applyField(
              name: "_sprintf",
              arguments: {
                string: "#(<!-- wp:paragraph .*?-->\\n?<p ?.*?>)%s(</p>\\n?<!-- /wp:paragraph -->)#",
                values: [$value]
              },
              setResultInResponse: true
            )
        @export(
          as: "coreParagraphContentReplacementsFrom",
        )
      @underJSONObjectProperty(
        by: { key: "to" }
      )
        @export(
          as: "coreParagraphContentReplacementsTo",
        )

Section 7 (performs the regex search and replace, replacing the property value inside the block's HTML with its translation):

    @underEachJSONObjectProperty(
      passKeyOnwardsAs: "customPostID"
      affectDirectivesUnderPos: [1, 2]
    )
      @applyField(
        name: "_propertyExistsInJSONObject"
        arguments: {
          object: $coreParagraphContentReplacementsFrom
          by: { key: $customPostID }
        }
        passOnwardsAs: "hasPostID"
      )
      @if(
        condition: $hasPostID
        affectDirectivesUnderPos: [1, 2, 3]
      )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $coreParagraphContentReplacementsFrom,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postCoreParagraphContentReplacementsFrom"
        )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $coreParagraphContentReplacementsTo,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postCoreParagraphContentReplacementsTo"
        )
        @strRegexReplaceMultiple(
          limit: 1,
          searchRegex: $postCoreParagraphContentReplacementsFrom,
          replaceWith: $postCoreParagraphContentReplacementsTo
        )

Let's adapt them for "kadence/testimonial". Notice that, with the exception of Section 6 (more details below), all sections are pretty much a copy/paste and adapt:

Section 1:

      @export(
        as: "originKadenceTestimonialTitleItems"
        type: DICTIONARY
      )
      @export(
        as: "originKadenceTestimonialTitleReplacementsFrom"
        type: DICTIONARY
      )
      @export(
        as: "originKadenceTestimonialTitleReplacementsTo"
        type: DICTIONARY
      )
 
      @export(
        as: "originKadenceTestimonialContentItems"
        type: DICTIONARY
      )
      @export(
        as: "originKadenceTestimonialContentReplacementsFrom"
        type: DICTIONARY
      )
      @export(
        as: "originKadenceTestimonialContentReplacementsTo"
        type: DICTIONARY
      )
 
      @export(
        as: "originKadenceTestimonialOccupationItems"
        type: DICTIONARY
      )
      @export(
        as: "originKadenceTestimonialOccupationReplacementsFrom"
        type: DICTIONARY
      )
      @export(
        as: "originKadenceTestimonialOccupationReplacementsTo"
        type: DICTIONARY
      )

Section 2:

      originKadenceTestimonial: blockFlattenedDataItems(
        filterBy: { include: "kadence/testimonial" }
      )
        @underEachArrayItem
          @underJSONObjectProperty(
            by: { path: "attributes.title" }
            failIfNonExistingKeyOrPath: false
          )
            @export(
              as: "originKadenceTestimonialTitleItems"
              type: DICTIONARY
            )
        @underEachArrayItem
          @underJSONObjectProperty(
            by: { path: "attributes.content" }
            failIfNonExistingKeyOrPath: false
          )
            @export(
              as: "originKadenceTestimonialContentItems"
              type: DICTIONARY
            )
        @underEachArrayItem
          @underJSONObjectProperty(
            by: { path: "attributes.occupation" }
            failIfNonExistingKeyOrPath: false
          )
            @export(
              as: "originKadenceTestimonialOccupationItems"
              type: DICTIONARY
            )

Section 3:

        @export(
          as: "kadenceTestimonialTitleItems"
          type: DICTIONARY
        )
        @export(
          as: "kadenceTestimonialTitleReplacementsFrom"
          type: DICTIONARY
        )
        @export(
          as: "kadenceTestimonialTitleReplacementsTo"
          type: DICTIONARY
        )
 
        @export(
          as: "kadenceTestimonialContentItems"
          type: DICTIONARY
        )
        @export(
          as: "kadenceTestimonialContentReplacementsFrom"
          type: DICTIONARY
        )
        @export(
          as: "kadenceTestimonialContentReplacementsTo"
          type: DICTIONARY
        )
 
        @export(
          as: "kadenceTestimonialOccupationItems"
          type: DICTIONARY
        )
        @export(
          as: "kadenceTestimonialOccupationReplacementsFrom"
          type: DICTIONARY
        )
        @export(
          as: "kadenceTestimonialOccupationReplacementsTo"
          type: DICTIONARY
        )

Section 4:

      originKadenceTestimonialTitleItems: _objectProperty(
        object: $originKadenceTestimonialTitleItems
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialTitleItems: _echo(value: $__originKadenceTestimonialTitleItems)
        @export(
          as: "kadenceTestimonialTitleItems"
          type: DICTIONARY
        )
        @remove
        
      originKadenceTestimonialTitleReplacementsFrom: _objectProperty(
        object: $originKadenceTestimonialTitleReplacementsFrom
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialTitleReplacementsFrom: _echo(value: $__originKadenceTestimonialTitleReplacementsFrom)
        @export(
          as: "kadenceTestimonialTitleReplacementsFrom"
          type: DICTIONARY
        )
        @remove
        
      originKadenceTestimonialTitleReplacementsTo: _objectProperty(
        object: $originKadenceTestimonialTitleReplacementsTo
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialTitleReplacementsTo: _echo(value: $__originKadenceTestimonialTitleReplacementsTo)
        @export(
          as: "kadenceTestimonialTitleReplacementsTo"
          type: DICTIONARY
        )
        @remove
        
 
      originKadenceTestimonialContentItems: _objectProperty(
        object: $originKadenceTestimonialContentItems
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialContentItems: _echo(value: $__originKadenceTestimonialContentItems)
        @export(
          as: "kadenceTestimonialContentItems"
          type: DICTIONARY
        )
        @remove
        
      originKadenceTestimonialContentReplacementsFrom: _objectProperty(
        object: $originKadenceTestimonialContentReplacementsFrom
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialContentReplacementsFrom: _echo(value: $__originKadenceTestimonialContentReplacementsFrom)
        @export(
          as: "kadenceTestimonialContentReplacementsFrom"
          type: DICTIONARY
        )
        @remove
        
      originKadenceTestimonialContentReplacementsTo: _objectProperty(
        object: $originKadenceTestimonialContentReplacementsTo
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialContentReplacementsTo: _echo(value: $__originKadenceTestimonialContentReplacementsTo)
        @export(
          as: "kadenceTestimonialContentReplacementsTo"
          type: DICTIONARY
        )
        @remove
 
 
      originKadenceTestimonialOccupationItems: _objectProperty(
        object: $originKadenceTestimonialOccupationItems
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialOccupationItems: _echo(value: $__originKadenceTestimonialOccupationItems)
        @export(
          as: "kadenceTestimonialOccupationItems"
          type: DICTIONARY
        )
        @remove
        
      originKadenceTestimonialOccupationReplacementsFrom: _objectProperty(
        object: $originKadenceTestimonialOccupationReplacementsFrom
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialOccupationReplacementsFrom: _echo(value: $__originKadenceTestimonialOccupationReplacementsFrom)
        @export(
          as: "kadenceTestimonialOccupationReplacementsFrom"
          type: DICTIONARY
        )
        @remove
        
      originKadenceTestimonialOccupationReplacementsTo: _objectProperty(
        object: $originKadenceTestimonialOccupationReplacementsTo
        by: { key: $__originCustomPostId }
        failIfNonExistingKeyOrPath: false
        valueWhenNonExistingKeyOrPath: []
      )
      kadenceTestimonialOccupationReplacementsTo: _echo(value: $__originKadenceTestimonialOccupationReplacementsTo)
        @export(
          as: "kadenceTestimonialOccupationReplacementsTo"
          type: DICTIONARY
        )
        @remove

Section 5:

    kadenceTestimonialTitle: {
      from: $kadenceTestimonialTitleItems,
      to: $kadenceTestimonialTitleItems,
    },
    kadenceTestimonialContent: {
      from: $kadenceTestimonialContentItems,
      to: $kadenceTestimonialContentItems,
    },
    kadenceTestimonialOccupation: {
      from: $kadenceTestimonialOccupationItems,
      to: $kadenceTestimonialOccupationItems,
    },

Section 6:

Analyzing the HTML for the "kadence/testimonial" block, we must identify how to reach each property that needs to be translated (title, content and occupation):

<!-- wp:kadence/testimonial {[...],\"title\":\"Here is my secret\",\"content\":\"My voice needs to be strong and clear. That's why I drink green tea every morning. Stay away from fried food!\",[...],\"occupation\":\"Opera singer\",[...]} /-->

Then we create the corresponding regex for each property. The regex must match everything that comes before and after the string to translate, like this:

#(match everything before)%s(match everything after)#

In our case, we obtain:

  • title: #(<!-- wp:kadence/testimonial .*?\"title\":\")%s(\".*? /-->)#
  • content: #(<!-- wp:kadence/testimonial .*?\"content\":\")%s(\".*? /-->)#
  • occupation: #(<!-- wp:kadence/testimonial .*?\"occupation\":\")%s(\".*? /-->)#

Finally, we inject the regex on the code below:

    @underJSONObjectProperty(
      by: { key: "kadenceTestimonialTitle" }
      affectDirectivesUnderPos: [1, 6]
    )
      @underJSONObjectProperty(
        by: { key: "from" }
        affectDirectivesUnderPos: [1, 4],
      )
        @underEachJSONObjectProperty
          @underEachArrayItem(
            passValueOnwardsAs: "value"
          )
            @applyField(
              name: "_sprintf",
              arguments: {
                string: "#(<!-- wp:kadence/testimonial .*?\"title\":\")%s(\".*? /-->)#",
                values: [$value]
              },
              setResultInResponse: true
            )
        @export(
          as: "kadenceTestimonialTitleReplacementsFrom",
        )
      @underJSONObjectProperty(
        by: { key: "to" }
      )
        @export(
          as: "kadenceTestimonialTitleReplacementsTo",
        )
 
    @underJSONObjectProperty(
      by: { key: "kadenceTestimonialContent" }
      affectDirectivesUnderPos: [1, 6]
    )
      @underJSONObjectProperty(
        by: { key: "from" }
        affectDirectivesUnderPos: [1, 4],
      )
        @underEachJSONObjectProperty
          @underEachArrayItem(
            passValueOnwardsAs: "value"
          )
            @applyField(
              name: "_sprintf",
              arguments: {
                string: "#(<!-- wp:kadence/testimonial .*?\"content\":\")%s(\".*? /-->)#",
                values: [$value]
              },
              setResultInResponse: true
            )
        @export(
          as: "kadenceTestimonialContentReplacementsFrom",
        )
      @underJSONObjectProperty(
        by: { key: "to" }
      )
        @export(
          as: "kadenceTestimonialContentReplacementsTo",
        )
 
    @underJSONObjectProperty(
      by: { key: "kadenceTestimonialOccupation" }
      affectDirectivesUnderPos: [1, 6]
    )
      @underJSONObjectProperty(
        by: { key: "from" }
        affectDirectivesUnderPos: [1, 4],
      )
        @underEachJSONObjectProperty
          @underEachArrayItem(
            passValueOnwardsAs: "value"
          )
            @applyField(
              name: "_sprintf",
              arguments: {
                string: "#(<!-- wp:kadence/testimonial .*?\"occupation\":\")%s(\".*? /-->)#",
                values: [$value]
              },
              setResultInResponse: true
            )
        @export(
          as: "kadenceTestimonialOccupationReplacementsFrom",
        )
      @underJSONObjectProperty(
        by: { key: "to" }
      )
        @export(
          as: "kadenceTestimonialOccupationReplacementsTo",
        )

Section 7:

    @underEachJSONObjectProperty(
      passKeyOnwardsAs: "customPostID"
      affectDirectivesUnderPos: [1, 2]
    )
      @applyField(
        name: "_propertyExistsInJSONObject"
        arguments: {
          object: $kadenceTestimonialTitleReplacementsFrom
          by: { key: $customPostID }
        }
        passOnwardsAs: "hasPostID"
      )
      @if(
        condition: $hasPostID
        affectDirectivesUnderPos: [1, 2, 3]
      )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $kadenceTestimonialTitleReplacementsFrom,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postKadenceTestimonialTitleReplacementsFrom"
        )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $kadenceTestimonialTitleReplacementsTo,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postKadenceTestimonialTitleReplacementsTo"
        )
        @strRegexReplaceMultiple(
          limit: 1,
          searchRegex: $postKadenceTestimonialTitleReplacementsFrom,
          replaceWith: $postKadenceTestimonialTitleReplacementsTo
        )
 
    @underEachJSONObjectProperty(
      passKeyOnwardsAs: "customPostID"
      affectDirectivesUnderPos: [1, 2]
    )
      @applyField(
        name: "_propertyExistsInJSONObject"
        arguments: {
          object: $kadenceTestimonialContentReplacementsFrom
          by: { key: $customPostID }
        }
        passOnwardsAs: "hasPostID"
      )
      @if(
        condition: $hasPostID
        affectDirectivesUnderPos: [1, 2, 3]
      )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $kadenceTestimonialContentReplacementsFrom,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postKadenceTestimonialContentReplacementsFrom"
        )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $kadenceTestimonialContentReplacementsTo,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postKadenceTestimonialContentReplacementsTo"
        )
        @strRegexReplaceMultiple(
          limit: 1,
          searchRegex: $postKadenceTestimonialContentReplacementsFrom,
          replaceWith: $postKadenceTestimonialContentReplacementsTo
        )
 
    @underEachJSONObjectProperty(
      passKeyOnwardsAs: "customPostID"
      affectDirectivesUnderPos: [1, 2]
    )
      @applyField(
        name: "_propertyExistsInJSONObject"
        arguments: {
          object: $kadenceTestimonialOccupationReplacementsFrom
          by: { key: $customPostID }
        }
        passOnwardsAs: "hasPostID"
      )
      @if(
        condition: $hasPostID
        affectDirectivesUnderPos: [1, 2, 3]
      )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $kadenceTestimonialOccupationReplacementsFrom,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postKadenceTestimonialOccupationReplacementsFrom"
        )
        @applyField(
          name: "_objectProperty",
          arguments: {
            object: $kadenceTestimonialOccupationReplacementsTo,
            by: {
              key: $customPostID
            }
          },
          passOnwardsAs: "postKadenceTestimonialOccupationReplacementsTo"
        )
        @strRegexReplaceMultiple(
          limit: 1,
          searchRegex: $postKadenceTestimonialOccupationReplacementsFrom,
          replaceWith: $postKadenceTestimonialOccupationReplacementsTo
        )

Working on some code editor (such as VSCode), copy the original GraphQL query into a new file (you can format it as GraphQL to have syntax highlighting), add the 7 sections for the new block, and copy the adapted query back into the GraphiQL client.

Then execute the query, and check if the translated post has the block properties translated (by editing the translated post in the WordPress editor and refreshing the page).

Repeat until it all works.

5. Persist the new GraphQL query via PHP code

The content of the Translate custom posts entry is overriden each time the plugin is activated or updated.

To make the changes permanent, you need to provide the final GraphQL query via PHP code.

Use either of these two hooks to inject the GraphQL query:

  • gatompl:persisted_query: Replace the GraphQL query contents
  • gatompl:persisted_query_file: Provide a different GraphQL query file

Using the gatompl:persisted_query hook

Add this PHP logic in your theme or plugin:

add_filter(
  'gatompl:persisted_query',
  function (string $persistedQuery, string $persistedQueryFile): string {
    if (str_ends_with($persistedQueryFile, '/translate-customposts-for-polylang.gql')) {
      return str_replace(
        [
          '##### Insert code for custom blocks (1) #####',
          '##### Insert code for custom blocks (2) #####',
          '##### Insert code for custom blocks (3) #####',
          '##### Insert code for custom blocks (4) #####',
          '##### Insert code for custom blocks (5) #####',
          '##### Insert code for custom blocks (6) #####',
          '##### Insert code for custom blocks (7) #####',
        ],
        [
          <<<GRAPHQL
          ##### Insert code for custom blocks (1) #####
          { your custom GraphQL logic for section 1 }
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (2) #####
          { your custom GraphQL logic for section 2 }
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (3) #####
          { your custom GraphQL logic for section 3 }
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (4) #####
          { your custom GraphQL logic for section 4 }
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (5) #####
          { your custom GraphQL logic for section 5 }
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (6) #####
          { your custom GraphQL logic for section 6 }
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (7) #####
          { your custom GraphQL logic for section 7 }
          GRAPHQL,
        ],
        $persistedQuery
      );
    }
    return $persistedQuery;
  },
  10,
  2
);

Using the gatompl:persisted_query_file hook

Store the GraphQL query under some .gql file in your theme or plugin, such as under file {my-plugin-name}/assets/gatomultilingual/overriding-translate-custom-posts.gql.

Then execute this logic:

add_filter(
  'gatompl:persisted_query_file',
  function (string $persistedQueryFile): string {
    if (str_ends_with($persistedQueryFile, '/translate-customposts-for-polylang.gql')) {
      return __DIR__ . '/assets/gatomultilingual/overriding-translate-custom-posts.gql';
    }
    return $persistedQueryFile;
  }
);

Example - Persisting the query for the testimonial block

For the "kadence/testimonial" block, using the gatompl:persisted_query hook, the PHP logic is this one (notice that inside <<<GRAPHQL, GraphQL variables must be escaped: \$):

add_filter(
  'gatompl:persisted_query',
  function (string $persistedQuery, string $persistedQueryFile): string {
    if (str_ends_with($persistedQueryFile, '/translate-customposts-for-polylang.gql')) {
      return str_replace(
        [
          '##### Insert code for custom blocks (1) #####',
          '##### Insert code for custom blocks (2) #####',
          '##### Insert code for custom blocks (3) #####',
          '##### Insert code for custom blocks (4) #####',
          '##### Insert code for custom blocks (5) #####',
          '##### Insert code for custom blocks (6) #####',
          '##### Insert code for custom blocks (7) #####',
        ],
        [
          <<<GRAPHQL
          ##### Insert code for custom blocks (1) #####
                @export(
                  as: "originKadenceTestimonialTitleItems"
                  type: DICTIONARY
                )
                @export(
                  as: "originKadenceTestimonialTitleReplacementsFrom"
                  type: DICTIONARY
                )
                @export(
                  as: "originKadenceTestimonialTitleReplacementsTo"
                  type: DICTIONARY
                )
 
                @export(
                  as: "originKadenceTestimonialContentItems"
                  type: DICTIONARY
                )
                @export(
                  as: "originKadenceTestimonialContentReplacementsFrom"
                  type: DICTIONARY
                )
                @export(
                  as: "originKadenceTestimonialContentReplacementsTo"
                  type: DICTIONARY
                )
 
                @export(
                  as: "originKadenceTestimonialOccupationItems"
                  type: DICTIONARY
                )
                @export(
                  as: "originKadenceTestimonialOccupationReplacementsFrom"
                  type: DICTIONARY
                )
                @export(
                  as: "originKadenceTestimonialOccupationReplacementsTo"
                  type: DICTIONARY
                )
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (2) #####
                originKadenceTestimonial: blockFlattenedDataItems(
                  filterBy: { include: "kadence/testimonial" }
                )
                  @underEachArrayItem
                    @underJSONObjectProperty(
                      by: { path: "attributes.title" }
                      failIfNonExistingKeyOrPath: false
                    )
                      @export(
                        as: "originKadenceTestimonialTitleItems"
                        type: DICTIONARY
                      )
                  @underEachArrayItem
                    @underJSONObjectProperty(
                      by: { path: "attributes.content" }
                      failIfNonExistingKeyOrPath: false
                    )
                      @export(
                        as: "originKadenceTestimonialContentItems"
                        type: DICTIONARY
                      )
                  @underEachArrayItem
                    @underJSONObjectProperty(
                      by: { path: "attributes.occupation" }
                      failIfNonExistingKeyOrPath: false
                    )
                      @export(
                        as: "originKadenceTestimonialOccupationItems"
                        type: DICTIONARY
                      )
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (3) #####
                @export(
                  as: "kadenceTestimonialTitleItems"
                  type: DICTIONARY
                )
                @export(
                  as: "kadenceTestimonialTitleReplacementsFrom"
                  type: DICTIONARY
                )
                @export(
                  as: "kadenceTestimonialTitleReplacementsTo"
                  type: DICTIONARY
                )
 
                @export(
                  as: "kadenceTestimonialContentItems"
                  type: DICTIONARY
                )
                @export(
                  as: "kadenceTestimonialContentReplacementsFrom"
                  type: DICTIONARY
                )
                @export(
                  as: "kadenceTestimonialContentReplacementsTo"
                  type: DICTIONARY
                )
 
                @export(
                  as: "kadenceTestimonialOccupationItems"
                  type: DICTIONARY
                )
                @export(
                  as: "kadenceTestimonialOccupationReplacementsFrom"
                  type: DICTIONARY
                )
                @export(
                  as: "kadenceTestimonialOccupationReplacementsTo"
                  type: DICTIONARY
                )
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (4) #####
                originKadenceTestimonialTitleItems: _objectProperty(
                  object: \$originKadenceTestimonialTitleItems
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialTitleItems: _echo(value: \$__originKadenceTestimonialTitleItems)
                  @export(
                    as: "kadenceTestimonialTitleItems"
                    type: DICTIONARY
                  )
                  @remove
                  
                originKadenceTestimonialTitleReplacementsFrom: _objectProperty(
                  object: \$originKadenceTestimonialTitleReplacementsFrom
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialTitleReplacementsFrom: _echo(value: \$__originKadenceTestimonialTitleReplacementsFrom)
                  @export(
                    as: "kadenceTestimonialTitleReplacementsFrom"
                    type: DICTIONARY
                  )
                  @remove
                  
                originKadenceTestimonialTitleReplacementsTo: _objectProperty(
                  object: \$originKadenceTestimonialTitleReplacementsTo
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialTitleReplacementsTo: _echo(value: \$__originKadenceTestimonialTitleReplacementsTo)
                  @export(
                    as: "kadenceTestimonialTitleReplacementsTo"
                    type: DICTIONARY
                  )
                  @remove
                  
 
                originKadenceTestimonialContentItems: _objectProperty(
                  object: \$originKadenceTestimonialContentItems
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialContentItems: _echo(value: \$__originKadenceTestimonialContentItems)
                  @export(
                    as: "kadenceTestimonialContentItems"
                    type: DICTIONARY
                  )
                  @remove
                  
                originKadenceTestimonialContentReplacementsFrom: _objectProperty(
                  object: \$originKadenceTestimonialContentReplacementsFrom
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialContentReplacementsFrom: _echo(value: \$__originKadenceTestimonialContentReplacementsFrom)
                  @export(
                    as: "kadenceTestimonialContentReplacementsFrom"
                    type: DICTIONARY
                  )
                  @remove
                  
                originKadenceTestimonialContentReplacementsTo: _objectProperty(
                  object: \$originKadenceTestimonialContentReplacementsTo
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialContentReplacementsTo: _echo(value: \$__originKadenceTestimonialContentReplacementsTo)
                  @export(
                    as: "kadenceTestimonialContentReplacementsTo"
                    type: DICTIONARY
                  )
                  @remove
 
 
                originKadenceTestimonialOccupationItems: _objectProperty(
                  object: \$originKadenceTestimonialOccupationItems
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialOccupationItems: _echo(value: \$__originKadenceTestimonialOccupationItems)
                  @export(
                    as: "kadenceTestimonialOccupationItems"
                    type: DICTIONARY
                  )
                  @remove
                  
                originKadenceTestimonialOccupationReplacementsFrom: _objectProperty(
                  object: \$originKadenceTestimonialOccupationReplacementsFrom
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialOccupationReplacementsFrom: _echo(value: \$__originKadenceTestimonialOccupationReplacementsFrom)
                  @export(
                    as: "kadenceTestimonialOccupationReplacementsFrom"
                    type: DICTIONARY
                  )
                  @remove
                  
                originKadenceTestimonialOccupationReplacementsTo: _objectProperty(
                  object: \$originKadenceTestimonialOccupationReplacementsTo
                  by: { key: \$__originCustomPostId }
                  failIfNonExistingKeyOrPath: false
                  valueWhenNonExistingKeyOrPath: []
                )
                kadenceTestimonialOccupationReplacementsTo: _echo(value: \$__originKadenceTestimonialOccupationReplacementsTo)
                  @export(
                    as: "kadenceTestimonialOccupationReplacementsTo"
                    type: DICTIONARY
                  )
                  @remove
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (5) #####
              kadenceTestimonialTitle: {
                from: \$kadenceTestimonialTitleItems,
                to: \$kadenceTestimonialTitleItems,
              },
              kadenceTestimonialContent: {
                from: \$kadenceTestimonialContentItems,
                to: \$kadenceTestimonialContentItems,
              },
              kadenceTestimonialOccupation: {
                from: \$kadenceTestimonialOccupationItems,
                to: \$kadenceTestimonialOccupationItems,
              },
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (6) #####
                @underJSONObjectProperty(
                  by: { key: "kadenceTestimonialTitle" }
                  affectDirectivesUnderPos: [1, 6]
                )
                  @underJSONObjectProperty(
                    by: { key: "from" }
                    affectDirectivesUnderPos: [1, 4],
                  )
                    @underEachJSONObjectProperty
                      @underEachArrayItem(
                        passValueOnwardsAs: "value"
                      )
                        @applyField(
                          name: "_sprintf",
                          arguments: {
                            string: "#(<!-- wp:kadence/testimonial .*?\"title\":\")%s(\".*? /-->)#",
                            values: [\$value]
                          },
                          setResultInResponse: true
                        )
                    @export(
                      as: "kadenceTestimonialTitleReplacementsFrom",
                    )
                  @underJSONObjectProperty(
                    by: { key: "to" }
                  )
                    @export(
                      as: "kadenceTestimonialTitleReplacementsTo",
                    )
 
                @underJSONObjectProperty(
                  by: { key: "kadenceTestimonialContent" }
                  affectDirectivesUnderPos: [1, 6]
                )
                  @underJSONObjectProperty(
                    by: { key: "from" }
                    affectDirectivesUnderPos: [1, 4],
                  )
                    @underEachJSONObjectProperty
                      @underEachArrayItem(
                        passValueOnwardsAs: "value"
                      )
                        @applyField(
                          name: "_sprintf",
                          arguments: {
                            string: "#(<!-- wp:kadence/testimonial .*?\"content\":\")%s(\".*? /-->)#",
                            values: [\$value]
                          },
                          setResultInResponse: true
                        )
                    @export(
                      as: "kadenceTestimonialContentReplacementsFrom",
                    )
                  @underJSONObjectProperty(
                    by: { key: "to" }
                  )
                    @export(
                      as: "kadenceTestimonialContentReplacementsTo",
                    )
 
                @underJSONObjectProperty(
                  by: { key: "kadenceTestimonialOccupation" }
                  affectDirectivesUnderPos: [1, 6]
                )
                  @underJSONObjectProperty(
                    by: { key: "from" }
                    affectDirectivesUnderPos: [1, 4],
                  )
                    @underEachJSONObjectProperty
                      @underEachArrayItem(
                        passValueOnwardsAs: "value"
                      )
                        @applyField(
                          name: "_sprintf",
                          arguments: {
                            string: "#(<!-- wp:kadence/testimonial .*?\"occupation\":\")%s(\".*? /-->)#",
                            values: [\$value]
                          },
                          setResultInResponse: true
                        )
                    @export(
                      as: "kadenceTestimonialOccupationReplacementsFrom",
                    )
                  @underJSONObjectProperty(
                    by: { key: "to" }
                  )
                    @export(
                      as: "kadenceTestimonialOccupationReplacementsTo",
                    )
          GRAPHQL,
 
          <<<GRAPHQL
          ##### Insert code for custom blocks (7) #####
                @underEachJSONObjectProperty(
                  passKeyOnwardsAs: "customPostID"
                  affectDirectivesUnderPos: [1, 2]
                )
                  @applyField(
                    name: "_propertyExistsInJSONObject"
                    arguments: {
                      object: \$kadenceTestimonialTitleReplacementsFrom
                      by: { key: \$customPostID }
                    }
                    passOnwardsAs: "hasPostID"
                  )
                  @if(
                    condition: \$hasPostID
                    affectDirectivesUnderPos: [1, 2, 3]
                  )
                    @applyField(
                      name: "_objectProperty",
                      arguments: {
                        object: \$kadenceTestimonialTitleReplacementsFrom,
                        by: {
                          key: \$customPostID
                        }
                      },
                      passOnwardsAs: "postKadenceTestimonialTitleReplacementsFrom"
                    )
                    @applyField(
                      name: "_objectProperty",
                      arguments: {
                        object: \$kadenceTestimonialTitleReplacementsTo,
                        by: {
                          key: \$customPostID
                        }
                      },
                      passOnwardsAs: "postKadenceTestimonialTitleReplacementsTo"
                    )
                    @strRegexReplaceMultiple(
                      limit: 1,
                      searchRegex: \$postKadenceTestimonialTitleReplacementsFrom,
                      replaceWith: \$postKadenceTestimonialTitleReplacementsTo
                    )
 
                @underEachJSONObjectProperty(
                  passKeyOnwardsAs: "customPostID"
                  affectDirectivesUnderPos: [1, 2]
                )
                  @applyField(
                    name: "_propertyExistsInJSONObject"
                    arguments: {
                      object: \$kadenceTestimonialContentReplacementsFrom
                      by: { key: \$customPostID }
                    }
                    passOnwardsAs: "hasPostID"
                  )
                  @if(
                    condition: \$hasPostID
                    affectDirectivesUnderPos: [1, 2, 3]
                  )
                    @applyField(
                      name: "_objectProperty",
                      arguments: {
                        object: \$kadenceTestimonialContentReplacementsFrom,
                        by: {
                          key: \$customPostID
                        }
                      },
                      passOnwardsAs: "postKadenceTestimonialContentReplacementsFrom"
                    )
                    @applyField(
                      name: "_objectProperty",
                      arguments: {
                        object: \$kadenceTestimonialContentReplacementsTo,
                        by: {
                          key: \$customPostID
                        }
                      },
                      passOnwardsAs: "postKadenceTestimonialContentReplacementsTo"
                    )
                    @strRegexReplaceMultiple(
                      limit: 1,
                      searchRegex: \$postKadenceTestimonialContentReplacementsFrom,
                      replaceWith: \$postKadenceTestimonialContentReplacementsTo
                    )
 
                @underEachJSONObjectProperty(
                  passKeyOnwardsAs: "customPostID"
                  affectDirectivesUnderPos: [1, 2]
                )
                  @applyField(
                    name: "_propertyExistsInJSONObject"
                    arguments: {
                      object: \$kadenceTestimonialOccupationReplacementsFrom
                      by: { key: \$customPostID }
                    }
                    passOnwardsAs: "hasPostID"
                  )
                  @if(
                    condition: \$hasPostID
                    affectDirectivesUnderPos: [1, 2, 3]
                  )
                    @applyField(
                      name: "_objectProperty",
                      arguments: {
                        object: \$kadenceTestimonialOccupationReplacementsFrom,
                        by: {
                          key: \$customPostID
                        }
                      },
                      passOnwardsAs: "postKadenceTestimonialOccupationReplacementsFrom"
                    )
                    @applyField(
                      name: "_objectProperty",
                      arguments: {
                        object: \$kadenceTestimonialOccupationReplacementsTo,
                        by: {
                          key: \$customPostID
                        }
                      },
                      passOnwardsAs: "postKadenceTestimonialOccupationReplacementsTo"
                    )
                    @strRegexReplaceMultiple(
                      limit: 1,
                      searchRegex: \$postKadenceTestimonialOccupationReplacementsFrom,
                      replaceWith: \$postKadenceTestimonialOccupationReplacementsTo
                    )
          GRAPHQL,
        ],
        $persistedQuery
      );
    }
    return $persistedQuery;
  },
  10,
  2
);

6. Regenerate the query in the DB

The last step is to disable and re-enable the Gato Multilingual for Polylang plugin.

This will regenerate the Translate custom posts entry, with your custom GraphQL query injected via hooks.

7. Your block will now be translated

When triggering the translation again, all properties in the block (as with "kadence/testimonial") will also be translated:

The testimonail block is now translated
The testimonail block is now translated