Donnez à chaque langue sa propre URL, puis annotez ces URL avec des balises hreflang réciproques. Un menu déroulant qui remplace les chaînes dans l’état React traduit l’interface pour la personne déjà présente sur votre site, mais ne crée rien qu’un moteur de recherche puisse indexer : vos versions allemande et espagnole restent donc invisibles. globalize.now est une infrastructure de localisation propulsée par l’IA qui extrait les chaînes codées en dur dans des catalogues PO Lingui versionnés et les synchronise à chaque Git push. C’est ce qui rend les routes par locale faciles à maintenir, plutôt qu’une tâche ponctuelle.
C’est l’étape que la plupart des créateurs Lovable sautent. L’app est traduite, le sélecteur fonctionne dans le navigateur, et le trafic n’arrive jamais.
Pourquoi Google n’indexe-t-il pas les autres langues de mon app Lovable ?
Parce que ces langues n’ont presque certainement pas d’adresse.
Le schéma par défaut produit par un builder IA est une valeur de locale useState et un menu déroulant qui la définit. Toutes les langues sont rendues à la même URL. Un robot qui demande cette URL reçoit une seule réponse, dans une seule langue, et indexe une seule page. Il n’a rien à découvrir et aucun signal lui indiquant que d’autres versions linguistiques existent.
La correction est structurelle, pas cosmétique. Les recommandations de Google sur les versions localisées indiquent clairement que les pages dans d’autres langues sont identifiées par leur URL et que les annotations qui les relient doivent être renvoyées par chaque page de l’ensemble. Si /de/pricing n’existe pas comme adresse accessible, aucun balisage ne la fera apparaître.
Que doit réellement faire un sélecteur de langue ?
Trois choses, dans cet ordre : modifier l’URL, garder l’utilisateur sur la même page et mémoriser son choix.
Modifier l’URL est la partie importante pour la recherche. Garder l’utilisateur sur la même page est ce qui compte pour lui, et c’est là que la plupart des implémentations échouent : passer de /en/pricing doit mener à /de/pricing, et non à /de/. Mémoriser le choix est pratique, mais cela ne doit jamais prendre le pas sur une locale explicitement indiquée dans l’URL.
Un anti-pattern mérite d’être nommé : les redirections automatiques selon la langue du navigateur. Si un robot situé aux États-Unis demande /de/pricing et est redirigé vers /en/pricing, la page allemande n’existe, de fait, pas. Proposez le sélecteur, ne l’imposez pas.
Comment ajouter le routage par locale à une app Lovable TanStack Start ?
Ajoutez un segment de locale au sommet de l’arbre des routes et activez le catalogue à partir de celui-ci.
Depuis mai 2026, Lovable utilise le rendu côté serveur avec TanStack Start pour les nouveaux projets. Une route préfixée par une locale renvoie donc du HTML entièrement traduit dès la première réponse. C’est exactement ce qu’un robot doit recevoir.
Créez un segment dynamique qui englobe le reste de vos routes :
src/routes/
$locale/
route.tsx
index.tsx
pricing.tsx
Activez ensuite le catalogue Lingui correspondant à cette locale avant le rendu des routes enfants :
// 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>
);
}
Deux détails font ici toute la différence. Refuser les locales inconnues avec notFound() empêche /xx/pricing de renvoyer un soft 200 pour chaque chemin incohérent testé par un robot. Charger le catalogue dans le loader de la route garantit que l’activation se fait côté serveur, avant la sérialisation du HTML.
Si vous utilisez l’ancienne stack, la forme des URL reste la même, mais le chemin de rendu diffère. Le guide de l’i18n Lovable avec Vite couvre ce cas, tandis que le guide de configuration TanStack Start détaille davantage la partie SSR.
Comment créer le composant de sélecteur de langue ?
Accédez à la même route avec un autre paramètre de locale et affichez chaque option comme un véritable lien.
// 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>
);
}
Affichez les options sous forme d’ancres, et non de boutons reliés à un appel du routeur. Les ancres sont explorables, ce qui offre aux moteurs de recherche un deuxième chemin de découverte vers vos routes par locale, en plus du sitemap. Nommez chaque option dans sa propre langue plutôt qu’avec un drapeau : les drapeaux représentent des pays, et l’espagnol n’est pas un pays.
Comment générer correctement les balises hreflang ?
Générez une annotation par locale sur chaque page de l’ensemble, y compris une référence à elle-même, ainsi qu’un x-default.
Google vérifie que les URL alternatives renvoient les unes vers les autres et ignore les annotations qui ne sont pas réciproques ou qui s’éloignent du canonical de la page. Les générer automatiquement est donc la seule approche raisonnable : quatre langues sur vingt pages représentent 320 annotations à maintenir manuellement.
Dans TanStack Start, construisez-les dans le head de la route :
// 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}` },
],
};
},
});
Comme les annotations sont dérivées du même tableau LOCALES que celui utilisé par le routeur, l’ajout d’une langue met à jour les routes, le sélecteur et l’ensemble hreflang en une seule fois. C’est cette propriété qu’il faut préserver : une liste, trois consommateurs.
Ajoutez les mêmes URL à votre sitemap avec des alternates xhtml:link et vérifiez que votre attribut <html lang> correspond à la locale active. L’attribut lang n’influence pas le hreflang, mais il a un effet sur les lecteurs d’écran et les invites de traduction du navigateur.
Pourquoi un widget de traduction à l’exécution ne peut-il pas faire cela ?
Parce que le widget traduit la page après qu’elle a déjà été servie en anglais.
Un widget d’exécution remplace le texte dans le navigateur une fois son script chargé. La première réponse reçue par un robot contient le DOM anglais, et la version traduite n’existe qu’après l’exécution du JavaScript côté client. Certains widgets proposent bien des modes par sous-domaine ou sous-répertoire avec un hreflang généré, mais la structure des URL et les annotations appartiennent alors à l’infrastructure du fournisseur, pas à votre repo.
Cette dépendance a un coût qui n’a plus rien d’hypothétique. Lovalingo, un widget de traduction populaire auprès des créateurs Lovable, fermera le 31 août 2026, et sa propre documentation recommande aux utilisateurs de migrer vers une internationalisation gérée par le projet. Lorsqu’un widget disparaît, les URL de langue qu’il générait disparaissent avec lui, et le site revient à l’anglais. Des catalogues versionnés et des routes définies dans votre propre code ne présentent pas ce risque. Nous avons détaillé la migration en pratique dans le comparatif des alternatives à Lovalingo.
Qu’en est-il de la stack Vite SPA classique ?
La structure des URL est identique, mais pas le rendu.
Les projets Lovable créés avant le passage à TanStack Start sont des apps monopages Vite avec React, servies sous forme de fichiers statiques et rendues dans le navigateur. Vous pouvez toujours router sur /:locale, générer le hreflang et conserver les catalogues dans le repo. En revanche, vous perdez la garantie que le premier octet reçu par un robot contient du contenu traduit.
Si le SEO est la raison pour laquelle vous localisez votre app, ajoutez du prerendering pour les routes par locale afin que chacune serve du véritable HTML, ou prévoyez de migrer vers la stack avec rendu côté serveur. Ne résolvez pas le problème en ajoutant un widget d’exécution à une app rendue côté client : vous empilez deux passes de rendu côté client et dégradez le first meaningful paint dans toutes les langues.
Comment empêcher quatre locales de diverger ?
Automatisez l’extraction et traitez les catalogues comme du code source.
Si le routage par locale est abandonné, ce n’est pas à cause du routage. C’est parce que chaque nouvelle fonctionnalité ajoute des chaînes anglaises que personne n’extrait. /de/ se remplit alors lentement de fragments anglais et finit par sembler pire que l’absence totale d’allemand. globalize.now gère cette couche : ajoutez la compétence lovable-i18n à votre espace de travail Lovable et demandez à Lovable de configurer l’i18n, ou installez les compétences en local :
npx skills add globalize-now/globalize-skills
Ajoutez --all pour l’installer pour chaque agent. Les nouvelles chaînes sont extraites, les clés générées, les catalogues traduits et les fichiers PO mis à jour sont commités à chaque Git push.
La tarification est de 20 € / mois par espace de travail, avec 20 € de crédit de traduction inclus — soit environ 200 000 mots — puis une consommation basée sur les tokens au même tarif par caractère. Aucun frais par siège ni par langue : ajouter une cinquième locale modifie votre configuration de routage, pas votre facture. L’inscription inclut 5 € de crédit de traduction et ne nécessite pas de carte bancaire. Plus de détails pour les développeurs et les équipes qui livrent des apps créées avec l’IA.
Le routage et le hreflang se configurent en une après-midi. Maintenir quatre catalogues complets à mesure que l’app évolue est la partie qui ne se termine jamais — et c’est précisément celle que globalize.now automatise.
globalize.now transforme le texte codé en dur de votre application en fichiers de traduction prêts à l'emploi et les maintient à jour au fur et à mesure de vos déploiements.
Essayer globalize.now gratuitement