WPML
状态
待解决
类型
Bug
相关插件/主题
WPBakery
主题标签
Compatibility

问题概述

带有自定义链接的单张图像图像画廊在翻译后的页面上会丢失图像。原始页面显示正常。

WPBakery 9.0 更改了带有链接的图像的保存方式。WPML 无法识别新值,因此翻译后的页面不显示图像。该链接也不再可翻译,并且翻译后的页面会保留原始页面的链接。

使用早期版本构建的页面也会受到影响。当您在编辑器中打开并保存该元素时,WPBakery 会对其进行转换。今天正常工作的页面在您编辑它之后可能会出现此问题。

这会影响WPBakery 页面构建器 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 ImageImage Gallery 的页面。再次保存该页面。这会注册该链接以供翻译。
  2. WPML → 字符串翻译中翻译该链接,或在翻译编辑器中与页面的其余部分一起翻译。
  3. 打开每个丢失了图像的翻译页面并再次保存。这会使图像重新出现。

预期结果

图像再次出现在翻译后的页面上。自定义链接变为可翻译状态,因此翻译后的页面可以指向翻译后的目标。

此临时解决方法涵盖 Single ImageImage Gallery 元素。这是 WPBakery 9.0 中以这种方式存储链接的两个元素。

作为副本翻译的页面会保留原始页面的链接。WPML 会从其原始页面复制出副本,因此链接翻译不适用于它。

在更新到修复此问题的 WPML 版本后,请删除该文件。

所有已知问题 →