La build en árabe parece rota y la traducción está bien. El menú lateral se queda a la izquierda, la flecha de retroceso apunta a la izquierda, el avatar se pega al borde izquierdo de cada tarjeta: cada palabra está en árabe, cada caja sigue donde la dejó la versión en inglés. La dirección del texto es un atributo HTML más un conjunto de decisiones CSS que viven en tus componentes, y ningún archivo de catálogo la registra. globalize.now es infraestructura de localización impulsada por IA que extrae las cadenas y mantiene los catálogos sincronizados en cada push a Git.
¿Por qué mi app de Lovable se sigue leyendo de izquierda a derecha en árabe?
Porque la dirección es una propiedad del documento y de la hoja de estilos, no del mensaje.
Tus catálogos son pares de cadenas fuente y cadenas traducidas. En cuanto compilan para ar y tus componentes lo renderizan, cambian las palabras y nada más. Da igual que esos catálogos sean Lingui, al que escribe globalize.now, o react-i18next. Ningún formato tiene dónde colocar «y el menú lateral pasa al otro lado».
Hay que cambiar dos cosas. El atributo dir en el elemento html le indica al navegador en qué sentido corre el eje en línea. Tu CSS tiene que dejar de nombrar lados y empezar a nombrar el inicio y el final de ese eje. Todo lo demás se deriva de esos dos cambios.
¿Qué se rompe realmente al añadir árabe a una app de Lovable?
Los fallos se concentran en sitios previsibles, y todos son de estilo, no de contenido.
- Los cajones de navegación y los menús laterales se quedan en el lado físico izquierdo.
- Los chevrones, las flechas de retroceso y los indicadores de progreso apuntan en el sentido equivocado.
- El padding asimétrico fija el contenido al borde equivocado, así que las etiquetas se amontonan en un lado.
text-leften un encabezado mantiene el árabe fijado a la izquierda de su contenedor.- Las insignias y los botones de cierre con posición absoluta aparecen en la esquina opuesta a la que espera quien lee.
- Los bordes izquierdos que marcan un elemento activo de una lista aparecen en el borde final en lugar de en el inicial.
- Un código de producto en inglés dentro de una frase en árabe se reordena de forma impredecible sin una indicación de dirección.
Ninguno de estos es un defecto de traducción. Todos son una dirección física incrustada en un nombre de clase.
¿Cómo derivo la dirección del texto a partir de la etiqueta de locale?
Pregúntale al locale qué dirección usa, y luego escribe la respuesta en el documento.
Intl.Locale.prototype.getTextInfo() devuelve una dirección, ya sea ltr o rtl, para una etiqueta de locale. Alcanzó el estado de disponibilidad reciente en Baseline en julio de 2026, lo que significa que llegó a la versión actual de todos los navegadores principales, pero no a la mayoría de los navegadores que realmente usan tus usuarios. Trata el fallback de abajo como la vía activa y la API como lo que acabará reemplazándolo.
// getTextInfo() is not yet in TypeScript's bundled Intl.Locale definitions.
declare global {
namespace Intl {
interface Locale {
getTextInfo?(): { direction: "ltr" | "rtl" };
}
}
}
// Stopgap for runtimes without getTextInfo(). This set is the one place where
// adding a new right-to-left language still costs you a code change.
const RTL_FALLBACK = new Set([
"ar", "he", "fa", "ur", "ps", "sd", "yi", "dv", "ckb", "ug", "nqo", "syr",
]);
export function directionFor(locale: string): "ltr" | "rtl" {
let tag: Intl.Locale | undefined;
try {
tag = new Intl.Locale(locale);
} catch {
// Malformed tag. Fall through to the string path below.
}
const direction = tag?.getTextInfo?.().direction;
if (direction === "ltr" || direction === "rtl") return direction;
const language = tag?.language ?? locale.split(/[-_]/)[0].toLowerCase();
return RTL_FALLBACK.has(language) ? "rtl" : "ltr";
}
Los dos casos límite de try importan. new Intl.Locale() lanza un RangeError ante una etiqueta mal formada, y getTextInfo() simplemente no existe en runtimes más antiguos. Manejar solo el segundo es el error habitual: un locale como ar_EG que llega desde un query string envía la función a su propia ruta de recuperación y de ahí directo a un error sin capturar.
Define ambos atributos allí donde tu app ya conoce el locale activo. Lovable genera actualmente dos formas de proyecto, una app de una sola página con Vite y una app renderizada en servidor con TanStack Start, así que comprueba qué generó realmente tu proyecto. En TanStack Start es el componente raíz del documento. En Vite es donde montes el provider de i18n.
<html lang={locale} dir={directionFor(locale)}>
Pon dir en html en lugar de en un wrapper div. Los portals, modales y toasts se renderizan fuera de tu árbol de componentes y también necesitan heredarlo.
¿Qué clases de Tailwind tengo que cambiar para el diseño de derecha a izquierda?
Las que nombran un lado físico. Sus equivalentes lógicos siguen el eje en línea y se invierten solos cuando cambia dir.
| Utilidad física | Reemplazo lógico | Qué define |
|---|---|---|
| ml-4 / mr-4 | ms-4 / me-4 | margin-inline-start / margin-inline-end |
| pl-6 / pr-6 | ps-6 / pe-6 | padding-inline-start / padding-inline-end |
| left-0 / right-0 | start-0 / end-0 | inset-inline-start / inset-inline-end |
| text-left / text-right | text-start / text-end | alineación de texto en el eje en línea |
| border-l / border-r | border-s / border-e | borde inicial / final |
| rounded-l-lg / rounded-r-lg | rounded-s-lg / rounded-e-lg | esquinas iniciales / finales |
| float-left / float-right | float-start / float-end | float en el eje en línea |
| clear-left / clear-right | clear-start / clear-end | clear en el eje en línea |
| scroll-ml-4 / scroll-mr-4 | scroll-ms-4 / scroll-me-4 | margen de scroll-snap |
La documentación de márgenes de Tailwind muestra el efecto directamente: ms-8 y me-8 renderizados dentro de contenedores dir="ltr" y dir="rtl" quedan en lados opuestos. Las utilidades de propiedades lógicas llegaron en Tailwind v3.3, así que un proyecto con historia puede no tenerlas.
Dos utilidades merecen una segunda mirada en lugar de un simple cambio de nombre. space-x-* y divide-x-* añaden espaciado y bordes entre elementos hermanos, así que en una fila invertida quedan en el lado equivocado de cada elemento. Tailwind incluye space-x-reverse y divide-x-reverse exactamente para ese caso. Las utilidades verticales como mt-* y pb-* no necesitan nada, porque la dirección solo rige el eje en línea.
¿Qué no pueden arreglar las propiedades lógicas?
Todo lo que codifica una dirección fuera del modelo de caja, que es donde las variantes rtl: y ltr: demuestran su valor.
<ChevronRight className="rtl:rotate-180" />
<div className="bg-[url(/hero.svg)] bg-left rtl:bg-right" />
<div className="shadow-[4px_0_8px_rgba(0,0,0,.15)] rtl:shadow-[-4px_0_8px_rgba(0,0,0,.15)]" />
Las transformaciones, posiciones de fondo, desplazamientos de sombra, ángulos de degradado, matemáticas de scroll de carruseles y cualquier icono que signifique «adelante» entran aquí. Mantén la lista corta. Si escribes rtl: en cada segundo elemento, es señal de que los estilos subyacentes siguen siendo físicos y deberían convertirse en su lugar.
El contenido generado por el usuario necesita un tratamiento propio, y los dos casos son distintos. Para un bloque completo cuyo idioma no controlas, dir="auto" deja que el navegador determine la dirección base del bloque a partir de su primer carácter fuerte.
<p dir="auto">{review.body}</p>
Para un fragmento en otra escritura dentro de una frase con dirección ya definida —un SKU en inglés incrustado en un texto en árabe—, dir="auto" no hace nada, porque establece la dirección base del párrafo en lugar de aislar un fragmento en línea. Envuelve ese fragmento por separado.
<bdi>{product.sku}</bdi>
¿Por qué un widget de traducción no puede invertir tu diseño por ti?
Porque un widget opera sobre nodos de texto en la página renderizada, y tu problema de dirección vive en los estilos que generaron esa página.
Un servicio en tiempo de ejecución puede inyectar dir="rtl" en el documento y traducir las cadenas visibles después de que se haya pintado la versión en inglés. No puede abrir tus componentes y convertir ml-4 en ms-4, porque ml-4 ya se ha compilado en margin-left dentro de una hoja de estilos que el servicio no controla. El resultado es texto en árabe dentro de un mobiliario de izquierda a derecha.
Hay un segundo coste. Lovalingo, un servicio de traducción en tiempo de ejecución usado en algunas apps de Lovable, ha anunciado que cierra el 31 de agosto de 2026 a las 23:59 CEST. Su guía de migración indica a los clientes que pasen a una internacionalización propia del proyecto y que reverifiquen las rutas de locale, las URL canónicas y el hreflang antes de eliminarlo. El trabajo de layout hecho dentro del producto de un proveedor se va con el proveedor. Las utilidades lógicas y un atributo dir en tu propio repositorio, no. Nuestra comparativa de Lovalingo plantea el mismo argumento sobre la capa de cadenas.
¿Cómo evitas que el próximo prompt en Lovable lo deshaga?
Convierte una vez, y luego haz visibles las utilidades físicas en la revisión.
- Instala el flujo de trabajo. En el workspace de Lovable, añade el Skill
lovable-i18ny pide a Lovable que configure i18n. Fuera del editor, la instalación esnpx skills add globalize-now/globalize-skills, con--allpara instalarlo en todos los agentes. - Define la dirección a partir del locale usando
directionForcomo arriba, en el elementohtml. - Convierte las utilidades físicas en lógicas en todos tus componentes, empezando por lo que envuelve tus páginas.
- Añade variantes
rtl:solo para transformaciones, fondos, sombras e iconos direccionales, y recurre a<bdi>para fragmentos en línea en otra escritura. - Añade un filtro con grep a la revisión para que una utilidad física recién generada falle antes de fusionarse.
rg -n '\b(ml|mr|pl|pr|scroll-m[lr])-|\b(float|clear|text)-(left|right)\b|\bborder-[lr]\b|\brounded-[lr]-|\b(left|right)-[0-9]' src/
El quinto paso es el que se mantiene. Lovable regenera componentes en cada prompt, y un modelo al que se le pide añadir un panel de configuración recurrirá a ml-4, porque las utilidades físicas dominan el código React con el que se entrenaron estos modelos. Una comprobación que hace fallar el pull request es más barata que descubrir el problema en una captura en árabe tres semanas después.
La mitad del ciclo que corresponde a las cadenas funciona sola. globalize.now extrae las nuevas cadenas codificadas de lo que Lovable haya generado, las escribe en tus catálogos y sincroniza en cada push a Git, así que la versión en árabe se mantiene al día mientras tú sigues escribiendo prompts. Es el mismo mecanismo descrito en la guía de Vite y en la guía de TanStack Start, y por eso las traducciones dejan de romperse en cada push.
Añadir árabe no cuesta nada extra en el plan. El plan Starter cuesta 20 € al mes por workspace, con 20 € de crédito de traducción incluidos (unas 200.000 palabras) y el uso continúa después a la misma tarifa por carácter, sin cargos por asiento ni por idioma, y con los idiomas de derecha a izquierda incluidos. Hay más detalles sobre la configuración en la página de integración con Lovable, detalle técnico en la página para desarrolladores, y un camino más corto para quienes no son desarrolladores en la página de Vibe Coders.
Un cambio de layout, y se queda arreglado
La dirección es una decisión que tu repositorio toma una sola vez. Las cadenas son la parte que cambia cada vez que escribes un prompt, y esa parte se gestiona sola.
globalize.now convierte el texto codificado de tu app en archivos de traducción listos para localizar y los mantiene actualizados en cada despliegue.
Prueba globalize.now gratis