Laravel: gestire le traduzioni (in modo SEO friendly)

Da un po’ di tempo a questa parte sto lavorando a un progetto per il quale ho deciso di utilizzare Laravel. È un progetto multilingua, la cui interfaccia dovrà essere tradotta in italiano, inglese, francese, spagnolo e portoghese.

Laravel Blade: come creare direttive personalizzate

L’obiettivo è quello di implementare una struttura basata sulle subdirectory, con url del tipo:

  • tuosito.com/it/pagina
  • tuosito.com/en/pagina
  • tuosito.com/fr/pagina

In questo modo potremo gestire le diverse lingue senza la necessità di mantenere più installazioni indipendenti, una soluzione quest’ultima che sicuramente offre il massimo dell’elasticità, ma che al contempo aumenta vertiginosamente i costi (tempo e soldi) di gestione.

siteground-banner

Cercando online, ho trovato diverse soluzioni (compresa quella suggerita sulla documentazione di Laravel) che permettono di gestire la localizzazione senza tuttavia avere uno slug “parlante”, ovvero andando solamente a modificare la lingua nella quale vengono richiamate le stringhe. Il ché è molto limitate da un punto di vista SEO.

Ho quindi lavorato a una soluzione specifica e penso possa essere d’aiuto anche a te e così eccomi qui: ti spiego tutto ciò che ho fatto.

Come gestire le traduzioni con Laravel

Laravel permette di gestire le traduzioni in modo molto semplice. Possiamo infatti utilizzare la cartella /lang e caricare qui i file necessari per le diverse localizzazioni.

Per quanto riguarda il formato, abbiamo due possibilità. Possiamo infatti utilizzare dei file .php oppure dei .json.

Nel primo caso dovremo utilizzare una struttura di questo tipo:

/lang
   /it
      messages.php
   /en
      messages.php

e formattare i file come segue:

<?php
// /lang/it/messages.php
return [
   'hello_world' => 'Ciao mondo!',
];

Potrai quindi richiamare una traduzione come segue:

/* Nota che in questo caso utilizzo 
la chiave dell'array per richiamare un valore */
echo __('messages.hello_world');

Nel secondo caso dovremo invece riferirci a questa struttura:

/lang
   it.json
   en.json

e formattare i file come segue:

{
  "Hello World!": "Ciao Mondo!"
}

Potrai quindi richiamare una traduzione come segue:

/* In questo caso utilizzi la localizzazione
di default come chiave dell'array */
echo __('Hello World!');

Per approfondire l’argomento ti consiglio di consultare la documentazione ufficiale di Laravel, sempre molto chiara ed esaustiva.

Come indicare a Laravel le lingue supportate

Prima di cominciare l’implementazione vera e propria, dobbiamo chiarire a Laravel le lingue che abbiamo intenzione di supportare. Per farlo apriamo il file:

/config/app.php

Qui possiamo trovare un array contenente alcuni parametri di configurazione. La chiave ‘locale’ determina la localizzazione di default della tua app. Puoi anche indicare un ‘fallback_locale’ che verrà utilizzato nel caso in cui la traduzione che la app cerca di caricare non venga, per qualche motivo, trovata.

Per impostare le localizzazioni supportate dobbiamo aggiungere la chiave ‘available_locales’ come segue:

/* La forma dell'array delle localizzazioni è a tua
scelta, tuttavia dovrai tenerne conto negli step
successivi. */
'available_locales' => [
   'English' => 'en',
   'Italiano' => 'it',
],

Per richiamare i valori contenuti in questo file, possiamo utilizzare la facade Config come segue:

\Illuminate\Support\Facades\Config::get('app.available_locales');

Creiamo i gruppi di rotte per le localizzazioni

Ora che abbiamo indicato le lingue in cui abbiamo intenzione di localizzare la nostra applicazione, dobbiamo creare le rotte. Voglio evitare di scrivere codice boilerplate, sia per semplificare la manutenzione del codice, sia perché vorrei che l’eventuale aggiunta di nuove lingue sia il più semplice possibile.

Iniziamo modificando il file /app/Providers/RouteServiceProvider.php aggiungendo queste righe di codice alla callable che viene passata al metodo routes:

foreach (Config::get('app.available_locales') as $availableLocale) {
   Route::middleware(['web', web.localized'])
      ->prefix($availableLocale)
      ->namespace($this->namespace)
      ->group(base_path("routes/localized/{$availableLocale}.php"));
}

Utilizzando la variable $availableLocale come prefisso delle rotte vado a creare la forma dello slug che sto ricercando. Inoltre, utilizzando la stessa variabile come nome del file, avrò la possibilità di dichiarare le rotte per tutte le lingue supportate.

Naturalmente dovrò provvedere alla creazione di tutti i file necessari rispettando la struttura che ho utilizzato qui).

Ad esempio, per creare la rotta per la home page in tutte le lingue supportate, mi basterà aggiungere la seguente riga di codice ai rispettivi file:

// Esempio per la home in inglese
Route::get('/', fn() => view('welcome')})->name('home_en');

Se ci fermassimo qui, tuttavia, avremmo a che fare con una soluzione poco elegante, perché la dichiarazione qui sopra dovrebbe essere sostanzialmente identica in ognuno dei file dedicati alle lingue e ciò va contro ai nostri obiettivi, ovvero avere un codice manutenibile e privo di ridondanza.

La soluzione che ho trovato è piuttosto semplice.

Prima di tutto creo una subdirectory all’interno di routes/localized che chiamo commons e in quest’ultima creo un file web.php. Questo sarà il file che contiene tutte le rotte comuni alle varie lingue.

Il file di una singola lingua, immaginiamo en.php, diventa quindi:

$locale = 'en';
include 'commons/web.php';

Ora, all’interno del file common posso dichiarare le rotte comuni come segue:

if(!empty($locale)) {
   Route::get('/', fn() => view('welcome')})->name("home_$locale");
   // Dichiaro qui le altre rotte comuni...
}

Creiamo le rotte non localizzate

Ho bisogno di altre due rotte. Una per modificare il locale dell’app in base alle scelte dell’utente e un’altra per gestire l’accesso alla root del sito.

La prima route che vado a creare è quella che segue:

Route::get('/language/{locale}', function (string $locale) {
   app()->setLocale($locale);
   session()->put('locale', $locale);
   return redirect()
      ->to(Localization::getLocalizedUrl(
         url()->previous(), $locale
      )
   );
});

La logica della funzione è piuttosto semplice. L’utente viene infatti indirizzato a una rotta con un parametro passato dinamicamente ({locale}) il quale determina il nuovo locale utilizzato dall’App. Viene quindi impostato richiamando il metodo setLocale e viene inoltre aggiunto ai valori della session.

Successivamente viene fatto un redirect all’url precedente, andando a modificare la parte dell’url relativa alla localizzazione. Vedremo il codice del metodo getLocalizedUrl tra poco.

Ora creo una rotta che mi permette di gestire le richieste per la root del sito.

Route::get('/')->middleware('infer.locale');

Come puoi vedere, la rotta non è particolarmente complessa. Fa tutto il middleware associato.

Quindi…

Creazione dei middleware necessari

Ho bisogno di due middleware. Il primo gestirà tutte le Request alle rotte localizzate, il secondo si occuperà invece della root.

Anche in questo caso, procediamo con ordine. Da terminale lancio il comando artisan per la creazione del middleware:

php artisan make:middleware Localization

Il metodo handle viene dunque modificato come segue:

public function handle(Request $request, Closure $next)
{
   $segments = $request->segments();
   if ($segments[0] !== Session::get('locale')) {
      App::setLocale($segments[0]);
      session()->put('locale', $segments[0]);
   } elseif (App::getLocale() !== Session::get('locale')){
      App::setLocale(Session::get('locale'));
   }
   return $next($request);
}

Cosa succede qui? Semplice: per prima cosa gestisco i cambi di locale da url, quando ad esempio un utente che ha in già navigato sul sito, decide di modificare direttamente l’indirizzo passando da una lingua all’altra.

Posso accorgermi agevolmente di quando si verifica questo caso specifico. Con il metodo segments della Request posso scomporre il path della pagina e averlo organizzato in un array. So che la localizzazione è sempre la prima parte dei miei path, per cui posso accedervi leggendo lo zeresimo elemento dell’array.

Se a questo punto della chiamata questa parte del path è diversa dal locale che ho in sessione significa che l’utente ha forzato il cambio per cui la precedenza è del path. Imposto il locale dell’app, aggiorno il valore in sessione, quindi lascio che la request continui per la sua strada.

Se i valori sono uguali, controllo che il locale dell’app sia lo stesso di quello conservato in sessione. Se sono diversi, significa che l’utente ha richiesto il cambio della lingua, quindi devo aggiornare il valore dell’app.

Il middleware Localization ha poi un altro metodo che ho utilizzato nella rotta ‘/language/{locale}‘, ovvero:

public static function getLocalizedUrl(string $url, string $locale)
{
   if (!str_ends_with($url, '/')) $url .= '/';
   return str_replace(
      array_values(
         array_map(
            fn($locale) => "/$locale/",
               Config::get('app.available_locales')
            )
         ),
      "/$locale/", $url
   );
}

Abbiamo visto che quando l’utente cambia la lingua del sito viene indirizzato alla rotta che modifica la localizzazione dell’app e redirige l’utente da dove era venuto. Per fare in modo che il path cambi insieme al contenuto, devo modificarlo come vedi qui sopra. In buona sostanza vado a sostituire il pezzo della stringa dell’url con la nuova lingua selezionata dall’utente.

Sembra complicata, ma in realtà è soltanto poco leggibile: cosa su cui dovrò lavorare.

Ora devo creare il secondo middleware per gestire la root.

php artisan make:middleware InferLocale

Con quest’ultimo middleware voglio cercare di indirizzare nel miglior modo possibile un utente che cerca di accedere alla root del sito. Ecco il metodo handle:

public function handle(Request $request, Closure $next)
{
   $locale = match (true) {
      Session::has('locale') => 
         Session::get('locale'),
      !empty($request->getPreferredLanguage()) => 
         substr($request->getPreferredLanguage(), 0, 2),
      default => 
         Config::get('app.locale'),
   };
   return redirect()->to("/$locale/");
}

Molto semplicemente, in base a quella che reputo essere la priorità migliore verifico quale locale impostare. In primo luogo verifico se ho un valore in sessione. Se non lo trovo, cerco di risalire alla lingua impostata sul browser e se ancora non ho trovato un valore, ricorro al locale di default dell’app.

Una volta recuperato il valore, faccio un redirect alla home specifica in quella lingua.

Come utilizzare i middleware

Ora che li abbiamo, dobbiamo anche dire a Laravel quando utilizzarli.

Anzitutto dobbiamo dichiararli sul file:

/app/Http/Kernel.php

Qui possiamo aggiungere due chiavi all’array $routeMiddleware come segue:

   'infer.locale' => InferLocale::class,
   'web.localized' => Localization::class

Per quanto riguarda i luoghi in cui utilizzarli in realtà te li ho già mostrati più sopra. Se infatti recuperi le dichiarazioni della rotta per la root e quella dei gruppi di rotte con prefisso, noterai che vengono richiamati i due middleware che abbiamo successivamente creato.

Ora resta solamente un’ultima cosa da fare: un selettore per cambiare la lingua.

HTML per modificare la lingua del sito

Ovviamente tralascio completamente lo stile e mi concentro sulle funzionalità.

<div>
   @foreach(Config::get('app.available_locales') 
      as $localeName => $availableLocale)
      @if($availableLocale === App::currentLocale())
         <span>{{ $localeName }}</span>
      @else
         <a href="{{url("language/$availableLocale")}}">
            <span>{{ $localeName }}</span>
         </a>
      @endif
   @endforeach
</div>

Ora dovrebbe essere più chiaro il motivo per cui ho creato la rotta ‘/language/{locale}‘. Come vedi, infatti, quando l’utente cliccherà su uno dei link del selettore delle varie lingue, sarà inviato alla rotta con il locale selezionato.

That’s all, folks!

Concludendo

Probabilmente abbiamo reso le cose un filo più complicate rispetto ad altre implementazioni ma, come si dice spesso, creare un sistema scalabile, ordinato ed elegante fa perdere un po’ di tempo in prima battuta e ne fa risparmiare una marea sul medio/lungo periodo.

Spero di essere stato utile e nel caso in cui volessi il mio supporto per la realizzazione del tuo progetto Laravel non esitare a contattarmi per ottenere un preventivo.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. Tutti i campi sono obbligatori.