wotz/laravel-locale-collection
A Laravel package that treats your app's supported locales as a collection, making it easy to register, query, and manage languages with clean helpers built for Laravel's container and views. Lightweight and configurable.
The package is tiny and undocumented in the wild (0 stars, 0 dependents), so treat the source as the docs. Start here:
Install and inspect the diff.
composer require wotzebra/laravel-locale-collection
Then open vendor/wotzebra/laravel-locale-collection/src/ and read two files first: the service provider (to see what it binds/singletons) and the config file (to see the shape of a "locale entry"). Everything else is a thin wrapper over those.
Publish config before writing any code.
php artisan vendor:publish --provider="Wotz\LocaleCollection\LocaleCollectionServiceProvider"
The config file is where you declare supported locales. Do not scatter ['en','fr','de'] arrays through your app — the whole point of this package is to make that config the single source of truth.
Smoke-test the binding.
// tinker
app(\Wotz\LocaleCollection\LocaleCollection::class)->all();
// or the facade if one is registered
\Wotz\LocaleCollection\Facades\LocaleCollection::all();
If you get back an array/Collection of locale objects or codes, you're wired up. If it autowires as a plain Illuminate\Support\Collection instead, check the provider's alias/binding name — this is the most common first stumble with small packages.
First real use case: kill the hardcoded locale list. Replace:
$locales = ['en', 'fr', 'nl'];
with a loop over the collection, and use the package to answer "is this locale supported?" in middleware/validation instead of in_array($x, config('app.locales')).
Once that's done, the second use case is almost always the language switcher in the navbar — see below.
Config as the source of truth, plus a thin presenter.
Keep native names, direction (LTR/RTL), and any regional metadata in the config array, and let the collection resolve the current locale against it. This prevents the classic bug where config('app.locale') says en_US but your switcher only knows en.
// config/locale-collection.php (typical shape for a package like this)
'locales' => [
'en' => ['name' => 'English', 'native' => 'English', 'dir' => 'ltr'],
'fr' => ['name' => 'French', 'native' => 'Français', 'dir' => 'ltr'],
'ar' => ['name' => 'Arabic', 'native' => 'العربية', 'dir' => 'rtl'],
],
'fallback' => 'en',
Locale-prefixed routing with a single middleware. The standard Laravel pattern, made cleaner by pulling the supported set from the package:
Route::middleware(\Wotz\LocaleCollection\Http\Middleware\SetLocale::class)
->prefix('{locale}')
->whereIn('locale', LocaleCollection::codes()->all())
->group(function () {
Route::get('/', HomeController::class)->name('home');
});
If the package ships its own middleware, use it rather than rolling one — but verify it actually calls App::setLocale() and sets the session, otherwise url()->previous() and redirects will wobble between locales.
Language switcher that preserves the current path.
Don't link to / — users expect to stay on the equivalent page:
@foreach (\Wotz\LocaleCollection\Facades\LocaleCollection::all() as $code => $locale)
<a href="{{ route(
request()->route()->getName(),
array_merge(request()->route()->parameters(), ['locale' => $code])
) }}" @class(['font-bold' => app()->getLocale() === $code])>
{{ $locale['native'] }}
</a>
@endforeach
Persist the choice, then apply it on every request.
Session is fine for guests; add a locale column for authenticated users and let a global middleware prefer the user column over the session over config('app.fallback_locale'). Wire the switcher as a small POST route that writes to both and redirects back.
Sync Carbon and validation. Two lines that are easy to forget and cause subtle bugs:
Carbon::setLocale($locale); // dates in the wrong language otherwise
'locale' => ['required', 'string', Rule::in(LocaleCollection::codes())],
Put the Carbon call in the same middleware that calls App::setLocale().
Testing helper.
Wrap "set the app locale for this test" in a base-test-case method that goes through the package ($this->actingAsLocale('fr')) so your tests exercise the same code path as production.
composer.json, and when behaviour surprises you, dump() inside the provider. Budget an hour to read the whole src/ directory — it will be short.register() but only read the published file. If nothing works, check config('locale-collection') in tinker before blaming your code.config:cache in production. If your locale list only lives in a runtime-edited config (e.g. an admin panel writes to the config file), it will not update until you re-run php artisan config:cache. Prefer a DB-backed source and feed the package from it if locales are user-managed.en_US vs en-us vs en is the #1 source of "why is my switcher blank" bugs. Normalise to one casing at the boundary and compare against LocaleCollection::codes() — never against raw request input.App::setLocale() does not do what users think. It changes validation messages and __() lookup but not Carbon, not the <html lang> attribute, and not cached responses. Handle each separately.\Illuminate\Session\Middleware\StartSession after your locale middleware in the priority list, session-stored locales won't be readable. Add your middleware to $middlewarePriority in bootstrap/app.php (Laravel 11+) or Kernel.php (10 and below) immediately after StartSession.app()->instance(...) in a service provider for tests; look for a ->extend()/->macro() hook in the collection class before reaching for that. For anything missing (e.g. a toSelectOptions() helper for form dropdowns), a Laravel Collection::macro in a service provider is the lean fix and won't fight the package.spatie/laravel-translatable — they solve different problems and mixing the two concepts causes architectural pain later.app()->getLocale() in tinker, (2) config('app.fallback_locale'), (3) middleware order, (4) browser Accept-Language if the package does auto-detection, (5) the lang/ files actually exist for that code.How can I help you explore Laravel packages today?