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

Intl Extra Laravel Package

twig/intl-extra

Twig Intl Extension adds locale-aware filters to Twig: look up country, currency, language, locale, and timezone names, get country timezones, and format currencies, numbers, dates, times, and lists using PHP's Intl extension.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install with the extension guarded and the security patch pinned — on ^3.30.0.

composer require twig/twig twig/intl-extra:^3.30.0
php -m | grep intl   # must be present; add "ext-intl": "*" to composer.json

Two floors matter, and they are different floors:

  • 3.26.0 fixes CVE-2026-46629, where IntlDateFormatter / NumberFormatter instances were memoised without bound. On long-running workers that is a real memory leak, so never pin below it.
  • 3.30.0 fixes #4932 / #4948: the extension was inheriting values ICU derived from a date formatter prototype, so a formatter built for one pattern could pick up layout/timezone behaviour computed by an earlier prototype in the same process. In practice this showed up as dates rendering with the wrong layout after mixing named lengths and explicit patterns on the same worker. The fix compares the derived prototype pattern against the stored one once per pattern, so the cost is negligible — treat it as a free correctness upgrade, not a perf trade.

Both are patch-level releases; there is no breaking change to format_* signatures, filter names, or the attribute-map arguments. Upgrading from the 3.26–3.29 line is a composer update and a worker restart.

2. Register the extension in whatever Twig environment you actually have.

Laravel rarely runs Twig for views — the realistic hosts are rcrowe/twigbridge (web/Blade coexistence), a standalone Twig\Environment you build for mailables and PDF/report rendering, or a queued export pipeline.

// config/twigbridge.php
'extensions' => [
    \Twig\Extra\Intl\IntlExtension::class,
],
// standalone environment (Mailable, DomPDF, CSV/HTML export)
$twig = new \Twig\Environment($loader, ['cache' => storage_path('framework/twig'), 'autoescape' => 'html']);
$twig->addExtension(new \Twig\Extra\Intl\IntlExtension());

3. Sync the ICU default locale before you render anything. This is the step people skip. App::setLocale() and setlocale() do not change \Locale::getDefault(), so every filter silently falls back to en_US unless you fix it.

// AppServiceProvider::boot()
\Locale::setDefault(config('app.locale')); // or per-request middleware from the user's preference

4. First use case: a localized invoice/receipt template. It exercises the three filters you'll use most and makes locale bugs immediately visible.

<p>{{ order.created_at|format_datetime('long', 'short', app_locale, order.timezone) }}</p>
<p>{{ order.total_minor / 100|format_currency(order.currency, app_locale) }}</p>
<p>{{ order.item_count|format_number(app_locale) }} {{ 'line_items'|trans }}</p>

Where to look first: the per-filter pages on twig.symfony.com (/format_currency, /format_datetime, …) are the only real docs; then vendor/twig/intl-extra/IntlExtension.php — the whole package is one readable class and the attribute maps are visible there. The formatter-prototype/pattern cache that #4932 and #4948 touch lives in that same class, so it is also the place to look when output is "almost right".


Implementation Patterns

Locale as explicit data, not ambient state. Pass the locale as a template variable (app_locale, order.locale, recipient.locale) rather than relying on Locale::getDefault(). Multi-recipient notifications, admin impersonation, and exports all break the "one request, one locale" assumption.

return $this->view('mail.orders.confirmation', [
    'app_locale' => $order->customer->locale ?? config('app.fallback_locale'),
    'order' => $order,
]);

Money: store minor units, format at the edge. The filter takes a number and a currency code, so convert once in the template or (better) expose a computed accessor.

{# preferred: model accessor total_major #}
{{ order.total_major|format_currency(order.currency, app_locale, {
    min_fraction_digits: 2,
    max_fraction_digits: 2
}) }}

The fourth argument is a map of NumberFormatter attribute constants in snake_case (min_fraction_digits, max_fraction_digits, grouping_used, rounding_mode, …). This is where you suppress decimals for JPY, force two for EUR, or set rounding policy — don't string-manipulate afterwards.

Dates: pin the timezone, split date and time formats.

{{ post.published_at|format_date('long', app_locale, app_timezone) }}
{{ event.starts_at|format_datetime('full', 'none', app_locale, event.timezone) }}
{{ event.starts_at|format_time('short', app_locale, event.timezone) }}

'none' cleanly suppresses a component instead of concatenating two filters. Carbon objects work as-is because they implement DateTimeInterface — but the timezone argument wins over whatever the Carbon instance carries, so be deliberate about which you pass.

ICU patterns when named lengths aren't enough. Every format_* filter accepts a pattern argument, which is how you get layouts the short/medium/long/full ladder can't express:

{{ invoice.due_at|format_date('none', app_locale, app_timezone, 'gregorian', 'd MMMM yyyy') }}

Keep pattern arguments static. This is now doubly true. 3.30.0 fixes prototype inheritance by comparing the derived pattern against the stored one once per pattern — so the set of distinct pattern strings is also the size of the pattern cache. A pattern string built from user input (a preference field, a locale-suffixed flag, a per-order template fragment) multiplies both the prototype comparisons and the memoised formatter count on a long-lived worker. Enumerate allowed patterns in PHP and pick one before it reaches Twig.

Reference-data filters for admin screens and <select>s. These are the sleeper value of the package — no lookup tables to maintain:

{{ 'FR'|country_name(app_locale) }}            {# France / Frankreich #}
{{ 'EUR'|currency_symbol(app_locale) }}        {# € #}
{{ currency_code|currency_name('de') }}        {# Euro #}
{{ 'pt_BR'|language_name(app_locale) }}        {# Portuguese (Brazil) #}
{{ 'de_AT'|locale_name(app_locale) }}
{{ 'America/New_York'|timezone_name(app_locale) }}
{% for tz in 'US'|country_timezones %} ... {% endfor %}

Note country_name takes a two-letter code for regions, language_name a language code, locale_name a full locale — mixing them up returns the input unchanged rather than erroring.

Centralize in macros so the locale contract lives in one file.

{% macro money(amount, currency, locale) %}
    {{ amount|format_currency(currency, locale) }}
{% endmacro %}

Then the day you switch from format_currency to a custom money filter, you edit one template.

Testing pattern. Formatting assertions against ICU output are brittle; compare against a freshly computed NumberFormatter/IntlDateFormatter in the test, or assert on normalized output (strip \u{00A0}, \u{202F}). Test de_DE, ja_JP, and one RTL locale (ar_EG) as your baseline matrix. If you have a regression pack for #4932-style bugs, add the mixed case explicitly: render a named-length date, then a custom-pattern date, then the named one again, in the same process — that is the sequence that used to inherit the wrong prototype.


Gotchas and Tips

  • Formatter prototypes are cached per process, and a stale worker keeps the old behaviour. The #4932 fix only takes effect in processes that booted the new code. After upgrading to 3.30.0, restart queue workers, Octane, and any long-lived scheduler process — composer update alone leaves pre-forked workers rendering with the old prototype-derived pattern until they recycle.
  • Argument order differs from the filters you already know. format_date(date, format, locale, timezone) — not date(format, timezone). format_currency(number, currency, locale, attrs) — not number_format(decimals, decimal_point, thousands_sep). Half of all "why is my date wrong" reports are this.
  • App::setLocale() doesn't reach ICU. If output looks stuck in English, check \Locale::getDefault() first, before you go hunting in templates.
  • Wrong layout only in a long-running process is a formatter-cache symptom, not a template bug. Before 3.30.0, a pattern could inherit values ICU derived from an earlier formatter prototype. If a queue worker or Octane process renders dates differently from php artisan tinker on identical input, suspect the prototype cache: upgrade, restart, and re-test before rewriting the template.
  • Non-breaking spaces in output. ICU inserts U+00A0 / U+202F in currency and number output. That breaks naive string assertions, JSON round-trips in tests, CSV exports, and PDF line-wrapping. Normalize deliberately where the output is data rather than display.
  • ICU version drift between dev and prod. INTL_ICU_VERSION differs across distros and PHP builds, and grouping/decimal conventions for some locales changed in recent ICU majors (Swedish, Finnish, and others). Log the version in your deploy diagnostics; a passing local test is not proof. Note that 3.30.0 hardens behaviour within an ICU version — it does not normalise across ICU versions.
  • Memoisation and long-running processes. Even on >= 3.30.0, formatters are cached per (locale, pattern, timezone) combination, and the prototype comparison is per pattern. A queue worker/Octane process that formats 200 locales × a dozen patterns accumulates instances. Keep pattern arguments static (don't build them from user input) and prefer one locale per job.
  • Unknown codes are echoed, not rejected. 'XX'|country_name returns XX and invalid currency codes come back unchanged. Do validation in Laravel (enum/rule) — the filters are display tools, not validators.
  • Missing ext-intl fails at runtime, not install time unless you declare it. A container without intl gives you Class "IntlDateFormatter" not found from deep inside Twig rendering — usually an email job in production. Add ext-intl to require and a startup check.
  • Don't format inside large loops. format_currency in a 5,000-row table template is measurably slow. Pre-format in PHP when building the dataset, or cache the rendered fragment.
  • Twig cache clearing. With twigbridge, stale compiled templates after a config change look like "my new extension isn't registered". Clear storage/framework/twig (or wherever cache points) alongside php artisan view:clear.
  • Extension points to reach for. Custom Twig filters are trivial via $twig->addFilter(new \Twig\TwigFilter('money', ...)) — a money_minor filter that divides by 100 and delegates to format_currency is usually worth writing, and it inherits the upstream formatter cache (and its fixes) for free. For Blade-only teams, don't adopt the package: wrap NumberFormatter/IntlDateFormatter in a Blade directive or a Money/LocalizedDate helper class instead; you get the same ICU behavior without dragging in Twig — but then you own the caching, including the prototype-inheritance class of bug that 3.30.0 just fixed.
  • Sandbox and autoescaping. All filters return plain strings, so autoescape is safe — but if you use Twig's sandbox, remember these filters need to be allowlisted alongside any custom ones.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource