Inicio / Artículos / El error de React que solo aparece cuando los lectores traducen tu página

El error de React que solo aparece cuando los lectores traducen tu página

La traducción de páginas en Chrome separa los nodos de texto de React, lo que provoca errores de removeChild o congelamientos silenciosos. Las pruebas en diferentes navegadores muestran cuándo la reparación de la vista previa es útil y cuándo es más seguro escribir dentro de los contenedores del traductor.

1432 palabras

El primer caso parecía trivial. Un contador de React mostraba un valor obsoleto mientras todos los controles cercanos seguían respondiendo. El estado de la aplicación contenía el valor correcto; la cadena mostrada, no. Para ese visitante estaba activado el traductor integrado de Chrome.

Una semana después, el mismo producto comenzó a generar el error NotFoundError: Failed to execute 'removeChild' on 'Node' y a destruir su propia raíz. Mismo mecanismo, error más grave.

Qué hace el traductor

El traductor de Chrome nunca modifica el nodo de texto en su ubicación original. En su lugar, crea uno de reemplazo, lo envuelve en un elemento <font>, lo inserta en la posición anterior y desconecta el nodo original del árbol activo.

El nodo original sigue asignado en memoria. React mantiene su referencia. Simplemente, el nodo ya no está presente en el documento.

Ese cambio estructural explica ambos modos de fallo. removeChild lanza un error porque el nodo que React quiere eliminar ya no tiene padre. Asignar nodeValue no lanza error alguno, pero modifica el texto que ya no es visible.

Los fallos dejan rastros en la pila de llamadas. Una interfaz de usuario congelada no deja nada útil. Un contador que deja de avanzar parece ser un error de estado, y ahí es donde los equipos suelen comenzar a investigar.

La solución que todos copian

Shuhei describió el conflicto en el DOM en el tracker de problemas de React en 2018. Dan Abramov marcó el problema como irreparable, pero la solución temporal surgida de esa discusión sigue siendo la respuesta estándar que se copia y pega. El parche evita que removeChild y insertBefore funcionen, de modo que regresan silenciosamente cuando el nodo no es hijo del padre previsto.

Los fallos desaparecen.

Las actualizaciones en tiempo real también desaparecen con ellos.

Se comparó la misma muestra de React bajo tres configuraciones durante una sesión de traducción. Dejar el árbol sin protección generó dos excepciones no capturadas que destruyeron la estructura raíz, incluidos los botones. Al instalar la protección copiada se eliminaron todos los errores; el contador se congeló, las cadenas eliminadas permanecieron en la pantalla, y un operador ternario que cambiaba el estado mostró ambas ramas al mismo tiempo.

El fallo visible se intercambia por una obsolescencia invisible.

Medir en lugar de adivinar

Se dejaron de lado las respuestas del foro en favor de la observación directa. Se compararon Chrome, Edge, Firefox, Yandex y el widget de traducción independiente de Google con una página registradora que tomaba capturas de cada nodo de texto, pausaba hasta que una persona activaba la traducción y luego ejecutaba dieciséis pruebas más cinco experimentos. Para medir los tiempos se utilizó Playwright contra una instancia real de Chrome con un perfil predefinido.

El primer resultado concreto: el error no está en todas partes.

En Edge y Firefox, el motor reescribe el nodo de texto sin separarlo, por lo que las mutaciones posteriores persisten. Un contador que antes marcaba un valor de una vez por segundo llegó a 6 en ambos navegadores. Los usuarios de esos motores nunca entraron en el escenario de caída ni en el de congelamiento. Ese hecho refuta a quienes venden soluciones universales, pero sigue siendo cierto, y por eso tanto el README como la página de demostración lo indican.

El hallazgo que cambió la naturaleza del problema

El traductor de Chrome funciona con lo que está visible en ese momento, no con toda la página al mismo tiempo.

Frente a un traductor inactivo, se enviaron diez intentos de activación, junto con pruebas silenciosas: lecturas forzadas del diseño; eventos falsos de cambio de tamaño, enfoque, visibilidad y movimiento del mouse; desplazamiento de la ventana hacia adelante y hacia atrás; además de actividades reales del cursor y de la rueda generadas por el propio navegador.

Solo element.scrollIntoView() provocó la traducción, después de 168 milisegundos. Los otros nueve intentos permanecieron sin actividad durante diez segundos completos.

Dos intentos fallidos fueron eventos reales del navegador, lo que descarta que la “entrada confiable” sea el único desencadenante. El desplazamiento a nivel de ventana también falló. El elemento traducido debe hacerse visible por sí mismo.

Dónde falló la primera conclusión

En el borrador se incluyó una afirmación contundente: después de que se restaure un nodo de texto separado, Chrome nunca lo vuelve a traducir. Una prueba parecía confirmarlo. La prueba se ejecutó, el nodo permaneció en inglés y la oración pasó a formar parte de la documentación.

Esa prueba quedó fuera del alcance visual.

La contradicción surgió solo después de anotar la regla del viewport: ambas afirmaciones no pueden coexistir. Al mover la sonda a la vista y repetir la ejecución, se observó que Chrome reparaba el nodo restaurado en 210 milisegundos. Fuera de la vista, nunca lo reparaba, independientemente del tiempo de espera.

Esa afirmación estuvo errónea durante dos días dentro de un documento cuya tesis es que la medición supera a las suposiciones.

Otro par de errores siguieron el mismo patrón. Una comparación directa con una biblioteca existente no tuvo sentido en la primera ejecución porque esa biblioteca nunca se cargó. Su paquete termina con un comentario //# sourceMappingURL=, y la línea añadida para exponerlo globalmente quedó dentro de ese comentario. Cada ejecución ahora indica qué métodos del DOM fue que cada versión modificó realmente antes de comenzar la medición.

También se exageró el caso de Firefox. Tres pruebas independientes mostraron siempre el número correcto, pero dos terminaron en francés y una en inglés. Debido a las actualizaciones continuas, un motor existente puede retrasarse y mostrar brevemente el idioma original.

Todas las tres correcciones permanecieron visibles en el informe, junto con lo que las reemplazó. Ocultar las revisiones obligaría a los lectores a confiar en secciones sin verificar.

Qué cuesta una corrección al lector

Una vez que un nodo desaparece del árbol, hay dos formas de recuperarlo: restaurar el nodo original y esperar a que el traductor se dé cuenta y lo traduzca de nuevo, o insertar el nuevo valor en el contenedor que ya utilizó el traductor.

La mayoría de las bibliotecas existentes optan por la restauración. Funcionalmente funciona, pero el lector debe asumir un costo visible. En cinco repeticiones con cuatro actualizaciones, tomando muestras de texto visible cada 50 milisegundos:

La restauración mostró cadenas en el idioma original durante 100–150 ms en cada actualización y 500–600 ms en una secuencia de cuatro pasos. Escribir en el contenedor mostró texto en el idioma original durante 0 ms en cada una de las veinte actualizaciones.

Un único destello es fácil de ignorar. Un contador en tiempo real muestra ese destello en cada actualización.

Un borrador anterior indicaba 150–200 ms por actualización y 700 ms para la secuencia. Esos números provinieron de una sola ejecución y no resistieron cinco repeticiones. Por lo tanto, el informe publicado mantiene el conjunto completo de cinco ejecuciones: una sola muestra de tiempo no constituye un dato fiable.

Dónde deja de funcionar el enfoque mejor

Inyectar un dígito nuevo en una oración ya traducida funciona en neerlandés. En ruso puede alterar la gramática.

Intl.PluralRules('ru') clasifica el número 4 como pocos y el 7 como muchos, y las terminaciones de los sustantivos siguen esa categoría. Una oración en ruso que utiliza la terminación de pocos para cuatro bombillas no debe mantener esa misma terminación cuando la cantidad pasa a ser siete. Una versión inicial produjo exactamente esa corrupción silenciosa en un idioma que el desarrollador no podía leer.

La lógica actual rechaza cualquier cambio en la categoría plural, la longitud del dígito o la estructura de la oración, así como cuando el idioma no es reconocido. Cuando la heurística falla, la interfaz muestra la cantidad correcta en el idioma no traducido. Esta reducción de calidad es intencionada: un número correcto en inglés es más seguro que una forma incorrecta de declinar el sustantivo en un idioma que nadie del equipo puede revisar.

En neerlandés y alemán, Intl.PluralRules devuelve other para todo número entero, por lo que la trampa de categoría plural nunca aparece al probar únicamente con esas configuraciones regionales.

Lo que queda sin medir

La fila de Safari está vacía a propósito. WebKit en Playwright no incluye un traductor, y desde 2012 no se ha lanzado ninguna versión de Safari para Windows, por lo que la cobertura automática no está disponible. Recopilar datos implica contar con una Mac física y una persona que pueda ignorar manualmente la solicitud de traducción. Adivinar un resultado sería peor que dejar la celda en blanco.

Cuando una grabación nunca observa una traducción activa, la puntuación se almacena como null en lugar de false. Esa distinción impide que los lectores posteriores consideren una sesión silenciosa como evidencia de que un motor es inofensivo.

La biblioteca

El paquete correspondiente es translate-shield en npm. Cuando Chrome reemplaza un nodo de texto por un contenedor <font>, la biblioteca registra esa relación y redirige las escrituras posteriores de React hacia dicho contenedor, de modo que la pantalla permanezca en el idioma seleccionado por el visitante. No existen dependencias en tiempo de ejecución y el tamaño del paquete es de aproximadamente 15 kB. Edge y Firefox reciben un camino de comportamiento vacío, ya que esos navegadores nunca separan el nodo en primer lugar.

El paquete ni traduce cadenas de texto ni sustituye a una biblioteca i18n.

Una demostración interactiva muestra un documento protegido junto a uno sin protección, mientras el navegador del visitante traduce ambos. Son obligatorios documentos separados: la actualización se aplica a todo el documento, por lo que una sola página no puede albergar un mecanismo de control independiente.

Toda estadística citada aquí está respaldada por un archivo JSON generado mediante una prueba reejecutable en el repositorio, incluyendo mediciones que fueron revisadas tras errores anteriores.

Leer más

Comparación interactiva: https://google-translate-simulation.netlify.app/

Repositorio con grabaciones en bruto de las pruebas: https://github.com/alievdavlat/translate-shield

Página del paquete publicado: https://www.npmjs.com/package/translate-shield