This is a step by step recipe for installing and configuring the Laravel Localization package in a Twill project.
Objectives:
In this example, we'll be configuring a site in English and French. Out of the box, Twill is configured for English by default. Let's add French as a secondary language:
config/translatable.php
1'locales' => [2 'en',3 'fr',4],
Make sure to enable translations and slugs. The rest is entirely optional:
1php artisan twill:make:module -TS articles
For simplicity, keep the default title and description fields.
Then, run the migrations, add the module to routes/admin.php
and to twill-navigation.php
.
On the Twill side, nothing else is needed. When creating an article, you can edit your content in both languages using the language selector. After editing a record, make sure to mark all languages as "Live":
Install the package in your project via composer:
1composer require mcamara/laravel-localization
Then publish the configuration file:
1php artisan vendor:publish --provider="Mcamara\LaravelLocalization\LaravelLocalizationServiceProvider"
This will create the file: config/laravellocalization.php
Like Twill, the package is configured for English by default. A large number of languages are made available in
the supportedLocales
array. Let's uncomment the line for French:
config/laravellocalization.php
1'supportedLocales' => [2 'en' => ['name' => 'English', 'script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'],3 'fr' => ['name' => 'French', 'script' => 'Latn', 'native' => 'franais', 'regional' => 'fr_FR'],4 // ... other unused languages can remain commented ...5],
For this example, nothing else needs to be customized in this file.
To enable the various localization features such as route translations, language detection and redirect, register the package's middleware:
app/Http/Kernel.php
1protected $routeMiddleware = [ 2 // ... built-in Laravel middleware (auth, cache, etc.) ... 3 4 // Add the following middleware from Laravel Localization: 5 'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class, 6 'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class, 7 'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class, 8 'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class, 9 'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class10];
We'll create a basic index page to list all the news articles. Let's start with a /news
route:
routes/web.php
1use App\Models\Article; 2 3Route::group([ 4 'prefix' => LaravelLocalization::setLocale(), 5 'middleware' => ['localize', 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath'], 6], function () { 7 Route::get('news', function () { 8 return view('site.articles.index', [ 9 'articles' => Article::published()->orderBy('created_at', 'desc')->get(),10 ]);11 })->name('articles');12});
Then, add a generic layout for all news pages:
1<!DOCTYPE html> 2<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> 3 <head> 4 <meta charset="utf-8"> 5 <title>The News</title> 6 </head> 7 <body> 8 <header> 9 <ul>10 @foreach(LaravelLocalization::getSupportedLocales() as $localeCode => $properties)11 <li>12 <a13 rel="alternate"14 hreflang="{{ $localeCode }}"15 href="{{ LaravelLocalization::getLocalizedURL($localeCode, null, [], true) }}"16 >17 {{ strtoupper($localeCode) }}18 </a>19 </li>20 @endforeach21 </ul>22 </header>23 <main>24 @yield('content')25 </main>26 </body>27</html>
Then, add a template for the articles index page:
1@extends('site.layouts.news') 2 3@section('content') 4 <h1>{{ __('news.all_news') }}</h1> 5 6 @if ($articles->isNotEmpty()) 7 <ul> 8 @foreach ($articles as $article) 9 <li>10 <a href="{{ route('article', $article->slug) }}">{{ $article->title }}</a>11 </li>12 @endforeach13 </ul>14 @else15 <p>{{ __('news.no_news') }}</p>16 @endif17@endsection
With this, we have a functioning language switcher. Switching between languages should change the language code prefix in the URL. The list of article titles should also follow the selected language. Let's keep going!
Add the /news/article-slug
route to the translated routes group defined earlier:
1use App\Models\Article; 2use Illuminate\Support\Facades\Route; 3use Illuminate\Support\Facades\Route; 4 5Route::group([ 6 'prefix' => LaravelLocalization::setLocale(), 7 'middleware' => ['localize', 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath'], 8], function () { 9 Route::get('news', function () {10 return view('site.articles.index', [11 'articles' => Article::published()->orderBy('created_at', 'desc')->get(),12 ]);13 })->name('articles');14 15 Route::get('news/{article}', function (Article $article) { 16 return view('site.articles.show', [17 'article' => $article,18 ]);19 })->name('article'); 20});
Since we're trying to rely on Laravel's route-model binding, we must make a small addition to our Article
model to find the appropriate article by slug:
app/Models/Article.php
1<?php 2 3namespace App\Models; 4 5use A17\Twill\Models\Behaviors\HasSlug; 6use A17\Twill\Models\Behaviors\HasTranslation; 7use A17\Twill\Models\Model; 8use App\Repositories\ArticleRepository; 9use Mcamara\LaravelLocalization\Interfaces\LocalizedUrlRoutable;10 11class Article extends Model implements LocalizedUrlRoutable12{13 use HasTranslation;14 use HasSlug;15 16 protected $fillable = [17 'published',18 'title',19 'description',20 ];21 22 public $translatedAttributes = [23 'title',24 'description',25 ];26 27 public $slugAttributes = [28 'title',29 ];30 31 public function resolveRouteBinding($slug, $field = null)32 {33 $article = app(ArticleRepository::class)->forSlug($slug);34 35 abort_if(! $article, 404);36 37 return $article;38 }39 40 // #region routekey41 public function getLocalizedRouteKey($locale)42 {43 return $this->getSlug($locale);44 }45 46 // #endregion routekey47}
We're making good progress, but there are a few problems with our current solution:
/news
in both languages)To translate route segments, Laravel Localization uses standard Laravel language files. We'll create two new files for our route translations in English and French:
resources/lang/en/routes.php
1<?php2 3return [4 'articles' => 'news',5 'article' => 'news/{article}',6];
resources/lang/fr/routes.php
1<?php2 3return [4 'articles' => 'actualites',5 'article' => 'actualites/{article}',6];
Then, we'll update our route definitions to make use of the transRoute
helper instead of hardcoded values:
routes/web.php
1use App\Models\Article; 2use Illuminate\Support\Facades\Route; 3use Mcamara\LaravelLocalization\Facades\LaravelLocalization; 4 5Route::group([ 6 'prefix' => LaravelLocalization::setLocale(), 7 'middleware' => ['localize', 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath'], 8], function () { 9 Route::get(LaravelLocalization::transRoute('routes.articles'), function () {10 return view('site.articles.index', [11 'articles' => Article::published()->orderBy('created_at', 'desc')->get(),12 ]);13 })->name('articles');14 15 Route::get(LaravelLocalization::transRoute('routes.article'), function (Article $article) { 16 return view('site.articles.show', [17 'article' => $article,18 ]);19 })->name('article');20});
As alluded to before, the same method can be used to translate our static text. We'll create two additional files for our text translations in English and French:
resources/lang/en/news.php
1<?php2 3return [4 'all_news' => 'All Articles',5 'no_news' => 'Nothing here :(',6 'back' => 'Back',7];
resources/lang/fr/news.php
1<?php2 3return [4 'all_news' => 'Tous les articles',5 'no_news' => 'Rien ici :(',6 'back' => 'Retour',7];
Because we're using route-model binding, all that's needed to fix our language switcher issue is to implement
the LocalizedUrlRoutable
interface from Laravel Localization in our model. For this, we'll add a new method:
app/Models/Article.php
1<?php 2 3namespace App\Models; 4 5use A17\Twill\Models\Behaviors\HasSlug; 6use A17\Twill\Models\Behaviors\HasTranslation; 7use A17\Twill\Models\Model; 8use App\Repositories\ArticleRepository; 9use Mcamara\LaravelLocalization\Interfaces\LocalizedUrlRoutable;10 11class Article extends Model implements LocalizedUrlRoutable12{13 use HasTranslation;14 use HasSlug;15 16 protected $fillable = [17 'published',18 'title',19 'description',20 ];21 22 public $translatedAttributes = [23 'title',24 'description',25 ];26 27 public $slugAttributes = [28 'title',29 ];30 31 public function resolveRouteBinding($slug, $field = null)32 {33 $article = app(ArticleRepository::class)->forSlug($slug);34 35 abort_if(! $article, 404);36 37 return $article;38 }39 40 // #region routekey41 public function getLocalizedRouteKey($locale)42 {43 return $this->getSlug($locale);44 }45 46 // #endregion routekey47}
And there we have it, a fully translated frontend! The articles index URLs are:
and the single article URLs will look like:
The config/laravellocalization.php
file contains a few more options that can be customized. One of the most useful is
probably hideDefaultLocaleInURL
, which can be used to hide the language prefix from the URLs for your default
language (/en
, in this example).
Similarly, the config/translatable.php
file published by Twill can be customized.
Twill's own UI has also been translated in multiple languages. This can be configured in config/twill.php
:
1<?php2 3return [4 'locale' => 'fr',5 'fallback_locale' => 'en',6 //...7];
In this example, all articles are shown on the index page, regardless of their active state ("Live" checkbox in the CMS) . Filtering articles on the "active" property is a simple way to hide inactive article translations:
routes/web.php
1Route::get(LaravelLocalization::transRoute('routes.articles'), function () {2 return view('site.articles.index', [3 'articles' => Article::published()->orderBy('created_at', 'desc')->get()4 ->filter(function ($article) { return $article->active; }),5 ]);6})->name('articles');
From the HasTranslation
model behavior, the hasActiveTranslation()
method can be used to check if a given article
has an active translation for a given locale. To check against the current locale:
1$isActive = $article->hasActiveTranslation(app()->getLocale());
This way, you can decide what to do when a user tries to access an inactive translation (e.g. redirect to another language, redirect to the index page, trigger a 404, etc.)
Out of the box, every time a slug is changed for a given article in the CMS, Twill keeps a record of the old slug in
the article_slugs
table. We can leverage this to conveniently redirect our users (and search engine crawlers ;) to the
most up to date URL:
routes/web.php
1Route::get(LaravelLocalization::transRoute('routes.article'), function (Article $article) { 2 if ($article->redirect) { 3 if ($article->hasActiveTranslation(app()->getLocale())) { 4 return redirect(route('article', $article->slug)); 5 } 6 abort(404); 7 } 8 9 return view('site.articles.show', [10 'article' => $article,11 ]);12})->name('article');