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](/assets/plugins/gatomultilingual-polylang/guides/post-with-testimonial-block.webp)
When executing the translation, that block will remain in its original language:
![Editing the translated post with a testimonials block](/assets/plugins/gatomultilingual-polylang/guides/translation-post-with-testimonial-block.webp)
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](/assets/plugins/gatomultilingual-polylang/guides/enable-advanced-mode.webp)
This will make available the plugin's Queries CPT:
![Queries CPT enabled](/assets/plugins/gatomultilingual-polylang/guides/queries-cpt-enabled.webp)
2. Edit the Gato GraphQL query
On the Queries list page, edit the Translate custom posts entry:
![Translate custom posts entry](/assets/plugins/gatomultilingual-polylang/guides/edit-translate-custom-posts-in-queries-list.webp)
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](/assets/plugins/gatomultilingual-polylang/guides/edit-translate-custom-posts-entry.webp)
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:
- Execute some automatic translation (eg: publish a new post)
- 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](/assets/plugins/gatomultilingual-polylang/guides/identifying-block-properties-to-translate.png)
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](/assets/plugins/gatomultilingual-polylang/guides/identifying-properties-to-translate-in-block-html.png)
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 contentsgatompl:persisted_query_file
: Provide a different GraphQL query file
gatompl:persisted_query
hook Using the
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
);
gatompl:persisted_query_file
hook Using the
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](/assets/plugins/gatomultilingual-polylang/guides/adapted-translation-post-with-testimonial-block.webp)