mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-01 04:57:23 +00:00
[Intl] Add format_list filter using PHP 8.5's IntlListFormatter
This commit is contained in:
committed by
Fabien Potencier
parent
d1abdf483a
commit
163f1b6341
@@ -23,6 +23,7 @@ jobs:
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
- '8.5'
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
@@ -68,6 +69,7 @@ jobs:
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
- '8.5'
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
@@ -119,6 +121,7 @@ jobs:
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
- '8.5'
|
||||
extension:
|
||||
- 'cache-extra'
|
||||
- 'cssinliner-extra'
|
||||
@@ -183,7 +186,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
php-version:
|
||||
- '8.4'
|
||||
- '8.5'
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# 3.29.0 (2026-XX-XX)
|
||||
|
||||
* Add a `format_list` filter to `IntlExtension` to format a list of strings using PHP 8.5's `IntlListFormatter`
|
||||
|
||||
# 3.28.1 (2026-XX-XX)
|
||||
|
||||
* Fix duplicated macro argument names triggering a PHP fatal error instead of a `SyntaxError`
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
``format_list``
|
||||
===============
|
||||
|
||||
.. versionadded:: 3.29
|
||||
|
||||
The ``format_list`` filter was added in Twig 3.29.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``format_list`` filter requires the ``IntlListFormatter`` class, which is available since PHP 8.5.
|
||||
|
||||
The ``format_list`` filter formats a list of strings as a single, locale-aware string:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{# en: Apple, Banana, and Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list }}
|
||||
|
||||
The list of supported types:
|
||||
|
||||
* ``and``: Formats the list as a conjunction (default)::
|
||||
|
||||
{# en: Apple, Banana, and Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list(type: 'and', locale: 'en') }}
|
||||
|
||||
* ``or``: Formats the list as a disjunction::
|
||||
|
||||
{# en: Apple, Banana, or Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list(type: 'or', locale: 'en') }}
|
||||
|
||||
* ``units``: Formats the list as a compound unit, without implying a conjunction or disjunction::
|
||||
|
||||
{# en: Apple, Banana, Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list(type: 'units', locale: 'en') }}
|
||||
|
||||
The list of supported widths:
|
||||
|
||||
* ``wide``: The full-length form (default)::
|
||||
|
||||
{# en: Apple, Banana, and Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list(width: 'wide', locale: 'en') }}
|
||||
|
||||
* ``short``: A shorter form::
|
||||
|
||||
{# en: Apple, Banana, & Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list(width: 'short', locale: 'en') }}
|
||||
|
||||
* ``narrow``: The most compact form::
|
||||
|
||||
{# en: Apple, Banana, Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list(width: 'narrow', locale: 'en') }}
|
||||
|
||||
By default, the filter uses the current locale. You can pass it explicitly::
|
||||
|
||||
{# fr: Apple, Banana et Cherry #}
|
||||
{{ ['Apple', 'Banana', 'Cherry']|format_list(locale: 'fr') }}
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The ``format_list`` filter is part of the ``IntlExtension`` which is not installed by default. Install it first:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ composer require twig/intl-extra
|
||||
|
||||
Then, on Symfony projects, install the ``twig/extra-bundle``:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ composer require twig/extra-bundle
|
||||
|
||||
Otherwise, add the extension explicitly on the Twig environment::
|
||||
|
||||
use Twig\Extra\Intl\IntlExtension;
|
||||
|
||||
$twig = new \Twig\Environment(...);
|
||||
$twig->addExtension(new IntlExtension());
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
* ``type``: The type of the list output
|
||||
* ``width``: The width of the list output
|
||||
* ``locale``: The locale code as defined in `RFC 5646`_
|
||||
|
||||
.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646
|
||||
@@ -24,6 +24,7 @@ Filters
|
||||
format_currency
|
||||
format_date
|
||||
format_datetime
|
||||
format_list
|
||||
format_number
|
||||
format_time
|
||||
html_attr_merge
|
||||
|
||||
@@ -56,6 +56,40 @@ final class IntlExtension extends AbstractExtension
|
||||
return $formats;
|
||||
}
|
||||
|
||||
private static function availableListTypes(): array
|
||||
{
|
||||
static $types = null;
|
||||
|
||||
if (null !== $types) {
|
||||
return $types;
|
||||
}
|
||||
|
||||
$types = [
|
||||
'and' => \IntlListFormatter::TYPE_AND,
|
||||
'or' => \IntlListFormatter::TYPE_OR,
|
||||
'units' => \IntlListFormatter::TYPE_UNITS,
|
||||
];
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
private static function availableListWidths(): array
|
||||
{
|
||||
static $widths = null;
|
||||
|
||||
if (null !== $widths) {
|
||||
return $widths;
|
||||
}
|
||||
|
||||
$widths = [
|
||||
'wide' => \IntlListFormatter::WIDTH_WIDE,
|
||||
'short' => \IntlListFormatter::WIDTH_SHORT,
|
||||
'narrow' => \IntlListFormatter::WIDTH_NARROW,
|
||||
];
|
||||
|
||||
return $widths;
|
||||
}
|
||||
|
||||
private const TIME_FORMATS = [
|
||||
'none' => \IntlDateFormatter::NONE,
|
||||
'short' => \IntlDateFormatter::SHORT,
|
||||
@@ -149,6 +183,7 @@ final class IntlExtension extends AbstractExtension
|
||||
|
||||
private $dateFormatters = [];
|
||||
private $numberFormatters = [];
|
||||
private $listFormatters = [];
|
||||
private $dateFormatterPrototype;
|
||||
private $numberFormatterPrototype;
|
||||
|
||||
@@ -176,6 +211,7 @@ final class IntlExtension extends AbstractExtension
|
||||
new TwigFilter('format_datetime', [$this, 'formatDateTime'], ['needs_environment' => true]),
|
||||
new TwigFilter('format_date', [$this, 'formatDate'], ['needs_environment' => true]),
|
||||
new TwigFilter('format_time', [$this, 'formatTime'], ['needs_environment' => true]),
|
||||
new TwigFilter('format_list', [$this, 'formatList']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -406,6 +442,18 @@ final class IntlExtension extends AbstractExtension
|
||||
return $this->formatDateTime($env, $date, 'none', $timeFormat, $pattern, $timezone, $calendar, $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|\Stringable> $strings A list of items to be joined into a formatted list
|
||||
*/
|
||||
public function formatList(array $strings, string $type = 'and', string $width = 'wide', ?string $locale = null): string
|
||||
{
|
||||
if (!class_exists('IntlListFormatter')) {
|
||||
throw new RuntimeError('The "format_list" filter requires the "IntlListFormatter" class, which is available since PHP 8.5.');
|
||||
}
|
||||
|
||||
return $this->createListFormatter($locale, $type, $width)->format($strings);
|
||||
}
|
||||
|
||||
private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormat, string $pattern, ?\DateTimeZone $timezone, string $calendar): \IntlDateFormatter
|
||||
{
|
||||
$dateFormats = self::availableDateFormats();
|
||||
@@ -530,4 +578,37 @@ final class IntlExtension extends AbstractExtension
|
||||
|
||||
return $this->numberFormatters[$hash];
|
||||
}
|
||||
|
||||
private function createListFormatter(?string $locale, string $type, string $width): \IntlListFormatter
|
||||
{
|
||||
$listTypes = self::availableListTypes();
|
||||
|
||||
if (!isset($listTypes[$type])) {
|
||||
throw new RuntimeError(\sprintf('The list type "%s" does not exist, known types are: "%s".', $type, implode('", "', array_keys($listTypes))));
|
||||
}
|
||||
|
||||
$listWidths = self::availableListWidths();
|
||||
|
||||
if (!isset($listWidths[$width])) {
|
||||
throw new RuntimeError(\sprintf('The list width "%s" does not exist, known widths are: "%s".', $width, implode('", "', array_keys($listWidths))));
|
||||
}
|
||||
|
||||
if (null === $locale) {
|
||||
$locale = \Locale::getDefault();
|
||||
}
|
||||
|
||||
$listTypeValue = $listTypes[$type];
|
||||
$listWidthValue = $listWidths[$width];
|
||||
|
||||
$hash = $locale.'|'.$listTypeValue.'|'.$listWidthValue;
|
||||
|
||||
if (!isset($this->listFormatters[$hash])) {
|
||||
if (\count($this->listFormatters) >= self::MAX_CACHED_FORMATTERS) {
|
||||
array_shift($this->listFormatters);
|
||||
}
|
||||
$this->listFormatters[$hash] = new \IntlListFormatter($locale, $listTypeValue, $listWidthValue);
|
||||
}
|
||||
|
||||
return $this->listFormatters[$hash];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ This package is a Twig extension that provides the following:
|
||||
* [`format_number`][9] filter: formats a number;
|
||||
* [`format_datetime`][10] filter: formats a date time;
|
||||
* [`format_date`][11] filter: formats a date;
|
||||
* [`format_time`][12] filter: formats a time.
|
||||
* [`format_time`][12] filter: formats a time;
|
||||
* [`format_list`][13] filter: formats a list of strings as a single, locale-aware string.
|
||||
|
||||
[1]: https://twig.symfony.com/country_name
|
||||
[2]: https://twig.symfony.com/currency_name
|
||||
@@ -28,3 +29,4 @@ This package is a Twig extension that provides the following:
|
||||
[10]: https://twig.symfony.com/format_datetime
|
||||
[11]: https://twig.symfony.com/format_date
|
||||
[12]: https://twig.symfony.com/format_time
|
||||
[13]: https://twig.symfony.com/format_list
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
--TEST--
|
||||
"format_list" filter
|
||||
--CONDITION--
|
||||
PHP_VERSION_ID < 80500
|
||||
--TEMPLATE--
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list() }}
|
||||
--DATA--
|
||||
return []
|
||||
--EXCEPTION--
|
||||
Twig\Error\RuntimeError: The "format_list" filter requires the "IntlListFormatter" class, which is available since PHP 8.5 in "index.twig" at line 2.
|
||||
@@ -0,0 +1,34 @@
|
||||
--TEST--
|
||||
"format_list" filter
|
||||
--CONDITION--
|
||||
PHP_VERSION_ID >= 80500
|
||||
--TEMPLATE--
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list() }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list(width='short')|raw }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('or') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list(locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('and', 'short', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('and', 'narrow', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('or', 'wide', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('or', 'short', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('or', 'narrow', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('units', 'wide', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('units', 'short', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('units', 'narrow', locale='fr') }}
|
||||
{{ ['Alice', 'Bob', 'Carol']|format_list('units', 'narrow', locale='de') }}
|
||||
--DATA--
|
||||
return [];
|
||||
--EXPECT--
|
||||
Alice, Bob, and Carol
|
||||
Alice, Bob, & Carol
|
||||
Alice, Bob, or Carol
|
||||
Alice, Bob et Carol
|
||||
Alice, Bob et Carol
|
||||
Alice, Bob, Carol
|
||||
Alice, Bob ou Carol
|
||||
Alice, Bob ou Carol
|
||||
Alice, Bob ou Carol
|
||||
Alice, Bob et Carol
|
||||
Alice, Bob et Carol
|
||||
Alice Bob Carol
|
||||
Alice, Bob und Carol
|
||||
Reference in New Issue
Block a user