Dale a cada idioma su propia URL y anota esas URLs con etiquetas hreflang recíprocas. Un desplegable que cambia cadenas en el state de React traduce la interfaz para la persona que ya está en tu sitio, pero no crea nada que un motor de búsqueda pueda indexar, así que tus versiones en alemán y español siguen siendo invisibles. globalize.now es infraestructura de localización impulsada por IA que extrae cadenas codificadas a catálogos PO de Lingui en commit y las mantiene sincronizadas en cada push a Git, lo cual es lo que hace que las rutas por locale sean viables de mantener en lugar de una tarea puntual.

Este es el paso que la mayoría de quienes construyen con Lovable se saltan. La app se traduce, el selector funciona en el navegador, y el tráfico nunca llega.


¿Por qué Google no indexa los otros idiomas de mi app de Lovable?

Porque esos idiomas casi seguro no tienen direcciones.

El patrón por defecto que produce un builder de IA es un valor de locale useState y un desplegable que lo cambia. Cada idioma se renderiza en la misma URL. Un crawler que solicita esa URL recibe una sola respuesta, en un solo idioma, e indexa una sola página. No hay nada que descubrir, y ninguna señal que le indique que existen otras versiones en otros idiomas.

La solución es estructural, no cosmética. La guía de Google sobre versiones localizadas es explícita al decir que las páginas en idiomas alternativos se identifican por URL, y que las anotaciones que las conectan tienen que ser devueltas por cada página del conjunto. Si /de/pricing no existe como dirección accesible, ninguna cantidad de markup lo hará visible.


¿Qué tiene que hacer realmente un selector de idioma?

Tres cosas, en orden: cambiar la URL, mantener al usuario en la misma página, y recordar la elección.

Cambiar la URL es la parte que importa para las búsquedas. Mantener al usuario en la misma página es la parte que importa para el usuario, y es donde fallan la mayoría de las implementaciones: cambiar desde /en/pricing debería llevar a /de/pricing, no a /de/. Recordar la elección es una comodidad, y nunca debería anular un locale explícito en la URL.

Un antipatrón que merece mención: las redirecciones automáticas basadas en el idioma del navegador. Si un crawler en Estados Unidos solicita /de/pricing y se lo redirige a /en/pricing, la página en alemán prácticamente no existe. Ofrece el cambio, no lo impongas.


¿Cómo añado locale routing a una app de Lovable con TanStack Start?

Añade un segmento de locale en la parte superior del route tree y activa el catálogo a partir de él.

Lovable pasó los proyectos nuevos a server-side rendering en TanStack Start en mayo de 2026, así que una ruta con prefijo de locale devuelve HTML completamente traducido en la primera respuesta. Eso es exactamente lo que quieres que reciba un crawler.

Crea un segmento dinámico que envuelva el resto de tus rutas:

src/routes/
  $locale/
    route.tsx
    index.tsx
    pricing.tsx

Después, activa el catálogo de Lingui para ese locale antes de que se rendericen los hijos:

// src/routes/$locale/route.tsx
import { createFileRoute, Outlet, notFound } from "@tanstack/react-router";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";

const LOCALES = ["en", "de", "es", "fr"] as const;

export const Route = createFileRoute("/$locale")({
  loader: async ({ params }) => {
    if (!LOCALES.includes(params.locale as (typeof LOCALES)[number])) {
      throw notFound();
    }
    const { messages } = await import(
      `../../locales/${params.locale}/messages.po`
    );
    i18n.loadAndActivate({ locale: params.locale, messages });
    return { locale: params.locale };
  },
  component: LocaleLayout,
});

function LocaleLayout() {
  return (
    <I18nProvider i18n={i18n}>
      <Outlet />
    </I18nProvider>
  );
}

Dos detalles hacen aquí el trabajo real. Rechazar locales desconocidos con notFound() evita que /xx/pricing devuelva un 200 «blando» para cada ruta basura que intente un crawler. Cargar el catálogo en el route loader significa que la activación ocurre en el servidor, antes de que el HTML se serialice.

Si sigues con el stack anterior, la misma estructura de URL aplica, pero la ruta de renderizado es distinta. La guía de i18n para Lovable con Vite cubre ese caso, y la guía de configuración de TanStack Start profundiza en el lado del SSR.


¿Cómo construyo el componente selector de idioma?

Navega a la misma ruta con un parámetro de locale distinto, y renderiza cada opción como un enlace real.

// src/components/LanguageSwitcher.tsx
import { Link, useLocation, useParams } from "@tanstack/react-router";

const LOCALES = {
  en: "English",
  de: "Deutsch",
  es: "Español",
  fr: "Français",
} as const;

export function LanguageSwitcher() {
  const { locale } = useParams({ from: "/$locale" });
  const { pathname } = useLocation();
  const rest = pathname.replace(`/${locale}`, "") || "/";

  return (
    <nav aria-label="Language">
      <ul>
        {Object.entries(LOCALES).map(([code, label]) => (
          <li key={code}>
            <Link
              to={`/${code}${rest}`}
              hrefLang={code}
              aria-current={code === locale ? "true" : undefined}
            >
              {label}
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  );
}

Renderiza las opciones como anchors, no como botones conectados a una llamada al router. Los anchors son rastreables, lo que le da a los motores de búsqueda una segunda vía de descubrimiento hacia tus rutas por locale, además del sitemap. Etiqueta cada opción en su propio idioma en lugar de con una bandera: las banderas representan países, y el español no es un país.


¿Cómo emito correctamente las etiquetas hreflang?

Emite una anotación por locale en cada página del conjunto, incluida una autorreferencia, más un x-default.

Google verifica que las URLs alternativas se enlacen entre sí, e ignora las anotaciones que no son recíprocas o que apuntan fuera del canonical propio de una página. Eso hace que generarlas de forma programática sea el único enfoque sensato, porque un conjunto de cuatro idiomas en veinte páginas son 320 anotaciones que mantener consistentes a mano.

En TanStack Start, constrúyelas en el head de la ruta:

// src/routes/$locale/route.tsx (excerpt)
const SITE = "https://example.com";
const LOCALES = ["en", "de", "es", "fr"] as const;

export const Route = createFileRoute("/$locale")({
  head: ({ params, location }) => {
    const rest = location.pathname.replace(`/${params.locale}`, "") || "/";
    return {
      links: [
        { rel: "canonical", href: `${SITE}/${params.locale}${rest}` },
        ...LOCALES.map((code) => ({
          rel: "alternate",
          hrefLang: code,
          href: `${SITE}/${code}${rest}`,
        })),
        { rel: "alternate", hrefLang: "x-default", href: `${SITE}/en${rest}` },
      ],
    };
  },
});

Como las anotaciones se derivan del mismo array LOCALES que usa el router, añadir un idioma actualiza a la vez las rutas, el selector y el conjunto de hreflang. Esa es la propiedad que hay que proteger: una sola lista, tres consumidores.

Añade las mismas URLs a tu sitemap con alternates de xhtml:link, y asegúrate de que tu atributo <html lang> coincide con el locale activo. El atributo lang no influye en el hreflang, pero sí afecta a los lectores de pantalla y a los avisos de traducción del navegador.


¿Por qué un widget de traducción en runtime no puede hacer esto?

Porque el widget traduce después de que la página ya se ha servido en inglés.

Un widget en runtime cambia el texto en el navegador una vez que su script se carga. La primera respuesta que recibe un crawler es el DOM en inglés, y la versión traducida solo existe después de que se haya ejecutado JavaScript en el cliente. Algunos widgets sí ofrecen modos de subdominio o subdirectorio con hreflang generado, pero entonces la estructura de URL y las anotaciones pertenecen a la infraestructura del proveedor, no a tu repositorio.

Esa dependencia tiene un coste que dejó de ser hipotético. Lovalingo, un widget de traducción popular entre quienes construyen con Lovable, cierra el 31 de agosto de 2026, y su propia guía indica a los usuarios que migren a internacionalización propia del proyecto. Cuando un widget desaparece, las URLs de idioma que generó desaparecen con él, y el sitio vuelve al inglés. Los catálogos en commit y las rutas definidas en tu propio código no tienen ese interruptor. Escribimos la versión práctica de esa migración en la comparativa de alternativas a Lovalingo.


¿Qué pasa con el clásico stack de Vite SPA?

La estructura de URL es idéntica; el renderizado no.

Los proyectos de Lovable creados antes del cambio a TanStack Start son apps de una sola página con Vite y React, servidas como archivos estáticos y renderizadas en el navegador. Puedes seguir haciendo routing sobre /:locale, seguir emitiendo hreflang, y seguir manteniendo los catálogos en el repo. Lo que pierdes es la garantía de que el primer byte que recibe un crawler contenga contenido traducido.

Si el SEO es la razón por la que estás localizando, añade prerenderizado para las rutas por locale para que cada una publique HTML real, o planea una migración al stack renderizado en servidor. No lo resuelvas añadiendo un widget en runtime encima de una app renderizada en cliente; eso apila dos pasadas de renderizado en cliente y empeora el first meaningful paint en todos los idiomas.


¿Cómo evitas que cuatro locales se descoordinen?

Automatiza la extracción, y trata los catálogos como código fuente.

La razón por la que se abandona el routing por locale no es el routing. Es que cada nueva función añade cadenas en inglés que nadie extrae, así que /de/ se va llenando poco a poco de fragmentos en inglés y empieza a verse peor que no tener alemán en absoluto. globalize.now se encarga de esa capa: añade el Skill lovable-i18n en tu workspace de Lovable y pídele a Lovable que configure el i18n, o instala los Skills localmente:

npx skills add globalize-now/globalize-skills

Añade --all para instalarlo en cada agente. Las cadenas nuevas se extraen, se generan las claves, se traducen los catálogos, y los archivos PO actualizados se hacen commit de vuelta en cada push a Git.

El precio es de 20 € al mes por workspace, incluidos 20 € de crédito de traducción (unas 200.000 palabras) al mes, y a partir de ahí uso basado en tokens al mismo precio por carácter. No hay cargos por asiento ni por idioma, así que añadir un quinto locale cambia tu configuración de routing, no tu factura. El registro incluye 5 € de crédito de traducción y no requiere tarjeta. Más detalles para desarrolladores y para quienes lanzan apps generadas con IA.

El routing y el hreflang son trabajo de una tarde. Mantener cuatro catálogos completos mientras la app cambia es la parte que nunca termina, y es la parte que automatiza globalize.now.

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