---
title: Localization
published: 2026-11-02
lastmod: 2026-11-02
url: https://larasell.dev/docs/localization
---

## Table of contents
- [Choosing your locales](#choosing-your-locales)
- [Choosing the locale for each request](#choosing-the-locale-for-each-request)
  - [From the browser's language](#from-the-browsers-language)
  - [From the URL](#from-the-url)
  - [Linking the languages of a page](#linking-the-languages-of-a-page)
- [Translating your catalog](#translating-your-catalog)
  - [Adding a language](#adding-a-language)
  - [Removing a language](#removing-a-language)
- [Formatting prices](#formatting-prices)
- [Translation files](#translation-files)

# 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](/docs/reference/locale) 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:

```bash
php artisan vendor:publish --tag=larasell-config
```

Then list the locales your store serves in `config/larasell/locale.php` and pick a default:

```php
use Larasell\Larasell\Enums\Locale;

return [
    'supported' => [Locale::ENGLISH, Locale::GERMAN, Locale::FRENCH_CANADA],
    'default' => Locale::ENGLISH,
];
```

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:

```php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Larasell\Larasell\Settings\LocaleSettings;
use Symfony\Component\HttpFoundation\Response;

class NegotiateLocale
{
    public function __construct(private LocaleSettings $locales) {}

    public function handle(Request $request, Closure $next): Response
    {
        $locale = $this->locales->negotiate($request->header('Accept-Language'));

        App::setLocale($locale->value);

        $response = $next($request);
        $response->headers->set('Content-Language', $locale->value);
        $response->setVary('Accept-Language', false);

        return $response;
    }
}
```

`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`:

```php
use App\Http\Middleware\NegotiateLocale;
use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [NegotiateLocale::class]);
})
```

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](/docs/reference/locale#negotiating-a-locale) 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.

```php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Larasell\Larasell\Enums\Locale;
use Larasell\Larasell\Settings\LocaleSettings;
use Symfony\Component\HttpFoundation\Response;

class SetLocaleFromUrl
{
    public function __construct(private LocaleSettings $locales) {}

    public function handle(Request $request, Closure $next): Response
    {
        $locale = Locale::tryFrom((string) $request->route('locale'));

        abort_unless(in_array($locale, $this->locales->supported(), true), 404);

        App::setLocale($locale->value);
        URL::defaults(['locale' => $locale->value]);
        $request->route()->forgetParameter('locale');

        $response = $next($request);
        $response->headers->set('Content-Language', $locale->value);

        return $response;
    }
}
```

Group the localized routes under a `{locale}` prefix:

```php
use App\Http\Middleware\SetLocaleFromUrl;
use Illuminate\Support\Facades\Route;

Route::prefix('{locale}')->middleware(SetLocaleFromUrl::class)->group(function () {
    Route::get('products/{product}', [ProductController::class, 'show'])->name('products.show');
});
```

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:

```php
use Inertia\Inertia;
use Inertia\Response;
use Larasell\Larasell\Enums\Locale;
use Larasell\Larasell\Http\Requests\ProductDetailRequest;
use Larasell\Larasell\Settings\LocaleSettings;

public function show(ProductDetailRequest $request, LocaleSettings $locales): Response
{
    $product = $request->product();

    $alternates = collect($locales->supported())
        ->filter(fn (Locale $locale): bool => $product->slug->has($locale->value))
        ->mapWithKeys(fn (Locale $locale): array => [
            $locale->value => route('products.show', [
                'locale' => $locale->value,
                'product' => $product->slug->get($locale->value),
            ]),
        ])
        ->all();

    return Inertia::render('Products/Show', [
        'name' => $product->name->get(),
    ])->withViewData(['alternates' => $alternates]);
}
```

Then render the links and the page language in `app.blade.php`:

```blade
<html lang="{{ app()->currentLocale() }}">
    <head>
        {{-- ... --}}
        @foreach ($alternates ?? [] as $hreflang => $url)
            <link rel="alternate" hreflang="{{ $hreflang }}" href="{{ $url }}" />
        @endforeach
        <x-inertia::head />
    </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:

```php
use Larasell\Larasell\Models\Product;
use Larasell\Larasell\Price;

Product::create([
    'name' => ['en' => 'Bicycle', 'de' => 'Fahrrad'],
    'slug' => ['en' => 'bicycle', 'de' => 'fahrrad'],
    'price' => Price::of(19900),
]);
```

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:

```php
use Larasell\Larasell\Models\Product;

$untranslated = Product::whereNull('slug->fr')->get();
```

Add each translation with `with`, which keeps the existing ones:

```php
$product->update([
    'name' => $product->name->with('fr', 'Vélo'),
    'slug' => $product->slug->with('fr', 'velo'),
]);
```

Categories work the same way. See [Translating the product](https://larasell.dev/docs/product.md#translating-the-product)
and [Translating the category](https://larasell.dev/docs/category.md#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](/docs/reference/locale#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`:

```php
$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:

```php
use Larasell\Larasell\Enums\Currency;
use Larasell\Larasell\Price;

App::setLocale('de');
Price::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.
