laravel-lang/native-locale-names
Native locale names for Laravel: a lightweight package providing localized language names in their own scripts, ready to use for language switchers and locale lists.
Install and confirm you have no setup work.
composer require laravel-lang/native-locale-names
If you are on Laravel 13, require >=2.8.0 (support landed there). On Laravel 11/12 anything in the 2.x line is fine.
There is no service provider, no publishable config, no migration, no facade registration. If composer require succeeds, you are done installing. Resist the urge to run vendor:publish — there is nothing to publish.
Where to look first. Two places, in this order:
vendor/laravel-lang/native-locale-names/src/ — one static class (LaravelLang\NativeLocaleNames\LocaleNames) plus generated data.vendor/laravel-lang/native-locale-names/resources/ (or the data directory alongside src/) — the raw PHP arrays mapping locale codes to endonyms. Skim one file for a locale you care about before writing any code; it tells you exactly what shape the data is and whether pt-BR vs pt exists.First move: interrogate it in Tinker, don't guess the API.
php artisan tinker
use LaravelLang\NativeLocaleNames\LocaleNames;
LocaleNames::get(); // dump the whole map: ['en' => 'English', 'ru' => 'Русский', ...]
array_key_exists('pt-BR', LocaleNames::get());
LocaleNames::get()['xx'] ?? 'MISSING';
Do this before wiring anything into views. You need three facts pinned down on day one, and none of them are reliably answerable from blog posts: the exact entry-point method name, whether your target locales are keys in the map, and what a missing key returns (null, exception, or the raw code). The public surface is deliberately tiny and the 2.x line renamed things once already — trust the vendor directory, not Google.
First real use case: a language switcher. This package exists to solve exactly one problem well — putting "Deutsch" instead of "German" in a <select>. That is your pilot integration, and it should be a single Blade component. Do not start by replacing every locale label across the app; start with one selector, look at it, then expand.
Wrap it. Always wrap it. The single most important pattern here is that no Blade file, Livewire component, or controller should ever reference LaravelLang\NativeLocaleNames\LocaleNames directly. The package is a data vendor; your app owns the policy (which locales are supported, what to do when one is missing, how they sort).
// config/locales.php — product decision, versioned in git
return [
'supported' => ['en', 'de', 'fr', 'pt-BR', 'ja', 'ar'],
'fallback' => 'en',
];
// app/Support/SupportedLocales.php
namespace App\Support;
use LaravelLang\NativeLocaleNames\LocaleNames;
final class SupportedLocales
{
private ?array $labels = null;
public function labels(): array
{
// memoize — the package is static data, but don't rebuild the map per call
return $this->labels ??= collect(config('locales.supported'))
->mapWithKeys(fn (string $code) => [
$code => LocaleNames::get()[$code] ?? $code, // verify the real fallback behaviour first
])
->all();
}
public function label(string $code): string
{
return $this->labels()[$code] ?? $code;
}
}
Fifty lines buys you: one place to swap in intl or a fork, one place to enforce the fallback contract, one place to control sort order, and one class to unit test.
Sort, or your switcher looks broken. Sorting was disabled by default in 2.1.0, so the array arrives in whatever order the data generator produced — roughly code-point order. That means "العربية" and "日本語" land at the bottom and your dropdown reads as unsorted to every non-English user. Sort explicitly:
if (class_exists(\Collator::class)) {
$collator = new \Collator(app()->getLocale()); // or 'und' for root collation
$collator->asort($labels);
}
If intl is not available, ksort() by key is at least stable and predictable — but decide this deliberately, and write a comment saying so.
Surface it consistently across all four delivery channels. The whole point is that the label for de is "Deutsch" in Blade, in Livewire, in Inertia props, and in your JSON API:
// Inertia shared props / HandleInertiaRequests
'supportedLocales' => app(SupportedLocales::class)->labels(),
{{-- resources/views/components/language-switcher.blade.php --}}
<select name="locale">
@foreach (app(App\Support\SupportedLocales::class)->labels() as $code => $label)
<option value="{{ $code }}" @selected(app()->getLocale() === $code)>
{{ $label }}
</option>
@endforeach
</select>
For a JS-driven frontend that needs the map at build time, add a small Artisan command that dumps labels() to JSON and run it in your deploy pipeline. Do not fetch it over HTTP at runtime — it is static data.
Keep it out of the domain layer. Nothing in app/Models, app/Actions, or your queue jobs should depend on this. It is presentation data. If you find yourself importing LocaleNames from anything other than App\Support\SupportedLocales, the abstraction has leaked.
Test the seam you own, not the vendor. Snapshot the label set for your allow-list:
public function test_supported_locale_labels(): void
{
$this->assertSame(
['en' => 'English', 'de' => 'Deutsch', 'ja' => '日本語'],
app(SupportedLocales::class)->labels(),
);
}
This converts an auto-generated data release from a production surprise into a red test.
Anything pre-2.0 is wrong. The namespace was LaravelLang\NativeLocaleNames\Native and became \LocaleNames in 2.0.0; the locale source moved to laravel-lang/locale-list; and in 2.1.0 sorting was turned off by default. Every Stack Overflow answer and most blog snippets predate all three. Copy from the vendor directory.
Missing keys are your contract to define, not theirs. Coverage is governed by locale-list, not by you. If the product says "we support Klingon," and the package doesn't carry it, your wrapper must decide: fall back to the raw code (xx), to English, or throw loudly in CI. Pick one, document it in the wrapper, and add a test. Silent null in a <option> produces an empty row that no one notices until a user complains.
Region variants vs. bare language codes. pt → "Português" and pt-BR → "Português (Brasil)" are different keys. Decide at the config level whether your product ships region variants or bare codes, and be consistent — mixing produces a dropdown with both "Português" and "Português (Brasil)" as separate choices, which is a product bug, not a data bug.
Automated data churn is real. Releases like 2.7.0 and 2.9.0 are pure github-actions[bot] data updates. A caret constraint inside ^2.0 can silently change a user-visible label on composer update. If user-facing text stability matters more than freshness, pin the version and bump deliberately, with the snapshot test above as your review artifact. If you want freshness, keep the caret and let CI tell you what changed.
laravel-lang/locale-list is the likely install failure. If another Laravel-Lang package is already in the tree with a different major of locale-list, Composer will deadlock. Check composer why laravel-lang/locale-list before you blame this package.
Don't reach for intl reflexively — but know why you didn't. \Locale::getDisplayName('de', 'de') gives you "Deutsch" with zero dependencies. The tradeoff: its output is tied to the server's ICU version, so it can vary between your laptop, CI, and production, and it can shift under you on a base-image upgrade. This package is deterministic and ICU-independent. That is a genuine reason to choose it — write it in the wrapper's docblock so the next maintainer doesn't "simplify" it away.
Escape and encode correctly. Endonyms are UTF-8 with CJK, Cyrillic, Arabic, and Hebrew. Ensure utf8mb4 wherever these end up (columns, caches, session storage), use {{ }} in Blade as usual, and check your font stack actually has glyph coverage — a switcher rendering □□□ for Japanese is a font bug that looks like a package bug. Arabic and Hebrew names inside an otherwise-LTR <select> are fine, but test it visually; a mixed-script dropdown is one of the few places RTL rendering is genuinely awkward.
There is a _native pseudo-locale. Historical artifact of the _combined → _native rename in 1.3.0. Filter it out of any wildcard iteration (array_keys(LocaleNames::get())) or it will appear as a selectable language.
Don't derive flags from locale codes. Tempting, always wrong (which flag for en? ar? pt-BR?). If you want flags, curate them separately per locale in your own config, and accept that many locales have no defensible flag.
Bus factor of one. Seven stars, zero dependents, single maintainer plus a bot. Mitigated by the Laravel-Lang organization's reputation, and fully mitigated by the wrapper: worst case, you replace SupportedLocales::labels() with a hardcoded array in an afternoon. That is the real reason the wrapper is non-negotiable, and worth saying out loud in the PR description so the decision survives you.
How can I help you explore Laravel packages today?