Dê a cada idioma sua própria URL e depois anote essas URLs com tags hreflang recíprocas. Um menu suspenso que troca strings no estado do React traduz a interface para quem já está no seu site, mas não cria nada para um mecanismo de busca indexar, então suas versões em alemão e espanhol continuam invisíveis. O globalize.now é uma infraestrutura de localização com IA que extrai strings fixas para catálogos PO do Lingui versionados e os mantém sincronizados a cada push no Git, o que torna rotas por idioma viáveis de manter em vez de uma tarefa pontual.
Esta é a etapa que a maioria de quem constrói com Lovable pula. O app é traduzido, o seletor funciona no navegador, e o tráfego nunca chega.
Por que o Google não indexa os outros idiomas do meu app Lovable?
Porque esses idiomas quase certamente não têm endereços próprios.
O padrão que um construtor de IA gera por padrão é um valor de idioma useState e um menu suspenso que o define. Todo idioma é renderizado na mesma URL. Um crawler que solicita essa URL recebe uma resposta, em um idioma, e indexa uma página. Não há nada a descobrir, e nenhum sinal indicando que outras versões de idioma existem.
A correção é estrutural, não cosmética. As próprias orientações do Google sobre versões localizadas são claras: páginas em idiomas alternativos são identificadas por URL, e as anotações que as conectam precisam ser retornadas por cada página do conjunto. Se /de/pricing não existir como um endereço acessível, nenhuma marcação vai fazer com que ele apareça.
O que um seletor de idioma de verdade precisa fazer?
Três coisas, nesta ordem: mudar a URL, manter o usuário na mesma página e lembrar a escolha.
Mudar a URL é a parte que importa para a busca. Manter o usuário na mesma página é a parte que importa para o usuário, e é onde a maioria das implementações falha: trocar de /en/pricing deveria levar a /de/pricing, não a /de/. Lembrar a escolha é uma conveniência, e nunca deve sobrepor um idioma explícito na URL.
Um antipadrão que vale a pena citar: redirecionamentos automáticos baseados no idioma do navegador. Se um crawler nos Estados Unidos solicita /de/pricing e é redirecionado para /en/pricing, a página em alemão efetivamente não existe. Ofereça a troca, não a imponha.
Como adiciono roteamento por idioma a um app Lovable com TanStack Start?
Adicione um segmento de idioma no topo da árvore de rotas e ative o catálogo a partir dele.
O Lovable migrou novos projetos para renderização no servidor com TanStack Start em maio de 2026, então uma rota com prefixo de idioma retorna HTML já totalmente traduzido na primeira resposta. É exatamente isso que você quer que um crawler receba.
Crie um segmento dinâmico que englobe o restante das suas rotas:
src/routes/
$locale/
route.tsx
index.tsx
pricing.tsx
Depois ative o catálogo do Lingui para esse idioma antes de renderizar os filhos:
// 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>
);
}
Dois detalhes fazem a diferença de verdade aqui. Rejeitar idiomas desconhecidos com notFound() impede que /xx/pricing retorne um 200 disfarçado para todo caminho inválido que um crawler tentar. Carregar o catálogo no loader da rota garante que a ativação acontece no servidor, antes de o HTML ser serializado.
Se você estiver no stack mais antigo, a mesma estrutura de URL se aplica, mas o caminho de renderização é diferente. O guia de i18n para Lovable com Vite cobre esse caso, e o guia de configuração do TanStack Start aprofunda o lado do SSR.
Como construo o componente de seletor de idioma?
Navegue para a mesma rota com um parâmetro de idioma diferente, e renderize cada opção como um link de verdade.
// 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>
);
}
Renderize as opções como links (tags de âncora), não como botões conectados a uma chamada do roteador. Links são rastreáveis, o que dá aos mecanismos de busca um segundo caminho de descoberta para suas rotas de idioma, além do sitemap. Nomeie cada opção no próprio idioma, em vez de usar uma bandeira: bandeiras representam países, e espanhol não é um país.
Como emito as tags hreflang corretamente?
Emita uma anotação por idioma em cada página do conjunto, incluindo uma autorreferência, além de um x-default.
O Google verifica se as URLs alternativas apontam umas para as outras, e ignora anotações que não são recíprocas ou que apontam para fora do canonical da própria página. Isso torna gerá-las programaticamente a única abordagem sensata, porque um conjunto de quatro idiomas em vinte páginas soma 320 anotações para manter consistentes manualmente.
No TanStack Start, construa-as no head da rota:
// 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 as anotações são derivadas do mesmo array de LOCALES usado pelo roteador, adicionar um idioma atualiza as rotas, o seletor e o conjunto de hreflang de uma vez só. Essa é a propriedade a proteger: uma lista, três consumidores.
Adicione as mesmas URLs ao seu sitemap com alternates de xhtml:link, e garanta que seu atributo <html lang> corresponda ao idioma ativo. O atributo lang não influencia o hreflang, mas afeta leitores de tela e os avisos de tradução do navegador.
Por que um widget de tradução em tempo real não resolve isso?
Porque o widget traduz depois que a página já foi entregue em inglês.
Um widget em tempo real troca o texto no navegador assim que seu script carrega. A primeira resposta que um crawler recebe é o DOM em inglês, e a versão traduzida só existe depois que o JavaScript do lado do cliente é executado. Alguns widgets oferecem modos de subdomínio ou subdiretório com hreflang gerado, mas aí a estrutura de URL e as anotações pertencem à infraestrutura do fornecedor, não ao seu repositório.
Essa dependência tem um custo que deixou de ser hipotético. O Lovalingo, um widget de tradução popular entre quem constrói com Lovable, vai encerrar suas atividades em 31 de agosto de 2026, e suas próprias orientações dizem aos usuários para migrarem para uma internacionalização própria do projeto. Quando um widget desaparece, as URLs de idioma que ele gerou desaparecem junto, e o site volta ao inglês. Catálogos versionados e rotas definidas no seu próprio código não têm esse tipo de interruptor. Detalhamos a versão prática dessa migração na comparação de alternativas ao Lovalingo.
E o stack clássico de SPA com Vite?
A estrutura de URL é idêntica; a renderização não.
Projetos Lovable criados antes da mudança para o TanStack Start são SPAs em Vite com React, servidos como arquivos estáticos e renderizados no navegador. Você ainda pode rotear com base em /:locale, ainda pode emitir hreflang e ainda pode manter os catálogos no repositório. O que você perde é a garantia de que o primeiro byte que um crawler recebe já contém o conteúdo traduzido.
Se SEO é o motivo pelo qual você está localizando, adicione pré-renderização às rotas de idioma para que cada uma entregue HTML de verdade, ou planeje uma migração para o stack renderizado no servidor. Não resolva isso adicionando um widget em tempo real por cima de um app renderizado no cliente; isso empilha duas passagens de renderização no cliente e piora a primeira renderização significativa em todos os idiomas.
Como evitar que quatro idiomas fiquem desatualizados entre si?
Automatize a extração e trate os catálogos como código-fonte.
O motivo pelo qual o roteamento por idioma acaba sendo abandonado não é o roteamento em si. É que cada novo recurso adiciona strings em inglês que ninguém extrai, então /de/ vai lentamente se enchendo de fragmentos em inglês e começa a parecer pior do que simplesmente não ter alemão nenhum. O globalize.now cuida dessa camada: adicione a skill lovable-i18n no seu workspace Lovable e peça ao Lovable para configurar i18n, ou instale as skills localmente:
npx skills add globalize-now/globalize-skills
Adicione --all para instalar em todos os agentes. Novas strings são extraídas, chaves são geradas, catálogos são traduzidos, e os arquivos PO atualizados são enviados de volta ao repositório a cada push no Git.
O preço é de € 20 por mês por workspace, incluindo € 20 de crédito de tradução (cerca de 200 mil palavras) por mês, e depois uso baseado em tokens na mesma taxa por caractere. Não há cobrança por assento nem por idioma, então adicionar um quinto idioma muda sua configuração de roteamento, não sua fatura. O cadastro vem com € 5 de crédito de tradução e não exige cartão. Mais detalhes para desenvolvedores e para quem está lançando apps construídos com IA.
Roteamento e hreflang são trabalho de uma tarde. Manter quatro catálogos completos conforme o app muda é a parte que nunca termina, e é essa parte que o globalize.now automatiza.
O globalize.now transforma textos fixos do app em arquivos de locale prontos para tradução e os mantém atualizados a cada novo lançamento.
Experimente o globalize.now grátis