stefro/laravel-lang-country
A Laravel package providing ready-to-use language and country lists, ISO codes, localized names, and validation rules for country and language codes. It simplifies building multilingual, country-aware forms and APIs with consistent reference data and less boilerplate.
Install and publish the config
composer require stefro/laravel-lang-country
php artisan vendor:publish --tag=laravel-lang-country
If the tag isn't registered in your version, publish by provider instead:
php artisan vendor:publish --provider="Stefro\LaravelLangCountry\LaravelLangCountryServiceProvider"
Look here first
config/laravel-lang-country.php — the supported_locales array is the single most important knob. Everything the package does is gated by whether a lang_COUNTRY pair appears here.Stefro\LaravelLangCountry\Facades\LaravelLangCountry — it's a thin object (locale, lang, country, date format, set-locale). Read src/ once; it's small enough to finish in ten minutes.First use case: user picks language + country at signup
The package exists because app()->setLocale('en') is ambiguous — an en_US user and an en_GB user want the same language but different date rendering. Store both values on the user, then:
use Stefro\LaravelLangCountry\Facades\LaravelLangCountry;
LaravelLangCountry::setLocale($user->language, $user->country);
app()->setLocale(LaravelLangCountry::locale()); // "en_US"
Carbon::setLocale(LaravelLangCountry::locale());
// Later, when rendering a date:
$invoice->created_at->format(LaravelLangCountry::dateFormat()); // "m/d/Y"
That two-line pairing (setLocale → push into app() / Carbon) is the entire core workflow. Everything else is plumbing around it.
1. Resolve the locale in middleware, not in controllers
Register the package middleware on the web group, or wrap your own around it:
// bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php (10 and below)
$middleware->web(append: [
\Stefro\LaravelLangCountry\Http\Middleware\LaravelLangCountryMiddleware::class,
]);
Ordering matters: it must run after StartSession (it reads the session) and before anything that renders translations. If you're composing your own, the pattern is:
public function handle($request, Closure $next)
{
$lang = $request->user()?->language ?? $this->fromBrowser($request);
$country = $request->user()?->country ?? $this->fromBrowser($request);
LaravelLangCountry::setLocale($lang, $country);
app()->setLocale(LaravelLangCountry::locale());
Carbon::setLocale(LaravelLangCountry::locale());
return $next($request);
}
2. Keep the package as the validator, not the store
Don't persist LaravelLangCountry::locale() as a single string. Persist language and country as separate columns — users switch countries (relocation, travel) without switching language, and a single en_US column forces a full profile edit. Validate on write:
$valid = collect(config('laravel-lang-country.supported_locales'))
->contains("{$request->language}_{$request->country}");
3. A single date-format accessor
Wrap dateFormat() once so you can swap implementations later and so Blade stays clean:
// app/Support/DateFormat.php
public static function php(): string
{
return LaravelLangCountry::dateFormat();
}
{{ $order->placed_at->format(\App\Support\DateFormat::php()) }}
Do the same for JS if you render dates client-side — pass DateFormat::php() down as a data attribute and convert, or hand-maintain a Intl.DateTimeFormat locale string from LaravelLangCountry::locale(). Don't assume the package hands you a JS format; it does not.
4. Country/language selects
Build your signup dropdowns from the config so the UI can never submit an unsupported pair:
$countries = collect(config('laravel-lang-country.supported_locales'))
->map(fn ($l) => explode('_', $l)[1])
->unique()->sort()->values();
5. API responses
For JSON endpoints, skip the session entirely and negotiate per-request from Accept-Language, falling back to the config default:
LaravelLangCountry::setLocale($request->header('X-Lang', 'en'), $request->header('X-Country', 'US'));
6. Test it explicitly
The failure mode is silent — a wrong locale just renders the fallback and nobody notices in dev. Assert both halves:
$this->actingAs($user)->get('/dashboard')
->assertSee(__('nav.dashboard', [], 'nl')); // language applied
$this->assertSame('d/m/Y', LaravelLangCountry::dateFormat()); // country applied
supported_locales is your real safety net. If a pair isn't listed, setLocale() throws or falls back depending on your config's exception setting. Either way, always list every pair you ship translation files for — and no more. Listing de_AT when you only have de translations gives you a false sense of coverage.
The package does not translate anything. It resolves which locale to use and how to format dates. You still need lang/en/, lang/nl/, etc. shipped and populated. It's a resolver, not a translator.
dateFormat() returns a PHP format string, not a human-readable string. Passing it to ->isoFormat() (which expects ICU/CLDR tokens) produces garbage like literal m/d/Y. Use ->format() for PHP strings and reach for ->translatedFormat() or ->isoFormat() when you want localized month names — those ignore the package value entirely.
app()->setLocale() and Carbon::setLocale() are separate. The package sets neither for you (or only sets the facade's internal state, depending on version). Forgetting Carbon::setLocale() is the most common "why is my month still in English" bug.
Config caching. After editing supported_locales in production, php artisan config:clear — otherwise your deploy silently keeps the old list. Same trap as every config-driven package.
Middleware ordering / session dependency. If you get a Session store not set on request error, your middleware is above StartSession in the stack. In Laravel 11's bootstrap/app.php, use append to web, not prepend.
Browser detection is best-effort. Accept-Language parsing handles nl-BE,en;q=0.9 fine but breaks on malformed or spoofed headers. Never let it be the only source — always have an authenticated user preference win, and a hardcoded default underneath that.
Right-to-left is out of scope. The package will happily give you ar_SA, but flipping layouts is yours to do. Store an is_rtl flag alongside and set dir on <html> yourself.
Verify the facade surface against your installed version. The API is small and has shifted across releases. LaravelLangCountry::locale(), lang(), country(), setLocale(), and dateFormat() are the stable core; secondary helpers (browser detection, name lookups) vary. Run php artisan tinker and dd(app(LaravelLangCountry::class)) once, then write your own thin wrapper so a future major version only breaks one file.
Extension point: if you need per-country time formats, number formats, or currency display, don't fork the package. Publish its config and add your own parallel config keyed by the same country() value — the package's job stops at date notation, and pretending otherwise is where forks start.
How can I help you explore Laravel packages today?