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

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