How to translate content without giving access to 3rd-party translators to the backend of your WordPress site, using InstaWP and Google Translate
Translate content using Google Translate, synchronize it to a provisional site to have 3rd-party translators fix the translation, and then synchronize the content back to the site.


Integrations
Let's say your website is a WordPress multisite, where every site is a translation to a different language, and you employ 3rd-party translators to translate the content.
At the same time, you'd like to avoid giving access to your WordPress site's backend to external workers.
You can use InstaWP together with Gato GraphQL, plus the integration with Google Translate, to provide for this use case.
This video demonstrates the translation workflow:
The workflow involves three websites:
- The origin site (
content-staging.instawp.xyz
), containing the source of content in English - The provisional site (
translation-es.instawp.xyz
), created on InstaWP, for the translator to fix the translation - The translated site (
content-es.instawp.xyz
), containing the content in Spanish
Let's see how it works
Step 1: Google Translate your post in the origin site
Publish the blog post in your origin WordPress site, and translate it to the desired language using Gato GraphQL + Google Translate.
Demo How to translate posts including blocks with the Google Translate API describes this use case with more details.
Create a persisted query containing the following GraphQL query, and give it title Translate post with blocks:
query InitializeEmptyVariables {
emptyArray: _echo(value: [])
@export(as: "coreHeadingContentItems")
@export(as: "coreHeadingContentReplacementsFrom")
@export(as: "coreHeadingContentReplacementsTo")
@export(as: "coreParagraphContentItems")
@export(as: "coreParagraphContentReplacementsFrom")
@export(as: "coreParagraphContentReplacementsTo")
@export(as: "coreImageAltItems")
@export(as: "coreImageAltReplacementsFrom")
@export(as: "coreImageAltReplacementsTo")
@export(as: "coreImageCaptionItems")
@export(as: "coreImageCaptionReplacementsFrom")
@export(as: "coreImageCaptionReplacementsTo")
@export(as: "coreButtonTextItems")
@export(as: "coreButtonTextReplacementsFrom")
@export(as: "coreButtonTextReplacementsTo")
@export(as: "coreTableCaptionItems")
@export(as: "coreTableCaptionReplacementsFrom")
@export(as: "coreTableCaptionReplacementsTo")
@export(as: "coreTableBodyCellsContentItems")
@export(as: "coreTableBodyCellsContentReplacementsFrom")
@export(as: "coreTableBodyCellsContentReplacementsTo")
@export(as: "coreListItemContentItems")
@export(as: "coreListItemContentReplacementsFrom")
@export(as: "coreListItemContentReplacementsTo")
@export(as: "coreCoverAltItems")
@export(as: "coreCoverAltReplacementsFrom")
@export(as: "coreCoverAltReplacementsTo")
@export(as: "coreMediaTextAltItems")
@export(as: "coreMediaTextAltReplacementsFrom")
@export(as: "coreMediaTextAltReplacementsTo")
@export(as: "coreVerseContentItems")
@export(as: "coreVerseContentReplacementsFrom")
@export(as: "coreVerseContentReplacementsTo")
@export(as: "coreQuoteCitationItems")
@export(as: "coreQuoteCitationReplacementsFrom")
@export(as: "coreQuoteCitationReplacementsTo")
@export(as: "corePullquoteCitationItems")
@export(as: "corePullquoteCitationReplacementsFrom")
@export(as: "corePullquoteCitationReplacementsTo")
@export(as: "corePullquoteValueItems")
@export(as: "corePullquoteValueReplacementsFrom")
@export(as: "corePullquoteValueReplacementsTo")
@export(as: "coreAudioCaptionItems")
@export(as: "coreAudioCaptionReplacementsFrom")
@export(as: "coreAudioCaptionReplacementsTo")
@export(as: "coreVideoCaptionItems")
@export(as: "coreVideoCaptionReplacementsFrom")
@export(as: "coreVideoCaptionReplacementsTo")
@export(as: "corePreformattedContentItems")
@export(as: "corePreformattedContentReplacementsFrom")
@export(as: "corePreformattedContentReplacementsTo")
@export(as: "coreEmbedCaptionItems")
@export(as: "coreEmbedCaptionReplacementsFrom")
@export(as: "coreEmbedCaptionReplacementsTo")
@remove
}
query FetchData($postID: ID!)
@configureWarningsOnExportingDuplicateVariable(enabled: false)
@depends(on: "InitializeEmptyVariables")
{
post(by: { id: $postID } ) {
id
title
@export(as: "title")
rawContent
@export(as: "rawContent")
coreHeading: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreHeadingContentItems"
)
coreParagraph: blockFlattenedDataItems(
filterBy: { include: "core/paragraph" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreParagraphContentItems"
)
coreImage: blockFlattenedDataItems(
filterBy: { include: "core/image" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { key: "attributes" }
affectDirectivesUnderPos: [1, 3]
)
@underJSONObjectProperty(
by: { key: "alt" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreImageAltItems"
)
@underJSONObjectProperty(
by: { key: "caption" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreImageCaptionItems"
)
coreButton: blockFlattenedDataItems(
filterBy: { include: "core/button" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.text" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreButtonTextItems"
)
coreTable: blockFlattenedDataItems(
filterBy: { include: "core/table" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { key: "attributes" }
affectDirectivesUnderPos: [1, 3]
)
@underJSONObjectProperty(
by: { key: "caption" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreTableCaptionItems"
)
@underJSONObjectProperty(
by: { key: "body" }
failIfNonExistingKeyOrPath: false
)
@underEachArrayItem
@underJSONObjectProperty(
by: { key: "cells" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { key: "content" }
)
@export(
as: "coreTableBodyCellsContentItems"
)
coreListItem: blockFlattenedDataItems(
filterBy: { include: "core/list-item" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreListItemContentItems"
)
coreCover: blockFlattenedDataItems(
filterBy: { include: "core/cover" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.alt" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreCoverAltItems"
)
coreMediaText: blockFlattenedDataItems(
filterBy: { include: "core/media-text" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.mediaAlt" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreMediaTextAltItems"
)
coreVerse: blockFlattenedDataItems(
filterBy: { include: "core/verse" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreVerseContentItems"
)
coreQuote: blockFlattenedDataItems(
filterBy: { include: "core/quote" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.citation" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreQuoteCitationItems"
)
corePullquote: blockFlattenedDataItems(
filterBy: { include: "core/pullquote" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { key: "attributes" }
affectDirectivesUnderPos: [1, 3]
)
@underJSONObjectProperty(
by: { key: "citation" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "corePullquoteCitationItems"
)
@underJSONObjectProperty(
by: { key: "value" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "corePullquoteValueItems"
)
coreAudio: blockFlattenedDataItems(
filterBy: { include: "core/audio" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.caption" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreAudioCaptionItems"
)
coreVideo: blockFlattenedDataItems(
filterBy: { include: "core/video" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.caption" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreVideoCaptionItems"
)
corePreformatted: blockFlattenedDataItems(
filterBy: { include: "core/preformatted" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "corePreformattedContentItems"
)
coreEmbed: blockFlattenedDataItems(
filterBy: { include: "core/embed" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.caption" }
failIfNonExistingKeyOrPath: false
)
@export(
as: "coreEmbedCaptionItems"
)
}
}
query TransformData(
$translateToLang: String!
)
@depends(on: "FetchData")
{
transformations: _echo(value: {
meta: {
from: [""],
to: [$title],
}
coreHeadingContent: {
from: $coreHeadingContentItems,
to: $coreHeadingContentItems,
},
coreParagraphContent: {
from: $coreParagraphContentItems,
to: $coreParagraphContentItems,
},
coreImageAlt: {
from: $coreImageAltItems,
to: $coreImageAltItems,
},
coreImageCaption: {
from: $coreImageCaptionItems,
to: $coreImageCaptionItems,
},
coreButtonText: {
from: $coreButtonTextItems
to: $coreButtonTextItems
},
coreTableCaption: {
from: $coreTableCaptionItems,
to: $coreTableCaptionItems,
},
coreTableBodyCellsContent: {
from: $coreTableBodyCellsContentItems,
to: $coreTableBodyCellsContentItems,
},
coreListItemContent: {
from: $coreListItemContentItems,
to: $coreListItemContentItems,
},
coreCoverAlt: {
from: $coreCoverAltItems,
to: $coreCoverAltItems,
},
coreMediaTextAlt: {
from: $coreMediaTextAltItems,
to: $coreMediaTextAltItems,
},
coreVerseContent: {
from: $coreVerseContentItems,
to: $coreVerseContentItems,
},
coreQuoteCitation: {
from: $coreQuoteCitationItems,
to: $coreQuoteCitationItems,
},
corePullquoteCitation: {
from: $corePullquoteCitationItems,
to: $corePullquoteCitationItems,
},
corePullquoteValue: {
from: $corePullquoteValueItems,
to: $corePullquoteValueItems,
},
coreAudioCaption: {
from: $coreAudioCaptionItems,
to: $coreAudioCaptionItems,
},
coreVideoCaption: {
from: $coreVideoCaptionItems,
to: $coreVideoCaptionItems,
},
corePreformattedContent: {
from: $corePreformattedContentItems,
to: $corePreformattedContentItems,
},
coreEmbedCaption: {
from: $coreEmbedCaptionItems,
to: $coreEmbedCaptionItems,
},
})
@underEachJSONObjectProperty
@underJSONObjectProperty(by: { key: "to" })
@underEachArrayItem
@strTranslate(to: $translateToLang)
@export(as: "transformations")
}
query EscapeRegexStrings
@depends(on: "TransformData")
{
escapedRegexStrings: _echo(value: $transformations)
@underEachJSONObjectProperty
@underJSONObjectProperty(by: { key: "from" })
@underEachArrayItem
@strQuoteRegex
@underEachJSONObjectProperty(
filter: {
by: {
excludeKeys: "meta"
}
}
)
@underJSONObjectProperty(
by: { key: "to" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem
@strRegexReplace(
searchRegex: "#\\$(\\d+)#",
replaceWith: "\\\\\\$1"
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "$1%s$2",
values: [$value]
},
setResultInResponse: true
)
@export(as: "escapedRegexTransformations")
}
query CreateRegexReplacements
@depends(on: "EscapeRegexStrings")
{
regexReplacements: _echo(value: $escapedRegexTransformations)
@underJSONObjectProperty(
by: { key: "coreHeadingContent" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:heading .*?-->\\n?<h[1-6] ?.*?>)%s(</h[1-6]>\\n?<!-- /wp:heading -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreHeadingContentReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreHeadingContentReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreParagraphContent" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@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",
)
@underJSONObjectProperty(
by: { key: "coreImageAlt" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:image .*?-->\\n?.*<img .*?alt=\\\")%s(\\\".*>.*\\n?<!-- /wp:image -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreImageAltReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreImageAltReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreImageCaption" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:image .*?-->\\n?.*<figcaption ?.*?>)%s(</figcaption>.*\\n?<!-- /wp:image -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreImageCaptionReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreImageCaptionReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreButtonText" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:button .*?-->\\n?.*<a ?.*?>)%s(</a>.*\\n?<!-- /wp:button -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreButtonTextReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreButtonTextReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreTableCaption" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:table .*?-->\\n?.*<figcaption ?.*?>.*)%s(.*</figcaption>.*\\n?<!-- /wp:table -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreTableCaptionReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreTableCaptionReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreTableBodyCellsContent" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:table .*?-->\\n?.*<table ?.*?>.*)%s(.*</table>.*\\n?<!-- /wp:table -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreTableBodyCellsContentReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreTableBodyCellsContentReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreListItemContent" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:list-item .*?-->\\n?<li ?.*?>)%s(</li>\\n?<!-- /wp:list-item -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreListItemContentReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreListItemContentReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreCoverAlt" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:cover .*?-->\\n?.*<img .*?alt=\\\")%s(\\\".*>.*\\n?<!-- /wp:cover -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreCoverAltReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreCoverAltReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreMediaTextAlt" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:media-text .*?-->\\n?<div .*><figure .*><img .*?alt=\\\")%s(\\\")#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreMediaTextAltReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreMediaTextAltReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreVerseContent" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:verse .*?-->\\n?<pre ?.*?>)%s(</pre>\\n?<!-- /wp:verse -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreVerseContentReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreVerseContentReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreQuoteCitation" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:quote .*?-->\\n?<blockquote ?.*?>.*<cite ?.*?>)%s(</cite></blockquote>\\n?<!-- /wp:quote -->)#s",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreQuoteCitationReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreQuoteCitationReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "corePullquoteCitation" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:pullquote .*?-->\\n?<figure ?.*?><blockquote ?.*?><p ?.*?>.*</p><cite ?.*?>)%s(</cite></blockquote></figure>\\n?<!-- /wp:pullquote -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "corePullquoteCitationReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "corePullquoteCitationReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "corePullquoteValue" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:pullquote .*?-->\\n?<figure ?.*?><blockquote ?.*?><p ?.*?>)%s(</p>(?:<cite ?.*?>.*</cite>)?</blockquote></figure>\\n?<!-- /wp:pullquote -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "corePullquoteValueReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "corePullquoteValueReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreAudioCaption" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:audio .*?-->\\n?<figure ?.*?><audio ?.*?>.*</audio><figcaption ?.*?>)%s(</figcaption></figure>\\n?<!-- /wp:audio -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreAudioCaptionReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreAudioCaptionReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreVideoCaption" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:video .*?-->\\n?<figure ?.*?><video ?.*?>.*</video><figcaption ?.*?>)%s(</figcaption></figure>\\n?<!-- /wp:video -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreVideoCaptionReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreVideoCaptionReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "corePreformattedContent" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:preformatted .*?-->\\n?<pre ?.*?>)%s(</pre>\\n?<!-- /wp:preformatted -->)#",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "corePreformattedContentReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "corePreformattedContentReplacementsTo",
)
@underJSONObjectProperty(
by: { key: "coreEmbedCaption" }
affectDirectivesUnderPos: [1, 5]
)
@underJSONObjectProperty(
by: { key: "from" }
affectDirectivesUnderPos: [1, 3],
)
@underEachArrayItem(
passValueOnwardsAs: "value"
)
@applyField(
name: "_sprintf",
arguments: {
string: "#(<!-- wp:embed .*?-->\\n?<figure ?.*?><div ?.*?>.*</div><figcaption ?.*?>)%s(</figcaption></figure>\\n?<!-- /wp:embed -->)#s",
values: [$value]
},
setResultInResponse: true
)
@export(
as: "coreEmbedCaptionReplacementsFrom",
)
@underJSONObjectProperty(
by: { key: "to" }
)
@export(
as: "coreEmbedCaptionReplacementsTo",
)
}
query ExecuteRegexReplacements
@depends(on: "CreateRegexReplacements")
{
transformedRawContent: _echo(value: $rawContent)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreHeadingContentReplacementsFrom,
replaceWith: $coreHeadingContentReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreParagraphContentReplacementsFrom,
replaceWith: $coreParagraphContentReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreImageAltReplacementsFrom,
replaceWith: $coreImageAltReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreImageCaptionReplacementsFrom,
replaceWith: $coreImageCaptionReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreButtonTextReplacementsFrom,
replaceWith: $coreButtonTextReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreTableCaptionReplacementsFrom,
replaceWith: $coreTableCaptionReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreTableBodyCellsContentReplacementsFrom,
replaceWith: $coreTableBodyCellsContentReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreListItemContentReplacementsFrom,
replaceWith: $coreListItemContentReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreCoverAltReplacementsFrom,
replaceWith: $coreCoverAltReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreMediaTextAltReplacementsFrom,
replaceWith: $coreMediaTextAltReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreVerseContentReplacementsFrom,
replaceWith: $coreVerseContentReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreQuoteCitationReplacementsFrom,
replaceWith: $coreQuoteCitationReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $corePullquoteCitationReplacementsFrom,
replaceWith: $corePullquoteCitationReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $corePullquoteValueReplacementsFrom,
replaceWith: $corePullquoteValueReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreAudioCaptionReplacementsFrom,
replaceWith: $coreAudioCaptionReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreVideoCaptionReplacementsFrom,
replaceWith: $coreVideoCaptionReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $corePreformattedContentReplacementsFrom,
replaceWith: $corePreformattedContentReplacementsTo
)
@strRegexReplaceMultiple(
limit: 1,
searchRegex: $coreEmbedCaptionReplacementsFrom,
replaceWith: $coreEmbedCaptionReplacementsTo
)
@export(as: "transformedRawContent")
}
query PrepareMetaReplacements
@depends(on: "TransformData")
{
transformedMeta: _objectProperty(
object: $transformations,
by: { path: "meta.to" }
)
@underArrayItem(index: 0)
@export(as: "transformedTitle")
}
mutation TranslatePost($postID: ID!)
@depends(on: [
"ExecuteRegexReplacements",
"PrepareMetaReplacements"
]) {
updatePost(input: {
id: $postID,
title: $transformedTitle,
contentAs: {
html: $transformedRawContent
}
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
id
title
rawContent
}
}
}
Execute the persisted query providing the JSON dictionary with GraphQL variables:
postID
: The ID of the post to translatetranslateToLang
: the language code to translate to
For instance:
{
"postID": 40,
"translateToLang": "es"
}
The origin site will now contain the Google-Translated post as a duplicate from the original, with draft
status. We need now to move it to the provisional site.
Step 2: Copy the post from the origin site to the provisional site
Create a persisted query containing the following GraphQL query, and give it title Export post to another WordPress site:
query CheckHasPost($postSlug: String!)
{
post(by: { slug: $postSlug }, status: any)
@fail(
message: "There is no post in the upstream site with the provided slug"
data: {
slug: $postSlug
}
)
{
rawTitle
@export(as: "postTitle")
rawContent
@export(as: "postContent")
}
isMissingPostInUpstream: _isNull(value: $__post)
@export(as: "isMissingPostInUpstream")
}
query ExportDownstreamGraphQLQuery
@depends(on: "CheckHasPost")
@skip(if: $isMissingPostInUpstream)
{
query: _echo(value: """
mutation LoginUserAndUpdatePost(
$update: Boolean! = false
$username: String!
$userPassword: String!
$postSlug: String!
$postTitle: String!
$postContent: String!
) {
loginUser(by: {
credentials: {
usernameOrEmail: $username,
password: $userPassword
}
}) {
userID
}
post(by: { slug: $postSlug }, status: any)
@include(if: $update)
{
update(input: {
title: $postTitle,
contentAs: { html: $postContent },
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
title
slug
content
status
}
}
}
createPost(input: {
title: $postTitle,
slug: $postSlug,
contentAs: { html: $postContent },
status: draft
})
@skip(if: $update)
{
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
title
slug
content
status
}
}
}
"""
)
@export(as: "query")
@remove
}
query ExportPostToWPSite(
$downstreamServerGraphQLEndpointURL: String!
$update: Boolean! = false
$username: String!
$userPassword: String!
$postSlug: String!
)
@depends(on: "ExportDownstreamGraphQLQuery")
@skip(if: $isMissingPostInUpstream)
{
_sendGraphQLHTTPRequest(
input: {
endpoint: $downstreamServerGraphQLEndpointURL,
query: $query,
variables: [
{
name: "update",
value: $update
},
{
name: "username",
value: $username
},
{
name: "userPassword",
value: $userPassword
},
{
name: "postSlug",
value: $postSlug
},
{
name: "postTitle",
value: $postTitle
},
{
name: "postContent",
value: $postContent
}
]
}
)
}
This persisted query will export the post from the origin site to the provisional site. Execute it, providing the JSON dictionary with the needed GraphQL variables:
{
"downstreamServerGraphQLEndpointURL": "{ Gato GraphQL public endpoint on provisional site, with the 'nested mutations' feature enabled. Eg: https://translation-es.instawp.xyz/graphql/nested-mutations }",
"username": "{ Username on the provisional site, with `create_posts` capability }",
"userPassword": "{ Password for that username }",
"postSlug": "{ The slug of the post to sync }"
}
The provisional site will now contain the Google-Translated post.
Step 3: Have the 3rd-party workers fix the translation
You can now ask the translator to log-in to the provisional site and fix the translation, directly within the WordPress editor.

When the translation is completed, we must synchronize it to the translated site.
Step 4: Copy the post from the provisional site to the translated site
Log-in to the translated site and create a persisted query containing the following GraphQL query, with title Import post from another WordPress site:
query InitializeDynamicVariables
@configureWarningsOnExportingDuplicateVariable(enabled: false)
{
initVariablesWithFalse: _echo(value: false)
@export(as: "requestProducedErrors")
@export(as: "responseHasErrors")
@export(as: "postIsMissing")
@export(as: "postHasAuthor")
@export(as: "postHasFeaturedImage")
@export(as: "postHasCategories")
@export(as: "postHasTags")
@remove
initVariablesWithNull: _echo(value: null)
@export(as: "existingAuthorUsername")
@export(as: "existingFeaturedImageSlug")
@export(as: "featuredImageMutationInput")
@remove
initVariablesWithEmptyArray: _echo(value: [])
@export(as: "existingCategorySlugs")
@export(as: "existingTagSlugs")
@remove
}
query CheckIfPostExistsLocally($postSlug: String!)
@depends(on: "InitializeDynamicVariables")
{
localPost: post(
by: { slug: $postSlug }
status: any
) {
id
}
postAlreadyExists: _notNull(value: $__localPost)
@export(as: "postAlreadyExists")
}
query FailIfPostAlreadyExistsLocally($postSlug: String!)
@depends(on: "CheckIfPostExistsLocally")
@include(if: $postAlreadyExists)
{
errorMessage: _sprintf(
string: "Post with slug '%s' already exists locally",
values: [$postSlug]
) @remove
_fail(
message: $__errorMessage
data: {
slug: $postSlug
}
) @remove
createPost: _echo(value: null)
}
query ConnectToGraphQLAPI(
$upstreamServerGraphQLEndpointURL: String!
$postSlug: String!
)
@depends(on: "FailIfPostAlreadyExistsLocally")
@skip(if: $postAlreadyExists)
{
externalData: _sendGraphQLHTTPRequest(input:{
endpoint: $upstreamServerGraphQLEndpointURL,
query: """
query GetPost($postSlug: String!) {
post(by: { slug: $postSlug }, status: any) {
id
slug
rawTitle
rawContent
rawExcerpt
author {
id
username
}
featuredImage {
id
slug
}
categories {
id
slug
}
tags {
id
slug
}
}
}
""",
variables: [
{
name: "postSlug",
value: $postSlug
}
]
})
@export(as: "externalData")
requestProducedErrors: _isNull(value: $__externalData)
@export(as: "requestProducedErrors")
@remove
}
query ValidateResponse
@depends(on: "ConnectToGraphQLAPI")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
{
responseHasErrors: _propertyIsSetInJSONObject(
object: $externalData
by: {
key: "errors"
}
)
@export(as: "responseHasErrors")
@remove
postExists: _propertyIsSetInJSONObject(
object: $externalData
by: {
path: "data.post"
}
)
@remove
postIsMissing: _not(value: $__postExists)
@export(as: "postIsMissing")
@remove
}
query FailIfResponseHasErrors
@depends(on: "ValidateResponse")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
@skip(if: $postIsMissing)
@include(if: $responseHasErrors)
{
errors: _objectProperty(
object: $externalData,
by: {
key: "errors"
}
) @remove
_fail(
message: "Executing the GraphQL query against the upstream webserver produced error(s)"
data: {
errors: $__errors
}
) @remove
createPost: _echo(value: null)
}
query FailIfPostNotExists($postSlug: String!)
@depends(on: "FailIfResponseHasErrors")
@skip(if: $requestProducedErrors)
@include(if: $postIsMissing)
{
errorMessage: _sprintf(
string: "There is no post with slug '%s' in the origin",
values: [$postSlug]
) @remove
_fail(
message: $__errorMessage
data: {
slug: $postSlug
}
) @remove
createPost: _echo(value: null)
}
query ExportInputs
@depends(on: "FailIfPostNotExists")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
@skip(if: $responseHasErrors)
@skip(if: $postIsMissing)
{
postData: _objectProperty(
object: $externalData,
by: { path: "data.post" }
) @remove
postTitle: _objectProperty(
object: $__postData,
by: { key: "rawTitle" }
)
@export(as: "postTitle")
@remove
postContent: _objectProperty(
object: $__postData,
by: { key: "rawContent" }
)
@export(as: "postContent")
@remove
postExcerpt: _objectProperty(
object: $__postData,
by: { key: "rawExcerpt" }
)
@export(as: "postExcerpt")
@remove
postAuthorUsername: _objectProperty(
object: $__postData,
by: { key: "author" }
)
@passOnwards(
as: "author"
)
@applyField(
name: "_notNull",
arguments: {
value: $author
},
passOnwardsAs: "hasAuthor"
)
@if(condition: $hasAuthor)
@applyField(
name: "_objectProperty",
arguments: {
object: $author,
by: { key: "username" }
},
setResultInResponse: true
)
@export(as: "postAuthorUsername")
@remove
postHasAuthor: _notNull(
value: $__postAuthorUsername
)
@export(as: "postHasAuthor")
@remove
postFeaturedImageSlug: _objectProperty(
object: $__postData,
by: { key: "featuredImage" }
)
@passOnwards(
as: "featuredImage"
)
@applyField(
name: "_notNull",
arguments: {
value: $featuredImage
},
passOnwardsAs: "hasFeaturedImage"
)
@if(condition: $hasFeaturedImage)
@applyField(
name: "_objectProperty",
arguments: {
object: $featuredImage,
by: { key: "slug" }
},
setResultInResponse: true
)
@export(as: "postFeaturedImageSlug")
@remove
postHasFeaturedImage: _notNull(
value: $__postFeaturedImageSlug
)
@export(as: "postHasFeaturedImage")
@remove
postCategorySlugs: _objectProperty(
object: $__postData,
by: { key: "categories" }
)
@underEachArrayItem(
passValueOnwardsAs: "category"
)
@applyField(
name: "_objectProperty"
arguments: {
object: $category,
by: {
key: "slug"
}
}
setResultInResponse: true
)
@export(as: "postCategorySlugs")
@remove
postHasCategories: _notEmpty(
value: $__postCategorySlugs
)
@export(as: "postHasCategories")
@remove
postTagSlugs: _objectProperty(
object: $__postData,
by: { key: "tags" }
)
@underEachArrayItem(
passValueOnwardsAs: "tag"
)
@applyField(
name: "_objectProperty"
arguments: {
object: $tag,
by: {
key: "slug"
}
}
setResultInResponse: true
)
@export(as: "postTagSlugs")
@remove
postHasTags: _notEmpty(
value: $__postTagSlugs
)
@export(as: "postHasTags")
@remove
}
query ExportExistingResources
@depends(on: "ExportInputs")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
@skip(if: $responseHasErrors)
@skip(if: $postIsMissing)
{
existingAuthorByUsername: user(by: { username: $postAuthorUsername })
@include(if: $postHasAuthor)
{
id
username @export(as: "existingAuthorUsername")
}
existingFeaturedImageBySlug: mediaItem(by: { slug: $postFeaturedImageSlug })
@include(if: $postHasFeaturedImage)
{
id
slug @export(as: "existingFeaturedImageSlug")
}
existingCategoriesBySlug: postCategories(filter: { slugs: $postCategorySlugs })
@include(if: $postHasCategories)
{
id
slug @export(as: "existingCategorySlugs", type: LIST)
}
existingTagsBySlug: postTags(filter: { slugs: $postTagSlugs })
@include(if: $postHasTags)
{
id
slug @export(as: "existingTagSlugs", type: LIST)
}
}
query ExportMissingResources
@depends(on: "ExportExistingResources")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
@skip(if: $responseHasErrors)
@skip(if: $postIsMissing)
{
isAuthorMissing: _notEquals(
value1: $postAuthorUsername,
value2: $existingAuthorUsername
) @export(as: "isAuthorMissing")
isFeaturedImageMissing: _notEquals(
value1: $postFeaturedImageSlug,
value2: $existingFeaturedImageSlug
) @export(as: "isFeaturedImageMissing")
missingCategorySlugs: _arrayDiff(
arrays: [$postCategorySlugs, $existingCategorySlugs]
) @export(as: "missingCategorySlugs")
areCategoriesMissing: _notEmpty(
value: $__missingCategorySlugs
) @export(as: "areCategoriesMissing")
# missingTagSlugs: _arrayDiff(
# arrays: [$postTagSlugs, $existingTagSlugs]
# ) @export(as: "missingTagSlugs")
# areTagsMissing: _notEmpty(
# value: $__missingTagSlugs
# ) @export(as: "areTagsMissing")
isAnyResourceMissing: _or(
values: [
$__isAuthorMissing,
$__isFeaturedImageMissing,
$__areCategoriesMissing,
# $__areTagsMissing,
]
) @export(as: "isAnyResourceMissing")
}
query FailIfAnyResourceIsMissing
@depends(on: "ExportMissingResources")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
@skip(if: $postIsMissing)
@skip(if: $responseHasErrors)
@include(if: $isAnyResourceMissing)
{
performingValidations: id
@if(condition: $isAuthorMissing)
@fail(
message: "Author is missing in local site"
data: {
missingAuthorByUsername: $postAuthorUsername
}
condition: ALWAYS
)
@if(condition: $isFeaturedImageMissing)
@fail(
message: "Featured image is missing in local site"
data: {
missingFeaturedImageBySlug: $postFeaturedImageSlug
}
condition: ALWAYS
)
@if(condition: $areCategoriesMissing)
@fail(
message: "Categories are missing in local site"
data: {
missingCategoriesBySlug: $missingCategorySlugs
}
condition: ALWAYS
)
# @if(condition: $areTagsMissing)
# @fail(
# message: "Tags are missing in local site"
# data: {
# missingTagBySlug: $missingTagSlugs
# }
# condition: ALWAYS
# )
createPost: _echo(value: null)
}
query ExportMutationInputs
@depends(on: "FailIfAnyResourceIsMissing")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
@skip(if: $responseHasErrors)
@skip(if: $postIsMissing)
@skip(if: $isAnyResourceMissing)
{
featuredImageMutationInput: _echo(value: {
slug: $postFeaturedImageSlug
})
@include(if: $postHasFeaturedImage)
@export(as: "featuredImageMutationInput")
@remove
}
mutation ImportPostFromWPSite(
$postSlug: String!
)
@depends(on: "ExportMutationInputs")
@skip(if: $postAlreadyExists)
@skip(if: $requestProducedErrors)
@skip(if: $responseHasErrors)
@skip(if: $postIsMissing)
@skip(if: $isAnyResourceMissing)
{
createPost(input: {
status: draft,
slug: $postSlug
title: $postTitle
contentAs: {
html: $postContent
},
excerpt: $postExcerpt
authorBy: {
username: $postAuthorUsername
},
featuredImageBy: $featuredImageMutationInput,
categoriesBy: {
slugs: $postCategorySlugs
},
tagsBy: {
slugs: $postTagSlugs
}
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
id
date
status
slug
title
content
excerpt
author {
id
username
}
featuredImage {
id
slug
}
categories {
id
slug
}
tags {
id
slug
}
}
}
}
This persisted query will import the post from the provisional site to the translated site. Execute it, providing the JSON dictionary with the needed GraphQL variables:
{
"upstreamServerGraphQLEndpointURL": "{ Gato GraphQL public endpoint on translated site. Eg: https://content-es.instawp.xyz/graphql }",
"postSlug": "{ The slug of the translated post to sync }"
}
The translated site will now contain the Google-Translated post, and no access was ever given to the 3rd-party translators.