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

Polyfill Intl Icu Laravel Package

symfony/polyfill-intl-icu

Symfony polyfill providing limited “en”-locale fallbacks for the Intl extension’s ICU functions and classes, including Collator, NumberFormatter, Locale, IntlDateFormatter, and IntlListFormatter, for environments without intl installed.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a shim, not a feature. It defines Collator, NumberFormatter, Locale, IntlDateFormatter, IntlListFormatter, and the intl_* error functions only when ext-intl is not loaded. If your server has the extension compiled in, installing this package changes literally nothing at runtime.

So the first step is orientation, not installation:

php -m | grep -i intl                                   # do you even need it?
composer why symfony/polyfill-intl-icu                  # it usually arrives transitively
composer show symfony/polyfill-intl-icu | grep versions

It typically comes in through the Symfony stack (Intl/Translation/Validator) rather than being installed by hand. Add it explicitly only when you actually ship to hosts that lack intl — shared hosting, slim Docker images, some CI runners.

Read this file first: vendor/symfony/polyfill-intl-icu/bootstrap.php. It opens with an extension_loaded('intl') early return, which explains 90% of the behaviour you will later find confusing (see Gotchas). Then skim Collator.php, NumberFormatter.php, Locale.php — they are short and you will see exactly which methods are stubbed.

Minimal first use case — an app deployed to a box without intl that needs a direction attribute and formatted money:

// works with or without ext-intl
$fmt = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
echo $fmt->formatCurrency(1234.5, 'USD'); // $1,234.50
<html dir="{{ is_rtl(app()->getLocale()) ? 'rtl' : 'ltr' }}">

Keep ext-intl out of composer.json's require if you want the polyfill to be reachable — declaring it makes Composer refuse to install on the exact machines the polyfill exists for. Use suggest instead.

Implementation Patterns

1. One capability check, used everywhere. Wrap the detection so you can log and swap later:

final class Intl
{
    public static function isPolyfill(): bool
    {
        return !extension_loaded('intl') && class_exists(\Collator::class);
    }
}

Report it once at boot (Log::notice('intl polyfill active') in a service provider) so production silently degrading is visible in your logs rather than discovered by an Arabic customer.

2. Bind a formatter service, don't sprinkle new NumberFormatter() around. This is the single highest-leverage pattern here, because it's also your escape hatch the day you install real intl:

final class Money
{
    public function format(float $amount, string $currency = 'USD', string $locale = 'en'): string
    {
        if (!class_exists(\NumberFormatter::class)) {
            return number_format($amount, 2) . ' ' . $currency; // no intl, no polyfill
        }

        $fmt = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
        $out = $fmt->formatCurrency($amount, $currency);

        return intl_is_failure(intl_get_error_code()) ? number_format($amount, 2) : $out;
    }
}

3. RTL direction in Blade. Prefer a helper with a script-subtag fallback over calling the polyfill function directly:

function is_rtl(?string $locale = null): bool
{
    $locale ??= app()->getLocale();

    if (function_exists('locale_is_right_to_left')) {
        return locale_is_right_to_left($locale);
    }

    return (bool) preg_match('/-(Arab|Hebr|Thaa|Syrc|Nkoo|Adlm|Rohg|Mand|Samr)\b/i', $locale);
}

4. Sorting collections in PHP. Use Collator for user-visible lists; never for large datasets.

$collator = new Collator(app()->getLocale());
$sorted = $users->sort(fn ($a, $b) => $collator->compare($a->name, $b->name))->values();

Above a few hundred rows, sort in SQL (ORDER BY name COLLATE ...) and cache the result. The polyfill's comparison is orders of magnitude slower than the ICU-backed one.

5. Lists and dates. IntlListFormatter (added for PHP 8.5 parity) and IntlDateFormatter are both usable but English-only, so keep locale strings in config and treat non-en as "best effort":

$list = new IntlListFormatter('en', IntlListFormatter::TYPE_AND);
echo $list->format(['apples', 'pears', 'plums']); // apples, pears, and plums

6. Test in two matrices. Your normal CI image almost certainly has intl, so your polyfill branch is never exercised. Add a job that runs the suite with the extension disabled (php -n, or a minimal image) and one with it enabled, and tag polyfill-specific tests with @requires extension intl inversion (skip when intl is loaded). Without this, every polyfill codepath you write is untested.

Gotchas and Tips

  • English only, by design. The README says it plainly: limited to en. IntlDateFormatter will give you English date patterns, NumberFormatter English grouping/currency for whatever locale string you pass. Rendering ar_SA or th_TH through this without ext-intl produces plausible-looking but wrong output — worse than an obvious error. Gate non-English locales off explicitly.
  • locale_is_right_to_left() may not exist. It is not a PHP core function; it's supplied by this polyfill's bootstrap, which bails early when ext-intl is loaded. So function_exists('locale_is_right_to_left') is false on a machine with the extension. Always guard, and always have the script-subtag fallback shown above.
  • IntlListFormatter's availability is inconsistent. It exists natively only from PHP 8.5. With ext-intl on 8.2–8.4 it is absent; with the polyfill it is present. Feature-detect with class_exists(\IntlListFormatter::class), never with extension_loaded('intl').
  • Large parts of intl are simply not covered: MessageFormatter, Transliterator, IntlCalendar, IntlTimeZone, ResourceBundle, IntlChar, IntlBreakIterator, plus grapheme_*/normalizer_* (separate polyfills). If a package you pulled in needs MessageFormatter for ICU plural syntax, this package will not rescue it — Symfony's IntlFormatter path is out of scope. Run composer check-platform-reqs in CI to catch these.
  • Collator::compare() fallback is recent and rough. It only landed in v1.34; pin ^1.43 to get the sorting, English list-pattern and integer-rounding fixes. The fallback approximates byte order, so accented Latin (é, ü) and mixed-script strings sort wrong. Do not use it for search ranking or alphabetical indexes you care about.
  • NumberFormatter::SPELLOUT and rule-based spellout are not meaningfully implemented. Laravel's Illuminate\Support\Number::spell() goes down that road — verify it before using it, and prefer your own fallback.
  • Parse methods return false on failure, quietly. NumberFormatter::parse(), parseCurrency(), and friends can fail without throwing. Always follow with intl_is_failure(intl_get_error_code()).
  • Locale::setDefault() is static state — it leaks under Octane/RoadRunner. Setting it in a middleware will bleed the locale into the next request handled by that worker. Use App::setLocale() and let the framework manage lifecycle, or reset in terminate().
  • Don't fake the extension in Composer. "config": { "platform": { "ext-intl": "8.5" } } satisfies resolution for unrelated packages that require intl, and they then explode at runtime. Same for --ignore-platform-req=ext-intl. If you want the polyfill, leave the platform honest and detect at runtime.
  • The classes are class_aliased globals — you cannot subclass or decorate them. If you need to inject behaviour or mock in tests, that has to happen behind your own interface (Money, Direction, LocalizedDate), not by extending NumberFormatter. This is the real extension point, and it's worth the small up-front cost.
  • Debugging one-liner: php -r 'var_dump(extension_loaded("intl"), class_exists("Collator", false), (new Collator("en"))->compare("a","b"));' tells you instantly which implementation you're running against.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package