---
title: "WPML 5.1.0 deja el contenido nuevo sin idioma en Percona XtraDB Cluster"
description: "WPML 5.1.0 no asigna idioma al contenido nuevo en Percona XtraDB Cluster en modo ENFORCING porque depende de GET_LOCK(). Causa, comprobación, mu-plugin puente hasta la 5.1.1 y herramienta para recuperar lo huérfano."
url: https://wpdirecto.com/wpml-5-1-0-percona-xtradb-cluster-contenido-sin-idioma/
date: 2026-10-19
modified: 2026-10-19
author: "Editor WPDirecto"
image: https://wpdirecto.com/wp-content/uploads/2026/10/wpml-5-1-0-percona-xtradb-cluster-contenido-sin-idioma.png
categories: ["Desarrollo", "Plugins"]
tags: ["base de datos", "mu-plugins", "multilingüe", "mysql", "WPML"]
type: post
lang: es
---

# WPML 5.1.0 deja el contenido nuevo sin idioma en Percona XtraDB Cluster

**WPML 5.1.0 deja de asignar idioma a todo el contenido nuevo cuando la base de datos es un Percona XtraDB Cluster con `pxc_strict_mode` en `ENFORCING`, que es su configuración por defecto.** Entradas, páginas, adjuntos y etiquetas se guardan sin error, pero no reciben su fila en la tabla de traducciones y desaparecen de los listados de wp-admin. WPML lo corrige en la versión 5.1.1, que por ahora solo está publicada como beta. Hasta que llegue la estable, publicamos el mu-plugin que usamos como puente en un sitio afectado y la herramienta para recuperar lo que ya nació sin idioma.

**Las claves del fallo de WPML 5.1.0 en Percona XtraDB Cluster en 20 segundos**

- **Qué pasa:** WPML 5.1.0 pide un bloqueo `GET_LOCK()` de MySQL para crear grupos de traducción nuevos y, si no lo obtiene, se sale sin escribir nada.
- **A quién afecta:** a sitios cuya base de datos es Percona XtraDB Cluster en modo estricto `ENFORCING`, que prohíbe `GET_LOCK()`. El contenido antiguo no se toca; solo falla lo nuevo.
- **Arreglo definitivo:** WPML 5.1.1, que cae a un bloqueo sobre la tabla de opciones. La última versión estable publicada sigue siendo la 5.1.0, del 29/09/2026.
- **Mientras tanto:** un mu-plugin que sustituye el bloqueo sin editar WPML, no cambia nada en un MySQL normal y se aparta solo al instalar la 5.1.1.

El caso lo diagnosticamos en un sitio multiidioma en producción, cuyo nombre omitimos. Lo que lo hizo difícil de ver es que durante semanas la interfaz de WordPress aseguraba que todo estaba en orden. Contamos el síntoma, cómo confirmarlo con dos consultas, por qué ocurre según el código fuente de WPML y cómo salir de él sin tocar los ficheros del plugin ni la configuración del clúster.

## Existe, se puede editar y no aparece en ningún listado

El primer aviso fue un artículo recién escrito que no salía en Entradas. Se podía abrir por su URL directa (`post.php?post=<id>&action=edit`), se guardaba con normalidad y, dentro del editor, el selector de idioma de WPML mostraba «Español». Los cientos de artículos antiguos estaban perfectos. Las etiquetas nuevas tampoco aparecían donde debían.

La explicación está en cómo filtra WPML. El plugin añade a cada `WP_Query` una condición por idioma, y un elemento que no tiene fila en la tabla `wp_icl_translations` no pertenece a ningún idioma, así que queda fuera del listado. El selector del editor decía «Español» porque, a falta de dato, WPML muestra el idioma por defecto del sitio. Era un valor de reserva, no un dato guardado. Ese detalle es el que despista: la pantalla afirma que el idioma está asignado y la base de datos dice lo contrario.

## Cómo comprobar en un minuto si te afecta

Dos consultas bastan. La primera dice qué hay debajo de WordPress; la segunda, si la tabla de traducciones sigue recibiendo filas. Hay que sustituir wp_ por el prefijo real de las tablas:

```
SELECT @@version_comment, @@pxc_strict_mode, @@wsrep_on;

SELECT element_type, COUNT(*) AS filas, MAX(translation_id) AS ultima
FROM wp_icl_translations
GROUP BY element_type;Lenguaje del código: CSS (css)
```

Si `@@version_comment` menciona Percona XtraDB Cluster, `pxc_strict_mode` vale `ENFORCING` y no hay filas nuevas desde la fecha en que se actualizó a WPML 5.x, es este caso. En el sitio que revisamos no se había insertado ni una sola fila, de ningún tipo de elemento, desde la actualización: la tabla estaba congelada para cualquier alta.

Para ver el inventario de lo que se ha quedado huérfano:

```
-- Entradas, páginas y adjuntos sin fila de idioma
SELECT p.ID, p.post_type, p.post_status, LEFT(p.post_title,60) AS titulo, p.post_date
FROM wp_posts p
LEFT JOIN wp_icl_translations t
       ON t.element_id = p.ID AND t.element_type = CONCAT('post_', p.post_type)
WHERE t.translation_id IS NULL
  AND p.post_type IN ('post','page','attachment')
ORDER BY p.post_date;

-- Términos sin fila de idioma
SELECT tt.term_taxonomy_id, tt.taxonomy, t2.name, tt.count
FROM wp_term_taxonomy tt
INNER JOIN wp_terms t2 ON t2.term_id = tt.term_id
LEFT JOIN wp_icl_translations it
       ON it.element_id = tt.term_taxonomy_id AND it.element_type = CONCAT('tax_', tt.taxonomy)
WHERE it.translation_id IS NULL
ORDER BY tt.term_taxonomy_id;Lenguaje del código: PHP (php)
```

Ojo al leer la segunda: también devuelve términos de taxonomías internas que WPML no traduce, como `elementor_library_type`, `elementor_font_type`, `wp_theme` o `bsf_custom_fonts`. Esos no son huérfanos: nunca han tenido fila y es correcto que no la tengan. Solo cuentan `category`, `post_tag` y las taxonomías personalizadas marcadas como traducibles.

## GET_LOCK(), un bloqueo que el clúster se niega a dar

Percona XtraDB Cluster, en su modo estricto `ENFORCING`, rechaza `GET_LOCK()` y `RELEASE_LOCK()` y registra un error del tipo «Percona-XtraDB-Cluster prohibits use of GET_LOCK with pxc_strict_mode = ENFORCING». No es una mala configuración. Un bloqueo con nombre de MySQL vive en un solo nodo: si una conexión lo toma en el nodo 1, otra conexión en el nodo 2 puede tomar el mismo bloqueo sin enterarse. En un clúster multinodo no garantiza nada, y Percona prefiere negarlo antes que ofrecer una exclusión mutua falsa.

No es la primera aplicación que tropieza con esto. MediaWiki y Magento ya chocaron con la misma restricción hace años, y la respuesta de Percona fue siempre la misma: cambiar la aplicación para que use un bloqueo propio o relajar el modo estricto bajo responsabilidad del administrador.

El choque con WPML está en `classes/core-abstract-classes/class-wpml-set-language.php`, en el método `insert_new_row()`. Cuando el elemento es nuevo y todavía no tiene grupo de traducción (el `trid`), WPML intenta tomar un bloqueo durante un máximo de 15 segundos y, si falla, devuelve sin insertar:

```
if ( false === $trid ) {                     // contenido nuevo: aún no tiene grupo de traducción
    $trid_lock_acquired = $this->acquire_trid_allocation_lock();

    if ( ! $trid_lock_acquired ) {
        $this->log_trid_allocation_lock_failure( $el_id, $el_type, $language_code );
        return $el_id ? (int) $this->existing_element( $el_id, $el_type ) : 0;   // aborta sin insertar
    }
}Lenguaje del código: PHP (php)
```

El bloqueo, en `vendor/wpml/core-api/core/utility/AdvisoryLock.php` de la 5.1.0, no tiene alternativa:

```
public function acquire( int $timeoutSeconds ): bool {
    return '1' === $this->wpdb->get_var(
        $this->wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $this->name, $timeoutSeconds )
    );
}Lenguaje del código: PHP (php)
```

La cadena completa queda así:

1. Se guarda contenido nuevo, que no tiene `trid`, y WPML entra en la rama del bloqueo.
2. El clúster rechaza `GET_LOCK()` y `get_var()` devuelve `null`.
3. `'1' === null` es falso, así que `acquire()` devuelve `false` siempre.
4. `insert_new_row()` sale sin hacer el INSERT.
5. El elemento se queda sin idioma y WPML lo filtra de los listados.

El bloqueo tiene sentido: el `trid` nuevo se calcula como `MAX(trid) + 1`, y dos peticiones simultáneas podrían obtener el mismo número. Lo que falta es contemplar que la base de datos diga que no. Y aquí hay un detalle revelador: en la misma 5.1.0, la otra clase de bloqueo de WPML (`Lock.php`) sí detecta el rechazo y recurre a un mutex sobre la tabla de opciones. WPML ya daba por bueno ese sustituto; simplemente no se aplicó en `AdvisoryLock`. Parece una rama de error olvidada más que un problema de diseño.

Por qué solo falla lo nuevo también se explica por el código: la rama del bloqueo solo se recorre cuando `false === $trid`, es decir, cuando hay que crear un grupo de traducción. El contenido existente y las traducciones de contenido existente pasan un `trid` conocido y no la tocan. Por eso el fallo pasa desapercibido durante semanas.

### Daño colateral: la cola de String Translation atascada

En el mismo sitio había un aviso permanente de «2 pasos pendientes» en String Translation. La opción `wpml_st_deferred_upgrade` contenía dos tareas (`recompute-string-statuses` y `settings-rows-migration`) y el punto de control de la primera, `wpml_st_status_recompute_checkpoint`, no existía. Es decir, el primer paso nunca había completado ni su primer tramo. Todo apunta a que choca con el mismo tipo de bloqueo en cada carga de página, la cola nunca baja y el segundo paso nunca arranca. Una sola causa para dos síntomas que parecían independientes.

## Lo que corrige WPML 5.1.1

El registro de cambios de WPML 5.1.1-beta.2 recoge el arreglo: en clústeres que rechazan los bloqueos con nombre, como Percona XtraDB en modo estricto, WPML pasa por su cuenta a un bloqueo basado en tabla para que el administrador siga funcionando, y el instalador deja de registrar un error de base de datos en esos entornos. La nueva clase `LockBackend` intenta primero `GET_LOCK()`, memoriza el rechazo durante una hora en la opción `wpml_named_locks_refused_until` para no volver a probar en cada petición y usa una fila en `wp_options` con caducidad y propietario.

Además expone dos ajustes: el filtro `wpml_locks_backend`, que acepta `mysql`, `table` o `auto`, y la constante `WPML_DISABLE_MYSQL_LOCKS`, que fuerza el bloqueo por tabla. Ninguno de los dos existe en la 5.1.0, así que poner la constante en wp-config.php no sirve de nada todavía. En la 5.1.0 no hay nada que configurar: hay que sustituir la clase.

## Cuatro atajos que conviene descartar

- **Editar los ficheros de WPML.** Funciona hoy y se pierde en la siguiente actualización, que deja el sitio roto otra vez sin avisar y sin que nadie recuerde por qué.
- **Poner `pxc_strict_mode` en `PERMISSIVE`.** Rebaja las protecciones del clúster para todo el servidor y, como `GET_LOCK()` es local al nodo, lo que se compra es una garantía que no existe.
- **Borrar `wpml_st_deferred_upgrade`.** Es una cola activa cuyos pasos fallan por la misma causa. Borrarla deja la migración a medias y marcada como terminada.
- **Reescribir `GET_LOCK` como `SELECT 1` con el filtro `query` de `wpdb`.** Ese filtro pasa por todas las consultas del sitio, así que un fallo ahí tumba la web entera, y dejaría la asignación de `trid` sin protección, reabriendo la carrera que el bloqueo intenta evitar.

## El mu-plugin puente: cambiar la fábrica del bloqueo sin tocar WPML

WPML obtiene el bloqueo a través de su contenedor de inyección de dependencias, basado en Auryn:

```
$this->trid_allocation_lock = \WPML\Container\make( \WPML\Utilities\AdvisoryLockFactory::class )->create( 'trid' );Lenguaje del código: PHP (php)
```

Y la clase `WPML\Container\Container` ofrece `delegate()`, que asocia una clase a una función que la fabrica. Eso permite cambiar `AdvisoryLockFactory` por una propia desde un mu-plugin, sin modificar un solo fichero de WPML. Antes de fiarse hay que comprobar dos cosas en el código de Auryn: que `make()` consulta las instancias compartidas antes que las delegadas (WPML no comparte esta fábrica, la instancia cada vez) y que la delegación gana aunque ya se haya hecho un `make()` antes, porque el resultado no se guarda en caché.

Las decisiones de diseño del parche:

- **Es un mu-plugin**, así que sobrevive a las actualizaciones de WPML y se quita borrando un fichero.
- **Prueba primero `GET_LOCK()`** y solo toma el relevo si la base de datos lo rechaza. El mismo fichero es correcto en un clúster y en un MySQL convencional, donde no cambia nada.
- **Distingue «lo tiene otro» de «no se permite».** Si `GET_LOCK()` devuelve `0`, otra petición tiene el bloqueo y la respuesta es válida; recurrir a la tabla en ese caso rompería la exclusión mutua.
- **Memoriza el rechazo una hora**, para no pagar un intento fallido en cada petición.
- **Se desactiva solo** si existe la clase `LockBackend`, es decir, en WPML 5.1.1 o superior.
- **Usa los mismos nombres de fila que la 5.1.1** (`_wpml_lock_wpml_`), para que el paso a esa versión sea transparente.
- **Libera el bloqueo en `register_shutdown_function`** aunque PHP muera a mitad, admite reentrada con contador, caduca a los 300 segundos y un bloqueo caducado se recupera en lugar de atascar la petición.
- **Todas las consultas pasan por `$wpdb->prepare()`** y, si en el futuro desaparecen las clases que extiende, simplemente no se activa en lugar de provocar un error fatal.

Un detalle de implementación que no es capricho: las clases se declaran dentro de una función que se ejecuta en `plugins_loaded`. Un mu-plugin se carga antes que los plugins normales y, si las clases se declararan al leer el fichero, `extends \WPML\Utilities\AdvisoryLock` daría un error fatal porque la clase padre aún no existe.

### Instalación

1. Crear el fichero `wp-content/mu-plugins/wpdirecto-wpml-trid-lock-fix.php` con el código de abajo. Si la carpeta mu-plugins no existe, se crea.
2. No hace falta activarlo: WordPress carga los mu-plugins por estar en esa carpeta. Aparece en Plugins, pestaña «Imprescindibles».
3. Guardar una entrada de prueba y comprobar que ya tiene fila en `wp_icl_translations`.
4. Cuando WPML 5.1.1 o superior esté instalado, el mu-plugin deja de actuar y se puede borrar.

Funciona con PHP 7.2 o superior. Está pensado y probado para WPML 5.1.0; como antes de registrarse comprueba que existen `AdvisoryLock`, `AdvisoryLockFactory` y el método `delegate()`, en una versión que no tenga esas piezas no hace nada.

```
<?php
/**
 * Plugin Name: WPDirecto - WPML trid lock fix
 * Description: Corrige que WPML 5.1.0 no inserte filas en icl_translations (contenido nuevo sin idioma e invisible en el admin) en bases de datos que rechazan los bloqueos nombrados de MySQL, como Percona XtraDB Cluster con pxc_strict_mode = ENFORCING o Galera. Sustituye el bloqueo de asignacion de trid por un bloqueo equivalente sobre la tabla de opciones, el mismo mecanismo que WPML incorpora en 5.1.1. Comprueba primero si la base de datos acepta GET_LOCK y solo toma el relevo si lo rechaza. Se desactiva solo en WPML 5.1.1 o superior.
 * Version: 1.1.0
 * Requires PHP: 7.2
 * Author: David Carrero
 * Author URI: https://wpdirecto.com
 * License: GPL-2.0-or-later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 *
 * ---------------------------------------------------------------------------
 * QUE PROBLEMA RESUELVE
 *
 * WPML 5.1.0 protege la asignacion de un trid nuevo con un bloqueo nombrado de
 * MySQL. En classes/core-abstract-classes/class-wpml-set-language.php:
 *
 *     const TRID_ALLOCATION_LOCK_TIMEOUT_SECONDS = 15;
 *
 *     if ( false === $trid ) {
 *         $trid_lock_acquired = $this->acquire_trid_allocation_lock();
 *         if ( ! $trid_lock_acquired ) {
 *             $this->log_trid_allocation_lock_failure( ... );
 *             return $el_id ? (int) $this->existing_element( $el_id, $el_type ) : 0;
 *         }
 *     }
 *
 * Y el bloqueo, en vendor/wpml/core-api/core/utility/AdvisoryLock.php, es:
 *
 *     public function acquire( int $timeoutSeconds ): bool {
 *         return '1' === $this->wpdb->get_var(
 *             $this->wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $this->name, $timeoutSeconds )
 *         );
 *     }
 *
 * Sin ninguna alternativa. Percona XtraDB Cluster con pxc_strict_mode = ENFORCING
 * prohibe GET_LOCK (error 1792), get_var devuelve null, acquire() devuelve false
 * siempre y el INSERT nunca ocurre. Resultado: ninguna fila nueva en
 * icl_translations, de ningun element_type, y todo el contenido nuevo invisible
 * en el admin porque WPML filtra WP_Query por idioma.
 *
 * Solo afecta al contenido NUEVO: la rama del bloqueo se entra unicamente cuando
 * hace falta crear un grupo de traduccion nuevo (false === $trid). El contenido
 * existente pasa un trid y no la toca.
 *
 * WPML 5.1.1 lo corrige con la clase LockBackend, que cae a un bloqueo sobre la
 * tabla de opciones y expone el filtro wpml_locks_backend y la constante
 * WPML_DISABLE_MYSQL_LOCKS. Nada de eso existe en 5.1.0.
 *
 * ---------------------------------------------------------------------------
 * COMO SABER SI TE AFECTA
 *
 *     SELECT @@version_comment, @@pxc_strict_mode, @@wsrep_on;
 *
 * Y si no aparece ninguna fila nueva en wp_icl_translations desde la fecha en que
 * actualizaste a WPML 5.x, te afecta.
 *
 * ---------------------------------------------------------------------------
 * INSTALACION
 *
 * Subir este fichero a wp-content/mu-plugins/. Se activa solo por estar ahi.
 * Para desactivarlo, borrar el fichero.
 *
 * NO resucita el contenido que ya nacio sin fila. Para eso, despues de instalarlo,
 * usar WPML -> Support -> Fix Language Data -> "Set language information"
 * (requiere el plugin WPML Troubleshooting), o volver a guardar cada elemento.
 *
 * ---------------------------------------------------------------------------
 * ESTO ES UN PUENTE, NO UN DESTINO
 *
 * Mientras WPML siga en 5.1.0 el fichero tiene que quedarse. El arreglo definitivo
 * es actualizar a WPML 5.1.1 o superior; entonces este mu-plugin se aparta solo y
 * se puede borrar.
 */

defined( 'ABSPATH' ) || exit;

add_action( 'plugins_loaded', 'wpdirecto_wpml_lock_fix_boot', -PHP_INT_MAX );

/**
 * Registra la sustitucion en el contenedor de WPML, si y solo si hace falta.
 */
function wpdirecto_wpml_lock_fix_boot() {

	// WPML 5.1.1 o superior ya trae LockBackend: este puente se aparta solo.
	if ( class_exists( '\WPML\Utilities\LockBackend' ) ) {
		return;
	}

	if ( ! class_exists( '\WPML\Utilities\AdvisoryLock' )
		|| ! class_exists( '\WPML\Utilities\AdvisoryLockFactory' )
		|| ! class_exists( '\WPML\Container\Container' )
		|| ! method_exists( '\WPML\Container\Container', 'delegate' )
	) {
		return;
	}

	wpdirecto_wpml_lock_fix_define_classes();

	if ( ! class_exists( 'WPDirecto_WPML_Lock_Factory', false ) ) {
		return;
	}

	\WPML\Container\Container::delegate(
		array(
			'WPML\Utilities\AdvisoryLockFactory' => function () {
				return new WPDirecto_WPML_Lock_Factory();
			},
		)
	);
}

/**
 * Las clases se declaran aqui y no al cargar el fichero, porque un mu-plugin se
 * carga antes que WPML y la clase padre todavia no existiria.
 */
function wpdirecto_wpml_lock_fix_define_classes() {

	if ( class_exists( 'WPDirecto_WPML_Lock', false ) ) {
		return;
	}

	/**
	 * Bloqueo con dos backends, equivalente a LockBackend de WPML 5.1.1.
	 *
	 * Intenta primero el bloqueo nombrado de MySQL, que es lo que WPML quiere. Solo
	 * si la base de datos lo rechaza pasa a un bloqueo sobre la tabla de opciones.
	 * En ambos casos es exclusion mutua real: una sola peticion lo tiene a la vez,
	 * se libera al terminar la peticion aunque PHP muera, y caduca por TTL.
	 *
	 * Los nombres de fila son identicos a los de WPML 5.1.1 para que la transicion a
	 * esa version sea transparente.
	 */
	class WPDirecto_WPML_Lock extends \WPML\Utilities\AdvisoryLock {

		const BACKEND_MYSQL     = 'mysql';
		const BACKEND_TABLE     = 'table';
		const ROW_PREFIX        = '_wpml_lock_';
		const DEFAULT_TTL       = 300;
		const POLL_MICROSECONDS = 100000;
		const REFUSED_OPTION    = 'wpdirecto_wpml_named_locks_refused_until';
		const REFUSED_SECONDS   = 3600;
		const FILTER_BACKEND    = 'wpdirecto_wpml_locks_backend';

		/** @var array<string,array{owner:string,count:int,backend:string}> */
		private static $held = array();

		/** @var bool|null */
		private static $refused = null;

		/** @var \wpdb|null */
		private static $shutdown_wpdb = null;

		/** @var \wpdb */
		private $db;

		/** @var string Nombre del bloqueo nombrado, igual que lo calcula WPML. */
		private $lock_name;

		/** @var string Nombre de la fila de opciones, igual que en WPML 5.1.1. */
		private $row;

		/** @var int */
		private $ttl;

		public function __construct( \wpdb $wpdb, $name, $ttl_seconds = 0 ) {
			parent::__construct( $wpdb, (string) $name );

			$this->db        = $wpdb;
			$this->lock_name = 'wpml_' . md5( $wpdb->dbname . '|' . $wpdb->prefix . '|' . $name );
			$this->row       = self::ROW_PREFIX . $this->lock_name;
			$this->ttl       = (int) $ttl_seconds > 0 ? (int) $ttl_seconds : self::DEFAULT_TTL;
		}

		public function acquire( int $timeout_seconds ): bool {
			if ( isset( self::$held[ $this->row ] ) ) {
				++self::$held[ $this->row ]['count'];

				return true;
			}

			if ( self::BACKEND_TABLE !== $this->backend() ) {
				$named = $this->try_named_lock( $timeout_seconds );

				if ( true === $named ) {
					self::$held[ $this->row ] = array(
						'owner'   => '',
						'count'   => 1,
						'backend' => self::BACKEND_MYSQL,
					);

					return true;
				}

				// '0': lo tiene otra peticion. Respuesta valida, no hay que recurrir a nada.
				if ( false === $named ) {
					return false;
				}
			}

			return $this->acquire_row( $timeout_seconds );
		}

		public function release(): void {
			if ( ! isset( self::$held[ $this->row ] ) ) {
				return;
			}

			--self::$held[ $this->row ]['count'];
			if ( self::$held[ $this->row ]['count'] > 0 ) {
				return;
			}

			$backend = self::$held[ $this->row ]['backend'];
			$owner   = self::$held[ $this->row ]['owner'];
			unset( self::$held[ $this->row ] );

			if ( self::BACKEND_MYSQL === $backend ) {
				$this->quietly(
					function () {
						return $this->db->query( $this->db->prepare( 'SELECT RELEASE_LOCK(%s)', $this->lock_name ) );
					}
				);

				return;
			}

			self::delete_row( $this->db, $this->row, $owner );
		}

		/**
		 * Backend a usar: 'mysql', 'table', o null para decidirlo sobre la marcha.
		 */
		private function backend() {
			$forced = apply_filters( self::FILTER_BACKEND, 'auto' );
			if ( self::BACKEND_MYSQL === $forced || self::BACKEND_TABLE === $forced ) {
				return $forced;
			}

			if ( defined( 'WPML_DISABLE_MYSQL_LOCKS' ) && WPML_DISABLE_MYSQL_LOCKS ) {
				return self::BACKEND_TABLE;
			}

			return $this->named_locks_refused() ? self::BACKEND_TABLE : null;
		}

		/**
		 * true si lo obtuvo, false si lo tiene otro, null si la base de datos lo rechaza.
		 */
		private function try_named_lock( $timeout_seconds ) {
			$result = $this->quietly(
				function () use ( $timeout_seconds ) {
					return $this->db->get_var(
						$this->db->prepare( 'SELECT GET_LOCK(%s, %d)', $this->lock_name, (int) $timeout_seconds )
					);
				}
			);

			if ( '1' === (string) $result ) {
				$this->note_named_locks_work();

				return true;
			}

			if ( '0' === (string) $result ) {
				return false;
			}

			$this->note_refusal();

			return null;
		}

		private function named_locks_refused() {
			if ( null === self::$refused ) {
				$until         = (int) get_option( self::REFUSED_OPTION, 0 );
				self::$refused = $until > time();
			}

			return self::$refused;
		}

		private function note_refusal() {
			$error = isset( $this->db->last_error ) ? (string) $this->db->last_error : '';
			if ( '' === $error ) {
				return;
			}

			self::$refused = true;
			update_option( self::REFUSED_OPTION, time() + self::REFUSED_SECONDS, true );

			do_action( 'wpdirecto_wpml_locks_backend_switched', self::BACKEND_TABLE, $error );

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( '[WPDirecto] La base de datos rechaza los bloqueos nombrados de MySQL (' . $error . '); los bloqueos de WPML pasan a la tabla de opciones durante una hora.' );
			}
		}

		private function note_named_locks_work() {
			if ( self::$refused ) {
				self::$refused = false;
				delete_option( self::REFUSED_OPTION );
			}
		}

		private function acquire_row( $timeout_seconds ) {
			$owner    = bin2hex( random_bytes( 8 ) );
			$deadline = microtime( true ) + max( 0, (int) $timeout_seconds );

			while ( true ) {
				if ( $this->claim_row( $owner ) ) {
					self::$held[ $this->row ] = array(
						'owner'   => $owner,
						'count'   => 1,
						'backend' => self::BACKEND_TABLE,
					);
					$this->register_shutdown();

					return true;
				}

				$left = $deadline - microtime( true );
				if ( $left <= 0 ) {
					return false;
				}

				usleep( (int) min( self::POLL_MICROSECONDS, $left * 1000000 ) );
			}
		}

		/**
		 * Toma la fila si no existe, o si existe pero ya ha caducado.
		 */
		private function claim_row( $owner ) {
			$options = $this->db->options;

			$inserted = $this->db->query(
				$this->db->prepare(
					"INSERT IGNORE INTO {$options} (option_name, option_value, autoload) VALUES (%s, CONCAT(UNIX_TIMESTAMP() + %d, ':', %s), 'no') /* WPML LOCK */",
					$this->row,
					$this->ttl,
					$owner
				)
			);

			if ( 1 === (int) $inserted ) {
				return true;
			}

			$taken = $this->db->query(
				$this->db->prepare(
					"UPDATE {$options} SET option_value = CONCAT(UNIX_TIMESTAMP() + %d, ':', %s) WHERE option_name = %s AND CAST(SUBSTRING_INDEX(option_value, ':', 1) AS UNSIGNED) < UNIX_TIMESTAMP() /* WPML LOCK */",
					$this->ttl,
					$owner,
					$this->row
				)
			);

			return 1 === (int) $taken;
		}

		private static function delete_row( \wpdb $wpdb, $row, $owner ) {
			$wpdb->query(
				$wpdb->prepare(
					"DELETE FROM {$wpdb->options} WHERE option_name = %s AND SUBSTRING_INDEX(option_value, ':', -1) = %s /* WPML LOCK */",
					$row,
					$owner
				)
			);
		}

		private function register_shutdown() {
			if ( ! self::$shutdown_wpdb ) {
				register_shutdown_function( array( __CLASS__, 'release_all_on_shutdown' ) );
			}
			self::$shutdown_wpdb = $this->db;
		}

		/**
		 * Red de seguridad: si la peticion muere con el bloqueo tomado, se libera igual.
		 */
		public static function release_all_on_shutdown() {
			if ( ! self::$shutdown_wpdb ) {
				return;
			}

			foreach ( self::$held as $row => $data ) {
				if ( self::BACKEND_TABLE === $data['backend'] ) {
					self::delete_row( self::$shutdown_wpdb, $row, $data['owner'] );
				}
			}

			self::$held = array();
		}

		/**
		 * Ejecuta una consulta sin que un error de la base de datos se imprima ni
		 * ensucie el log: aqui un rechazo es informacion, no un fallo.
		 */
		private function quietly( callable $statement ) {
			$previous                     = isset( $this->db->suppress_errors ) ? $this->db->suppress_errors : false;
			$this->db->suppress_errors    = true;

			try {
				return $statement();
			} finally {
				$this->db->suppress_errors = $previous;
			}
		}
	}

	/**
	 * Fabrica que devuelve el bloqueo con los dos backends.
	 *
	 * El segundo parametro es opcional para aceptar tanto la llamada de WPML 5.1.0
	 * (create('trid')) como la de 5.1.1 (create('trid', 30)).
	 */
	class WPDirecto_WPML_Lock_Factory extends \WPML\Utilities\AdvisoryLockFactory {

		public function create( string $name, int $ttl_seconds = 0 ): \WPML\Utilities\AdvisoryLock {
			global $wpdb;

			return new WPDirecto_WPML_Lock( $wpdb, $name, $ttl_seconds );
		}
	}
}Lenguaje del código: HTML, XML (xml)
```

## Recuperar el contenido que ya nació sin idioma

El mu-plugin arregla las inserciones a partir de su instalación. No resucita lo que se creó mientras WPML no podía escribir. Para eso hay que asignar idioma a cada elemento huérfano, y aquí hay una trampa.

Volver a guardar la entrada por la API REST desde fuera, o con WP-CLI, no crea la fila. En `inc/post-translation/wpml-admin-post-actions.class.php`, WPML solo procesa el guardado si viene de la pantalla de edición o de una petición REST cuyo `Referer` sea esa pantalla, y `is_post_edit_page()` exige que contenga `wp-admin/post.php`, `post-new.php` o `edit.php`. Una llamada externa no lo cumple y WPML se sale antes de hacer nada. La consecuencia práctica es buena: abrir la entrada en el editor de bloques y pulsar «Actualizar» sí funciona, porque el editor envía ese `Referer`. Para unas pocas entradas es la vía más sencilla.

Para el resto hay dos opciones:

- **WPML Troubleshooting.** En WPML → Soporte → Fix Language Data, la opción «Set language information» asigna idioma al contenido que no lo tiene. Requiere el plugin WPML Troubleshooting. WPML la considera segura, pero no documenta qué idioma pone; lo previsible es el idioma por defecto del sitio. Copia de seguridad antes y, si el volumen es grande, prueba en un entorno de staging.
- **Una herramienta de reparación por lotes**, que es la que publicamos a continuación y la que usamos en el caso.

La herramienta añade el menú Herramientas → WPML: contenido sin idioma, lista los huérfanos y asigna idioma a los que se marquen. Tres decisiones la hacen segura. No escribe en la base de datos por su cuenta: llama a la acción oficial `wpml_set_element_language_details` con `trid` y `source_language_code` a `null`, para que WPML cree un grupo de traducción nuevo como contenido original y se disparen los hooks que otros plugins puedan estar escuchando. Pregunta a WPML qué es traducible con `is_translated_post_type()` e `is_translated_taxonomy()`, en lugar de usar una lista propia, lo que evita tocar las taxonomías internas citadas antes. Y no ejecuta nada sin confirmación: muestra el listado, deja elegir idioma y elementos, verifica nonce y permisos, y avisa de lo que está en la papelera, que muchas veces conviene borrar en vez de reparar.

Es de un solo uso: se sube a `wp-content/mu-plugins/`, se repara y se borra el fichero. Antes, copia de seguridad de la base de datos. Si al usarla fallan todos los elementos, la causa de fondo sigue sin corregir y el mu-plugin anterior no está funcionando.

```
<?php
/**
 * Plugin Name: WPDirecto - WPML reparar contenido sin idioma
 * Description: Herramienta de un solo uso para asignar idioma al contenido que quedo sin fila en icl_translations (invisible en el admin) mientras WPML no podia insertarlas. Anade Herramientas -> WPML: contenido sin idioma, con listado previo y reparacion por lotes usando la API oficial de WPML. Solo mira tipos de contenido y taxonomias que WPML considera traducibles. No ejecuta nada sin confirmacion.
 * Version: 1.0.0
 * Requires PHP: 7.2
 * Author: David Carrero
 * Author URI: https://wpdirecto.com
 * License: GPL-2.0-or-later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 *
 * ---------------------------------------------------------------------------
 * PARA QUE SIRVE
 *
 * Si WPML ha estado un tiempo sin poder escribir en wp_icl_translations (por
 * ejemplo por el fallo del bloqueo GET_LOCK en Percona XtraDB Cluster, que
 * corrige el mu-plugin wpdirecto-wpml-trid-lock-fix.php), todo el contenido
 * creado en ese periodo quedo sin idioma asignado y por tanto invisible en los
 * listados del admin, porque WPML filtra WP_Query por idioma.
 *
 * Arreglar la causa NO resucita ese contenido: hay que asignarle el idioma.
 * Esta herramienta lo localiza y lo asigna llamando a la API oficial de WPML:
 *
 *     do_action( 'wpml_set_element_language_details', array(
 *         'element_id'           => $id,
 *         'element_type'         => 'post_post',
 *         'trid'                 => null,      // null = crear grupo nuevo
 *         'language_code'        => 'es',
 *         'source_language_code' => null,      // es un original, no una traduccion
 *     ) );
 *
 * Nada de INSERT a mano: asi el trid se asigna como WPML espera y se disparan
 * los hooks que otros plugins puedan estar escuchando.
 *
 * ---------------------------------------------------------------------------
 * COMO USARLA
 *
 * 1. Subir a wp-content/mu-plugins/ (o a plugins/ y activarla).
 * 2. Herramientas -> WPML: contenido sin idioma.
 * 3. Revisar el listado. Elegir el idioma. Marcar lo que se quiera reparar.
 * 4. Reparar. Volver a comprobar.
 * 5. BORRAR EL FICHERO cuando se termine. Es de un solo uso.
 *
 * Antes de usarla: copia de seguridad de la base de datos.
 *
 * ---------------------------------------------------------------------------
 * POR QUE NO BASTA CON VOLVER A GUARDAR POR REST O WP-CLI
 *
 * WPML solo asigna el idioma si el guardado viene de la pantalla de edicion.
 * En inc/post-translation/wpml-admin-post-actions.class.php:
 *
 *     if ( ! $this->has_save_post_action( $post )
 *          && ! $http_referer->is_rest_request_called_from_post_edit_page() ) {
 *         return;
 *     }
 *
 * Y is_post_edit_page() exige que el Referer contenga wp-admin/post.php,
 * post-new.php o edit.php. Una llamada REST externa no lo cumple y WPML se sale
 * antes de hacer nada. Guardar desde el editor de bloques SI funciona; esta
 * herramienta existe para no tener que abrir y guardar decenas de elementos a
 * mano, y para los adjuntos y terminos, que no se guardan en ese editor.
 */

defined( 'ABSPATH' ) || exit;

add_action( 'admin_menu', 'wpdirecto_wmrl_menu' );

function wpdirecto_wmrl_cap() {
	return 'manage_options';
}

function wpdirecto_wmrl_menu() {
	add_management_page(
		__( 'WPML: contenido sin idioma', 'wpdirecto-wmrl' ),
		__( 'WPML: contenido sin idioma', 'wpdirecto-wmrl' ),
		wpdirecto_wmrl_cap(),
		'wpdirecto-wmrl',
		'wpdirecto_wmrl_page'
	);
}

/**
 * Tipos de contenido que WPML considera traducibles.
 *
 * Importante: preguntarselo a WPML y no inventar la lista. Hay taxonomias
 * internas (elementor_library_type, wp_theme, bsf_custom_fonts...) que NUNCA
 * tienen fila en icl_translations y es correcto que no la tengan. Tocarlas
 * seria crear basura.
 */
function wpdirecto_wmrl_translatable_post_types() {
	global $sitepress;

	$types = array();
	foreach ( get_post_types( array(), 'names' ) as $type ) {
		if ( method_exists( $sitepress, 'is_translated_post_type' ) && $sitepress->is_translated_post_type( $type ) ) {
			$types[] = $type;
		}
	}

	return $types;
}

function wpdirecto_wmrl_translatable_taxonomies() {
	global $sitepress;

	$taxes = array();
	foreach ( get_taxonomies( array(), 'names' ) as $tax ) {
		if ( method_exists( $sitepress, 'is_translated_taxonomy' ) && $sitepress->is_translated_taxonomy( $tax ) ) {
			$taxes[] = $tax;
		}
	}

	return $taxes;
}

/**
 * Posts de tipos traducibles sin fila de idioma.
 */
function wpdirecto_wmrl_find_posts( $limit = 200 ) {
	global $wpdb;

	$types = wpdirecto_wmrl_translatable_post_types();
	if ( ! $types ) {
		return array();
	}

	$ph  = implode( ', ', array_fill( 0, count( $types ), '%s' ) );
	$sql = "SELECT p.ID, p.post_type, p.post_status, p.post_title, p.post_date
			FROM {$wpdb->posts} p
			LEFT JOIN {$wpdb->prefix}icl_translations t
				   ON t.element_id = p.ID
				  AND t.element_type = CONCAT('post_', p.post_type)
			WHERE t.translation_id IS NULL
			  AND p.post_type IN ( $ph )
			  AND p.post_status <> 'auto-draft'
			ORDER BY p.post_date DESC
			LIMIT %d";

	$args   = $types;
	$args[] = (int) $limit;

	return $wpdb->get_results( $wpdb->prepare( $sql, $args ) );
}

/**
 * Terminos de taxonomias traducibles sin fila de idioma.
 */
function wpdirecto_wmrl_find_terms( $limit = 200 ) {
	global $wpdb;

	$taxes = wpdirecto_wmrl_translatable_taxonomies();
	if ( ! $taxes ) {
		return array();
	}

	$ph  = implode( ', ', array_fill( 0, count( $taxes ), '%s' ) );
	$sql = "SELECT tt.term_taxonomy_id, tt.term_id, tt.taxonomy, tm.name, tt.count
			FROM {$wpdb->term_taxonomy} tt
			INNER JOIN {$wpdb->terms} tm ON tm.term_id = tt.term_id
			LEFT JOIN {$wpdb->prefix}icl_translations it
				   ON it.element_id = tt.term_taxonomy_id
				  AND it.element_type = CONCAT('tax_', tt.taxonomy)
			WHERE it.translation_id IS NULL
			  AND tt.taxonomy IN ( $ph )
			ORDER BY tt.term_taxonomy_id DESC
			LIMIT %d";

	$args   = $taxes;
	$args[] = (int) $limit;

	return $wpdb->get_results( $wpdb->prepare( $sql, $args ) );
}

/**
 * Asigna el idioma a un elemento con la API oficial de WPML y verifica el resultado.
 *
 * @return bool true si al terminar existe la fila.
 */
function wpdirecto_wmrl_assign( $element_id, $element_type, $language_code ) {
	global $wpdb;

	do_action(
		'wpml_set_element_language_details',
		array(
			'element_id'           => (int) $element_id,
			'element_type'         => $element_type,
			'trid'                 => null,
			'language_code'        => $language_code,
			'source_language_code' => null,
		)
	);

	if ( 0 === strpos( $element_type, 'post_' ) ) {
		clean_post_cache( (int) $element_id );
	}

	$row = $wpdb->get_var(
		$wpdb->prepare(
			"SELECT translation_id FROM {$wpdb->prefix}icl_translations WHERE element_id = %d AND element_type = %s",
			(int) $element_id,
			$element_type
		)
	);

	return ! empty( $row );
}

function wpdirecto_wmrl_page() {
	global $sitepress;

	if ( ! current_user_can( wpdirecto_wmrl_cap() ) ) {
		wp_die( esc_html__( 'Sin permisos suficientes.', 'wpdirecto-wmrl' ) );
	}

	echo '<div class="wrap"><h1>' . esc_html__( 'WPML: contenido sin idioma', 'wpdirecto-wmrl' ) . '</h1>';

	if ( ! isset( $sitepress ) || ! is_object( $sitepress ) ) {
		echo '<div class="notice notice-error"><p>' . esc_html__( 'WPML no esta activo.', 'wpdirecto-wmrl' ) . '</p></div></div>';
		return;
	}

	$languages = apply_filters( 'wpml_active_languages', null );
	$default   = $sitepress->get_default_language();

	// ---------- Reparar ----------
	if ( isset( $_POST['wpdirecto_wmrl_run'] ) ) {
		check_admin_referer( 'wpdirecto_wmrl' );

		$lang = isset( $_POST['wpdirecto_wmrl_lang'] ) ? sanitize_text_field( wp_unslash( $_POST['wpdirecto_wmrl_lang'] ) ) : '';
		if ( ! is_array( $languages ) || ! isset( $languages[ $lang ] ) ) {
			echo '<div class="notice notice-error"><p>' . esc_html__( 'Idioma no valido.', 'wpdirecto-wmrl' ) . '</p></div>';
		} else {
			$items = isset( $_POST['wpdirecto_wmrl_item'] ) ? (array) wp_unslash( $_POST['wpdirecto_wmrl_item'] ) : array();
			$done  = 0;
			$fail  = array();

			foreach ( $items as $raw ) {
				$parts = explode( '|', sanitize_text_field( $raw ) );
				if ( 2 !== count( $parts ) ) {
					continue;
				}

				list( $element_id, $element_type ) = $parts;
				$element_id = absint( $element_id );

				// El tipo tiene que ser uno de los que la propia herramienta listo.
				if ( ! $element_id || ! preg_match( '/^(post|tax)_[A-Za-z0-9_\-]+$/', $element_type ) ) {
					continue;
				}

				if ( wpdirecto_wmrl_assign( $element_id, $element_type, $lang ) ) {
					++$done;
				} else {
					$fail[] = $element_id . ' (' . $element_type . ')';
				}
			}

			echo '<div class="notice notice-success"><p>' . sprintf(
				/* translators: %1$d elementos reparados, %2$s codigo de idioma */
				esc_html__( 'Reparados %1$d elementos con el idioma %2$s.', 'wpdirecto-wmrl' ),
				(int) $done,
				esc_html( $lang )
			) . '</p></div>';

			if ( $fail ) {
				echo '<div class="notice notice-error"><p>' . esc_html__( 'No se pudo asignar idioma a:', 'wpdirecto-wmrl' ) . ' '
					. esc_html( implode( ', ', $fail ) ) . '</p><p>'
					. esc_html__( 'Si fallan todos, la causa de fondo sigue sin corregir: WPML no puede insertar en icl_translations.', 'wpdirecto-wmrl' )
					. '</p></div>';
			}
		}
	}

	// ---------- Listado ----------
	$posts = wpdirecto_wmrl_find_posts();
	$terms = wpdirecto_wmrl_find_terms();

	if ( ! $posts && ! $terms ) {
		echo '<div class="notice notice-success"><p><strong>'
			. esc_html__( 'No queda contenido sin idioma. Ya puedes borrar este fichero.', 'wpdirecto-wmrl' )
			. '</strong></p></div></div>';
		return;
	}

	echo '<p>' . esc_html__( 'Estos elementos no tienen fila en icl_translations, por lo que WPML los excluye de los listados del admin. Solo se muestran tipos y taxonomias que WPML considera traducibles.', 'wpdirecto-wmrl' ) . '</p>';
	echo '<p><em>' . esc_html__( 'Haz copia de seguridad de la base de datos antes de reparar.', 'wpdirecto-wmrl' ) . '</em></p>';

	echo '<form method="post">';
	wp_nonce_field( 'wpdirecto_wmrl' );

	echo '<p><label for="wpdirecto_wmrl_lang"><strong>' . esc_html__( 'Asignar este idioma:', 'wpdirecto-wmrl' ) . '</strong></label> ';
	echo '<select name="wpdirecto_wmrl_lang" id="wpdirecto_wmrl_lang">';
	if ( is_array( $languages ) ) {
		foreach ( $languages as $code => $lang ) {
			printf(
				'<option value="%s"%s>%s (%s)</option>',
				esc_attr( $code ),
				selected( $code, $default, false ),
				esc_html( isset( $lang['native_name'] ) ? $lang['native_name'] : $code ),
				esc_html( $code )
			);
		}
	}
	echo '</select></p>';

	echo '<table class="widefat striped"><thead><tr>'
		. '<td class="check-column"><input type="checkbox" onclick="jQuery(\'.wpdirecto-wmrl-cb\').prop(\'checked\', this.checked);" /></td>'
		. '<th>' . esc_html__( 'Elemento', 'wpdirecto-wmrl' ) . '</th>'
		. '<th>' . esc_html__( 'Tipo', 'wpdirecto-wmrl' ) . '</th>'
		. '<th>' . esc_html__( 'Estado', 'wpdirecto-wmrl' ) . '</th>'
		. '<th>' . esc_html__( 'ID', 'wpdirecto-wmrl' ) . '</th>'
		. '</tr></thead><tbody>';

	foreach ( $posts as $p ) {
		$type  = 'post_' . $p->post_type;
		$value = $p->ID . '|' . $type;
		$title = '' !== trim( (string) $p->post_title ) ? $p->post_title : __( '(sin titulo)', 'wpdirecto-wmrl' );

		echo '<tr><th class="check-column"><input class="wpdirecto-wmrl-cb" type="checkbox" name="wpdirecto_wmrl_item[]" value="'
			. esc_attr( $value ) . '" checked /></th>';
		echo '<td><a href="' . esc_url( get_edit_post_link( $p->ID ) ) . '">' . esc_html( $title ) . '</a></td>';
		echo '<td>' . esc_html( $p->post_type ) . '</td>';
		echo '<td>' . esc_html( $p->post_status ) . ( 'trash' === $p->post_status ? ' <strong>' . esc_html__( '(en la papelera: valora borrarlo en vez de repararlo)', 'wpdirecto-wmrl' ) . '</strong>' : '' ) . '</td>';
		echo '<td>' . (int) $p->ID . '</td></tr>';
	}

	foreach ( $terms as $t ) {
		$type  = 'tax_' . $t->taxonomy;
		$value = $t->term_taxonomy_id . '|' . $type;

		echo '<tr><th class="check-column"><input class="wpdirecto-wmrl-cb" type="checkbox" name="wpdirecto_wmrl_item[]" value="'
			. esc_attr( $value ) . '" checked /></th>';
		echo '<td>' . esc_html( $t->name ) . '</td>';
		echo '<td>' . esc_html( $t->taxonomy ) . '</td>';
		echo '<td>' . sprintf( esc_html__( '%d usos', 'wpdirecto-wmrl' ), (int) $t->count ) . '</td>';
		echo '<td>ttid ' . (int) $t->term_taxonomy_id . '</td></tr>';
	}

	echo '</tbody></table>';

	submit_button( __( 'Asignar idioma a los elementos marcados', 'wpdirecto-wmrl' ), 'primary', 'wpdirecto_wmrl_run' );
	echo '</form></div>';
}Lenguaje del código: HTML, XML (xml)
```

Una advertencia más. En el foro de soporte de WPML circulan recetas SQL para «arreglar» la tabla de traducciones tocando el AUTO_INCREMENT o borrando supuestas entradas fantasma, y al menos a un usuario le destrozaron los datos de idioma. Con la causa identificada no hace falta ninguna.

## Cómo se probó antes de llevarlo a producción

El mu-plugin se probó antes de instalarlo, contra copias literales de las firmas de WPML 5.1.0 y contra el contenedor Auryn real extraído del paquete del plugin, con un wpdb simulado que reproduce el índice único de option_name y un interruptor para imitar el modo estricto. Son 30 pruebas, todas superadas. Las que más importan:

- En una base de datos que acepta `GET_LOCK()`, usa el bloqueo con nombre y no escribe ninguna fila en `wp_options`.
- En una que lo rechaza, consigue el bloqueo igualmente, cae a la tabla, memoriza el rechazo y no vuelve a intentarlo durante una hora.
- Con el bloqueo tomado, una segunda petición es rechazada al agotar su tiempo de espera. Es la prueba que distingue un bloqueo real de un simple return true.
- Un bloqueo caducado se recupera, la reentrada no libera antes de tiempo y una liberación ajena no borra la fila de otro.
- Con el contenedor real, la fábrica original de WPML reproduce el fallo; tras plugins_loaded se sirve la nueva, que adquiere el bloqueo; y con LockBackend presente el mu-plugin no toca nada.

La herramienta de reparación tiene otras 35 pruebas: filtrado de tipos traducibles con exclusión de las taxonomías internas, consultas preparadas con tantos marcadores como argumentos, unión de términos por term_taxonomy_id, rechazo de entradas manipuladas en el formulario, permisos, nonce, escapado de la salida y la comprobación de que el fichero no usa ningún método de escritura de wpdb. Esa última dio verde al principio por un motivo equivocado (encontraba la palabra «insertar» en un comentario) y hubo que reescribirla. Una prueba que pasa por la razón incorrecta es peor que no tenerla.

## Lo que deja este caso

La lección más útil no es técnica. Durante semanas la interfaz mostraba un idioma que no estaba guardado en ninguna parte. Cuando el síntoma es «existe pero no se ve», hay que mirar la tabla y no la pantalla. La segunda es de arquitectura: cada vez más sitios WordPress corren sobre bases de datos en clúster, y el software escrito pensando en un único MySQL da por hecho primitivas que ahí no están disponibles. El clúster hacía exactamente lo que tenía que hacer; lo que faltaba era una rama de error en el plugin. Quien gestione webs multiidioma puede repasar también [cómo automatizar un sitio multilingüe con WPML y Polylang](https://wpdirecto.com/como-automatizar-un-sitio-multilingue-en-wordpress-con-inteligencia-artificial-wpml-y-polylang/) o las [alternativas a Weglot para webs multilingües](https://wpdirecto.com/alternativas-a-weglot-para-webs-multilingues-en-2025-guia-mejorada-para-sitios-wordpress/).

## Preguntas frecuentes

### ¿Me afecta si mi hosting usa MySQL o MariaDB normal?

No. El fallo aparece cuando la base de datos rechaza GET_LOCK(), como hace Percona XtraDB Cluster en modo ENFORCING. En un MySQL o MariaDB de un solo servidor el bloqueo funciona y el mu-plugin no cambia nada si se instala.

### ¿Sirve definir WPML_DISABLE_MYSQL_LOCKS en wp-config.php?

En WPML 5.1.0, no: esa constante y el filtro wpml_locks_backend llegan con la 5.1.1. El mu-plugin sí respeta la constante, así que se puede dejar puesta.

### ¿Qué hago con el mu-plugin cuando actualice WPML?

Nada urgente. Si detecta la clase LockBackend de WPML 5.1.1 o superior, deja de actuar. A partir de ahí se puede borrar el fichero.

### ¿El mu-plugin recupera el contenido que ya no aparece?

No. Solo hace que las nuevas inserciones funcionen. Lo creado antes se recupera guardándolo de nuevo desde el editor, con la opción «Set language information» de WPML Troubleshooting o con la herramienta de reparación por lotes.

Fuentes:

- [WPML: Versions and Changelogs](https://wpml.org/version/)
- [WPML Multilingual CMS 5.1.0](https://wpml.org/version/wpml-5-1-0/)
- [Percona: Percona XtraDB Cluster strict mode](https://docs.percona.com/percona-xtradb-cluster/LATEST/strict-mode.html)
- [Foro de Percona: GET_LOCK y Magento en XtraDB Cluster](https://forums.percona.com/t/we-have-installed-magento-2-3-4-with-parcona-xtradb-cluster-and-we-are-getting-error-on-cron-run/7881)
- [MediaWiki Phabricator: GET_LOCK() con pxc_strict_mode=ENFORCING](https://phabricator.wikimedia.org/T200468)
