Localization
This guide shows you how to run your store in several languages: which locales to support, how to pick the right one for each visitor, and how to translate your catalog. The locale reference covers each class and method in detail.
Larasell writes locales as BCP 47 tags, like en, de-DE or zh-Hans.
Choosing your locales
Publish the Larasell config if you haven't already:
1php artisan vendor:publish --tag=larasell-configThen list the locales your store serves in config/larasell/locale.php and pick a default:
1use Larasell\Larasell\Enums\Locale;
2
3return [
4 'supported' => [Locale::ENGLISH, Locale::GERMAN, Locale::FRENCH_CANADA],
5 'default' => Locale::ENGLISH,
6];The default locale is the language visitors see when none of their languages is supported. It's also the translation Larasell falls back to when a product has no text in the visitor's language. Pick the language your catalog is most complete in.
Use a bare language like de unless you really have different texts per country. A de
translation serves visitors from Germany, Austria and Switzerland alike, because de-AT falls
back to de. Add a regional locale like de-CH only for the texts that differ.
Choosing the locale for each request
Larasell doesn't change the locale for you. Every request uses config('app.locale') until your
app sets a different one. Below are two common ways to set it.
From the browser's language
Browsers send the visitor's preferred languages in the Accept-Language header.
LocaleSettings::negotiate picks the best supported locale for it. Create a middleware:
1namespace App\Http\Middleware;
2
3use Closure;
4use Illuminate\Http\Request;
5use Illuminate\Support\Facades\App;
6use Larasell\Larasell\Settings\LocaleSettings;
7use Symfony\Component\HttpFoundation\Response;
8
9class NegotiateLocale
10{
11 public function __construct(private LocaleSettings $locales) {}
12
13 public function handle(Request $request, Closure $next): Response
14 {
15 $locale = $this->locales->negotiate($request->header('Accept-Language'));
16
17 App::setLocale($locale->value);
18
19 $response = $next($request);
20 $response->headers->set('Content-Language', $locale->value);
21 $response->setVary('Accept-Language', false);
22
23 return $response;
24 }
25}Content-Language tells the client which language it got. Vary: Accept-Language stops caches
and CDNs from serving a German page to an English visitor.
Register the middleware in bootstrap/app.php:
1use App\Http\Middleware\NegotiateLocale;
2use Illuminate\Foundation\Configuration\Middleware;
3
4->withMiddleware(function (Middleware $middleware): void {
5 $middleware->web(append: [NegotiateLocale::class]);
6})This works well for APIs and for headless storefronts. A visitor from Switzerland whose browser
sends de-CH,de;q=0.9,en;q=0.8 gets de. The
reference explains how matching works.
From the URL
For a storefront that search engines index, put the locale in the URL, like /de/products/fahrrad.
Every language then has its own URL, which search engines need to index each one.
1namespace App\Http\Middleware;
2
3use Closure;
4use Illuminate\Http\Request;
5use Illuminate\Support\Facades\App;
6use Illuminate\Support\Facades\URL;
7use Larasell\Larasell\Enums\Locale;
8use Larasell\Larasell\Settings\LocaleSettings;
9use Symfony\Component\HttpFoundation\Response;
10
11class SetLocaleFromUrl
12{
13 public function __construct(private LocaleSettings $locales) {}
14
15 public function handle(Request $request, Closure $next): Response
16 {
17 $locale = Locale::tryFrom((string) $request->route('locale'));
18
19 abort_unless(in_array($locale, $this->locales->supported(), true), 404);
20
21 App::setLocale($locale->value);
22 URL::defaults(['locale' => $locale->value]);
23 $request->route()->forgetParameter('locale');
24
25 $response = $next($request);
26 $response->headers->set('Content-Language', $locale->value);
27
28 return $response;
29 }
30}Group the localized routes under a {locale} prefix:
1use App\Http\Middleware\SetLocaleFromUrl;
2use Illuminate\Support\Facades\Route;
3
4Route::prefix('{locale}')->middleware(SetLocaleFromUrl::class)->group(function () {
5 Route::get('products/{product}', [ProductController::class, 'show'])->name('products.show');
6});Unsupported locales return a 404, and so does any spelling other than the canonical one, like
/de-de/. URL::defaults fills in the locale for you, so route('products.show', $slug) links
stay in the current language. Forgetting the parameter keeps your controllers' signatures free of
$locale.
You can combine both approaches: redirect / to the locale negotiate picks, and serve
everything else from the URL.
Linking the languages of a page
Search engines find the other languages of a page through hreflang links in its <head>.
With Inertia, render them in resources/views/app.blade.php, the root view. The server renders it
on every full page load, and crawlers always load pages that way, so this works without SSR.
Build the links in your controller. Each language uses its own slug, so only link the languages
the product has a slug for. Pass them with withViewData, which hands data to the root view
without adding it to your page props:
1use Inertia\Inertia;
2use Inertia\Response;
3use Larasell\Larasell\Enums\Locale;
4use Larasell\Larasell\Http\Requests\ProductDetailRequest;
5use Larasell\Larasell\Settings\LocaleSettings;
6
7public function show(ProductDetailRequest $request, LocaleSettings $locales): Response
8{
9 $product = $request->product();
10
11 $alternates = collect($locales->supported())
12 ->filter(fn (Locale $locale): bool => $product->slug->has($locale->value))
13 ->mapWithKeys(fn (Locale $locale): array => [
14 $locale->value => route('products.show', [
15 'locale' => $locale->value,
16 'product' => $product->slug->get($locale->value),
17 ]),
18 ])
19 ->all();
20
21 return Inertia::render('Products/Show', [
22 'name' => $product->name->get(),
23 ])->withViewData(['alternates' => $alternates]);
24}Then render the links and the page language in app.blade.php:
1<html lang="{{ app()->currentLocale() }}">
2 <head>
3 {{-- ... --}}
4 @foreach ($alternates ?? [] as $hreflang => $url)
5 <link rel="alternate" hreflang="{{ $hreflang }}" href="{{ $url }}" />
6 @endforeach
7 <x-inertia::head />
8 </head>Because the locale is already a BCP 47 tag, app()->currentLocale() needs no conversion for
lang. Inertia doesn't render the root view again on client-side visits, so the links in the
browser stay those of the first page. That's fine: they're only there for crawlers.
Translating your catalog
Translatable fields take an array keyed by locale:
1use Larasell\Larasell\Models\Product;
2use Larasell\Larasell\Price;
3
4Product::create([
5 'name' => ['en' => 'Bicycle', 'de' => 'Fahrrad'],
6 'slug' => ['en' => 'bicycle', 'de' => 'fahrrad'],
7 'price' => Price::of(19900),
8]);Give every product a slug in every supported locale. Names and descriptions fall back to the
default locale, but slugs don't. A product without a de slug can't be found at a German URL.
Adding a language
To add a language to a running store, first translate the catalog, then add the locale to
supported. This way visitors never see a language that's only half done.
Find the products that still need a translation by querying the JSON key:
1use Larasell\Larasell\Models\Product;
2
3$untranslated = Product::whereNull('slug->fr')->get();Add each translation with with, which keeps the existing ones:
1$product->update([
2 'name' => $product->name->with('fr', 'Vélo'),
3 'slug' => $product->slug->with('fr', 'velo'),
4]);Categories work the same way. See Translating the product and Translating the category for all methods.
Translate the names of product attributes and their values too, or the variant picker shows "Size / Small" on an otherwise French page. See Translatable fields for every field that needs a translation.
Removing a language
Remove the locale from supported first, so visitors stop getting it. You can keep the
translations in the database, or remove them with without:
1$product->update(['name' => $product->name->without('fr')]);without returns null when it removes the last translation, so keep at least one locale.
Formatting prices
Price::format formats prices in the current locale, so prices follow the visitor's language
once your middleware set it:
1use Larasell\Larasell\Enums\Currency;
2use Larasell\Larasell\Price;
3
4App::setLocale('de');
5Price::format(Price::of(19900), Currency::EUR); // '199,00 €'The locale only changes how the price looks. It doesn't change the currency. A shop in Switzerland
might serve de, fr and it but charge everybody in CHF, so keep choosing the currency
separately.
Translation files
Your app's own texts live in Laravel's lang/ folder. App::setLocale('de-DE') loads
lang/de-DE.json and lang/de-DE/*.php, so name these files and folders after the same tags you
use in config/larasell/locale.php.
Community packages like laravel-lang ship folders with underscores, like lang/pt_BR. Rename them
to lang/pt-BR when you install them.