Prefill new Polylang translations with the Etch layout
Polylang’s + link opens an empty draft for the new language. This snippet fills it with the source post’s Etch blocks, title and excerpt, so you translate the text instead of rebuilding the page.
Paste it into a new PHP snippet. Install steps are below the code.
<?php
/**
* Etch + Polylang Free
*
* Prefill a new translation opened with Polylang's "+" link using the
* source post's stored Etch blocks. WordPress handles saving and sanitization.
* This does not clone component definitions, metadata, or linked documents.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function ed_etch_pll_translation_source( $new_post ) {
global $pagenow;
if (
! is_admin()
|| 'post-new.php' !== $pagenow
|| ! ( $new_post instanceof WP_Post )
|| 'auto-draft' !== $new_post->post_status
|| $new_post->ID <= 0
|| ! isset(
$_GET['from_post'],
$_GET['new_lang'],
$_GET['post_type'],
$_GET['_wpnonce']
)
) {
return null;
}
foreach ( array( 'from_post', 'new_lang', 'post_type', '_wpnonce' ) as $parameter ) {
if ( ! is_string( $_GET[ $parameter ] ) ) {
return null;
}
}
// Reject malformed IDs instead of coercing them into another post's ID.
$source_id = filter_var(
wp_unslash( $_GET['from_post'] ),
FILTER_VALIDATE_INT,
array( 'options' => array( 'min_range' => 1 ) )
);
$new_lang = wp_unslash( $_GET['new_lang'] );
$post_type = wp_unslash( $_GET['post_type'] );
$nonce = wp_unslash( $_GET['_wpnonce'] );
if (
! $source_id
|| $source_id === (int) $new_post->ID
|| '' === $new_lang
|| sanitize_key( $new_lang ) !== $new_lang
|| sanitize_key( $post_type ) !== $post_type
|| ! wp_verify_nonce( $nonce, 'new-post-translation' )
|| $post_type !== $new_post->post_type
|| ! function_exists( 'pll_is_translated_post_type' )
|| ! function_exists( 'pll_languages_list' )
|| ! function_exists( 'pll_get_post_language' )
|| ! function_exists( 'pll_get_post' )
|| ! pll_is_translated_post_type( $post_type )
) {
return null;
}
$languages = pll_languages_list( array( 'fields' => 'slug' ) );
if ( ! is_array( $languages ) || ! in_array( $new_lang, $languages, true ) ) {
return null;
}
$source = get_post( $source_id );
if (
! ( $source instanceof WP_Post )
|| $source->post_type !== $post_type
|| in_array( $source->post_status, array( 'trash', 'auto-draft' ), true )
|| ! current_user_can( 'edit_post', $source_id )
|| ! current_user_can( 'edit_post', $new_post->ID )
|| false === strpos( (string) $source->post_content, '<!-- wp:etch/' )
) {
return null;
}
$post_type_object = get_post_type_object( $post_type );
if ( ! $post_type_object ) {
return null;
}
$create_capability = isset( $post_type_object->cap->create_posts )
? $post_type_object->cap->create_posts
: $post_type_object->cap->edit_posts;
if ( ! current_user_can( $create_capability ) ) {
return null;
}
$source_lang = pll_get_post_language( $source_id, 'slug' );
if ( ! is_string( $source_lang ) || '' === $source_lang || $new_lang === $source_lang ) {
return null;
}
// Do not prefill a duplicate translation. The current auto-draft is allowed.
$existing_id = (int) pll_get_post( $source_id, $new_lang );
if ( $existing_id && $existing_id !== (int) $new_post->ID ) {
return null;
}
return $source;
}
function ed_etch_pll_default_content( $content, $new_post ) {
if ( ! is_string( $content ) || '' !== trim( $content ) ) {
return $content;
}
$source = ed_etch_pll_translation_source( $new_post );
return $source
? (string) $source->post_content
: $content;
}
add_filter( 'default_content', 'ed_etch_pll_default_content', 20, 2 );
/*
* Prefill the title and excerpt only when no earlier filter supplied a value.
* Remove either filter registration below to leave that field empty.
*/
function ed_etch_pll_default_title( $title, $new_post ) {
if ( ! is_string( $title ) || '' !== trim( $title ) ) {
return $title;
}
$source = ed_etch_pll_translation_source( $new_post );
return $source
? (string) $source->post_title
: $title;
}
add_filter( 'default_title', 'ed_etch_pll_default_title', 20, 2 );
function ed_etch_pll_default_excerpt( $excerpt, $new_post ) {
if ( ! is_string( $excerpt ) || '' !== trim( $excerpt ) ) {
return $excerpt;
}
$source = ed_etch_pll_translation_source( $new_post );
return $source
? (string) $source->post_excerpt
: $excerpt;
}
add_filter( 'default_excerpt', 'ed_etch_pll_default_excerpt', 20, 2 );- Requires
- Etch + Polylang Free
- Runs on
- New translations in wp-admin
- Version
- 1.0.0
- License
- GPL-2.0-or-later
<?php
/**
* Etch + Polylang Free
*
* Prefill a new translation opened with Polylang's "+" link using the
* source post's stored Etch blocks. WordPress handles saving and sanitization.
* This does not clone component definitions, metadata, or linked documents.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function ed_etch_pll_translation_source( $new_post ) {
global $pagenow;
if (
! is_admin()
|| 'post-new.php' !== $pagenow
|| ! ( $new_post instanceof WP_Post )
|| 'auto-draft' !== $new_post->post_status
|| $new_post->ID <= 0
|| ! isset(
$_GET['from_post'],
$_GET['new_lang'],
$_GET['post_type'],
$_GET['_wpnonce']
)
) {
return null;
}
foreach ( array( 'from_post', 'new_lang', 'post_type', '_wpnonce' ) as $parameter ) {
if ( ! is_string( $_GET[ $parameter ] ) ) {
return null;
}
}
// Reject malformed IDs instead of coercing them into another post's ID.
$source_id = filter_var(
wp_unslash( $_GET['from_post'] ),
FILTER_VALIDATE_INT,
array( 'options' => array( 'min_range' => 1 ) )
);
$new_lang = wp_unslash( $_GET['new_lang'] );
$post_type = wp_unslash( $_GET['post_type'] );
$nonce = wp_unslash( $_GET['_wpnonce'] );
if (
! $source_id
|| $source_id === (int) $new_post->ID
|| '' === $new_lang
|| sanitize_key( $new_lang ) !== $new_lang
|| sanitize_key( $post_type ) !== $post_type
|| ! wp_verify_nonce( $nonce, 'new-post-translation' )
|| $post_type !== $new_post->post_type
|| ! function_exists( 'pll_is_translated_post_type' )
|| ! function_exists( 'pll_languages_list' )
|| ! function_exists( 'pll_get_post_language' )
|| ! function_exists( 'pll_get_post' )
|| ! pll_is_translated_post_type( $post_type )
) {
return null;
}
$languages = pll_languages_list( array( 'fields' => 'slug' ) );
if ( ! is_array( $languages ) || ! in_array( $new_lang, $languages, true ) ) {
return null;
}
$source = get_post( $source_id );
if (
! ( $source instanceof WP_Post )
|| $source->post_type !== $post_type
|| in_array( $source->post_status, array( 'trash', 'auto-draft' ), true )
|| ! current_user_can( 'edit_post', $source_id )
|| ! current_user_can( 'edit_post', $new_post->ID )
|| false === strpos( (string) $source->post_content, '<!-- wp:etch/' )
) {
return null;
}
$post_type_object = get_post_type_object( $post_type );
if ( ! $post_type_object ) {
return null;
}
$create_capability = isset( $post_type_object->cap->create_posts )
? $post_type_object->cap->create_posts
: $post_type_object->cap->edit_posts;
if ( ! current_user_can( $create_capability ) ) {
return null;
}
$source_lang = pll_get_post_language( $source_id, 'slug' );
if ( ! is_string( $source_lang ) || '' === $source_lang || $new_lang === $source_lang ) {
return null;
}
// Do not prefill a duplicate translation. The current auto-draft is allowed.
$existing_id = (int) pll_get_post( $source_id, $new_lang );
if ( $existing_id && $existing_id !== (int) $new_post->ID ) {
return null;
}
return $source;
}
function ed_etch_pll_default_content( $content, $new_post ) {
if ( ! is_string( $content ) || '' !== trim( $content ) ) {
return $content;
}
$source = ed_etch_pll_translation_source( $new_post );
return $source
? (string) $source->post_content
: $content;
}
add_filter( 'default_content', 'ed_etch_pll_default_content', 20, 2 );
/*
* Prefill the title and excerpt only when no earlier filter supplied a value.
* Remove either filter registration below to leave that field empty.
*/
function ed_etch_pll_default_title( $title, $new_post ) {
if ( ! is_string( $title ) || '' !== trim( $title ) ) {
return $title;
}
$source = ed_etch_pll_translation_source( $new_post );
return $source
? (string) $source->post_title
: $title;
}
add_filter( 'default_title', 'ed_etch_pll_default_title', 20, 2 );
function ed_etch_pll_default_excerpt( $excerpt, $new_post ) {
if ( ! is_string( $excerpt ) || '' !== trim( $excerpt ) ) {
return $excerpt;
}
$source = ed_etch_pll_translation_source( $new_post );
return $source
? (string) $source->post_excerpt
: $excerpt;
}
add_filter( 'default_excerpt', 'ed_etch_pll_default_excerpt', 20, 2 );What it does
When you click + next to a language, Polylang opens the translation as an auto-draft and passes the source post in the URL. The snippet verifies that request and copies the source post’s stored content, title and excerpt into the empty fields of the draft.
Etch stores the layout in the post content, so the translation opens with every section, class and component in place. Nothing is saved until you save the draft, and the two posts stay independent afterwards.
- Runs only on the new-translation screen, and only when the source post contains Etch blocks.
- Checks Polylang’s nonce and your permission to edit the source, edit the new draft and create posts of that type.
- Fills only empty fields. A value that another plugin already set stays as it is.
- Skips unknown languages, trashed posts and languages that already have a translation.
- Copies the stored block markup as is, without rendering or reformatting it.
Install it
- 01Add a new PHP snippet Use a snippet manager such as FluentSnippets, WPCodeBox or Code Snippets, or a site-specific plugin. Most managers add the opening PHP tag for you.
- 02Paste the code and run it in the admin area Running it everywhere works too: the code returns early outside wp-admin.
- 03Replace any earlier copy Updating from an older version of this snippet? Delete the old one first. Never run two versions side by side.
- 04Create a translation Open a post built with Etch and click + next to a language. The draft opens with the source layout, title and excerpt.
What it leaves alone
- It does not translate text or rewrite links. Translate the copy and update internal links yourself.
- Custom fields, the featured image and terms are not copied by this snippet. Polylang’s synchronization settings handle those.
- Etch components stay shared. Both languages use the same component definitions, so a change to a component shows up in every language.
Test it on a staging site first, with the Etch and Polylang versions you run in production. Create a translation, save it, reopen it and check the front end.
- 1.0.0 Sep 26, 2026First public version.
Tell me and I will fix the snippet
The versions above are the ones this code was tested on. If yours differ, or the code fails on your site, send me the details.