diff --git a/.travis.yml b/.travis.yml
index ba56a4c0b..d36161487 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,11 +12,13 @@ before_install:
install:
- travis_retry composer install
- (cd extra/html-extra && travis_retry composer install)
+ - (cd extra/intl-extra && travis_retry composer install)
- (cd extra/markdown-extra && travis_retry composer install)
script:
- ./vendor/bin/simple-phpunit
- (cd extra/html-extra && ./vendor/bin/simple-phpunit)
+ - (cd extra/intl-extra && ./vendor/bin/simple-phpunit)
- (cd extra/markdown-extra && ./vendor/bin/simple-phpunit)
jobs:
diff --git a/CHANGELOG b/CHANGELOG
index c999e2563..b577bb9d9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,7 +1,14 @@
* 2.12.0 (2019-XX-XX)
- * added the Markdown extension in the "extra" repositories
- * added the HtmlExtension extension in the "extra" repositories
+ * added Intl extension in the "extra" repositories: "country_name",
+ "currency_name", "currency_symbol", "language_name", "locale_name",
+ "timezone_name", "country_timezones", "format_currency", "format_number",
+ "format_*_number", "format_datetime", "format_date", and "format_time"
+ filters
+ * added the Markdown extension in the "extra" repositories: "markdown_to_html",
+ and "html_to_markdown" filters
+ * added the HtmlExtension extension in the "extra" repositories: "date_uri"
+ filter, and "html_classes" function
* fixed cache when opcache is installed but disabled
* fixed using macros in arrow functions
diff --git a/doc/filters/country_name.rst b/doc/filters/country_name.rst
new file mode 100644
index 000000000..9bc7a059b
--- /dev/null
+++ b/doc/filters/country_name.rst
@@ -0,0 +1,41 @@
+``country_name``
+================
+
+.. versionadded:: 2.12
+ The ``country_name`` filter was added in Twig 2.12.
+
+The ``country_name`` filter returns the country name given its ISO-3166
+two-letter code:
+
+.. code-block:: twig
+
+ {# France #}
+ {{ 'FR'|country_name }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# États-Unis #}
+ {{ 'US'|country_name('fr') }}
+
+.. note::
+
+ The ``country_name`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
diff --git a/doc/filters/country_timezones.rst b/doc/filters/country_timezones.rst
new file mode 100644
index 000000000..dc987753c
--- /dev/null
+++ b/doc/filters/country_timezones.rst
@@ -0,0 +1,29 @@
+``country_timezones``
+=====================
+
+.. versionadded:: 2.12
+ The ``country_timezones`` filter was added in Twig 2.12.
+
+The ``country_timezones`` filter returns the names of the countries associated
+with a given timezone:
+
+.. code-block:: twig
+
+ {# Europe/Paris #}
+ {{ 'FR'|country_timezones|join(', ') }}
+
+.. note::
+
+ The ``country_timezones`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
diff --git a/doc/filters/currency_name.rst b/doc/filters/currency_name.rst
new file mode 100644
index 000000000..ca46ec9c7
--- /dev/null
+++ b/doc/filters/currency_name.rst
@@ -0,0 +1,44 @@
+``currency_name``
+================
+
+.. versionadded:: 2.12
+ The ``currency_name`` filter was added in Twig 2.12.
+
+The ``currency_name`` filter returns the currency name given its three-letter
+code:
+
+.. code-block:: twig
+
+ {# Euro #}
+ {{ 'EUR'|currency_name }}
+
+ {# Japanese Yen #}
+ {{ 'JPY'|currency_name }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# yen japonais #}
+ {{ 'JPY'|currency_name('fr_FR') }}
+
+.. note::
+
+ The ``currency_name`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
diff --git a/doc/filters/currency_symbol.rst b/doc/filters/currency_symbol.rst
new file mode 100644
index 000000000..bf13bbbb9
--- /dev/null
+++ b/doc/filters/currency_symbol.rst
@@ -0,0 +1,44 @@
+``currency_symbol``
+===================
+
+.. versionadded:: 2.12
+ The ``currency_symbol`` filter was added in Twig 2.12.
+
+The ``currency_symbol`` filter returns the currency name given its three-letter
+code:
+
+.. code-block:: twig
+
+ {# € #}
+ {{ 'EUR'|currency_symbol }}
+
+ {# ¥ #}
+ {{ 'JPY'|currency_symbol }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# ¥ #}
+ {{ 'JPY'|currency_symbol('fr') }}
+
+.. note::
+
+ The ``currency_symbol`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
diff --git a/doc/filters/format_currency.rst b/doc/filters/format_currency.rst
new file mode 100644
index 000000000..8b91a7e0d
--- /dev/null
+++ b/doc/filters/format_currency.rst
@@ -0,0 +1,73 @@
+``format_currency``
+===================
+
+.. versionadded:: 2.12
+ The ``format_currency`` filter was added in Twig 2.12.
+
+The ``format_currency`` filter formats a number as a currency:
+
+.. code-block:: twig
+
+ {# €1,000,000.00 #}
+ {{ '1000000'|format_currency('EUR') }}
+
+You can pass attributes to tweak the output:
+
+.. code-block:: twig
+
+ {# €12.34 #}
+ {{ '12.345'|format_currency('EUR', {rounding_mode: 'floor'}) }}
+
+ {# €1,000,000.0000 #}
+ {{ '1000000'|format_currency('EUR', {fraction_digit: 4}) }}
+
+The list of supported options:
+
+* ``grouping_used``;
+* ``decimal_always_shown``;
+* ``max_integer_digit``;
+* ``min_integer_digit``;
+* ``integer_digit``;
+* ``max_fraction_digit``;
+* ``min_fraction_digit``;
+* ``fraction_digit``;
+* ``multiplier``;
+* ``grouping_size``;
+* ``rounding_mode``;
+* ``rounding_increment``;
+* ``format_width``;
+* ``padding_position``;
+* ``secondary_grouping_size``;
+* ``significant_digits_used``;
+* ``min_significant_digits_used``;
+* ``max_significant_digits_used``;
+* ``lenient_parse``.
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# 1.000.000,00 € #}
+ {{ '1000000'|format_currency('EUR', locale='de') }}
+
+.. note::
+
+ The ``format_currency`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
+* ``attrs``: A map of attributes
diff --git a/doc/filters/format_date.rst b/doc/filters/format_date.rst
new file mode 100644
index 000000000..ccbfbf427
--- /dev/null
+++ b/doc/filters/format_date.rst
@@ -0,0 +1,31 @@
+``format_date``
+===============
+
+.. versionadded:: 2.12
+ The ``format_date`` filter was added in Twig 2.12.
+
+The ``format_date`` filter formats a date. It behaves in the exact same way as
+the ``format_datetime`` filter, but without the time.
+
+.. note::
+
+ The ``format_date`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
+* ``dateFormat``: The date format
+* ``pattern``: A date time pattern
diff --git a/doc/filters/format_datetime.rst b/doc/filters/format_datetime.rst
new file mode 100644
index 000000000..4ffb13d7c
--- /dev/null
+++ b/doc/filters/format_datetime.rst
@@ -0,0 +1,67 @@
+``format_datetime``
+===================
+
+.. versionadded:: 2.12
+ The ``format_datetime`` filter was added in Twig 2.12.
+
+The ``format_datetime`` filter formats a date time:
+
+ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
+
+.. code-block:: twig
+
+ {# Aug 7, 2019, 11:39:12 PM #}
+ {{ '2019-08-07 23:39:12'|format_datetime() }}
+
+You can tweak the output for the date part and the time part:
+
+.. code-block:: twig
+
+ {# 23:39 #}
+ {{ '2019-08-07 23:39:12'|format_datetime('none', 'short', locale='fr') }}
+
+ {# 07/08/2019 #}
+ {{ '2019-08-07 23:39:12'|format_datetime('short', 'none', locale='fr') }}
+
+ {# mercredi 7 août 2019 23:39:12 UTC #}
+ {{ '2019-08-07 23:39:12'|format_datetime('full', 'full', locale='fr') }}
+
+Supported values are: ``none``, ``short``, ``medium``, ``long``, and ``full``.
+
+For greater flexiblity, you can even define your own pattern:
+
+.. code-block:: twig
+
+ {# 11 oclock PM, GMT #}
+ {{ '2019-08-07 23:39:12'|format_datetime(pattern="hh 'oclock' a, zzzz") }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# 7 août 2019 23:39:12 #}
+ {{ '2019-08-07 23:39:12'|format_datetime(locale='fr') }}
+
+.. note::
+
+ The ``format_datetime`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
+* ``dateFormat``: The date format
+* ``timeFormat``: The time format
+* ``pattern``: A date time pattern
diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst
new file mode 100644
index 000000000..3c32725eb
--- /dev/null
+++ b/doc/filters/format_number.rst
@@ -0,0 +1,117 @@
+``format_number``
+=================
+
+.. versionadded:: 2.12
+ The ``format_number`` filter was added in Twig 2.12.
+
+The ``format_number`` filter formats a number:
+
+.. code-block:: twig
+
+ {{ '12.345'|format_number }}
+
+You can pass attributes to tweak the output:
+
+.. code-block:: twig
+
+ {# 12.34 #}
+ {{ '12.345'|format_number({rounding_mode: 'floor'}) }}
+
+ {# 1000000.0000 #}
+ {{ '1000000'|format_number({fraction_digit: 4}) }}
+
+The list of supported options:
+
+* ``grouping_used``;
+* ``decimal_always_shown``;
+* ``max_integer_digit``;
+* ``min_integer_digit``;
+* ``integer_digit``;
+* ``max_fraction_digit``;
+* ``min_fraction_digit``;
+* ``fraction_digit``;
+* ``multiplier``;
+* ``grouping_size``;
+* ``rounding_mode``;
+* ``rounding_increment``;
+* ``format_width``;
+* ``padding_position``;
+* ``secondary_grouping_size``;
+* ``significant_digits_used``;
+* ``min_significant_digits_used``;
+* ``max_significant_digits_used``;
+* ``lenient_parse``.
+
+Besides plain numbers, the filter can also format numbers in various styles:
+
+.. code-block:: twig
+
+ {# 1,234% #}
+ {{ '12.345'|format_number(style='percent') }}
+
+ {# twelve point three four five #}
+ {{ '12.345'|format_number(style='spellout') }}
+
+ {# 12 sec. #}
+ {{ '12'|format_duration_number }}
+
+The list of supported styles:
+
+* ``decimal``;
+* ``currency``;
+* ``percent``;
+* ``scientific``;
+* ``spellout``;
+* ``ordinal``;
+* ``duration``.
+
+As a shortcut, you can use the ``format_*_number`` filters by replacing `*` with
+a style:
+
+.. code-block:: twig
+
+ {# 1,234% #}
+ {{ '12.345'|format_percent_number }}
+
+ {# twelve point three four five #}
+ {{ '12.345'|format_spellout_number }}
+
+You can pass attributes to tweak the output:
+
+.. code-block:: twig
+
+ {# €12.34 #}
+ {{ '12.345'|format_number('EUR', {rounding_mode: 'floor'}) }}
+
+ {# €1,000,000.0000 #}
+ {{ '1000000'|format_number('EUR', {fraction_digit: 4}) }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# 12,345 #}
+ {{ '12.345'|format_number(locale='fr') }}
+
+.. note::
+
+ The ``format_number`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
+* ``attrs``: A map of attributes
+* ``style``: The style of the number output
diff --git a/doc/filters/format_time.rst b/doc/filters/format_time.rst
new file mode 100644
index 000000000..34ddac289
--- /dev/null
+++ b/doc/filters/format_time.rst
@@ -0,0 +1,31 @@
+``format_time``
+===============
+
+.. versionadded:: 2.12
+ The ``format_time`` filter was added in Twig 2.12.
+
+The ``format_time`` filter formats a time. It behaves in the exact same way as
+the ``format_timetime`` filter, but without the date.
+
+.. note::
+
+ The ``format_time`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
+* ``timeFormat``: The time format
+* ``pattern``: A date time pattern
diff --git a/doc/filters/index.rst b/doc/filters/index.rst
index ae8e8ee84..08a4250c0 100644
--- a/doc/filters/index.rst
+++ b/doc/filters/index.rst
@@ -9,6 +9,10 @@ Filters
capitalize
column
convert_encoding
+ country_name
+ country_timezones
+ currency_name
+ currency_symbol
data_uri
date
date_modify
@@ -17,12 +21,19 @@ Filters
filter
first
format
+ format_currency
+ format_date
+ format_datetime
+ format_number
+ format_time
html_to_markdown
join
json_encode
keys
+ language_name
last
length
+ locale_name
lower
map
markdown_to_html
@@ -39,6 +50,7 @@ Filters
spaceless
split
striptags
+ timezone_name
title
trim
upper
diff --git a/doc/filters/language_name.rst b/doc/filters/language_name.rst
new file mode 100644
index 000000000..ddc57656a
--- /dev/null
+++ b/doc/filters/language_name.rst
@@ -0,0 +1,44 @@
+``language_name``
+=================
+
+.. versionadded:: 2.12
+ The ``language_name`` filter was added in Twig 2.12.
+
+The ``language_name`` filter returns the language name given its two-letter
+code:
+
+.. code-block:: twig
+
+ {# German #}
+ {{ 'de'|language_name }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# allemand #}
+ {{ 'de'|language_name('fr') }}
+
+ {# français canadien #}
+ {{ 'fr_CA'|language_name('fr_FR') }}
+
+.. note::
+
+ The ``language_name`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
diff --git a/doc/filters/locale_name.rst b/doc/filters/locale_name.rst
new file mode 100644
index 000000000..d77b70db4
--- /dev/null
+++ b/doc/filters/locale_name.rst
@@ -0,0 +1,44 @@
+``locale_name``
+===============
+
+.. versionadded:: 2.12
+ The ``locale_name`` filter was added in Twig 2.12.
+
+The ``locale_name`` filter returns the locale name given its two-letter
+code:
+
+.. code-block:: twig
+
+ {# German #}
+ {{ 'de'|locale_name }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# allemand #}
+ {{ 'de'|locale_name('fr') }}
+
+ {# français (Canada) #}
+ {{ 'fr_CA'|locale_name('fr_FR') }}
+
+.. note::
+
+ The ``locale_name`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
diff --git a/doc/filters/timezone_name.rst b/doc/filters/timezone_name.rst
new file mode 100644
index 000000000..f97f7e4ef
--- /dev/null
+++ b/doc/filters/timezone_name.rst
@@ -0,0 +1,44 @@
+``timezone_name``
+=================
+
+.. versionadded:: 2.12
+ The ``timezone_name`` filter was added in Twig 2.12.
+
+The ``timezone_name`` filter returns the timezone name given its two-letter
+code:
+
+.. code-block:: twig
+
+ {# Central European Time (Paris) #}
+ {{ 'Europe/Paris'|timezone_name }}
+
+ {# Pacific Time (Los Angeles) #}
+ {{ 'America/Los_Angeles'|timezone_name }}
+
+By default, the filter uses the current locale. You can pass it explicitly:
+
+.. code-block:: twig
+
+ {# heure du Pacifique nord-américain (Los Angeles) #}
+ {{ 'America/Los_Angeles'|timezone_name('fr') }}
+
+.. note::
+
+ The ``timezone_name`` filter is part of the ``IntlExtension`` which is not
+ installed by default. Install it first:
+
+ .. code-block:: bash
+
+ $ composer req twig/intl-extra
+
+ Then, add it on the Twig environment::
+
+ use Twig\Extra\Intl\IntlExtension;
+
+ $twig = new \Twig\Environment(...);
+ $twig->addExtension(new IntlExtension());
+
+Arguments
+---------
+
+* ``locale``: The locale
diff --git a/extra/intl-extra/.gitignore b/extra/intl-extra/.gitignore
new file mode 100644
index 000000000..76367ee5b
--- /dev/null
+++ b/extra/intl-extra/.gitignore
@@ -0,0 +1,4 @@
+vendor/
+composer.lock
+phpunit.xml
+.phpunit.result.cache
diff --git a/extra/intl-extra/LICENSE b/extra/intl-extra/LICENSE
new file mode 100644
index 000000000..1a1869751
--- /dev/null
+++ b/extra/intl-extra/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2019 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/extra/intl-extra/README.md b/extra/intl-extra/README.md
new file mode 100644
index 000000000..c8a453cde
--- /dev/null
+++ b/extra/intl-extra/README.md
@@ -0,0 +1,29 @@
+Twig Intl Extension
+===================
+
+This package is a Twig extension that provides the following:
+
+ * [`country_name`][1] filter: returns the country name given its ISO-3166 two-letter code;
+ * [`currency_name`][2] filter: returns the currency name given its three-letter code;
+ * [`currency_symbol`][3] filter: returns the currency symbol given its three-letter code;
+ * [`language_name`][4] filter: returns the language name given its two-letter code;
+ * [`locale_name`][5] filter: returns the language name given its two-letter code;
+ * [`timezone_name`][6] filter: returns the timezone name given its identifier;
+ * [`country_timezones`][7] filter: returns the names of the countries associated with a given timezone;
+ * [`format_currency`][8] filter: formats a number as a currency;
+ * [`format_number`][9] filter: formats a number;
+ * [`format_datetime`][9] filter: formats a date time;
+ * [`format_date`][10] filter: formats a date;
+ * [`format_time`][11] filter: formats a time.
+
+[1]: https://twig.symfony.com/country_name
+[2]: https://twig.symfony.com/currency_name
+[3]: https://twig.symfony.com/currency_symbol
+[4]: https://twig.symfony.com/language_name
+[5]: https://twig.symfony.com/locale_name
+[6]: https://twig.symfony.com/timezone_name
+[7]: https://twig.symfony.com/country_timezones
+[8]: https://twig.symfony.com/format_currency
+[9]: https://twig.symfony.com/format_number
+[10]: https://twig.symfony.com/format_datetime
+[11]: https://twig.symfony.com/format_time
diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json
new file mode 100644
index 000000000..73da47c76
--- /dev/null
+++ b/extra/intl-extra/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "twig/intl-extra",
+ "type": "library",
+ "description": "A Twig extension for Intl",
+ "keywords": ["twig", "intl"],
+ "homepage": "https://twig.symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ }
+ ],
+ "require": {
+ "php": "^7.1.3",
+ "twig/twig": "^2.4|^3.0",
+ "symfony/intl": "^4.3"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^4.4@dev"
+ },
+ "autoload": {
+ "psr-4" : {
+ "Twig\\Extra\\Intl\\" : "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4" : {
+ "Twig\\Extra\\Intl\\Tests\\" : "tests/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.12-dev"
+ }
+ }
+}
diff --git a/extra/intl-extra/phpunit.xml.dist b/extra/intl-extra/phpunit.xml.dist
new file mode 100644
index 000000000..7794b5467
--- /dev/null
+++ b/extra/intl-extra/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+ ./tests/
+
+
+
+
+
+ ./
+
+ ./tests
+ ./vendor
+
+
+
+
diff --git a/extra/intl-extra/src/IntlExtension.php b/extra/intl-extra/src/IntlExtension.php
new file mode 100644
index 000000000..1dc2bfd2d
--- /dev/null
+++ b/extra/intl-extra/src/IntlExtension.php
@@ -0,0 +1,354 @@
+ \IntlDateFormatter::NONE,
+ 'short' => \IntlDateFormatter::SHORT,
+ 'medium' => \IntlDateFormatter::MEDIUM,
+ 'long' => \IntlDateFormatter::LONG,
+ 'full' => \IntlDateFormatter::FULL,
+ ];
+ private const NUMBER_TYPES = [
+ 'default' => \NumberFormatter::TYPE_DEFAULT,
+ 'int32' => \NumberFormatter::TYPE_INT32,
+ 'int64' => \NumberFormatter::TYPE_INT64,
+ 'double' => \NumberFormatter::TYPE_DOUBLE,
+ 'currency' => \NumberFormatter::TYPE_CURRENCY,
+ ];
+ private const NUMBER_STYLES = [
+ 'decimal' => \NumberFormatter::DECIMAL,
+ 'currency' => \NumberFormatter::CURRENCY,
+ 'percent' => \NumberFormatter::PERCENT,
+ 'scientific' => \NumberFormatter::SCIENTIFIC,
+ 'spellout' => \NumberFormatter::SPELLOUT,
+ 'ordinal' => \NumberFormatter::ORDINAL,
+ 'duration' => \NumberFormatter::DURATION,
+ ];
+ private const NUMBER_ATTRIBUTES = [
+ 'grouping_used' => \NumberFormatter::GROUPING_USED,
+ 'decimal_always_shown' => \NumberFormatter::DECIMAL_ALWAYS_SHOWN,
+ 'max_integer_digit' => \NumberFormatter::MAX_INTEGER_DIGITS,
+ 'min_integer_digit' => \NumberFormatter::MIN_INTEGER_DIGITS,
+ 'integer_digit' => \NumberFormatter::INTEGER_DIGITS,
+ 'max_fraction_digit' => \NumberFormatter::MAX_FRACTION_DIGITS,
+ 'min_fraction_digit' => \NumberFormatter::MIN_FRACTION_DIGITS,
+ 'fraction_digit' => \NumberFormatter::FRACTION_DIGITS,
+ 'multiplier' => \NumberFormatter::MULTIPLIER,
+ 'grouping_size' => \NumberFormatter::GROUPING_SIZE,
+ 'rounding_mode' => \NumberFormatter::ROUNDING_MODE,
+ 'rounding_increment' => \NumberFormatter::ROUNDING_INCREMENT,
+ 'format_width' => \NumberFormatter::FORMAT_WIDTH,
+ 'padding_position' => \NumberFormatter::PADDING_POSITION,
+ 'secondary_grouping_size' => \NumberFormatter::SECONDARY_GROUPING_SIZE,
+ 'significant_digits_used' => \NumberFormatter::SIGNIFICANT_DIGITS_USED,
+ 'min_significant_digits_used' => \NumberFormatter::MIN_SIGNIFICANT_DIGITS,
+ 'max_significant_digits_used' => \NumberFormatter::MAX_SIGNIFICANT_DIGITS,
+ 'lenient_parse' => \NumberFormatter::LENIENT_PARSE,
+ ];
+ private const NUMBER_ROUNDING_ATTRIBUTES = [
+ 'ceiling' => \NumberFormatter::ROUND_CEILING,
+ 'floor' => \NumberFormatter::ROUND_FLOOR,
+ 'down' => \NumberFormatter::ROUND_DOWN,
+ 'up' => \NumberFormatter::ROUND_UP,
+ 'halfeven' => \NumberFormatter::ROUND_HALFEVEN,
+ 'halfdown' => \NumberFormatter::ROUND_HALFDOWN,
+ 'halfup' => \NumberFormatter::ROUND_HALFUP,
+ ];
+ private const NUMBER_PADDONG_ATTRIBUTES = [
+ 'before_prefix' => \NumberFormatter::PAD_BEFORE_PREFIX,
+ 'after_prefix' => \NumberFormatter::PAD_AFTER_PREFIX,
+ 'before_suffix' => \NumberFormatter::PAD_BEFORE_SUFFIX,
+ 'after_suffix' => \NumberFormatter::PAD_AFTER_SUFFIX,
+ ];
+ private const NUMBER_TEXT_ATTRIBUTES = [
+ 'positive_prefix' => \NumberFormatter::POSITIVE_PREFIX,
+ 'positive_suffix' => \NumberFormatter::POSITIVE_SUFFIX,
+ 'negative_prefix' => \NumberFormatter::NEGATIVE_PREFIX,
+ 'negative_suffix' => \NumberFormatter::NEGATIVE_SUFFIX,
+ 'padding_character' => \NumberFormatter::PADDING_CHARACTER,
+ 'currency_mode' => \NumberFormatter::CURRENCY_CODE,
+ 'default_ruleset' => \NumberFormatter::DEFAULT_RULESET,
+ 'public_rulesets' => \NumberFormatter::PUBLIC_RULESETS,
+ ];
+ private const NUMBER_SYMBOLS = [
+ 'decimal_separator' => \NumberFormatter::DECIMAL_SEPARATOR_SYMBOL,
+ 'grouping_separator' => \NumberFormatter::GROUPING_SEPARATOR_SYMBOL,
+ 'pattern_separator' => \NumberFormatter::PATTERN_SEPARATOR_SYMBOL,
+ 'percent' => \NumberFormatter::PERCENT_SYMBOL,
+ 'zero_digit' => \NumberFormatter::ZERO_DIGIT_SYMBOL,
+ 'digit' => \NumberFormatter::DIGIT_SYMBOL,
+ 'minus_sign' => \NumberFormatter::MINUS_SIGN_SYMBOL,
+ 'plus_sign' => \NumberFormatter::PLUS_SIGN_SYMBOL,
+ 'currency' => \NumberFormatter::CURRENCY_SYMBOL,
+ 'intl_currency' => \NumberFormatter::INTL_CURRENCY_SYMBOL,
+ 'monetary_separator' => \NumberFormatter::MONETARY_SEPARATOR_SYMBOL,
+ 'exponential' => \NumberFormatter::EXPONENTIAL_SYMBOL,
+ 'permill' => \NumberFormatter::PERMILL_SYMBOL,
+ 'pad_escape' => \NumberFormatter::PAD_ESCAPE_SYMBOL,
+ 'infinity' => \NumberFormatter::INFINITY_SYMBOL,
+ 'nan' => \NumberFormatter::NAN_SYMBOL,
+ 'significant_digit' => \NumberFormatter::SIGNIFICANT_DIGIT_SYMBOL,
+ 'monetary_grouping_separator' => \NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL,
+ ];
+
+ private $dateFormatters = [];
+ private $numberFormatters = [];
+ private $dateFormatterPrototype;
+ private $numberFormatterPrototype;
+
+ public function __construct(\IntlDateFormatter $dateFormatterPrototype = null, \NumberFormatter $numberFormatterPrototype = null)
+ {
+ $this->dateFormatterPrototype = $dateFormatterPrototype;
+ $this->numberFormatterPrototype = $numberFormatterPrototype;
+ }
+
+ public function getFilters()
+ {
+ return [
+ // internationalized names
+ new TwigFilter('country_name', [$this, 'getCountryName']),
+ new TwigFilter('currency_name', [$this, 'getCurrencyName']),
+ new TwigFilter('currency_symbol', [$this, 'getCurrencySymbol']),
+ new TwigFilter('language_name', [$this, 'getLanguageName']),
+ new TwigFilter('locale_name', [$this, 'getLocaleName']),
+ new TwigFilter('timezone_name', [$this, 'getTimezoneName']),
+ new TwigFilter('country_timezones', [$this, 'getCountryTimezones']),
+
+ // localized formatters
+ new TwigFilter('format_currency', [$this, 'formatCurrency']),
+ new TwigFilter('format_number', [$this, 'formatNumber']),
+ new TwigFilter('format_*_number', [$this, 'formatNumberStyle']),
+ 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]),
+ ];
+ }
+
+ public function getCountryName(string $country, string $locale = null): string
+ {
+ return Countries::getName($country, $locale);
+ }
+
+ public function getCurrencyName(string $currency, string $locale = null): string
+ {
+ return Currencies::getName($currency, $locale);
+ }
+
+ public function getCurrencySymbol(string $currency, string $locale = null): string
+ {
+ return Currencies::getSymbol($currency, $locale);
+ }
+
+ public function getLanguageName(string $language, string $locale = null): string
+ {
+ return Languages::getName($language, $locale);
+ }
+
+ public function getLocaleName(string $data, string $locale = null): string
+ {
+ return Locales::getName($data, $locale);
+ }
+
+ public function getTimezoneName(string $timezone, string $locale = null): string
+ {
+ return Timezones::getName($timezone, $locale);
+ }
+
+ public function getCountryTimezones(string $country): array
+ {
+ return Timezones::forCountryCode($country);
+ }
+
+ public function formatCurrency($amount, string $currency, array $attrs = [], string $locale = null): string
+ {
+ $formatter = $this->createNumberFormatter($locale, 'currency', $attrs);
+
+ if (false === $ret = $formatter->formatCurrency($amount, $currency)) {
+ throw new RuntimeError('Unable to format the given number as a currency.');
+ }
+
+ return $ret;
+ }
+
+ public function formatNumber($number, array $attrs = [], string $style = 'decimal', string $type = 'default', string $locale = null): string
+ {
+ if (!isset(self::NUMBER_TYPES[$type])) {
+ throw new RuntimeError(sprintf('The type "%s" does not exist, known types are: "%s".', $type, implode('", "', array_keys(self::NUMBER_TYPES))));
+ }
+
+ $formatter = $this->createNumberFormatter($locale, $style, $attrs);
+
+ if (false === $ret = $formatter->format($number, self::NUMBER_TYPES[$type])) {
+ throw new RuntimeError('Unable to format the given number.');
+ }
+
+ return $ret;
+ }
+
+ public function formatNumberStyle(string $style, $number, array $attrs = [], string $type = 'default', string $locale = null): string
+ {
+ return $this->formatNumber($number, $attrs, $style, $type, $locale);
+ }
+
+ /**
+ * @param \DateTimeInterface|string|null $date A date or null to use the current time
+ * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
+ */
+ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
+ {
+ $date = \twig_date_converter($env, $date, $timezone);
+ $formatter = $this->createDateFormatter($locale, $dateFormat, $timeFormat, $pattern, $date->getTimezone(), $calendar);
+
+ if (false === $ret = $formatter->format($date)) {
+ throw new RuntimeError('Unable to format the given date.');
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @param \DateTimeInterface|string|null $date A date or null to use the current time
+ * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
+ */
+ public function formatDate(Environment $env, $date, ?string $dateFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
+ {
+ return $this->formatDateTime($env, $date, $dateFormat, 'none', $pattern, $timezone, $calendar, $locale);
+ }
+
+ /**
+ * @param \DateTimeInterface|string|null $date A date or null to use the current time
+ * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
+ */
+ public function formatTime(Environment $env, $date, ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
+ {
+ return $this->formatDateTime($env, $date, 'none', $timeFormat, $pattern, $timezone, $calendar, $locale);
+ }
+
+ private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormat, string $pattern = '', \DateTimeZone $timezone, string $calendar): \IntlDateFormatter
+ {
+ if (null !== $dateFormat && !isset(self::DATE_FORMATS[$dateFormat])) {
+ throw new RuntimeError(sprintf('The date format "%s" does not exist, known formats are: "%s".', $dateFormat, implode('", "', array_keys(self::DATE_FORMATS))));
+ }
+
+ if (null !== $timeFormat && !isset(self::DATE_FORMATS[$timeFormat])) {
+ throw new RuntimeError(sprintf('The time format "%s" does not exist, known formats are: "%s".', $timeFormat, implode('", "', array_keys(self::DATE_FORMATS))));
+ }
+
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ $calendar = 'gregorian' === $calendar ? \IntlDateFormatter::GREGORIAN : \IntlDateFormatter::TRADITIONAL;
+
+ $dateFormatValue = self::DATE_FORMATS[$dateFormat] ?? null;
+ $timeFormatValue = self::DATE_FORMATS[$timeFormat] ?? null;
+
+ if ($this->dateFormatterPrototype) {
+ $dateFormatValue = $dateFormatValue ?: $this->dateFormatterPrototype->getDateType();
+ $timeFormatValue = $timeFormatValue ?: $this->dateFormatterPrototype->getTimeType();
+ $timezone = $timezone ?: $this->dateFormatterPrototype->getTimeType();
+ $calendar = $calendar ?: $this->dateFormatterPrototype->getCalendar();
+ $pattern = $pattern ?: $this->dateFormatterPrototype->getPattern();
+ }
+
+ $hash = $locale.'|'.$dateFormatValue.'|'.$timeFormatValue.'|'.$timezone->getName().'|'.$calendar.'|'.$pattern;
+
+ if (!isset($this->dateFormatters[$hash])) {
+ $this->dateFormatters[$hash] = new \IntlDateFormatter($locale, $dateFormatValue, $timeFormatValue, $timezone, $calendar, $pattern);
+ }
+
+ return $this->dateFormatters[$hash];
+ }
+
+ private function createNumberFormatter(?string $locale, string $style, array $attrs = []): \NumberFormatter
+ {
+ if (!isset(self::NUMBER_STYLES[$style])) {
+ throw new RuntimeError(sprintf('The style "%s" does not exist, known styles are: "%s".', $style, implode('", "', array_keys(self::NUMBER_STYLES))));
+ }
+
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ // textAttrs and symbols can only be set on the prototype as there is probably no
+ // use case for setting it on each call.
+ $textAttrs = [];
+ $symbols = [];
+ if ($this->numberFormatterPrototype) {
+ foreach (self::NUMBER_ATTRIBUTES as $name) {
+ if (!isset($attrs[$name])) {
+ $attrs[$name] = $this->numberFormatterPrototype->getAttribute($name);
+ }
+ }
+
+ foreach (self::NUMBER_TEXT_ATTRIBUTES as $name) {
+ $textAttrs[$name] = $this->numberFormatterPrototype->getTextAttribute($name);
+ }
+
+ foreach (self::NUMBER_SYMBOLS as $name) {
+ $symbols[$name] = $this->numberFormatterPrototype->getSymbol($name);
+ }
+ }
+
+ ksort($attrs);
+ $hash = $locale.'|'.$style.'|'.json_encode($attrs).'|'.json_encode($textAttrs).'|'.json_encode($symbols);
+
+ if (!isset($this->numberFormatters[$hash])) {
+ $this->numberFormatters[$hash] = new \NumberFormatter($locale, self::NUMBER_STYLES[$style]);
+ }
+
+ foreach ($attrs as $name => $value) {
+ if (!isset(self::NUMBER_ATTRIBUTES[$name])) {
+ throw new RuntimeError(sprintf('The number formatter attribute "%s" does not exist, known attributes are: "%s".', $name, implode('", "', array_keys(self::NUMBER_ATTRIBUTES))));
+ }
+
+ if ('rounding_mode' === $name) {
+ if (!isset(self::NUMBER_ROUNDING_ATTRIBUTES[$value])) {
+ throw new RuntimeError(sprintf('The number formatter rounding mode "%s" does not exist, known modes are: "%s".', $value, implode('", "', array_keys(self::NUMBER_ROUNDING_ATTRIBUTES))));
+ }
+
+ $value = self::NUMBER_ROUNDING_ATTRIBUTES[$value];
+ } elseif ('padding_position' === $name) {
+ if (!isset(self::NUMBER_PADDONG_ATTRIBUTES[$value])) {
+ throw new RuntimeError(sprintf('The number formatter padding position "%s" does not exist, known positions are: "%s".', $value, implode('", "', array_keys(self::NUMBER_PADDONG_ATTRIBUTES))));
+ }
+
+ $value = self::NUMBER_PADDONG_ATTRIBUTES[$value];
+ }
+
+ $this->numberFormatters[$hash]->setAttribute(self::NUMBER_ATTRIBUTES[$name], $value);
+ }
+
+ foreach ($textAttrs as $name => $value) {
+ $this->numberFormatters[$hash]->setTextAttribute($name, $value);
+ }
+
+ foreach ($symbols as $name => $value) {
+ $this->numberFormatters[$hash]->setSymbol($name, $value);
+ }
+
+ return $this->numberFormatters[$hash];
+ }
+}
diff --git a/extra/intl-extra/tests/Fixtures/country_name.test b/extra/intl-extra/tests/Fixtures/country_name.test
new file mode 100644
index 000000000..d1b729dd1
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/country_name.test
@@ -0,0 +1,14 @@
+--TEST--
+"country_name" filter
+--TEMPLATE--
+{{ 'FR'|country_name }}
+{{ 'US'|country_name }}
+{{ 'US'|country_name('fr') }}
+{{ 'CH'|country_name('fr_CA') }}
+--DATA--
+return [];
+--EXPECT--
+France
+United States
+États-Unis
+Suisse
diff --git a/extra/intl-extra/tests/Fixtures/country_timezones.test b/extra/intl-extra/tests/Fixtures/country_timezones.test
new file mode 100644
index 000000000..727b19100
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/country_timezones.test
@@ -0,0 +1,10 @@
+--TEST--
+"country_timezones" filter
+--TEMPLATE--
+{{ 'FR'|country_timezones|join(', ') }}
+{{ 'US'|country_timezones|join(', ') }}
+--DATA--
+return [];
+--EXPECT--
+Europe/Paris
+America/Adak, America/Anchorage, America/Boise, America/Chicago, America/Denver, America/Detroit, America/Indiana/Knox, America/Indiana/Marengo, America/Indiana/Petersburg, America/Indiana/Tell_City, America/Indiana/Vevay, America/Indiana/Vincennes, America/Indiana/Winamac, America/Indianapolis, America/Juneau, America/Kentucky/Monticello, America/Los_Angeles, America/Louisville, America/Menominee, America/Metlakatla, America/New_York, America/Nome, America/North_Dakota/Beulah, America/North_Dakota/Center, America/North_Dakota/New_Salem, America/Phoenix, America/Sitka, America/Yakutat, Pacific/Honolulu
diff --git a/extra/intl-extra/tests/Fixtures/currency_name.test b/extra/intl-extra/tests/Fixtures/currency_name.test
new file mode 100644
index 000000000..d0e1cf4aa
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/currency_name.test
@@ -0,0 +1,14 @@
+--TEST--
+"currency_name" filter
+--TEMPLATE--
+{{ 'EUR'|currency_name }}
+{{ 'JPY'|currency_name }}
+{{ 'EUR'|currency_name('fr') }}
+{{ 'JPY'|currency_name('fr_FR') }}
+--DATA--
+return [];
+--EXPECT--
+Euro
+Japanese Yen
+euro
+yen japonais
diff --git a/extra/intl-extra/tests/Fixtures/currency_symbol.test b/extra/intl-extra/tests/Fixtures/currency_symbol.test
new file mode 100644
index 000000000..9212d4316
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/currency_symbol.test
@@ -0,0 +1,10 @@
+--TEST--
+"currency_symbol" filter
+--TEMPLATE--
+{{ 'EUR'|currency_symbol }}
+{{ 'JPY'|currency_symbol }}
+--DATA--
+return [];
+--EXPECT--
+€
+¥
diff --git a/extra/intl-extra/tests/Fixtures/format_currency.test b/extra/intl-extra/tests/Fixtures/format_currency.test
new file mode 100644
index 000000000..097815071
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/format_currency.test
@@ -0,0 +1,16 @@
+--TEST--
+"format_currency" filter
+--TEMPLATE--
+{{ '1000000'|format_currency('EUR') }}
+{{ '1000000'|format_currency('EUR', locale='de') }}
+{{ '1000000'|format_currency('EUR', {fraction_digit: 2}) }}
+{{ '12.345'|format_currency('EUR', {rounding_mode: 'floor'}) }}
+{{ '125000'|format_currency('YEN') }}
+--DATA--
+return [];
+--EXPECT--
+€1,000,000.00
+1.000.000,00 €
+€1,000,000.00
+€12.34
+YEN125,000.00
diff --git a/extra/intl-extra/tests/Fixtures/format_date.test b/extra/intl-extra/tests/Fixtures/format_date.test
new file mode 100644
index 000000000..a1fe8c484
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/format_date.test
@@ -0,0 +1,26 @@
+--TEST--
+"format_date" filter
+--TEMPLATE--
+{{ '2019-08-07 23:39:12'|format_datetime() }}
+{{ '2019-08-07 23:39:12'|format_datetime(locale='fr') }}
+{{ '2019-08-07 23:39:12'|format_datetime('none', 'short', locale='fr') }}
+{{ '2019-08-07 23:39:12'|format_datetime('short', 'none', locale='fr') }}
+{{ '2019-08-07 23:39:12'|format_datetime('full', 'full', locale='fr') }}
+{{ '2019-08-07 23:39:12'|format_datetime(pattern="hh 'oclock' a, zzzz") }}
+
+{{ '2019-08-07 23:39:12'|format_date }}
+{{ '2019-08-07 23:39:12'|format_date(locale='fr') }}
+{{ '2019-08-07 23:39:12'|format_time }}
+--DATA--
+return [];
+--EXPECT--
+Aug 7, 2019, 11:39:12 PM
+7 août 2019 23:39:12
+23:39
+07/08/2019
+mercredi 7 août 2019 23:39:12 UTC
+11 oclock PM, GMT
+
+Aug 7, 2019
+7 août 2019
+11:39:12 PM
diff --git a/extra/intl-extra/tests/Fixtures/format_number.test b/extra/intl-extra/tests/Fixtures/format_number.test
new file mode 100644
index 000000000..d52e132de
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/format_number.test
@@ -0,0 +1,28 @@
+--TEST--
+"format_number" filter
+--TEMPLATE--
+{{ '12.345'|format_number }}
+{{ '12.345'|format_number(locale='fr') }}
+{{ '12.345'|format_number(style='percent') }}
+{{ '12.345'|format_number(style='spellout') }}
+{{ '12.345'|format_percent_number }}
+{{ '12.345'|format_spellout_number }}
+{{ '80.345'|format_spellout_number(locale='fr_FR') }}
+{{ '80.345'|format_spellout_number(locale='fr_CH') }}
+{{ '12'|format_duration_number }}
+{{ '0.12'|format_percent_number({fraction_digit: 1}) }}
+{{ '0.12345'|format_percent_number({rounding_mode: 'ceiling'}) }}
+--DATA--
+return [];
+--EXPECT--
+12.345
+12,345
+1,234%
+twelve point three four five
+1,234%
+twelve point three four five
+quatre-vingts virgule trois quatre cinq
+huitante virgule trois quatre cinq
+12 sec.
+12.0%
+13%
diff --git a/extra/intl-extra/tests/Fixtures/language_name.test b/extra/intl-extra/tests/Fixtures/language_name.test
new file mode 100644
index 000000000..8339ae90b
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/language_name.test
@@ -0,0 +1,16 @@
+--TEST--
+"language_name" filter
+--TEMPLATE--
+{{ 'de'|language_name }}
+{{ 'fr'|language_name }}
+{{ 'de'|language_name('fr') }}
+{{ 'fr'|language_name('fr_FR') }}
+{{ 'fr_CA'|language_name('fr_FR') }}
+--DATA--
+return [];
+--EXPECT--
+German
+French
+allemand
+français
+français canadien
diff --git a/extra/intl-extra/tests/Fixtures/locale_name.test b/extra/intl-extra/tests/Fixtures/locale_name.test
new file mode 100644
index 000000000..e3dc5694c
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/locale_name.test
@@ -0,0 +1,16 @@
+--TEST--
+"locale_name" filter
+--TEMPLATE--
+{{ 'de'|locale_name }}
+{{ 'fr'|locale_name }}
+{{ 'de'|locale_name('fr') }}
+{{ 'fr'|locale_name('fr_FR') }}
+{{ 'fr_CA'|locale_name('fr_FR') }}
+--DATA--
+return [];
+--EXPECT--
+German
+French
+allemand
+français
+français (Canada)
diff --git a/extra/intl-extra/tests/Fixtures/timezone_name.test b/extra/intl-extra/tests/Fixtures/timezone_name.test
new file mode 100644
index 000000000..501a40ad2
--- /dev/null
+++ b/extra/intl-extra/tests/Fixtures/timezone_name.test
@@ -0,0 +1,12 @@
+--TEST--
+"timezone_name" filter
+--TEMPLATE--
+{{ 'Europe/Paris'|timezone_name }}
+{{ 'America/Los_Angeles'|timezone_name }}
+{{ 'America/Los_Angeles'|timezone_name('fr') }}
+--DATA--
+return [];
+--EXPECT--
+Central European Time (Paris)
+Pacific Time (Los Angeles)
+heure du Pacifique nord-américain (Los Angeles)
diff --git a/extra/intl-extra/tests/IntegrationTest.php b/extra/intl-extra/tests/IntegrationTest.php
new file mode 100644
index 000000000..7b191bacd
--- /dev/null
+++ b/extra/intl-extra/tests/IntegrationTest.php
@@ -0,0 +1,30 @@
+end()
;
+ $rootNode
+ ->children()
+ ->arrayNode('intl')
+ ->{class_exists(IntlExtension::class) ? 'canBeDisabled' : 'canBeEnabled'}()
+ ->end()
+ ->end()
+ ;
+
return $treeBuilder;
}
}
diff --git a/extra/twig-extra-bundle/src/DependencyInjection/TwigExtraExtension.php b/extra/twig-extra-bundle/src/DependencyInjection/TwigExtraExtension.php
index b8cdbb565..7a05f8710 100644
--- a/extra/twig-extra-bundle/src/DependencyInjection/TwigExtraExtension.php
+++ b/extra/twig-extra-bundle/src/DependencyInjection/TwigExtraExtension.php
@@ -38,5 +38,9 @@ class TwigExtraExtension extends Extension
if ($this->isConfigEnabled($container, $config['markdown'])) {
$loader->load('markdown.xml');
}
+
+ if ($this->isConfigEnabled($container, $config['intl'])) {
+ $loader->load('intl.xml');
+ }
}
}
diff --git a/extra/twig-extra-bundle/src/MissingExtensionSuggestor.php b/extra/twig-extra-bundle/src/MissingExtensionSuggestor.php
index 0c974a805..fbc12df62 100644
--- a/extra/twig-extra-bundle/src/MissingExtensionSuggestor.php
+++ b/extra/twig-extra-bundle/src/MissingExtensionSuggestor.php
@@ -16,9 +16,31 @@ use Twig\Error\SyntaxError;
final class MissingExtensionSuggestor
{
private const FILTERS = [
+ // HTML
'data_uri' => ['HtmlExtension', 'twig/html-extra'],
- 'markdown_to_html' => ['MarkdownExtension', 'twig/markdown-extra'],
+
+ // Markdown
'html_to_markdown' => ['MarkdownExtension', 'twig/markdown-extra'],
+ 'markdown_to_html' => ['MarkdownExtension', 'twig/markdown-extra'],
+
+ // Intl
+ 'country_name' => ['IntlExtension', 'twig/intl-extra'],
+ 'currency_name' => ['IntlExtension', 'twig/intl-extra'],
+ 'currency_symbol' => ['IntlExtension', 'twig/intl-extra'],
+ 'language_name' => ['IntlExtension', 'twig/intl-extra'],
+ 'country_timezones' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_currency' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_decimal_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_currency_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_percent_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_scientific_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_spellout_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_ordinal_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_duration_number' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_date' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_datetime' => ['IntlExtension', 'twig/intl-extra'],
+ 'format_time' => ['IntlExtension', 'twig/intl-extra'],
];
private const FUNCTIONS = [
diff --git a/extra/twig-extra-bundle/src/Resources/config/intl.xml b/extra/twig-extra-bundle/src/Resources/config/intl.xml
new file mode 100644
index 000000000..36e5fff45
--- /dev/null
+++ b/extra/twig-extra-bundle/src/Resources/config/intl.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+