WPML
Статус
Открыто
Тип
Ошибка
Связанный плагин/тема
WPBakery
Теги темы
Compatibility

Обзор проблемы

Элемент Одиночное изображение или Галерея изображений с пользовательской ссылкой теряет своё изображение на переведённой странице. Оригинальная страница отображается корректно.

В WPBakery 9.0 изменился способ сохранения изображения со ссылкой. WPML не распознаёт новое значение, поэтому на переведённой странице изображение не отображается. Ссылка также перестаёт быть переводимой, и переведённая страница сохраняет ссылку оригинальной страницы.

Это также затрагивает страницы, созданные в более ранних версиях. WPBakery преобразует элемент, когда Вы открываете его в редакторе и сохраняете. На странице, которая работает сегодня, эта проблема может появиться после того, как Вы её измените.

Это затрагивает WPBakery Page Builder 9.0 и более поздних версий при использовании с WPML 4.9 и WPML 5.0.

Обходное решение

Сделайте полную резервную копию Вашего сайта перед началом работы.

  1. Создайте папку wp-content/mu-plugins/, если на Вашем сайте её нет.
  2. Создайте файл wp-content/mu-plugins/wpml-wpbakery-9-linked-images.php. Это обязательный плагин (mu-plugin). Он загружается самостоятельно, и ничего активировать не нужно. Добавьте следующее содержимое:
Показать код
<?php
/**
 * Plugin Name: WPML fix - WPBakery 9 images with a link
 * Description: Keeps images that have a custom link visible on translated pages, and makes the link translatable.
 * Version: 1.1
 *
 * WPBakery 9.0 saves the custom link inside the same attribute as the image ID.
 * This file teaches WPML to read that value. It does two things:
 *
 * 1. Translates the image IDs, so the image appears on the translated page.
 * 2. Registers each link URL for translation, so a translated page can point
 * to a translated target.
 *
 * It covers the Single Image and Image Gallery elements. Remove this file after
 * you update to the WPML version that fixes this issue.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/* -------------------------------------------------------------------------
 * Part 1 - the image IDs
 * ---------------------------------------------------------------------- */

/**
 * The WPBakery elements that changed in 9.0, and the attribute that holds the image IDs.
 */
function wpml_wpb9_attributes() {
	return [
		'vc_single_image' => 'image',
		'vc_gallery'      => 'images',
	];
}

/**
 * Step 1. Stop WPML from rewriting these two attributes.
 */
add_filter(
	'option_wpml_pb_media_shortcode',
	function ( $shortcodes ) {
		if ( ! is_array( $shortcodes ) ) {
			return $shortcodes;
		}

		$attributes = wpml_wpb9_attributes();

		foreach ( $shortcodes as $index => $shortcode ) {
			$tag = isset( $shortcode['tag']['name'] ) ? $shortcode['tag']['name'] : '';

			if ( isset( $attributes[ $tag ], $shortcode['attributes'][ $attributes[ $tag ] ] ) ) {
				unset( $shortcodes[ $index ]['attributes'][ $attributes[ $tag ] ] );
			}
		}

		return $shortcodes;
	}
);

/**
 * Step 2. Translate the image IDs, in both storage formats.
 */
class WPML_WPB9_Linked_Images {

	private $lang;

	public function translate( $post ) {
		$details = apply_filters( 'wpml_post_language_details', null, $post->ID );

		if ( ! isset( $details['language_code'] ) ) {
			return;
		}

		$this->lang = $details['language_code'];
		$content    = $post->post_content;

		foreach ( wpml_wpb9_attributes() as $tag => $attribute ) {
			if ( false === strpos( $content, '[' . $tag ) ) {
				continue;
			}

			$pattern = '/(\[' . preg_quote( $tag, '/' ) . '(?: [^\]]* | )' . preg_quote( $attribute, '/' ) . '=(?:"|\'))([^"\']*)/';
			$content = preg_replace_callback( $pattern, [ $this, 'replace' ], $content );
		}

		if ( $content === $post->post_content ) {
			return;
		}

		$post->post_content = $content;

		kses_remove_filters();
		wpml_update_escaped_post(
			[
				'ID'           => $post->ID,
				'post_content' => $content,
				'tags_input'   => wp_get_post_tags( $post->ID, [ 'fields' => 'ids' ] ),
			],
			$this->lang
		);
		kses_init();
	}

	private function replace( array $matches ) {
		$value = $matches[2];

		// WPBakery 9.0 and later: a JSON map of image ID to link data.
		if ( '{' === substr( ltrim( $value ), 0, 1 ) ) {
			$data = json_decode( str_replace( [ '`{`', '`}`', '``' ], [ '[', ']', '"' ], $value ), true );

			if ( ! is_array( $data ) ) {
				return $matches[0];
			}

			$translated = [];

			foreach ( $data as $id => $link ) {
				$new_id = (string) $this->translate_id( $id );

				if ( ! isset( $translated[ $new_id ] ) ) {
					$translated[ $new_id ] = $link;
				}
			}

			$json = (string) wp_json_encode( (object) $translated, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );

			return $matches[1] . str_replace( [ '"', '[', ']' ], [ '``', '`{`', '`}`' ], $json );
		}

		// WPBakery 8.x and earlier: image IDs separated by commas.
		if ( ! preg_match( '/^[\d\s,]*$/', $value ) ) {
			return $matches[0];
		}

		$ids = array_map( [ $this, 'translate_id' ], explode( ',', $value ) );

		return $matches[1] . implode( ',', $ids );
	}

	private function translate_id( $id ) {
		$id = (int) $id;

		if ( $id < 1 ) {
			return $id;
		}

		$translated = apply_filters( 'wpml_object_id', $id, 'attachment', true, $this->lang );

		return $translated ? (int) $translated : $id;
	}

	/**
	 * WPML calls these two while it collects the media a page uses.
	 * This file only translates the IDs, so there is nothing to collect.
	 */
	public function find_media( $post ) {
	}

	public function get_media() {
		return [];
	}
}

add_filter(
	'wpml_pb_get_media_updaters',
	function ( $updaters ) {
		$updaters['wpml_wpb9_linked_images'] = new WPML_WPB9_Linked_Images();

		return $updaters;
	},
	PHP_INT_MAX
);

/* -------------------------------------------------------------------------
 * Part 2 - the custom link URLs
 * ---------------------------------------------------------------------- */

const WPML_WPB9_ENCODING = 'wpb9_image_links';

/** Which keys inside each link object a translator may edit. */
function wpml_wpb9_translatable_keys() {
	return [ 'url', 'title' ];
}

/** All keys we keep, in WPBakery's own order. */
function wpml_wpb9_link_keys() {
	return [ 'url', 'title', 'target', 'rel' ];
}

/**
 * Step 1. Declare the attribute translatable, with our encoding.
 *
 * This hooks `wpml_config_array`. WPML rebuilds its shortcode settings from the
 * plugin configuration, so the attribute has to be added there. A change made
 * anywhere else is discarded when WPML reads the configuration again.
 *
 * The entry carries only an encoding, not a type. WPML does not register a
 * string for an attribute that already has the `media-ids` type, so the links
 * need their own entry.
 */
add_filter(
	'wpml_config_array',
	function ( $config ) {
		$targets = wpml_wpb9_attributes();

		if ( ! isset( $config['wpml-config']['shortcodes']['shortcode'] ) ) {
			return $config;
		}

		$shortcodes = &$config['wpml-config']['shortcodes']['shortcode'];

		foreach ( $shortcodes as &$shortcode ) {
			$tag = isset( $shortcode['tag']['value'] ) ? $shortcode['tag']['value'] : '';

			if ( ! isset( $targets[ $tag ], $shortcode['attributes']['attribute'] ) ) {
				continue;
			}

			$attribute = $targets[ $tag ];
			$list      = $shortcode['attributes']['attribute'];

			if ( ! is_array( $list ) || ! is_numeric( key( $list ) ) ) {
				$list = [ $list ];
			}

			foreach ( $list as $existing ) {
				$encoding = isset( $existing['attr']['encoding'] ) ? $existing['attr']['encoding'] : '';

				if ( ( $existing['value'] ?? '' ) === $attribute && WPML_WPB9_ENCODING === $encoding ) {
					continue 2;
				}
			}

			$list[] = [
				'value' => $attribute,
				'attr'  => [ 'encoding' => WPML_WPB9_ENCODING ],
			];

			$shortcode['attributes']['attribute'] = $list;
		}

		unset( $shortcode );

		return $config;
	},
	9
);

/**
 * Step 2. Decode: one translatable string per URL.
 */
add_filter(
	'wpml_pb_shortcode_decode',
	function ( $content, $encoding, $encoded_content = '' ) {
		if ( WPML_WPB9_ENCODING !== $encoding ) {
			return $content;
		}

		$value = is_string( $encoded_content ) && '' !== $encoded_content ? $encoded_content : $content;

		if ( ! is_string( $value ) || '{' !== substr( ltrim( $value ), 0, 1 ) ) {
			return [];
		}

		$data = json_decode( str_replace( [ '`{`', '`}`', '``' ], [ '[', ']', '"' ], $value ), true );

		if ( ! is_array( $data ) ) {
			return [];
		}

		$translatable = wpml_wpb9_translatable_keys();
		$out          = [];

		foreach ( $data as $id => $link ) {
			if ( ! is_array( $link ) ) {
				continue;
			}

			foreach ( wpml_wpb9_link_keys() as $key ) {
				$out[ $key . '_' . $id ] = [
					'value'     => isset( $link[ $key ] ) ? (string) $link[ $key ] : '',
					'translate' => in_array( $key, $translatable, true ),
				];
			}
		}

		return $out;
	},
	10,
	3
);

/**
 * Step 3. Encode: rebuild the JSON with the translated URLs, same image IDs.
 */
add_filter(
	'wpml_pb_shortcode_encode',
	function ( $content, $encoding, $decoded_content = null ) {
		if ( WPML_WPB9_ENCODING !== $encoding ) {
			return $content;
		}

		$source = is_array( $content ) ? $content : (array) $decoded_content;

		if ( ! $source ) {
			return $content;
		}

		$map = [];

		foreach ( $source as $combined_key => $value ) {
			$parts = explode( '_', $combined_key );
			$id    = array_pop( $parts );
			$key   = implode( '_', $parts );

			if ( '' === $id || '' === $key ) {
				continue;
			}

			$map[ $id ][ $key ] = is_array( $value ) && isset( $value['value'] ) ? $value['value'] : $value;
		}

		if ( ! $map ) {
			return $content;
		}

		$out = [];

		foreach ( $map as $id => $link ) {
			$entry = [];

			foreach ( wpml_wpb9_link_keys() as $key ) {
				$entry[ $key ] = isset( $link[ $key ] ) ? (string) $link[ $key ] : '';
			}

			$out[ (string) $id ] = $entry;
		}

		$json = (string) wp_json_encode( (object) $out, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );

		return str_replace( [ '"', '[', ']' ], [ '``', '`{`', '`}`' ], $json );
	},
	10,
	3
);
  1. Откройте каждую страницу, где используется Single Image или Image Gallery с пользовательской ссылкой. Сохраните страницу снова. Это зарегистрирует ссылку для перевода.
  2. Переведите ссылку в WPML → Перевод строк или в редакторе переводов вместе с остальной частью страницы.
  3. Откройте каждую переведённую страницу, на которой исчезло изображение, и сохраните её снова. Это вернёт изображение.

Ожидаемый результат

Изображение снова появится на переведённой странице. Пользовательская ссылка станет переводимой, поэтому переведённая страница сможет указывать на переведённый целевой адрес.

Данное обходное решение применяется к элементам Single Image и Image Gallery. В WPBakery 9.0 только эти два элемента хранят ссылку таким образом.

Страница, переведённая как дубликат, сохраняет ссылки оригинальной страницы. WPML копирует дубликат с оригинала, поэтому перевод ссылок к нему не применяется.

Удалите этот файл после обновления до версии WPML, в которой исправлена данная проблема.

Все известные проблемы →