- Status
- Aberto
- Tipo
- Bug
- Plugin/tema relacionado
- WPBakery
- Tags de tópico
- Compatibility
Visão geral do problema
Uma Imagem única ou uma Galeria de imagens que tem um link personalizado perde sua imagem na página traduzida. A página original está correta.
O WPBakery 9.0 mudou a forma como salva uma imagem que tem um link. O WPML não reconhece o novo valor, então a página traduzida não mostra nenhuma imagem. O link também deixa de ser traduzível, e a página traduzida mantém o link da página original.
Páginas criadas com versões anteriores também são afetadas. O WPBakery converte o elemento quando você o abre no editor e salva. Uma página que funciona hoje pode apresentar esse problema depois que você a editar.
Isso afeta o WPBakery Page Builder 9.0 e posterior, com o WPML 4.9 e o WPML 5.0.
Solução alternativa
Faça um backup completo do seu site antes de começar.
- Crie a pasta
wp-content/mu-plugins/se o seu site não a tiver. - Crie o arquivo
wp-content/mu-plugins/wpml-wpbakery-9-linked-images.php. Ele é um plugin Must-Use (mu-plugin). Ele carrega sozinho e não há nada para ativar. Adicione este conteúdo:
Mostrar o código
<?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
);
- Abra cada página que usa uma Imagem única ou uma Galeria de imagens com um link personalizado. Salve a página novamente. Isso registra o link para tradução.
- Traduza o link em WPML → Tradução de Strings ou no editor de tradução, junto com o restante da página.
- Abra cada página traduzida que perdeu sua imagem e salve-a novamente. Isso traz a imagem de volta.
O que esperar
A imagem aparece novamente na página traduzida. O link personalizado se torna traduzível, para que uma página traduzida possa apontar para um destino traduzido.
A solução alternativa abrange os elementos Imagem única e Galeria de imagens. Esses são os dois elementos que armazenam um link dessa forma no WPBakery 9.0.
Uma página traduzida como uma duplicata mantém os links da página original. O WPML copia uma duplicata do seu original, portanto, a tradução de links não se aplica a ela.
Remova o arquivo após atualizar para a versão do WPML que corrige esse problema.