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

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