Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Localization Laravel Package

torann/localization

Torann Localization brings flexible, database-backed localization to Laravel apps. It offers automatic locale detection, localized routes, translation caching, and helpers for managing language files and strings, making multi-language sites easier to build and maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Verify what the package actually is before you install it. The metadata in circulation is contradictory: the repo name and several assessments describe subdomain-based locale routing middleware, while at least one description describes database-backed translations with cache and import/export artisan commands. Those are different packages with different install steps. Spend ten minutes resolving it:

composer require torann/localization:^2.0
# then, before writing any code:
cat vendor/torann/localization/composer.json
ls vendor/torann/localization/src

If you see a Middleware/ directory and a subdomain parser, you have the routing flavor. If you see models, migrations, and Console/Commands, you have the translation flavor. Everything below branches on that answer, so settle it first.

2. Pin exactly. Two stars, zero dependents, one maintainer. composer require torann/localization:2.0.4 (the Laravel 12 line) rather than ^2.0. There is no ecosystem to catch a bad tag for you; treat every upgrade as a deliberate, reviewed event.

3. Publish and read the config.

php artisan vendor:publish --provider="Torann\Localization\LocalizationServiceProvider"

If nothing publishes, run php artisan vendor:publish interactively — Torann packages aren't consistent about tags. You want config/localization.php in front of you before you write a route. The locale map is the package's entire allowlist, so its shape is your contract:

return [
    'locales' => ['en' => 'en', 'fr' => 'fr', 'de' => 'de'],
    'default' => 'en',
];

4. Register the middleware (routing flavor) in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \Torann\Localization\Middleware\Localization::class,
    ]);
})

It must run after TrustProxies (or the host is your CDN's, not the client's) and before anything that reads the locale: auth, signed URLs, view composers.

5. First use case — one secondary market, one subdomain.

Route::domain('{locale}.example.com')
    ->middleware('localization')
    ->group(function () {
        Route::get('/', HomeController::class)->name('home');
    });

Then confirm it works with a throwaway route that returns the facts you care about side by side:

Route::get('/_locale', fn () => [
    'host'   => request()->getHost(),
    'locale' => app()->getLocale(),
]);

Hitting https://fr.example.com/_locale and seeing fr answers most of your opening questions in one response.

6. Where to look first, in this order: the config file (your contract), the middleware (30–60 lines — exactly how the subdomain is parsed and what happens on unknown hosts), the service provider (what it binds, what it registers implicitly), and composer.json (real illuminate/* and PHP constraints). The whole package is an afternoon's read. Do it before you build on it.

Implementation Patterns

Call URL::defaults() once, or every route() call will betray you. Routes carrying a {locale} domain parameter need a default value to generate URLs. Without it, route('home') throws a missing-parameter exception or silently emits the current/apex host. This is the single most valuable line of glue in the integration:

// app/Http/Middleware/SetLocaleDefaults.php
public function handle($request, Closure $next)
{
    URL::defaults(['locale' => app()->getLocale()]);

    return $next($request);
}

Build the locale switcher as a named route that validates before it redirects.

Route::get('/switch/{locale}', function (string $locale) {
    abort_unless(array_key_exists($locale, config('localization.locales')), 404);

    return redirect()->to(
        preg_replace('/^[a-z]{2,}/', $locale, url()->previous())
    );
})->name('locale.switch');

Whitelist against config before the value reaches redirect(). An unvalidated locale segment feeding a redirect is the plausible open-redirect in this design.

Share the locale to the frontend once, from the server. Inertia:

// HandleInertiaRequests::share()
'locale' => app()->getLocale(),
'available_locales' => config('localization.locales'),

Livewire components should read app()->getLocale() per request, never cache it on a component property — the property survives the request cycle and goes stale on navigation.

Carry locale on every queued payload. Mailables are the classic leak: a user browses fr.example.com, the invoice job renders in en because that's how the worker booted.

class SendInvoice implements ShouldQueue
{
    public function __construct(public Invoice $invoice, public string $locale) {}

    public function handle(): void
    {
        App::setLocale($this->locale);
        // ...render and send
    }
}

Same for notifications, broadcast events, and any job that touches __() or a Blade mail view.

Choose a route-group shape deliberately. Two viable options:

  • One parameterized group — Route::domain('{locale}.example.com'), middleware validates {locale}. Fewer definitions; the middleware is the gatekeeper.
  • One group per locale — explicit Route::domain('fr.example.com') groups. Verbose, but each can carry locale-specific middleware (payment provider, content root) without conditionals.

For a single-market pilot the second is easier to reason about and trivially reversible.

Feature-test against full hosts, not paths. The package's behavior is a function of the Host header; $this->get('/') tests nothing relevant.

it('resolves the locale from the subdomain', function () {
    $this->get('https://fr.example.com/')->assertOk()->assertSee('fr');
});

it('handles the apex host', function () {
    $this->get('https://example.com/')->assertRedirect(); // or assertOk — decide, then lock it
});

it('404s an unknown locale', function () {
    $this->get('https://xx.example.com/')->assertNotFound();
});

Write these in week one. They pin down the behavior you actually depend on and survive a future fork.

If you got the database-translation flavor instead, the day-to-day shape is different: run the import command once to seed from lang/, then treat the translations table as the source of truth, keep a cache store configured, and invalidate on write. Read the cache key format out of the source before you write an admin editor — bulk edits that fire per-row cache flushes will be noticeably slow, and the fix is a single flush after the transaction, not a package setting.

If both flavors are present, keep them in separate layers. Routing decides which locale the request is in; the translatable-attributes trait decides how a column is stored per locale. They compose (the trait reads app()->getLocale(), which the middleware set), but debugging is far easier if you can point at which one is wrong.

Gotchas and Tips

Host header spoofing is the real attack surface. State is derived from a client-supplied header. Two consequences: TrustProxies must be correct behind your CDN or the "host" becomes the proxy's, and the resolved locale must always be validated against the config allowlist before it reaches a redirect, a cache key, or URL::defaults(). Add a test that sends Host: evil.example.com and asserts a 404 — not a redirect.

route() output drifts outside an HTTP request. In-request it generally inherits the current host and works. In console commands, queued jobs, scheduled tasks, and mailables rendered by a worker there is no request host, so generated URLs fall back to APP_URL — usually the apex, usually wrong. Audit every route() call reachable from those contexts. This is the sleeper task of the whole integration.

Locale leaks in Octane and long-lived workers. Static or singleton locale state persists across requests and across jobs. Verify the middleware sets locale per request, not per boot, and reset it in the handle() of any long-lived process. If the package stores locale on a singleton, flush that singleton per request.

Dynamic domain parameters plus route:cache plus config:cache is the exact combination that breaks quietly. Run php artisan optimize in CI, not just on your laptop, and hit a locale URL afterward.

Apex and unknown-host behavior is a product decision, not a bug. Does example.com/ 301 to en.example.com, serve the default in place, or 404? Read it out of the middleware and write it down. You also need a canonical and hreflang story per locale — the package provides neither.

Session cookies bleed or don't, and both hurt. Sessions set without an explicit domain land on fr.example.com only, so users appear logged out after switching locale. Set 'domain' => '.example.com' in config/session.php — and accept that every subdomain now shares a session. Decide which failure you'd rather have.

Name collision with mcamara/laravel-localization. Both have historically shipped a LaravelLocalization alias. Muscle memory or a monorepo dependency pulling in mcamara gives you a fatal alias conflict. Alias deliberately in config/app.php or use FQCNs everywhere.

Two locale mechanisms in one request is a debugging nightmare. If you already have a session- or cookie-based switcher, declare the winner explicitly. With subdomain localization the host wins; the switcher becomes a redirector and nothing else.

No Accept-Language negotiation, no cookie fallback, no user preference. If you need any of that it's your middleware, layered before or after this one. Same for translated route slugs — out of scope, and the reason to reach for mcamara if it becomes a requirement.

Vendor it before it reaches production. Two stars, zero dependents, one maintainer, thin code, BSD-2-Clause. Copy it into app/Packages/localization, pin the upstream commit in a README, keep a diff, and add your own tests around hostname parsing. You trade a day of work for an owned ~200-line component and no bus factor. Budget a quarterly upstream diff review and a smoke test on each Laravel major.

Extension points are the config and the middleware — that's it. No plugin system, no events to hook. When you need different behavior you subclass or replace the middleware, which is one more argument for owning the source rather than depending on the tag.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource