- Statut
- Ouverte
- Type
- Bug
- Extension/thème associé
- WPBakery
- Étiquettes de sujet
- Compatibility
Aperçu de l’anomalie
Une Image unique ou une Galerie d’images qui possède un lien personnalisé perd son image sur la page traduite. La page originale est correcte.
WPBakery 9.0 a modifié la façon dont il enregistre une image qui possède un lien. WPML ne reconnaît pas la nouvelle valeur, la page traduite n’affiche donc aucune image. Le lien cesse également d’être traduisible, et la page traduite conserve le lien de la page originale.
Les pages créées avec des versions antérieures sont également affectées. WPBakery convertit l’élément lorsque vous l’ouvrez dans l’éditeur et que vous l’enregistrez. Une page qui fonctionne aujourd’hui peut présenter ce problème après que vous l’avez modifiée.
Cela affecte WPBakery Page Builder 9.0 et les versions ultérieures, avec WPML 4.9 et WPML 5.0.
Solution de contournement
Faites une sauvegarde complète de votre site avant de commencer.
- Créez le dossier
wp-content/mu-plugins/s’il n’existe pas sur votre site. - Créez le fichier
wp-content/mu-plugins/wpml-wpbakery-9-linked-images.php. Il s’agit d’une extension Must-Use (mu-plugin). Elle se charge automatiquement et il n’y a rien à activer. Ajoutez ce contenu :
Afficher le code
<?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
);
- Ouvrez chaque page qui utilise une Single Image ou une Image Gallery avec un lien personnalisé. Enregistrez à nouveau la page. Cela enregistre le lien pour la traduction.
- Traduisez le lien dans WPML → Traduction de chaînes, ou dans l’éditeur de traduction avec le reste de la page.
- Ouvrez chaque page traduite qui a perdu son image et enregistrez-la à nouveau. Cela fait réapparaître l’image.
À quoi s’attendre
L’image apparaît à nouveau sur la page traduite. Le lien personnalisé devient traduisible, afin qu’une page traduite puisse pointer vers une cible traduite.
La solution de contournement couvre les éléments Single Image et Image Gallery. Ce sont les deux éléments qui stockent un lien de cette façon dans WPBakery 9.0.
Une page traduite en tant que duplicata conserve les liens de la page d’origine. WPML copie un duplicata à partir de son original, la traduction des liens ne s’y applique donc pas.
Supprimez le fichier après avoir mis à jour WPML vers la version qui corrige ce problème.