WPML
Trạng thái
Đang mở
Loại
Lỗi
Plugin/giao diện liên quan
WPBakery
Thẻ chủ đề
Compatibility

Tổng quan về sự cố

Một Single Image hoặc một Image Gallery có liên kết tùy chỉnh sẽ bị mất hình ảnh trên trang đã dịch. Trang gốc vẫn hiển thị chính xác.

WPBakery 9.0 đã thay đổi cách lưu một hình ảnh có chứa liên kết. WPML không nhận dạng được giá trị mới, do đó trang đã dịch không hiển thị hình ảnh. Liên kết cũng không còn có thể dịch được, và trang đã dịch sẽ giữ nguyên liên kết của trang gốc.

Các trang được xây dựng bằng những phiên bản cũ hơn cũng bị ảnh hưởng. WPBakery sẽ chuyển đổi phần tử khi bạn mở nó trong trình chỉnh sửa và lưu lại. Một trang hiện đang hoạt động bình thường có thể xuất hiện sự cố này sau khi bạn chỉnh sửa nó.

Sự cố này ảnh hưởng đến WPBakery Page Builder 9.0 trở lên, với WPML 4.9 và WPML 5.0.

Giải pháp tạm thời

Tạo một bản sao lưu đầy đủ cho trang web của bạn trước khi bắt đầu.

  1. Tạo thư mục wp-content/mu-plugins/ nếu trang web của bạn chưa có.
  2. Tạo tệp wp-content/mu-plugins/wpml-wpbakery-9-linked-images.php. Đây là một Must-Use Plugin (mu-plugin). Nó tự động tải và không có gì cần kích hoạt. Thêm nội dung này:
Hiển thị mã
<?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. Mở từng trang sử dụng Single Image hoặc Image Gallery có liên kết tùy chỉnh. Lưu lại trang. Việc này sẽ đăng ký liên kết để dịch.
  2. Dịch liên kết trong WPML → Dịch chuỗi, hoặc trong trình biên tập dịch thuật cùng với phần còn lại của trang.
  3. Mở từng trang đã dịch bị mất hình ảnh và lưu lại. Việc này sẽ hiển thị lại hình ảnh.

Kết quả mong đợi

Hình ảnh xuất hiện trở lại trên trang đã dịch. Liên kết tùy chỉnh trở nên có thể dịch, vì vậy một trang đã dịch có thể trỏ đến một đích đến đã dịch.

Giải pháp tạm thời này áp dụng cho các phần tử Single ImageImage Gallery. Đây là hai phần tử lưu trữ liên kết theo cách này trong WPBakery 9.0.

Một trang được dịch dưới dạng nhân bản sẽ giữ nguyên các liên kết của trang gốc. WPML sao chép một bản nhân bản từ bản gốc của nó, do đó việc dịch liên kết không áp dụng cho trang này.

Xóa tệp sau khi bạn cập nhật lên phiên bản WPML đã khắc phục sự cố này.

Tất cả các lỗi đã ghi nhận →