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

Locale List Laravel Package

umpirsky/locale-list

PHP library offering ready-to-use lists of locales, countries, languages, currencies, timezones, scripts and continents. Bundled data and a simple API make it easy to look up, validate and populate select menus, so you can localize apps without maintaining your own reference data.

Deep Wiki
Context7

Getting Started

This is a data repository, not a library. There is no service provider, no facade, no Locale:: helper, no config file. What you get is a generated snapshot of Unicode CLDR locale display names, emitted once per display language in five formats (JSON, PHP, CSV, XML, YAML). Your job is to write the ~30 lines that read it and cache it.

1. Get the data in place

composer require umpirsky/locale-list
# if it doesn't resolve, vendor it instead:
#   git submodule add ... or copy data/ into resources/data/locale-list

2. Look at one file before writing any code. Open data/en/locale.json (verify the exact filename/shape — it's usually data/{displayLocale}/locale.{ext}). Expect a flat map:

{ "af_ZA": "Afrikaans (South Africa)", "zh_Hant_TW": "Chinese (Traditional, Taiwan)" }

Confirm three things immediately, because everything downstream depends on them: the key format (lang_SCRIPT_REGION, underscores), the value format (Name (Region)), and which display languages actually exist in the directory listing.

3. Wire a minimal loader

// app/Support/LocaleList.php
namespace App\Support;

class LocaleList
{
    /** @var array<string, array<string,string>> */
    private array $loaded = [];

    public function __construct(
        private string $dataPath,
        private string $fallback = 'en',
    ) {}

    /** @return array<string,string> code => display name */
    public function all(string $display = 'en'): array
    {
        $display = is_file("{$this->dataPath}/{$display}/locale.php")
            ? $display
            : $this->fallback;

        return $this->loaded[$display] ??= require "{$this->dataPath}/{$display}/locale.php";
    }

    public function name(string $locale, string $display = 'en'): ?string
    {
        return $this->all($display)[$locale] ?? null;
    }
}
// app/Providers/AppServiceProvider.php
$this->app->singleton(LocaleList::class, fn () => new LocaleList(
    base_path('vendor/umpirsky/locale-list/data')
));

Prefer the PHP format over JSON: require returns an array directly, gets opcached, and costs essentially nothing after the first hit.

First real use case: a locale picker on a user profile or tenant settings page. Rule::in(array_keys($list->all())) for validation, a <select> in Blade, $list->name($user->locale, app()->getLocale()) to render it.

Implementation Patterns

Resolve the vendor path, don't hardcode it. base_path('vendor/...') breaks under composer install --no-dev with a custom vendor-dir or in a monorepo:

use Composer\InstalledVersions;
$path = InstalledVersions::getInstallPath('umpirsky/locale-list') . '/data';

Subset at build time, not runtime. Shipping 500 display languages to a 3-locale app is waste. Write an Artisan command that reads the package data and emits only what you need into resources/data/locales/{lang}.php, commit the output, and keep the package as a require-dev. Deploys then have zero vendor coupling and a tiny diff when CLDR updates.

// php artisan locales:build --display=en,fr,de --only=app_locales
foreach ($supported as $display) {
    $names = array_intersect_key($package->all($display), array_flip($allLocales));
    file_put_contents(resource_path("data/locales/{$display}.php"), '<?php return ' . var_export($names, true) . ';');
}

Constrain Accept-Language negotiation. The package list is the whitelist; your config is the allowlist. Intersect them in middleware so en-GB degrades to en rather than 500ing:

$requested = $request->getLanguages(); // Symfony helper, already ordered by q
$match = collect($requested)->first(fn ($l) => in_array($l, config('app.supported_locales'), true))
      ?? config('app.fallback_locale');
app()->setLocale($match);

Compose with ext-intl rather than replacing it. Locale::getDisplayName($locale, $display) gives you the same names from ICU with no files. Use the package data when you need a pinned snapshot or run without the extension; use intl as the fallback for keys the package is missing:

public function name(string $locale, string $display = 'en'): string
{
    return $this->all($display)[$locale]
        ?? (extension_loaded('intl') ? \Locale::getDisplayName($locale, $display) : $locale);
}

Sort locale-aware. If your data file is sorted for en, it's wrong for sr. Re-sort with Collator:

$collator = new \Collator($display);
uasort($names, fn ($a, $b) => $collator->compare($a, $b));

Blade component, not inline loops. One <x-locale-select :value="$user->locale" :display="app()->getLocale()" /> shared across profile, tenant, invoice and export forms keeps the naming and formatting rules in one place.

Testing. Add a test that asserts every code in config('app.supported_locales') resolves to a non-null name in every display language you ship — this catches silent drops after a data update.

Gotchas and Tips

Key format mismatch is the #1 bug. CLDR uses underscores (en_US, zh_Hant_TW); BCP-47 and browser navigator.language use hyphens (en-US). Pick one canonical form for your app and normalize at every boundary with str_replace(['-', '_'], '_', $code); otherwise in_array lookups fail silently and everyone falls back to English.

Not every display language is complete. Some locale files are missing entries, some are missing entirely. Always implement an en fallback path and never assume all($display) returns the same keys as all('en').

Legacy and macrolanguage codes. iw/in/ji are deprecated aliases, and no vs nb vs nn is a perennial source of duplicate rows. Normalize on input, and be aware sr vs sr_Latn are distinct keys with different names.

Don't read the directory or re-parse per row. glob() on data/ at request time, or json_decode inside a foreach over 1000 users, will show up in profiling. Load once, memoize in the singleton (as above), and Cache::rememberForever("locales:{$display}") for cross-request caching keyed on a config data_version that you bump on deploy.

Names contain parentheses, commas, non-breaking spaces and RTL text. Escape with {{ }}, never build HTML from them. Avoid Str::title()/mb_strtoupper() — they mangle names like "Português (Brasil)" and break diacritics.

Don't rely on the shipped sort order. It may be generated for the display language or for a default; if the ordering matters for UX, sort explicitly with Collator.

Data freshness. The repo mirrors a CLDR snapshot; names change between releases ("Turkey" → "Türkiye", "Macedonia" → "North Macedonia"). If those strings appear on invoices or legal documents, pin the commit and review diffs on upgrade rather than floating on ^1.0.

Honest build-vs-buy call: if ext-intl is available (it usually is on php:8.x images), Locale::getDisplayName() gives you the same data with no files, no loader, and automatic CLDR versioning via the extension. This package earns its place when you need reproducible offline data, a subset you control, or you're generating bulk exports/job payloads where hitting ICU per row is measurably slower. Benchmark once before adding hundreds of data files to your repo.

Extension point: the loader class is your seam. Swap it for a database-backed locales table (seeded from the package data) when you need per-tenant enablement, translations of names by your own copy team, or admin-editable ordering — the rest of the app depends only on LocaleList::name() and all().

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
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
cgv/localized-resource