From afafcc64462a0b8c7863e0ecb1662ee372032a3e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 7 Dec 2023 19:13:09 +0100 Subject: [PATCH] locale --- src/Latte/Engine.php | 7 ++- src/Latte/Essential/CoreExtension.php | 2 + src/Latte/Essential/Filters.php | 85 ++++++++++++++++++++++----- src/Latte/Locale.php | 54 +++++++++++++++++ 4 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 src/Latte/Locale.php diff --git a/src/Latte/Engine.php b/src/Latte/Engine.php index 9fc8510df..46aeaf4d9 100644 --- a/src/Latte/Engine.php +++ b/src/Latte/Engine.php @@ -49,7 +49,7 @@ class Engine private ?Policy $policy = null; private bool $sandboxed = false; private ?string $phpBinary = null; - private ?string $locale = null; + private ?Locale $locale = null; public function __construct() @@ -552,12 +552,13 @@ public function isStrictParsing(): bool public function setLocale(?string $locale): static { - $this->locale = $locale; + $this->locale = $locale ? new Locale($locale) : null; return $this; } - public function getLocale(): ?string + /** @internal */ + public function getLocale(): ?Locale { return $this->locale; } diff --git a/src/Latte/Essential/CoreExtension.php b/src/Latte/Essential/CoreExtension.php index 1c45cfd5d..7547926b4 100644 --- a/src/Latte/Essential/CoreExtension.php +++ b/src/Latte/Essential/CoreExtension.php @@ -151,6 +151,8 @@ public function getFilters(): array ? [$this->filters, 'lower'] : fn() => throw new RuntimeException('Filter |lower requires mbstring extension.'), 'number' => [$this->filters, 'number'], + 'numberInWords' => [$this->filters, 'numberInWords'], + 'orderNumber' => [$this->filters, 'orderNumber'], 'padLeft' => [$this->filters, 'padLeft'], 'padRight' => [$this->filters, 'padRight'], 'query' => [$this->filters, 'query'], diff --git a/src/Latte/Essential/Filters.php b/src/Latte/Essential/Filters.php index a9259c989..9b7688db3 100644 --- a/src/Latte/Essential/Filters.php +++ b/src/Latte/Essential/Filters.php @@ -204,10 +204,11 @@ public static function date(string|int|\DateTimeInterface|\DateInterval|null $ti * Local date/time formatting. * https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax */ - public static function localDate(string|int|\DateTimeInterface|null $time, string $format = 'date'): ?string + public function localDate(string|int|\DateTimeInterface|null $time, string $format = 'date'): ?string { - if (!class_exists(\IntlDateFormatter::class)) { - throw new Latte\RuntimeException("Filters |localDate and |localTime requires 'intl' extension."); + $locale = $this->engine->getLocale(); + if (!$locale) { + throw new Latte\RuntimeException('Filters |localDate and |localTime require the locale to be set using Engine::setLocale()'); } elseif ($time == null) { // intentionally == return null; } elseif (is_numeric($time)) { @@ -229,6 +230,7 @@ public static function localDate(string|int|\DateTimeInterface|null $time, strin 'full' => [\IntlDateFormatter::FULL, \IntlDateFormatter::NONE], default => $format, }; + $res = $locale->getDateFormatter()->format($time); $res = \IntlDateFormatter::formatObject($time, $format); $res = preg_replace('~(\d\.) ~', "\$1\u{a0}", $res); return $res; @@ -236,20 +238,70 @@ public static function localDate(string|int|\DateTimeInterface|null $time, strin /** - * Formats a number with grouped thousands and optionally decimal digits. + * Formats a number with grouped thousands and optionally decimal digits according to locale. */ public function number( - float $num, + float $number, int $decimals = 0, - ?string $decimalSeparator = '.', - ?string $thousandsSeparator = ',', + string $decimalSeparator = '.', + string $thousandsSeparator = ',', ): string { if ($locale = $this->engine->getLocale()) { + $formatter = clone $locale->getNumberFormatter(); + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $decimals); + return $formatter->format($number); + } + + return number_format($number, $decimals, $decimalSeparator, $thousandsSeparator); + } + + /** + * Prints the order number according to locale. + */ + public function orderNumber(float $number): string + { + $locale = $this->engine->getLocale(); + if (!$locale) { + throw new Latte\RuntimeException('Filter |orderNumber requires the locale to be set using Engine::setLocale()'); } - return number_format($num, $decimals, $decimalSeparator, $thousandsSeparator); + $formatter = new \NumberFormatter($locale->getNumberFormatter()->getLocale(), \NumberFormatter::ORDINAL); + return $formatter->format($number); + } + + + /** + * Prints the number in words according to locale. + */ + public function numberInWords(float $number): string + { + $locale = $this->engine->getLocale(); + if (!$locale) { + throw new Latte\RuntimeException('Filter |numberInWords requires the locale to be set using Engine::setLocale()'); + } + + $formatter = new \NumberFormatter($locale->getNumberFormatter()->getLocale(), \NumberFormatter::SPELLOUT); + return $formatter->format($number); + } + + + /** + * Format the currency value according to locale. + */ + public function money( + float $amount, + ?string $currency = null, + ): string + { + $locale = $this->engine->getLocale(); + if (!$locale) { + throw new Latte\RuntimeException('Filter |money requires the locale to be set using Engine::setLocale()'); + } + return $currency === null + ? $locale->getMoneyFormatter()->format($amount, \NumberFormatter::TYPE_DEFAULT) + : $locale->getMoneyFormatter()->formatCurrency($amount, $currency); } @@ -510,22 +562,23 @@ public static function batch(iterable $list, int $length, $rest = null): \Genera /** * Sorts elements using the comparison function and preserves the key association. */ - public static function sort( + public function sort( iterable $iterable, ?\Closure $comparison = null, bool $byKey = false, string|int|\Closure|null $by = null, ): iterable { - if (!class_exists(\Collator::class)) { - throw new Latte\RuntimeException("Sort filter requires the 'intl' extension to be installed."); + if ($comparison) { + } elseif ($locale = $this->engine->getLocale()) { + $collator = $locale->getCollator(); + $comparison = fn($a, $b) => is_string($a) && is_string($b) + ? $collator->compare($a, $b) + : $a <=> $b; + } else { + $comparison = fn($a, $b) => $a <=> $b; } - $collator = new \Collator($locale); - $comparison ??= fn($a, $b) => is_string($a) && is_string($b) - ? $collator->compare($a, $b) - : $a <=> $b; - $comparison = match (true) { $by === null => $comparison, $by instanceof \Closure => fn($a, $b) => $comparison($by($a), $by($b)), diff --git a/src/Latte/Locale.php b/src/Latte/Locale.php new file mode 100644 index 000000000..b1f280e3b --- /dev/null +++ b/src/Latte/Locale.php @@ -0,0 +1,54 @@ +setTextAttribute(NumberFormatter::CURRENCY_CODE, 'EUR'); + + private \Collator $collator; + private \NumberFormatter $numberFormatter; + private \NumberFormatter $moneyFormatter; + private \IntlDateFormatter $dateFormatter; + + + public function __construct( + private string $locale, + ) { + if (!extension_loaded('intl')) { + throw new RuntimeException("Locate requires the 'intl' extension to be installed."); + } + } + + + public function getCollator(): \Collator + { + return $this->collator ??= new \Collator($this->locale); + } + + + public function getNumberFormatter(): \NumberFormatter + { + return $this->numberFormatter ??= new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL); + } + + + public function getMoneyFormatter(): \NumberFormatter + { + return $this->moneyFormatter ??= new \NumberFormatter($this->locale, \NumberFormatter::CURRENCY); + } + + + public function getDateFormatter(): \IntlDateFormatter + { + return $this->dateFormatter ??= new \IntlDateFormatter($this->locale, \IntlDateFormatter::FULL, \IntlDateFormatter::FULL); + } +}