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 الذي يحل هذه المشكلة.

جميع المشكلات المعروفة →