vendor/twig/intl-extra/IntlExtension.php line 40

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Twig.
  4.  *
  5.  * (c) Fabien Potencier
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Twig\Extra\Intl;
  11. use Symfony\Component\Intl\Countries;
  12. use Symfony\Component\Intl\Currencies;
  13. use Symfony\Component\Intl\Exception\MissingResourceException;
  14. use Symfony\Component\Intl\Languages;
  15. use Symfony\Component\Intl\Locales;
  16. use Symfony\Component\Intl\Timezones;
  17. use Twig\Environment;
  18. use Twig\Error\RuntimeError;
  19. use Twig\Extension\AbstractExtension;
  20. use Twig\TwigFilter;
  21. use Twig\TwigFunction;
  22. final class IntlExtension extends AbstractExtension
  23. {
  24.     private const DATE_FORMATS = [
  25.         'none' => \IntlDateFormatter::NONE,
  26.         'short' => \IntlDateFormatter::SHORT,
  27.         'medium' => \IntlDateFormatter::MEDIUM,
  28.         'long' => \IntlDateFormatter::LONG,
  29.         'full' => \IntlDateFormatter::FULL,
  30.     ];
  31.     private const NUMBER_TYPES = [
  32.         'default' => \NumberFormatter::TYPE_DEFAULT,
  33.         'int32' => \NumberFormatter::TYPE_INT32,
  34.         'int64' => \NumberFormatter::TYPE_INT64,
  35.         'double' => \NumberFormatter::TYPE_DOUBLE,
  36.         'currency' => \NumberFormatter::TYPE_CURRENCY,
  37.     ];
  38.     private const NUMBER_STYLES = [
  39.         'decimal' => \NumberFormatter::DECIMAL,
  40.         'currency' => \NumberFormatter::CURRENCY,
  41.         'percent' => \NumberFormatter::PERCENT,
  42.         'scientific' => \NumberFormatter::SCIENTIFIC,
  43.         'spellout' => \NumberFormatter::SPELLOUT,
  44.         'ordinal' => \NumberFormatter::ORDINAL,
  45.         'duration' => \NumberFormatter::DURATION,
  46.     ];
  47.     private const NUMBER_ATTRIBUTES = [
  48.         'grouping_used' => \NumberFormatter::GROUPING_USED,
  49.         'decimal_always_shown' => \NumberFormatter::DECIMAL_ALWAYS_SHOWN,
  50.         'max_integer_digit' => \NumberFormatter::MAX_INTEGER_DIGITS,
  51.         'min_integer_digit' => \NumberFormatter::MIN_INTEGER_DIGITS,
  52.         'integer_digit' => \NumberFormatter::INTEGER_DIGITS,
  53.         'max_fraction_digit' => \NumberFormatter::MAX_FRACTION_DIGITS,
  54.         'min_fraction_digit' => \NumberFormatter::MIN_FRACTION_DIGITS,
  55.         'fraction_digit' => \NumberFormatter::FRACTION_DIGITS,
  56.         'multiplier' => \NumberFormatter::MULTIPLIER,
  57.         'grouping_size' => \NumberFormatter::GROUPING_SIZE,
  58.         'rounding_mode' => \NumberFormatter::ROUNDING_MODE,
  59.         'rounding_increment' => \NumberFormatter::ROUNDING_INCREMENT,
  60.         'format_width' => \NumberFormatter::FORMAT_WIDTH,
  61.         'padding_position' => \NumberFormatter::PADDING_POSITION,
  62.         'secondary_grouping_size' => \NumberFormatter::SECONDARY_GROUPING_SIZE,
  63.         'significant_digits_used' => \NumberFormatter::SIGNIFICANT_DIGITS_USED,
  64.         'min_significant_digits_used' => \NumberFormatter::MIN_SIGNIFICANT_DIGITS,
  65.         'max_significant_digits_used' => \NumberFormatter::MAX_SIGNIFICANT_DIGITS,
  66.         'lenient_parse' => \NumberFormatter::LENIENT_PARSE,
  67.     ];
  68.     private const NUMBER_ROUNDING_ATTRIBUTES = [
  69.         'ceiling' => \NumberFormatter::ROUND_CEILING,
  70.         'floor' => \NumberFormatter::ROUND_FLOOR,
  71.         'down' => \NumberFormatter::ROUND_DOWN,
  72.         'up' => \NumberFormatter::ROUND_UP,
  73.         'halfeven' => \NumberFormatter::ROUND_HALFEVEN,
  74.         'halfdown' => \NumberFormatter::ROUND_HALFDOWN,
  75.         'halfup' => \NumberFormatter::ROUND_HALFUP,
  76.     ];
  77.     private const NUMBER_PADDING_ATTRIBUTES = [
  78.         'before_prefix' => \NumberFormatter::PAD_BEFORE_PREFIX,
  79.         'after_prefix' => \NumberFormatter::PAD_AFTER_PREFIX,
  80.         'before_suffix' => \NumberFormatter::PAD_BEFORE_SUFFIX,
  81.         'after_suffix' => \NumberFormatter::PAD_AFTER_SUFFIX,
  82.     ];
  83.     private const NUMBER_TEXT_ATTRIBUTES = [
  84.         'positive_prefix' => \NumberFormatter::POSITIVE_PREFIX,
  85.         'positive_suffix' => \NumberFormatter::POSITIVE_SUFFIX,
  86.         'negative_prefix' => \NumberFormatter::NEGATIVE_PREFIX,
  87.         'negative_suffix' => \NumberFormatter::NEGATIVE_SUFFIX,
  88.         'padding_character' => \NumberFormatter::PADDING_CHARACTER,
  89.         'currency_code' => \NumberFormatter::CURRENCY_CODE,
  90.         'default_ruleset' => \NumberFormatter::DEFAULT_RULESET,
  91.         'public_rulesets' => \NumberFormatter::PUBLIC_RULESETS,
  92.     ];
  93.     private const NUMBER_SYMBOLS = [
  94.         'decimal_separator' => \NumberFormatter::DECIMAL_SEPARATOR_SYMBOL,
  95.         'grouping_separator' => \NumberFormatter::GROUPING_SEPARATOR_SYMBOL,
  96.         'pattern_separator' => \NumberFormatter::PATTERN_SEPARATOR_SYMBOL,
  97.         'percent' => \NumberFormatter::PERCENT_SYMBOL,
  98.         'zero_digit' => \NumberFormatter::ZERO_DIGIT_SYMBOL,
  99.         'digit' => \NumberFormatter::DIGIT_SYMBOL,
  100.         'minus_sign' => \NumberFormatter::MINUS_SIGN_SYMBOL,
  101.         'plus_sign' => \NumberFormatter::PLUS_SIGN_SYMBOL,
  102.         'currency' => \NumberFormatter::CURRENCY_SYMBOL,
  103.         'intl_currency' => \NumberFormatter::INTL_CURRENCY_SYMBOL,
  104.         'monetary_separator' => \NumberFormatter::MONETARY_SEPARATOR_SYMBOL,
  105.         'exponential' => \NumberFormatter::EXPONENTIAL_SYMBOL,
  106.         'permill' => \NumberFormatter::PERMILL_SYMBOL,
  107.         'pad_escape' => \NumberFormatter::PAD_ESCAPE_SYMBOL,
  108.         'infinity' => \NumberFormatter::INFINITY_SYMBOL,
  109.         'nan' => \NumberFormatter::NAN_SYMBOL,
  110.         'significant_digit' => \NumberFormatter::SIGNIFICANT_DIGIT_SYMBOL,
  111.         'monetary_grouping_separator' => \NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL,
  112.     ];
  113.     private $dateFormatters = [];
  114.     private $numberFormatters = [];
  115.     private $dateFormatterPrototype;
  116.     private $numberFormatterPrototype;
  117.     public function __construct(\IntlDateFormatter $dateFormatterPrototype null\NumberFormatter $numberFormatterPrototype null)
  118.     {
  119.         $this->dateFormatterPrototype $dateFormatterPrototype;
  120.         $this->numberFormatterPrototype $numberFormatterPrototype;
  121.     }
  122.     public function getFilters()
  123.     {
  124.         return [
  125.             // internationalized names
  126.             new TwigFilter('country_name', [$this'getCountryName']),
  127.             new TwigFilter('currency_name', [$this'getCurrencyName']),
  128.             new TwigFilter('currency_symbol', [$this'getCurrencySymbol']),
  129.             new TwigFilter('language_name', [$this'getLanguageName']),
  130.             new TwigFilter('locale_name', [$this'getLocaleName']),
  131.             new TwigFilter('timezone_name', [$this'getTimezoneName']),
  132.             // localized formatters
  133.             new TwigFilter('format_currency', [$this'formatCurrency']),
  134.             new TwigFilter('format_number', [$this'formatNumber']),
  135.             new TwigFilter('format_*_number', [$this'formatNumberStyle']),
  136.             new TwigFilter('format_datetime', [$this'formatDateTime'], ['needs_environment' => true]),
  137.             new TwigFilter('format_date', [$this'formatDate'], ['needs_environment' => true]),
  138.             new TwigFilter('format_time', [$this'formatTime'], ['needs_environment' => true]),
  139.         ];
  140.     }
  141.     public function getFunctions()
  142.     {
  143.         return [
  144.             // internationalized names
  145.             new TwigFunction('country_timezones', [$this'getCountryTimezones']),
  146.         ];
  147.     }
  148.     public function getCountryName(?string $countrystring $locale null): string
  149.     {
  150.         if (null === $country) {
  151.             return '';
  152.         }
  153.         try {
  154.             return Countries::getName($country$locale);
  155.         } catch (MissingResourceException $exception) {
  156.             return $country;
  157.         }
  158.     }
  159.     public function getCurrencyName(?string $currencystring $locale null): string
  160.     {
  161.         if (null === $currency) {
  162.             return '';
  163.         }
  164.         try {
  165.             return Currencies::getName($currency$locale);
  166.         } catch (MissingResourceException $exception) {
  167.             return $currency;
  168.         }
  169.     }
  170.     public function getCurrencySymbol(?string $currencystring $locale null): string
  171.     {
  172.         if (null === $currency) {
  173.             return '';
  174.         }
  175.         try {
  176.             return Currencies::getSymbol($currency$locale);
  177.         } catch (MissingResourceException $exception) {
  178.             return $currency;
  179.         }
  180.     }
  181.     public function getLanguageName(?string $languagestring $locale null): string
  182.     {
  183.         if (null === $language) {
  184.             return '';
  185.         }
  186.         try {
  187.             return Languages::getName($language$locale);
  188.         } catch (MissingResourceException $exception) {
  189.             return $language;
  190.         }
  191.     }
  192.     public function getLocaleName(?string $datastring $locale null): string
  193.     {
  194.         if (null === $data) {
  195.             return '';
  196.         }
  197.         try {
  198.             return Locales::getName($data$locale);
  199.         } catch (MissingResourceException $exception) {
  200.             return $data;
  201.         }
  202.     }
  203.     public function getTimezoneName(?string $timezonestring $locale null): string
  204.     {
  205.         if (null === $timezone) {
  206.             return '';
  207.         }
  208.         try {
  209.             return Timezones::getName($timezone$locale);
  210.         } catch (MissingResourceException $exception) {
  211.             return $timezone;
  212.         }
  213.     }
  214.     public function getCountryTimezones(string $country): array
  215.     {
  216.         try {
  217.             return Timezones::forCountryCode($country);
  218.         } catch (MissingResourceException $exception) {
  219.             return [];
  220.         }
  221.     }
  222.     public function formatCurrency($amountstring $currency, array $attrs = [], string $locale null): string
  223.     {
  224.         $formatter $this->createNumberFormatter($locale'currency'$attrs);
  225.         if (false === $ret $formatter->formatCurrency($amount$currency)) {
  226.             throw new RuntimeError('Unable to format the given number as a currency.');
  227.         }
  228.         return $ret;
  229.     }
  230.     public function formatNumber($number, array $attrs = [], string $style 'decimal'string $type 'default'string $locale null): string
  231.     {
  232.         if (!isset(self::NUMBER_TYPES[$type])) {
  233.             throw new RuntimeError(sprintf('The type "%s" does not exist, known types are: "%s".'$typeimplode('", "'array_keys(self::NUMBER_TYPES))));
  234.         }
  235.         $formatter $this->createNumberFormatter($locale$style$attrs);
  236.         if (false === $ret $formatter->format($numberself::NUMBER_TYPES[$type])) {
  237.             throw new RuntimeError('Unable to format the given number.');
  238.         }
  239.         return $ret;
  240.     }
  241.     public function formatNumberStyle(string $style$number, array $attrs = [], string $type 'default'string $locale null): string
  242.     {
  243.         return $this->formatNumber($number$attrs$style$type$locale);
  244.     }
  245.     /**
  246.      * @param \DateTimeInterface|string|null  $date     A date or null to use the current time
  247.      * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  248.      */
  249.     public function formatDateTime(Environment $env$date, ?string $dateFormat 'medium', ?string $timeFormat 'medium'string $pattern ''$timezone nullstring $calendar 'gregorian'string $locale null): string
  250.     {
  251.         $date \twig_date_converter($env$date$timezone);
  252.         $formatter $this->createDateFormatter($locale$dateFormat$timeFormat$pattern$date->getTimezone(), $calendar);
  253.         if (false === $ret $formatter->format($date)) {
  254.             throw new RuntimeError('Unable to format the given date.');
  255.         }
  256.         return $ret;
  257.     }
  258.     /**
  259.      * @param \DateTimeInterface|string|null  $date     A date or null to use the current time
  260.      * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  261.      */
  262.     public function formatDate(Environment $env$date, ?string $dateFormat 'medium'string $pattern ''$timezone nullstring $calendar 'gregorian'string $locale null): string
  263.     {
  264.         return $this->formatDateTime($env$date$dateFormat'none'$pattern$timezone$calendar$locale);
  265.     }
  266.     /**
  267.      * @param \DateTimeInterface|string|null  $date     A date or null to use the current time
  268.      * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  269.      */
  270.     public function formatTime(Environment $env$date, ?string $timeFormat 'medium'string $pattern ''$timezone nullstring $calendar 'gregorian'string $locale null): string
  271.     {
  272.         return $this->formatDateTime($env$date'none'$timeFormat$pattern$timezone$calendar$locale);
  273.     }
  274.     private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormatstring $pattern\DateTimeZone $timezonestring $calendar): \IntlDateFormatter
  275.     {
  276.         if (null !== $dateFormat && !isset(self::DATE_FORMATS[$dateFormat])) {
  277.             throw new RuntimeError(sprintf('The date format "%s" does not exist, known formats are: "%s".'$dateFormatimplode('", "'array_keys(self::DATE_FORMATS))));
  278.         }
  279.         if (null !== $timeFormat && !isset(self::DATE_FORMATS[$timeFormat])) {
  280.             throw new RuntimeError(sprintf('The time format "%s" does not exist, known formats are: "%s".'$timeFormatimplode('", "'array_keys(self::DATE_FORMATS))));
  281.         }
  282.         if (null === $locale) {
  283.             $locale \Locale::getDefault();
  284.         }
  285.         $calendar 'gregorian' === $calendar \IntlDateFormatter::GREGORIAN \IntlDateFormatter::TRADITIONAL;
  286.         $dateFormatValue self::DATE_FORMATS[$dateFormat] ?? null;
  287.         $timeFormatValue self::DATE_FORMATS[$timeFormat] ?? null;
  288.         if ($this->dateFormatterPrototype) {
  289.             $dateFormatValue $dateFormatValue ?: $this->dateFormatterPrototype->getDateType();
  290.             $timeFormatValue $timeFormatValue ?: $this->dateFormatterPrototype->getTimeType();
  291.             $timezone $timezone ?: $this->dateFormatterPrototype->getTimeType();
  292.             $calendar $calendar ?: $this->dateFormatterPrototype->getCalendar();
  293.             $pattern $pattern ?: $this->dateFormatterPrototype->getPattern();
  294.         }
  295.         $hash $locale.'|'.$dateFormatValue.'|'.$timeFormatValue.'|'.$timezone->getName().'|'.$calendar.'|'.$pattern;
  296.         if (!isset($this->dateFormatters[$hash])) {
  297.             $this->dateFormatters[$hash] = new \IntlDateFormatter($locale$dateFormatValue$timeFormatValue$timezone$calendar$pattern);
  298.         }
  299.         return $this->dateFormatters[$hash];
  300.     }
  301.     private function createNumberFormatter(?string $localestring $style, array $attrs = []): \NumberFormatter
  302.     {
  303.         if (!isset(self::NUMBER_STYLES[$style])) {
  304.             throw new RuntimeError(sprintf('The style "%s" does not exist, known styles are: "%s".'$styleimplode('", "'array_keys(self::NUMBER_STYLES))));
  305.         }
  306.         if (null === $locale) {
  307.             $locale \Locale::getDefault();
  308.         }
  309.         // textAttrs and symbols can only be set on the prototype as there is probably no
  310.         // use case for setting it on each call.
  311.         $textAttrs = [];
  312.         $symbols = [];
  313.         if ($this->numberFormatterPrototype) {
  314.             foreach (self::NUMBER_ATTRIBUTES as $name => $const) {
  315.                 if (!isset($attrs[$name])) {
  316.                     $value $this->numberFormatterPrototype->getAttribute($const);
  317.                     if ('rounding_mode' === $name) {
  318.                         $value array_flip(self::NUMBER_ROUNDING_ATTRIBUTES)[$value];
  319.                     } elseif ('padding_position' === $name) {
  320.                         $value array_flip(self::NUMBER_PADDING_ATTRIBUTES)[$value];
  321.                     }
  322.                     $attrs[$name] = $value;
  323.                 }
  324.             }
  325.             foreach (self::NUMBER_TEXT_ATTRIBUTES as $name => $const) {
  326.                 $textAttrs[$name] = $this->numberFormatterPrototype->getTextAttribute($const);
  327.             }
  328.             foreach (self::NUMBER_SYMBOLS as $name => $const) {
  329.                 $symbols[$name] = $this->numberFormatterPrototype->getSymbol($const);
  330.             }
  331.         }
  332.         ksort($attrs);
  333.         $hash $locale.'|'.$style.'|'.json_encode($attrs).'|'.json_encode($textAttrs).'|'.json_encode($symbols);
  334.         if (!isset($this->numberFormatters[$hash])) {
  335.             $this->numberFormatters[$hash] = new \NumberFormatter($localeself::NUMBER_STYLES[$style]);
  336.         }
  337.         foreach ($attrs as $name => $value) {
  338.             if (!isset(self::NUMBER_ATTRIBUTES[$name])) {
  339.                 throw new RuntimeError(sprintf('The number formatter attribute "%s" does not exist, known attributes are: "%s".'$nameimplode('", "'array_keys(self::NUMBER_ATTRIBUTES))));
  340.             }
  341.             if ('rounding_mode' === $name) {
  342.                 if (!isset(self::NUMBER_ROUNDING_ATTRIBUTES[$value])) {
  343.                     throw new RuntimeError(sprintf('The number formatter rounding mode "%s" does not exist, known modes are: "%s".'$valueimplode('", "'array_keys(self::NUMBER_ROUNDING_ATTRIBUTES))));
  344.                 }
  345.                 $value self::NUMBER_ROUNDING_ATTRIBUTES[$value];
  346.             } elseif ('padding_position' === $name) {
  347.                 if (!isset(self::NUMBER_PADDING_ATTRIBUTES[$value])) {
  348.                     throw new RuntimeError(sprintf('The number formatter padding position "%s" does not exist, known positions are: "%s".'$valueimplode('", "'array_keys(self::NUMBER_PADDING_ATTRIBUTES))));
  349.                 }
  350.                 $value self::NUMBER_PADDING_ATTRIBUTES[$value];
  351.             }
  352.             $this->numberFormatters[$hash]->setAttribute(self::NUMBER_ATTRIBUTES[$name], $value);
  353.         }
  354.         foreach ($textAttrs as $name => $value) {
  355.             $this->numberFormatters[$hash]->setTextAttribute(self::NUMBER_TEXT_ATTRIBUTES[$name], $value);
  356.         }
  357.         foreach ($symbols as $name => $value) {
  358.             $this->numberFormatters[$hash]->setSymbol(self::NUMBER_SYMBOLS[$name], $value);
  359.         }
  360.         return $this->numberFormatters[$hash];
  361.     }
  362. }