WPML
상태
미해결
유형
버그
관련 플러그인/테마
WPBakery
주제 태그
Compatibility

문제 개요

사용자 정의 링크가 있는 단일 이미지 또는 이미지 갤러리는 번역된 페이지에서 이미지가 손실됩니다. 원본 페이지는 정상적으로 표시됩니다.

WPBakery 9.0은 링크가 있는 이미지를 저장하는 방식을 변경했습니다. WPML은 새로운 값을 인식하지 못하므로 번역된 페이지에 이미지가 표시되지 않습니다. 또한 링크도 더 이상 번역할 수 없게 되며, 번역된 페이지는 원본 페이지의 링크를 유지합니다.

이전 버전으로 빌드된 페이지도 영향을 받습니다. 편집기에서 요소를 열고 저장할 때 WPBakery가 요소를 변환합니다. 현재 정상적으로 작동하는 페이지라도 편집한 후에는 이 문제가 발생할 수 있습니다.

이 문제는 WPML 4.9 및 WPML 5.0을 사용하는 WPBakery Page Builder 9.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. 사용자 정의 링크가 있는 단일 이미지 또는 이미지 갤러리를 사용하는 각 페이지를 엽니다. 페이지를 다시 저장하세요. 이렇게 하면 링크가 번역되도록 등록됩니다.
  2. WPML → 문자열 번역에서 링크를 번역하거나, 번역 에디터에서 페이지의 나머지 부분과 함께 번역하세요.
  3. 이미지가 사라진 번역된 페이지를 각각 열고 다시 저장하세요. 이렇게 하면 이미지가 다시 나타납니다.

예상 결과

번역된 페이지에 이미지가 다시 나타납니다. 사용자 정의 링크가 번역 가능해지므로 번역된 페이지가 번역된 대상을 가리킬 수 있습니다.

이 임시 해결 방법은 단일 이미지이미지 갤러리 요소에 적용됩니다. 이 두 요소는 WPBakery 9.0에서 이러한 방식으로 링크를 저장합니다.

복제로 번역된 페이지는 원본 페이지의 링크를 유지합니다. WPML은 원본에서 복제본을 복사하므로 링크 번역이 적용되지 않습니다.

이 문제를 해결하는 WPML 버전으로 업데이트한 후 해당 파일을 제거하세요.

모든 알려진 문제 →