Desenvolupem la teva web presencial SPA DES DE 300€. Si, es una bogeria. Web SPA DES DE 300€ — Si, es una bogeria. Parlem →

Com traduir les URL del teu web sense duplicar les rutes a cada idioma

Com traduir les URL del teu web sense duplicar les rutes a cada idioma

Quan un web ha de parlar tres idiomes, la part de les URL es resol gairebé sempre en cinc minuts i malament, perquè la solució ràpida sembla funcionar. La factura arriba al posicionament, mesos després. Aquí veuràs com declarar els tres camins una sola vegada, per què l'idioma ha d'anar davant al nom de la ruta i què es trenca si el poses darrere. Els exemples van en Laravel perquè és una de les nostres especialitats a AndorraDev, però el plantejament serveix igual en qualsevol framework amb enrutament amb nom.

Un prefix d'idioma no és una URL traduïda

La primera solució que apareix quan algú demana un web multiidioma és posar l'idioma com a paràmetre de ruta:

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

Una sola declaració i funciona. El problema és el que produeix: /es/search, /ca/search, /fr/search. L'idioma canvia i el camí no.

I aquest camí és contingut. Algú que cerca en català no escriu «search», escriu «cercar». La paraula clau a la URL és un senyal petit però real, i si a més la teva competència local la té i tu no, és un senyal que regales. El que vols és /buscar, /ca/cercar i /fr/rechercher.

L'idioma ha d'anar al nom de la ruta, i davant

Anomenar rutes sembla una qüestió de gust fins que n'hi ha tres per pàgina. A partir d'aquí, com componguis aquest nom decideix si pots resoldre l'idioma amb una operació de text o necessites una taula d'equivalències.

Si registres tres rutes per al mateix destí, cadascuna necessita un nom diferent. Hi ha dues maneres de compondre'l i no són equivalents.

Com a sufix: contact, contact.ca, contact.fr. És el que resulta intuïtiu i té una conseqüència immediata: route('contact') retorna sempre la versió en l'idioma per defecte, així que cal escriure un ajudant propi i cridar-lo a tot arreu.

Com a prefix: es.contact, ca.contact, fr.contact. Sembla el mateix i no ho és, perquè permet una cosa que el sufix no permet: interceptar la resolució de noms de manera genèrica. Amb l'idioma davant, un generador d'URL pot provar primer {locale}.{nom} i caure al nom pelat si no existeix.

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);
}

Amb això, route('web.contact') retorna la URL correcta en l'idioma actiu sense canviar ni una crida a les vistes.

Declarar els tres camins en un sol lloc

La declaració queda així, amb les tres traduccions juntes i a la vista:

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

Això registra tres rutes reals, amb els seus noms prefixats per idioma, i aplica a l'idioma per defecte un prefix buit perquè se serveixi a l'arrel.

Funciona igual amb paràmetres, inclosos els enllaços amb clau personalitzada:

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');

I el que importa per al manteniment: les tres traduccions són a la mateixa pantalla. Quan algú canvia el slug espanyol, veu al costat el català i l'anglès, així que no se n'oblida cap.

Substituir el generador d'URL és el que evita tocar les vistes

En comparar solucions es mira el que cal escriure, i gairebé mai el que cal reescriure. El teu projecte ja és ple de crides a route(), i no totes les vas escriure tu.

Pots aconseguir el mateix resultat amb un ajudant propi, del tipus transroute('contact'), i funciona perfectament per a les crides que escrius tu. El problema són les que no escrius tu.

El paginador genera enllaços cridant route(). Les notificacions del framework generen enllaços cridant route(). Els paquets d'autenticació generen enllaços cridant route(). Cap d'ells no cridarà el teu ajudant.

Substituint el generador d'URL de l'aplicació, tot això hereta el comportament de franc. I hi ha un cas concret que il·lustra per què compensa: les URL signades també es localitzen soles, perquè el mètode que les genera crida internament route().

Andie recomana

Decideix una sola cosa i mantén-la: o l'idioma per defecte viu a l'arrel, o tots porten prefix. El primer és l'habitual i el que millor posiciona, perquè el teu idioma principal es queda amb les URL netes i la resta queden clarament separats. Configurat així des del principi, no cal tocar redireccions més endavant.

Fora d'una petició HTTP no hi ha idioma que valgui

Tot el mecanisme es recolza en l'idioma actiu de l'aplicació, i aquest idioma el posa un middleware en mirar la URL. N'hi ha prou amb sortir d'una petició HTTP perquè aquest middleware no existeixi, i és aquí on comencen a sortir enllaços en l'idioma que no tocava.

La resolució automàtica fa servir l'idioma actiu de l'aplicació, que el fixa un middleware a partir de la URL. En una tasca en cua, en una ordre de consola o en generar l'enllaç d'una notificació no ha passat cap middleware, així que l'idioma actiu és el de per defecte i la URL surt en aquest idioma.

I aquest és justament el cas en què més importa encertar, perquè estàs generant un enllaç per a l'idioma del destinatari, no per al teu. La solució és explícita i cal assumir-la:

$locale = $client->locale;

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

Compondre el nom a mà aquí no és una mancança, és la conseqüència lògica que l'idioma es dedueixi del context: fora d'una petició no hi ha context del qual deduir-lo.

Per què això importa especialment a Andorra

Andorra és dels pocs llocs on un web en tres idiomes no és un caprici, és l'escenari normal.

El català és l'única llengua oficial, i alhora s'hi parlen castellà, portuguès i francès. Un web comercial seriós cobreix com a mínim català, castellà i francès, i això significa tres conjunts d'URL competint cadascun en el seu idioma.

Amb camins traduïts tens tres pàgines amb la paraula clau correcta a la URL i amb etiquetes hreflang que s'enllacen entre si de manera neta. Amb un prefix i el mateix camí a tots, tens tres versions de la mateixa URL en anglès. Ho desenvolupem a l'article sobre webs multiidioma a Andorra i a posicionament web.

Quines alternatives hi ha i en què es diferencien

Les tres opcions reals, amb el seu compromís honest:

El prefix amb paràmetre de ruta és la més barata i la que menys codi afegeix. Si les teves URL són tècniques o el teu producte viu darrere d'un inici de sessió, on ningú no cercarà per aquestes paraules, és perfectament raonable i no hi ha motiu per complicar-la.

El bucle manual sobre els idiomes, declarant cada ruta una vegada per idioma, és explícit i no afegeix dependències. Amb un nombre modest de pàgines es porta perfectament i és una opció del tot raonable. El cost apareix amb el volum: cada ruta s'escriu tantes vegades com idiomes tinguis, i l'ajudant que resol els noms només cobreix les crides pròpies.

mcamara/laravel-localization és la referència coneguda de l'ecosistema per a aquest problema. La diferència d'enfocament que convé valorar és on viu la traducció del camí: en fitxers de traducció a part, o en la mateixa declaració de la ruta. Amb moltes rutes, tenir-les centralitzades en fitxers té la seva lògica; amb un nombre manejable, veure-les juntes a la declaració evita saltar entre dos llocs per canviar un slug.

El que a nosaltres ens va decidir va ser la substitució del generador d'URL, perquè és el que fa que la resta del framework se n'assabenti sense tocar res.

Com es veu en producció

A Abodara hi ha 53 rutes localitzades declarades en tres fitxers de rutes, en tres idiomes, incloent-hi grups amb subdomini.

I el número que millor mesura si l'enfocament funciona: a les seves vistes hi ha 774 crides a route() i cap no porta prefix d'idioma. Totes fan servir el nom neutre i la resolució passa sola. Això és exactament el que vols, perquè significa que afegir un quart idioma no toca ni una vista.

I si no treballes amb Laravel

Els criteris es traslladen a qualsevol framework amb enrutament per nom:

  • Tradueix el camí, no només el prefix, si les teves pàgines són públiques i competeixen als cercadors.
  • Posa l'idioma davant a l'identificador de la ruta. És el que permet resoldre'l de manera genèrica en comptes de cas a cas.
  • Intercepta la generació d'URL, no les crides. Si la teva solució obliga a fer servir una funció especial, tot el que generi enllaços des de dins del framework et queda fora.
  • I assumeix que fora d'una petició cal dir l'idioma explícitament. Els correus són el cas, i és on més es nota si t'equivoques.

On és el codi

El paquet és a Packagist, el codi a GitHub i la documentació del paquet recull la configuració i els middlewares disponibles. És MIT, sense taules i sense ordres: només un registrador de rutes, un generador d'URL i un grapat de middlewares per fixar l'idioma segons la URL, la sessió o el navegador.

Si estàs muntant un web en diversos idiomes per al mercat andorrà, aquesta decisió es pren el primer dia i condiciona la resta. La treballem a desenvolupament Laravel i posicionament web, i si ens expliques quants idiomes i quantes pàgines et diem quin enfocament compensa.

Escrit per
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desenvolupador full-stack amb més de 15 anys d'experiència en Laravel, React, Node.js i arquitectures cloud. Ajudo empreses a Andorra a construir la seva presència digital.

Partner de diseño · ionospace.
Necessites ajuda? ×
Andie by AndorraDev
Assistent IA + equip humà
Assistent IA d'AndorraDev
Andie
Hola! Soc Andie, l'assistent IA d'AndorraDev. En què et puc ajudar? Si necessites parlar amb Edu, només demana-ho.
17:30