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

Multi Language Laravel Package

michele-angioni/multi-language

A Laravel package for building multi-language applications, with tools for managing translations, localized routes, and language switching so your app can serve content in multiple languages.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin layer over Laravel's native localization: it gives you a languages table + Language model, a config file, and (usually) a route middleware that reads a {locale} route parameter and calls App::setLocale(). Treat it as a bootstrap for URL-prefixed locales, not a full i18n suite — you still supply the translations, the switcher UI, and the localized content model.

1. Install and pin. It's a Laravel 5-era package (last release Feb 2017), so constrain both sides:

composer require michele-angioni/multi-language

If you're on Laravel 5.4/5.5 the constraint resolves cleanly; on 5.6+ expect to fork and patch removed helpers (see Gotchas). Check composer.json first — the illuminate/* requirement tells you immediately whether this is a drop-in or a fork job.

2. Register the provider. Laravel 5.5 package auto-discovery was not standard when this shipped, so register manually in config/app.php:

'providers' => [
    // ...
    MicheleAngioni\MultiLanguage\MultiLanguageServiceProvider::class,
],

'aliases' => [
    // ...
    'MultiLanguage' => MicheleAngioni\MultiLanguage\Facades\MultiLanguage::class,
],

(Confirm the exact FQCNs by opening vendor/micheleangioni/multi-language/src — the facade class name is the one thing worth verifying before you wire up views.)

3. Publish and migrate.

php artisan vendor:publish --provider="MicheleAngioni\MultiLanguage\MultiLanguageServiceProvider"
php artisan migrate

4. Seed the languages. The migration creates the schema but almost never the rows you actually want. Add a seeder with updateOrCreate so it's re-runnable:

// database/seeds/LanguageSeeder.php
foreach ([
    ['slug' => 'en', 'name' => 'English', 'default' => true],
    ['slug' => 'it', 'name' => 'Italiano', 'default' => false],
] as $lang) {
    Language::updateOrCreate(['slug' => $lang['slug']], $lang);
}

First use case: prefix every web route with {locale} and attach the package middleware to a group. That single move gets you /it/chi-siamo alongside /en/about, which is what 90% of "multi-language" tickets actually ask for.

// routes/web.php
Route::group([
    'prefix' => '{locale}',
    'middleware' => 'locale',   // registered in app/Http/Kernel.php
], function () {
    Route::get('/', 'HomeController@index')->name('home');
});

// app/Http/Kernel.php $routeMiddleware
'locale' => \MicheleAngioni\MultiLanguage\Http\Middleware\SetLanguage::class,

Look here first, in this order: config/multi-language.php (or whatever the publish tag produces — the default-locale key and redirect behaviour live there), the middleware class (that's the whole runtime behaviour), then the Language model (scopes/helpers like default-language lookup).

Implementation Patterns

Constrain the locale segment — this is the highest-value five minutes you'll spend. An unconstrained {locale} swallows /admin, /login, and every future top-level path. Build the constraint from the DB once per request (cache it):

$slugs = Cache::rememberForever('language_slugs', fn () => Language::pluck('slug')->all());

Route::group([
    'prefix' => '{locale}',
    'middleware' => 'locale',
    'where' => ['locale' => implode('|', $slugs)],
], function () { /* ... */ });

Make the middleware do more than setLocale. Wrap/replace the package middleware with your own that also sets Carbon, the translator fallback, and records the active Language instance for later use:

public function handle($request, Closure $next)
{
    $slug = $request->route('locale');
    $language = Language::where('slug', $slug)->firstOrFail();

    App::setLocale($language->slug);
    Carbon::setLocale($language->slug);
    View::share('currentLanguage', $language);
    View::share('languages', Language::all());

    return $next($request);
}

Note the firstOrFail() — combined with the route constraint it makes unknown locales a 404 rather than a silent fallback to English, which is what you want for SEO.

One helper for link generation, used everywhere. Manually threading ['locale' => app()->getLocale()] through 200 route() calls is where this pattern usually rots:

function lroute(string $name, array $params = [], bool $absolute = true): string
{
    return route($name, array_merge(['locale' => app()->getLocale()], $params), $absolute);
}

Blade: <a href="{{ lroute('products.show', $product) }}">. Because locale is just another route parameter, route('products.show', $product) without it will throw UrlGenerationException — good, it fails loudly instead of silently emitting the wrong URL.

Language switcher that preserves context. Never hardcode the home page as the switch target:

@foreach ($languages as $language)
    <a href="{{ lroute(Route::currentRouteName() ?: 'home',
                       array_merge(request()->route()->parameters(),
                                   ['locale' => $language->slug])) }}"
       hreflang="{{ $language->slug }}"
       @class(['active' => $language->slug === app()->getLocale()])>
        {{ $language->name }}
    </a>
@endforeach

request()->route()->parameters() carries through route-model-bound IDs; the ['locale' => ...] merge overwrites just the locale.

Persist the choice across sessions. Three options, in increasing fidelity: session key (fragile, breaks in API/queue), cookie (works for guests), user column (correct for logged-in users). Do it in the switch route, not on every request:

Route::get('language/{locale}', function ($locale) {
    abort_unless(Language::where('slug', $locale)->exists(), 404);
    session(['locale' => $locale]);
    if (auth()->check()) {
        auth()->user()->update(['locale' => $locale]);
    }
    return redirect()->back();
})->name('language.switch');

Then, in the locale-detection middleware, order the resolution explicitly: route parameter → authenticated user → session → Accept-Language → default. Write that chain down in a comment; it's the thing people argue about in code review.

Localized content: pick a storage model on day one. Two viable shapes and you must not mix them:

// (a) column-per-locale — simple, breaks down past ~3 locales or long text
$products = Product::select('id', "name_{$locale} as name", "description_{$locale} as description")->get();

// (b) translations table — the scalable one, FK back to languages.slug
$product->translations()->where('locale', app()->getLocale())->first()?->name;

With (b), add a Product::translated() scope plus a withTranslation() eager-load and use fallback_locale for missing rows — never return a blank title because a translator was late.

SEO plumbing is mandatory, not optional. Emit hreflang alternates and a canonical for every localized page:

@foreach ($languages as $language)
    <link rel="alternate" hreflang="{{ $language->slug }}"
          href="{{ lroute(Route::currentRouteName(), array_merge(request()->route()->parameters(), ['locale' => $language->slug])) }}">
@endforeach
<link rel="canonical" href="{{ url()->current() }}">

Background work needs explicit locale. Queued jobs, mailables, and notifications do not inherit the request locale. Pass it as a constructor argument and App::setLocale($this->locale) at the top of handle() / build().

API variant. For JSON endpoints, skip the URL prefix entirely and resolve from Accept-Language in a separate middleware, then reuse the same Language table for validation. Mixing the two — prefix and header — is a reliable source of "why is my de request returning Italian" bugs.

Gotchas and Tips

  • Laravel 5 only, and it shows. The 2017 release predates Str::slug, Arr::get, and the removal of the str_slug()/array_get() helpers in Laravel 6. If you're on 6+, expect Call to undefined function str_slug() on boot and a handful of type-hint mismatches. The fork is typically 10–20 lines in one or two files. Budget an afternoon, not a day.
  • No auto-discovery. Adding the provider only to config/app.php is not enough if you also use config:cache — run php artisan config:clear after editing, or you'll debug a middleware that "isn't registered" for an hour.
  • Middleware ordering matters if you read the locale from session. web (which includes StartSession) must run before your locale middleware. In $middlewareGroups['web'], put 'locale' at the end of the array, not the beginning.
  • Never trust the DB on every request. Language::where('slug', ...) in middleware is one query per request. Cache the slug list and the language collection forever, and forget the cache in the Language model's saved/deleted events.
  • Slug ≠ Laravel locale. en/it are fine; pt-BR, zh_CN, en_GB are not URL-safe habits. Keep a locale column separate from slug and set app()->setLocale($language->locale), not the slug. Doing this late means renaming URLs and losing links.
  • Duplicate content if the default locale is both prefixed and unprefixed. Decide once: either / redirects 301 to /en/, or /en/* redirects 301 to /*. Never serve both 200.
  • Route caching works, closures don't. php artisan route:cache will fail if you used closure routes in the localized group — move them to controllers before deploying, and re-run route:cache after any locale add.
  • config/app.php fallback_locale should equal your default language slug. The package's own fallback and Laravel's are two different mechanisms; setting both to the same value avoids the "some strings translated, some not, differently" bug.
  • Translation files are keyed by app()->getLocale(), not by the route segment. If the slug is de but the locale is de_DE, you need resources/lang/de_DE/. Mismatch here silently renders the fallback language with no error.
  • route('name') without locale throws. That's a feature — it surfaces every unlocalized link at test time rather than in production. Add a smoke test that crawls your nav and asserts no UrlGenerationException.
  • Test the switch, not just the render. $this->get('/it/prodotti')->assertSee('Prodotti'); $this->assertEquals('it', app()->getLocale()); — and reset App::setLocale() in tearDown(), because the locale leaks between tests in the same process.
  • Extension points to prefer over editing vendor code: your own middleware wrapping the package's, a Language model subclass bound in a service provider, and a lroute() helper. All three survive a composer update; patching src/ does not.
  • Accessibility/HTML basics the package won't do: <html lang="{{ app()->getLocale() }}"> and dir="rtl" for ar/he. Add them in the layout the same day you add the first non-Latin language — retrofitting RTL into a shipped CSS is the expensive version of this task.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr