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

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