Newsletter

Hear about new files and fixes

I send an email when I publish something new or release a fix for a file you might already use. There’s no fixed schedule.

Every email has a one-click unsubscribe link, and none of them contain tracking pixels.

Code snippet PHP Updated Oct 18, 2026

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.

Download .php

Paste it into a new PHP snippet. Install steps are below the code.

Requires
Etch + Polylang Free
Runs on
New translations in wp-admin
Version
1.0.0
License
GPL-2.0-or-later
polylang-support.php
<?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

  1. 01
    Add 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.
  2. 02
    Paste the code and run it in the admin area Running it everywhere works too: the code returns early outside wp-admin.
  3. 03
    Replace any earlier copy Updating from an older version of this snippet? Delete the old one first. Never run two versions side by side.
  4. 04
    Create 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.
Before you use it

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.

Changelog
  • 1.0.0 Sep 26, 2026
    First public version.
Tags
  • polylang
  • multilingual
  • translation
  • php
Something not working?

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.