- Estado
- Abierto
- Tipo
- Error
- Plugin/tema relacionado
- WPBakery
- Etiquetas de temática
- Compatibility
Resumen del problema
Una Imagen única o una Galería de imágenes que tiene un enlace personalizado pierde su imagen en la página traducida. La página original es correcta.
WPBakery 9.0 cambió la forma en que guarda una imagen que tiene un enlace. WPML no reconoce el nuevo valor, por lo que la página traducida no muestra ninguna imagen. El enlace también deja de ser traducible, y la página traducida mantiene el enlace de la página original.
Las páginas creadas con versiones anteriores también se ven afectadas. WPBakery convierte el elemento cuando lo abre en el editor y lo guarda. Una página que funciona hoy puede mostrar este problema después de editarla.
Esto afecta a WPBakery Page Builder 9.0 y versiones posteriores, con WPML 4.9 y WPML 5.0.
Solución alternativa
Haga una copia de seguridad completa de su sitio antes de empezar.
- Cree la carpeta
wp-content/mu-plugins/si su sitio no la tiene. - Cree el archivo
wp-content/mu-plugins/wpml-wpbakery-9-linked-images.php. Es un Must-Use Plugin (mu-plugin). Se carga por sí solo y no hay nada que activar. Añada este contenido:
Mostrar el 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 utilice una Single Image o una Image Gallery con un enlace personalizado. Guarde la página de nuevo. Esto registra el enlace para su traducción.
- Traduzca el enlace en WPML → Traducción de cadenas o en el editor de traducción junto con el resto de la página.
- Abra cada página traducida que haya perdido su imagen y guárdela de nuevo. Esto hace que la imagen vuelva a aparecer.
Qué esperar
La imagen vuelve a aparecer en la página traducida. El enlace personalizado pasa a ser traducible, por lo que una página traducida puede apuntar a un destino traducido.
La solución alternativa cubre los elementos Single Image e Image Gallery. Estos son los dos elementos que almacenan un enlace de esta manera en WPBakery 9.0.
Una página traducida como duplicado mantiene los enlaces de la página original. WPML copia un duplicado de su original, por lo que la traducción de enlaces no se le aplica.
Elimine el archivo después de actualizar a la versión de WPML que soluciona este problema.