Nous developpons votre site vitrine SPA À PARTIR DE 300€. Oui, c'est dingue. Web SPA À PARTIR DE 300€ — Oui, c'est dingue. Parlons-en →

Comment traduire les URL de votre site sans dupliquer les routes dans chaque langue

Comment traduire les URL de votre site sans dupliquer les routes dans chaque langue

Quand un site doit parler trois langues, la question des URL est presque toujours réglée en cinq minutes et mal, parce que la solution rapide semble fonctionner. La facture arrive côté référencement, des mois plus tard. Vous verrez ici comment déclarer les trois chemins une seule fois, pourquoi la langue doit figurer en tête du nom de route et ce qui casse si vous la mettez à la fin. Les exemples sont en Laravel, l'une de nos spécialités chez AndorraDev, mais le raisonnement vaut pour tout framework avec des routes nommées.

Un préfixe de langue n'est pas une URL traduite

La première solution qui vient quand quelqu'un demande un site multilingue est de passer la langue en paramètre de route :

Route::prefix('{locale}')->where(['locale' => 'es|ca|fr'])->group(function () {
    Route::get('/search', [SearchController::class, 'index'])->name('search');
});

Une seule déclaration et ça marche. Le problème, c'est ce que ça produit : /es/search, /ca/search, /fr/search. La langue change et le chemin, non.

Or ce chemin est du contenu. Quelqu'un qui cherche en catalan n'écrit pas « search », il écrit « cercar ». Le mot-clé dans l'URL est un signal petit mais réel, et si en plus votre concurrence locale l'a et vous non, c'est un signal que vous offrez. Ce que vous voulez, c'est /buscar, /ca/cercar et /fr/rechercher.

La langue doit figurer dans le nom de la route, et devant

Nommer des routes paraît affaire de goût jusqu'à ce qu'il y en ait trois par page. Dès lors, la façon dont vous composez ce nom décide si vous pouvez résoudre la langue par une opération de chaîne ou s'il vous faut une table de correspondances.

Si vous enregistrez trois routes vers la même destination, chacune a besoin d'un nom différent. Il y a deux façons de le composer et elles ne sont pas équivalentes.

En suffixe : contact, contact.ca, contact.fr. C'est l'option intuitive et elle a une conséquence immédiate : route('contact') renvoie toujours la version dans la langue par défaut, il faut donc écrire un helper maison et l'appeler partout.

En préfixe : es.contact, ca.contact, fr.contact. Cela semble pareil et ne l'est pas, parce que cela permet une chose que le suffixe ne permet pas : intercepter la résolution des noms de manière générique. Avec la langue devant, un générateur d'URL peut essayer d'abord {locale}.{nom} et retomber sur le nom nu s'il n'existe pas.

public function route($name, $parameters = [], $absolute = true)
{
    $locale = app()->getLocale();

    if (! $this->isAlreadyLocalized($name)) {
        if ($route = $this->routes->getByName("{$locale}.{$name}")) {
            return $this->toRoute($route, $parameters, $absolute);
        }
    }

    return parent::route($name, $parameters, $absolute);
}

Avec ça, route('web.contact') renvoie la bonne URL dans la langue active sans changer un seul appel dans les vues.

Déclarer les trois chemins au même endroit

La déclaration ressemble à ceci, avec les trois traductions réunies et sous les yeux :

LocalizedRoute::get('/contact', [
    'en' => '/contact',
    'es' => '/contacto',
    'ca' => '/contacte'
], function () {
    return view('web.contact');
})->name('web.contact');

Cela enregistre trois routes réelles, avec leurs noms préfixés par langue, et applique à la langue par défaut un préfixe vide pour qu'elle soit servie à la racine.

Cela fonctionne pareil avec des paramètres, y compris les liaisons avec clé personnalisée :

LocalizedRoute::get('/search/{location:slug}/{propertyFamily:slug}', [
    'en' => '/search/{location:slug}/{propertyFamily:slug}',
    'es' => '/buscar/{location:slug}/{propertyFamily:slug}',
    'ca' => '/cercar/{location:slug}/{propertyFamily:slug}'
], [SearchController::class, 'index'])->name('web.search');

Et le point qui compte pour la maintenance : les trois traductions sont sur le même écran. Quand quelqu'un change le slug espagnol, il voit à côté le catalan et l'anglais, donc il n'en oublie aucun.

Remplacer le générateur d'URL, c'est ce qui évite de toucher aux vues

Quand on compare des solutions, on regarde ce qu'il faut écrire, et presque jamais ce qu'il faut réécrire. Votre projet est déjà plein d'appels à route(), et vous ne les avez pas tous écrits.

Vous pouvez obtenir le même résultat avec un helper maison, du type transroute('contact'), et il fonctionne parfaitement pour les appels que vous écrivez vous-même. Le problème, ce sont ceux que vous n'écrivez pas.

Le paginateur génère des liens en appelant route(). Les notifications du framework génèrent des liens en appelant route(). Les paquets d'authentification génèrent des liens en appelant route(). Aucun n'appellera votre helper.

En remplaçant le générateur d'URL de l'application, tout cela hérite du comportement gratuitement. Et il y a un cas concret qui illustre pourquoi cela vaut le coup : les URL signées se localisent aussi toutes seules, parce que la méthode qui les génère appelle route() en interne.

Andie recommande

Tranchez une seule chose et tenez-vous-y : soit la langue par défaut vit à la racine, soit toutes portent un préfixe. La première est la plus courante et la meilleure pour le référencement, car votre langue principale garde les URL propres et les autres restent clairement séparées. Configuré ainsi dès le départ, aucune redirection à retoucher plus tard.

Hors d'une requête HTTP, il n'y a aucune langue à deviner

Tout le mécanisme repose sur la langue active de l'application, et cette langue est posée par un middleware qui regarde l'URL. Il suffit de sortir d'une requête HTTP pour que ce middleware n'existe plus, et c'est là que commencent à sortir des liens dans la mauvaise langue.

La résolution automatique utilise la langue active de l'application, fixée par un middleware à partir de l'URL. Dans un job en file d'attente, dans une commande de console ou au moment de générer le lien d'une notification, aucun middleware n'est passé, donc la langue active est celle par défaut et l'URL sort dans cette langue.

Et c'est justement le cas où il importe le plus de viser juste, parce que vous générez un lien pour la langue du destinataire, pas pour la vôtre. La solution est explicite et il faut l'assumer :

$locale = $client->locale;

$url = URL::temporarySignedRoute(
    "{$locale}.client.password.setup",
    now()->addDays(7),
    ['subdomain' => $subdomain]
);

Composer le nom à la main à cet endroit n'est pas un manque, c'est la conséquence logique du fait que la langue se déduit du contexte : hors d'une requête, il n'y a pas de contexte d'où la déduire.

Pourquoi cela compte particulièrement en Andorre

L'Andorre fait partie des rares endroits où un site en trois langues n'est pas un caprice, c'est le scénario normal.

Le catalan est la seule langue officielle, et on y parle en même temps espagnol, portugais et français. Un site commercial sérieux couvre au minimum le catalan, l'espagnol et le français, et cela signifie trois ensembles d'URL qui se positionnent chacun dans sa langue.

Avec des chemins traduits, vous avez trois pages avec le bon mot-clé dans l'URL et des balises hreflang qui se relient proprement entre elles. Avec un préfixe et le même chemin partout, vous avez trois versions de la même URL en anglais. Nous développons cela dans l'article sur les sites multilingues en Andorre et en référencement web.

Quelles alternatives existent et ce qui les distingue

Les trois options réelles, avec leur compromis honnête :

Le préfixe en paramètre de route est la moins chère et celle qui ajoute le moins de code. Si vos URL sont techniques ou si votre produit vit derrière une authentification, là où personne ne cherchera avec ces mots, c'est parfaitement raisonnable et il n'y a aucune raison de compliquer.

La boucle manuelle sur les langues, en déclarant chaque route une fois par langue, est explicite et n'ajoute aucune dépendance. Avec un nombre modeste de pages, cela se tient très bien et c'est une option tout à fait raisonnable. Le coût apparaît avec le volume : chaque route s'écrit autant de fois que vous avez de langues, et le helper qui résout les noms ne couvre que vos propres appels.

mcamara/laravel-localization est la référence connue de l'écosystème sur ce sujet. La différence d'approche à peser est l'endroit où vit la traduction du chemin : dans des fichiers de traduction à part, ou dans la déclaration de la route elle-même. Avec beaucoup de routes, les centraliser dans des fichiers a sa logique ; avec un nombre gérable, les voir réunies dans la déclaration évite de sauter entre deux endroits pour changer un slug.

Ce qui nous a décidés, c'est le remplacement du générateur d'URL, parce que c'est ce qui fait que le reste du framework en tient compte sans rien toucher.

À quoi cela ressemble en production

Sur Abodara, il y a 53 routes localisées déclarées dans trois fichiers de routes, en trois langues, y compris des groupes avec sous-domaine.

Et le chiffre qui mesure le mieux si l'approche fonctionne : dans ses vues, il y a 774 appels à route() et aucun ne porte de préfixe de langue. Tous utilisent le nom neutre et la résolution se fait toute seule. C'est exactement ce que vous voulez, parce que cela signifie qu'ajouter une quatrième langue ne touche pas une seule vue.

Et si vous ne travaillez pas avec Laravel

Les critères se transposent à n'importe quel framework avec routage nommé :

  • Traduisez le chemin, pas seulement le préfixe, si vos pages sont publiques et se positionnent dans les moteurs de recherche.
  • Mettez la langue devant dans l'identifiant de la route. C'est ce qui permet de la résoudre de manière générique plutôt que cas par cas.
  • Interceptez la génération des URL, pas les appels. Si votre solution impose d'utiliser une fonction spéciale, tout ce qui génère des liens depuis l'intérieur du framework vous échappe.
  • Et partez du principe que, hors d'une requête, il faut indiquer la langue explicitement. Les e-mails sont le cas typique, et c'est là que l'erreur se voit le plus.

Où se trouve le code

Le paquet est sur Packagist, le code sur GitHub et la documentation du paquet présente la configuration et les middlewares disponibles. Il est en MIT, sans tables et sans commandes : juste un enregistreur de routes, un générateur d'URL et une poignée de middlewares pour fixer la langue selon l'URL, la session ou le navigateur.

Si vous montez un site en plusieurs langues pour le marché andorran, cette décision se prend le premier jour et conditionne le reste. Nous la travaillons en développement Laravel et en référencement web, et si vous nous dites combien de langues et combien de pages, nous vous dirons quelle approche est la plus rentable.

Écrit par
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Développeur full-stack avec plus de 15 ans d'expérience en Laravel, React, Node.js et architectures cloud. J'aide les entreprises en Andorre à construire leur présence digitale.

Partner de diseño · ionospace.
Besoin d'aide? ×
Andie by AndorraDev
Assistant IA + équipe humaine
Assistant IA d'AndorraDev
Andie
Bonjour! Je suis Andie, l'assistant IA d'AndorraDev. Comment puis-je vous aider? Si vous souhaitez parler avec Edu, il suffit de le demander.
17:31