diff --git a/bin/release b/bin/release index 0f45b18914..10ebea1182 100755 --- a/bin/release +++ b/bin/release @@ -18,6 +18,7 @@ use Composer\Semver\VersionParser; use Tempest\Console\Console; use Tempest\Console\ConsoleApplication; use Tempest\Console\Exceptions\InterruptException; + use function Tempest\get; use function Tempest\Support\arr; use function Tempest\Support\str; @@ -282,10 +283,10 @@ function ensureTagDoesNotExist(string $version): void } /* -|-------------------------------------------------------------------------- -| Script starts here. -|-------------------------------------------------------------------------- -*/ + * |-------------------------------------------------------------------------- + * | Script starts here. + * |-------------------------------------------------------------------------- + */ try { ConsoleApplication::boot(); @@ -313,7 +314,7 @@ try { if (! $console->confirm("The next tag will be {$tag}. Release?")) { $console->error('Cancelled.'); - exit; + exit(); } // Bump PHP packages @@ -364,6 +365,6 @@ try { $tag, )); - exit; + exit(); } catch (InterruptException) { } diff --git a/composer.json b/composer.json index 2652d92b72..0cb78e8335 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "symfony/uid": "^7.1", "symfony/var-dumper": "^7.1", "symfony/var-exporter": "^7.1", + "symfony/yaml": "^7.3", "tempest/highlight": "^2.11.4", "vlucas/phpdotenv": "^5.6", "voku/portable-ascii": "^2.0.3" @@ -82,6 +83,7 @@ "tempest/generation": "self.version", "tempest/http": "self.version", "tempest/http-client": "self.version", + "tempest/intl": "self.version", "tempest/log": "self.version", "tempest/mapper": "self.version", "tempest/reflection": "self.version", @@ -116,6 +118,7 @@ "Tempest\\Generation\\": "packages/generation/src", "Tempest\\HttpClient\\": "packages/http-client/src", "Tempest\\Http\\": "packages/http/src", + "Tempest\\Intl\\": "packages/intl/src", "Tempest\\Log\\": "packages/log/src", "Tempest\\Mapper\\": "packages/mapper/src", "Tempest\\Reflection\\": "packages/reflection/src", @@ -136,6 +139,8 @@ "packages/datetime/src/functions.php", "packages/debug/src/functions.php", "packages/event-bus/src/functions.php", + "packages/intl/src/Number/functions.php", + "packages/intl/src/functions.php", "packages/mapper/src/functions.php", "packages/reflection/src/functions.php", "packages/router/src/functions.php", @@ -144,11 +149,9 @@ "packages/support/src/Filesystem/functions.php", "packages/support/src/Html/functions.php", "packages/support/src/Json/functions.php", - "packages/support/src/Language/functions.php", "packages/support/src/Math/constants.php", "packages/support/src/Math/functions.php", "packages/support/src/Namespace/functions.php", - "packages/support/src/Number/functions.php", "packages/support/src/Path/functions.php", "packages/support/src/Random/functions.php", "packages/support/src/Regex/functions.php", @@ -173,6 +176,7 @@ "Tempest\\Generation\\Tests\\": "packages/generation/tests", "Tempest\\HttpClient\\Tests\\": "packages/http-client/tests", "Tempest\\Http\\Tests\\": "packages/http/tests", + "Tempest\\Intl\\Tests\\": "packages/intl/tests", "Tempest\\Log\\Tests\\": "packages/log/tests", "Tempest\\Mapper\\Tests\\": "packages/mapper/tests", "Tempest\\Reflection\\Tests\\": "packages/reflection/tests", @@ -201,6 +205,7 @@ "phpstan": "vendor/bin/phpstan analyse src tests --memory-limit=1G", "rector": "vendor/bin/rector process --no-ansi", "merge": "php -d\"error_reporting = E_ALL & ~E_DEPRECATED\" vendor/bin/monorepo-builder merge", + "intl:plural": "./packages/intl/bin/plural-rules.php", "release": [ "composer qa", "./bin/release" diff --git a/packages/database/composer.json b/packages/database/composer.json index 76d73d0e32..3fe7e0ba3f 100644 --- a/packages/database/composer.json +++ b/packages/database/composer.json @@ -9,6 +9,7 @@ "tempest/container": "dev-main", "tempest/event-bus": "dev-main", "tempest/mapper": "dev-main", + "tempest/intl": "dev-main", "tempest/support": "dev-main" }, "autoload": { diff --git a/packages/database/tests/Tables/PluralizedSnakeCaseStrategyTest.php b/packages/database/tests/Tables/PluralizedSnakeCaseStrategyTest.php index ff927664eb..e2f460d4f4 100644 --- a/packages/database/tests/Tables/PluralizedSnakeCaseStrategyTest.php +++ b/packages/database/tests/Tables/PluralizedSnakeCaseStrategyTest.php @@ -9,7 +9,7 @@ use Tempest\Container\GenericContainer; use Tempest\Database\Migrations\Migration; use Tempest\Database\Tables\PluralizedSnakeCaseStrategy; -use Tempest\Support\Pluralizer\PluralizerInitializer; +use Tempest\Intl\Pluralizer\PluralizerInitializer; /** * @internal diff --git a/packages/datetime/composer.json b/packages/datetime/composer.json index 2b8bdacf04..56661c1111 100644 --- a/packages/datetime/composer.json +++ b/packages/datetime/composer.json @@ -5,6 +5,7 @@ "minimum-stability": "dev", "require": { "php": "^8.4", + "tempest/intl": "dev-main", "tempest/support": "dev-main" }, "autoload": { diff --git a/packages/datetime/src/DateTime.php b/packages/datetime/src/DateTime.php index 893ad102f0..5d9eda1407 100644 --- a/packages/datetime/src/DateTime.php +++ b/packages/datetime/src/DateTime.php @@ -6,7 +6,7 @@ use DateTimeInterface as NativeDateTimeInterface; use IntlCalendar; -use Tempest\Support\Language\Locale; +use Tempest\Intl\Locale; /** * Represents a date and time in a specific timezone. diff --git a/packages/datetime/src/DateTimeConvenienceMethods.php b/packages/datetime/src/DateTimeConvenienceMethods.php index 2333f03fb5..1c82e76da8 100644 --- a/packages/datetime/src/DateTimeConvenienceMethods.php +++ b/packages/datetime/src/DateTimeConvenienceMethods.php @@ -4,7 +4,7 @@ namespace Tempest\DateTime; -use Tempest\Support\Language\Locale; +use Tempest\Intl\Locale; use Tempest\Support\Math; /** diff --git a/packages/datetime/src/DateTimeInterface.php b/packages/datetime/src/DateTimeInterface.php index e2e659f4d7..06d14efc4a 100644 --- a/packages/datetime/src/DateTimeInterface.php +++ b/packages/datetime/src/DateTimeInterface.php @@ -4,7 +4,7 @@ namespace Tempest\DateTime; -use Tempest\Support\Language\Locale; +use Tempest\Intl\Locale; interface DateTimeInterface extends TemporalInterface { diff --git a/packages/datetime/src/TemporalConvenienceMethods.php b/packages/datetime/src/TemporalConvenienceMethods.php index fc37c81106..e9a22a1549 100644 --- a/packages/datetime/src/TemporalConvenienceMethods.php +++ b/packages/datetime/src/TemporalConvenienceMethods.php @@ -6,9 +6,9 @@ use DateTimeImmutable as NativeDateTimeImmutable; use DateTimeInterface as NativeDateTimeInterface; +use Tempest\Intl\Locale; use Tempest\Support\Comparison; use Tempest\Support\Comparison\Order; -use Tempest\Support\Language\Locale; /** * @require-implements TemporalInterface diff --git a/packages/datetime/src/TemporalInterface.php b/packages/datetime/src/TemporalInterface.php index 82738338d9..c0452b2584 100644 --- a/packages/datetime/src/TemporalInterface.php +++ b/packages/datetime/src/TemporalInterface.php @@ -7,10 +7,10 @@ use DateTimeInterface as NativeDateTimeInterface; use JsonSerializable; use Stringable; +use Tempest\Intl\Locale; use Tempest\Support\Comparison\Comparable; use Tempest\Support\Comparison\Equable; use Tempest\Support\Comparison\Order; -use Tempest\Support\Language\Locale; /** * Represents a temporal object that can be manipulated and compared. diff --git a/packages/datetime/src/Timestamp.php b/packages/datetime/src/Timestamp.php index 759a11474a..a7ac0bb198 100644 --- a/packages/datetime/src/Timestamp.php +++ b/packages/datetime/src/Timestamp.php @@ -6,7 +6,7 @@ use Tempest\Clock\Clock; use Tempest\Container\GenericContainer; -use Tempest\Support\Language\Locale; +use Tempest\Intl\Locale; use Tempest\Support\Math; use Tempest\Support\Math\Exception\ArithmeticException; use Tempest\Support\Math\Exception\DivisionByZeroException; diff --git a/packages/datetime/src/functions.php b/packages/datetime/src/functions.php index 87dc40c0ef..2ba8e1344c 100644 --- a/packages/datetime/src/functions.php +++ b/packages/datetime/src/functions.php @@ -12,7 +12,7 @@ use Tempest\DateTime\Timestamp; use Tempest\DateTime\TimeStyle; use Tempest\DateTime\Timezone; - use Tempest\Support\Language\Locale; + use Tempest\Intl\Locale; use function hrtime; use function microtime; diff --git a/packages/datetime/tests/DateTimeTest.php b/packages/datetime/tests/DateTimeTest.php index 19c934a7b5..9bf1d8a597 100644 --- a/packages/datetime/tests/DateTimeTest.php +++ b/packages/datetime/tests/DateTimeTest.php @@ -18,7 +18,7 @@ use Tempest\DateTime\TimeStyle; use Tempest\DateTime\Timezone; use Tempest\DateTime\Weekday; -use Tempest\Support\Language\Locale; +use Tempest\Intl\Locale; use function Tempest\DateTime\create_intl_date_formatter; use function time; diff --git a/packages/datetime/tests/TimestampTest.php b/packages/datetime/tests/TimestampTest.php index 5ee222bcb3..37e326ab19 100644 --- a/packages/datetime/tests/TimestampTest.php +++ b/packages/datetime/tests/TimestampTest.php @@ -16,8 +16,8 @@ use Tempest\DateTime\SecondsStyle; use Tempest\DateTime\Timestamp; use Tempest\DateTime\Timezone; +use Tempest\Intl\Locale; use Tempest\Support\Comparison\Order; -use Tempest\Support\Language\Locale; use Tempest\Support\Math; use function Tempest\DateTime\create_intl_date_formatter; diff --git a/packages/intl/.gitattributes b/packages/intl/.gitattributes new file mode 100644 index 0000000000..3f7775660b --- /dev/null +++ b/packages/intl/.gitattributes @@ -0,0 +1,14 @@ +# Exclude build/test files from the release +.github/ export-ignore +tests/ export-ignore +.gitattributes export-ignore +.gitignore export-ignore +phpunit.xml export-ignore +README.md export-ignore + +# Configure diff output +*.view.php diff=html +*.php diff=php +*.css diff=css +*.html diff=html +*.md diff=markdown diff --git a/packages/intl/LICENCE.md b/packages/intl/LICENCE.md new file mode 100644 index 0000000000..e403836c23 --- /dev/null +++ b/packages/intl/LICENCE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2024 Brent Roose brendt@stitcher.io + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/intl/bin/plural-rules.php b/packages/intl/bin/plural-rules.php new file mode 100755 index 0000000000..f413c49043 --- /dev/null +++ b/packages/intl/bin/plural-rules.php @@ -0,0 +1,374 @@ +#!/usr/bin/env php +className}\n{\n"; + $output .= $this->generateHelperMethods(); + + $pluralRules = $this->data['supplemental']['plurals-type-cardinal'] ?? []; + foreach ($pluralRules as $locale => $rules) { + $output .= $this->generateLanguageMethod($locale, $rules); + } + + $output .= $this->generateDispatcherMethod(array_keys($pluralRules)); + $output .= "}\n"; + + return $output; + } + + private function generateHelperMethods(): string + { + return <<<'PHP' + /** + * Extracts the integer part of a number. + */ + private static function getIntegerPart(float|int $n): int + { + return (int) abs($n); + } + + /** + * Counts visible fractional digits. + */ + private static function getVisibleFractionalDigits(float|int $n): int + { + $str = (string) $n; + + if (!str_contains($str, '.')) { + return 0; + } + + return strlen(rtrim(explode('.', $str)[1], '0')); + } + + /** + * Gets fractional digits as integer. + */ + private static function getFractionalDigits(float|int $n): int + { + $str = (string) $n; + + if (!str_contains($str, '.')) { + return 0; + } + + return (int) rtrim(explode('.', $str)[1], '0') ?: 0; + } + + /** + * Gets compact decimal exponent (magnitude). + */ + private static function getCompactExponent(float|int $n): int + { + if ($n === 0 || $n === 0.0) { + return 0; + } + + $abs = abs($n); + + if ($abs >= 1000000) { + return 6; + } + + if ($abs >= 1000) { + return 3; + } + + return 0; + } + + /** + * Gets the exponent for scientific notation. + */ + private static function getExponent(float|int $n): int + { + if ($n === 0 || $n === 0.0) { + return 0; + } + + return (int) floor(log10(abs($n))); + } + + /** + * Checks if number is in range. + */ + private static function inRange(int|float $value, int|float $start, int|float $end): bool + { + return $value >= $start && $value <= $end; + } + + /** + * Checks if number matches any value in comma-separated list. + */ + private static function matchesValues(int|float $value, string $values): bool + { + $parts = explode(',', $values); + + foreach ($parts as $part) { + $part = trim($part); + + if (str_contains($part, '~')) { + [$start, $end] = explode('~', $part); + + if (self::inRange($value, (float) trim($start), (float) trim($end))) { + return true; + } + } elseif (str_contains($part, '..')) { + [$start, $end] = explode('..', $part); + + if (self::inRange($value, (float) trim($start), (float) trim($end))) { + return true; + } + } elseif ((float) $part === (float) $value) { + return true; + } + } + return false; + } + + PHP; + } + + private function generateLanguageMethod(string $locale, array $rules): string + { + $methodName = 'getPluralCategory' . ucfirst(str_replace('-', '_', $locale)); + + $output = " /**\n"; + $output .= " * Gets the plural category for the {$locale} locale.\n"; + $output .= " */\n"; + $output .= " private static function {$methodName}(float|int \$n): string\n"; + $output .= " {\n"; + $output .= " \$i = self::getIntegerPart(\$n);\n"; + $output .= " \$v = self::getVisibleFractionalDigits(\$n);\n"; + $output .= " \$f = self::getFractionalDigits(\$n);\n"; + $output .= " \$t = self::getCompactExponent(\$n);\n"; + $output .= " \$e = self::getExponent(\$n);\n\n"; + + $priority = ['zero', 'one', 'two', 'few', 'many', 'other']; + $sortedRules = []; + + foreach ($priority as $category) { + $ruleKey = "pluralRule-count-{$category}"; + + if (isset($rules[$ruleKey])) { + $sortedRules[$category] = $rules[$ruleKey]; + } + } + + foreach ($sortedRules as $category => $rule) { + if ($category === 'other') { + $output .= " return '{$category}';\n"; + break; + } + + if ($condition = $this->parseRule($rule)) { + $output .= " if ({$condition}) {\n"; + $output .= " return '{$category}';\n"; + $output .= " }\n\n"; + } + } + + $output .= " }\n\n"; + + return $output; + } + + private function parseRule(string $rule): string + { + $rulePart = trim(explode('@', $rule)[0]); + + if (! $rulePart) { + return ''; + } + + return $this->parseCondition($rulePart); + } + + private function parseCondition(string $condition): string + { + $condition = trim($condition); + + if (str_contains($condition, ' or ')) { + $orParts = explode(' or ', $condition); + $parsedParts = array_map([$this, 'parseCondition'], $orParts); + return '(' . implode(') || (', $parsedParts) . ')'; + } + + if (str_contains($condition, ' and ')) { + $andParts = explode(' and ', $condition); + $parsedParts = array_map([$this, 'parseCondition'], $andParts); + return '(' . implode(') && (', $parsedParts) . ')'; + } + + return $this->parseSingleCondition($condition); + } + + private function parseSingleCondition(string $condition): string + { + $condition = trim($condition); + + if (preg_match('/^([nifvet])\s*%\s*(\d+)\s*(=|!=)\s*(.+)$/', $condition, $matches)) { + $var = $this->getVariable($matches[1]); + $mod = $matches[2]; + $op = $matches[3] === '=' ? '===' : '!=='; + $values = $matches[4]; + + return $this->parseValueCondition("({$var} % {$mod})", $op, $values); + } + + if (preg_match('/^([nifvet])\s*(=|!=)\s*(.+)$/', $condition, $matches)) { + $var = $this->getVariable($matches[1]); + $op = $matches[2] === '=' ? '===' : '!=='; + $values = $matches[3]; + + return $this->parseValueCondition($var, $op, $values); + } + + return $condition; + } + + private function parseValueCondition(string $varExpression, string $operator, string $values): string + { + $values = trim($values); + $isNegative = $operator === '!=='; + + if (preg_match('/^\d+(?:\.\d+)?$/', $values)) { + return "{$varExpression} {$operator} {$values}"; + } + + if (preg_match('/^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/', $values, $matches)) { + $start = $matches[1]; + $end = $matches[2]; + $condition = "self::inRange({$varExpression}, {$start}, {$end})"; + return $isNegative ? "!{$condition}" : $condition; + } + + if (str_contains($values, ',')) { + $parts = array_map('trim', explode(',', $values)); + $conditions = []; + + foreach ($parts as $part) { + if (str_contains($part, '..')) { + if (preg_match('/^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/', $part, $matches)) { + $start = $matches[1]; + $end = $matches[2]; + $conditions[] = "self::inRange({$varExpression}, {$start}, {$end})"; + } + } elseif (str_contains($part, '~')) { + if (preg_match('/^(\d+(?:\.\d+)?)~(\d+(?:\.\d+)?)$/', $part, $matches)) { + $start = $matches[1]; + $end = $matches[2]; + $conditions[] = "self::inRange({$varExpression}, {$start}, {$end})"; + } + } else { + $conditions[] = "{$varExpression} === {$part}"; + } + } + + if (! $conditions) { + return 'false'; + } + + $combined = '(' . implode(' || ', $conditions) . ')'; + return $isNegative ? "!{$combined}" : $combined; + } + + if (str_contains($values, '~')) { + if (preg_match('/^(\d+(?:\.\d+)?)~(\d+(?:\.\d+)?)$/', $values, $matches)) { + $start = $matches[1]; + $end = $matches[2]; + $condition = "self::inRange({$varExpression}, {$start}, {$end})"; + return $isNegative ? "!{$condition}" : $condition; + } + } + + $condition = "self::matchesValues({$varExpression}, '{$values}')"; + + return $isNegative ? "!{$condition}" : $condition; + } + + private function getVariable(string $var): string + { + return match ($var) { + 'n' => '$n', + 'i' => '$i', + 'v' => '$v', + 'f' => '$f', + 't' => '$t', + 'e' => '$e', + default => '$n', + }; + } + + private function generateDispatcherMethod(array $locales): string + { + $output = " /**\n"; + $output .= " * Gets the plural category for a number in the specified locale.\n"; + $output .= " */\n"; + $output .= " public static function getPluralCategory(Locale \$locale, float|int \$number): string\n"; + $output .= " {\n"; + $output .= " return match(\$locale->getLanguage()) {\n"; + + foreach ($locales as $locale) { + $methodName = 'getPluralCategory' . ucfirst(str_replace('-', '_', $locale)); + $output .= " '{$locale}' => self::{$methodName}(\$number),\n"; + } + + $output .= " default => 'other'\n"; + $output .= " };\n"; + $output .= " }\n\n"; + + $output .= " /**\n"; + $output .= " * Gets all supported locales.\n"; + $output .= " */\n"; + $output .= " public static function getSupportedLocales(): array\n"; + $output .= " {\n"; + $output .= " return ['" . implode("', '", $locales) . "'];\n"; + $output .= " }\n\n"; + + return $output; + } +} + +// --- + +ConsoleApplication::boot(); + +$className = 'PluralRulesMatcher'; +$data = Json\decode(file_get_contents('https://raw.githubusercontent.com/unicode-org/cldr-json/refs/heads/main/cldr-json/cldr-core/supplemental/plurals.json')); + +Filesystem\delete($target = __DIR__ . "/../src/PluralRules/{$className}.php"); +Filesystem\write_file($target, new PluralRulesMatcherGenerator($data, $className)->generate()); + +$console = get(Console::class); +$console->writeln(); +$console->success("Generated "); diff --git a/packages/intl/composer.json b/packages/intl/composer.json new file mode 100644 index 0000000000..28250f76f8 --- /dev/null +++ b/packages/intl/composer.json @@ -0,0 +1,29 @@ +{ + "name": "tempest/intl", + "description": "A component for working with internationalization.", + "license": "MIT", + "minimum-stability": "dev", + "require": { + "php": "^8.4", + "doctrine/inflector": "^2.0", + "symfony/yaml": "^7.3", + "tempest/core": "dev-main", + "tempest/container": "dev-main", + "tempest/datetime": "dev-main", + "tempest/support": "dev-main" + }, + "autoload": { + "files": [ + "src/functions.php", + "src/Number/functions.php" + ], + "psr-4": { + "Tempest\\Intl\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tempest\\Intl\\Tests\\": "tests" + } + } +} diff --git a/packages/intl/phpunit.xml b/packages/intl/phpunit.xml new file mode 100644 index 0000000000..1c13eda788 --- /dev/null +++ b/packages/intl/phpunit.xml @@ -0,0 +1,13 @@ + + + + + tests + + + + + src + + + diff --git a/packages/intl/src/Catalog/Catalog.php b/packages/intl/src/Catalog/Catalog.php new file mode 100644 index 0000000000..71f3fee86e --- /dev/null +++ b/packages/intl/src/Catalog/Catalog.php @@ -0,0 +1,23 @@ +get(IntlConfig::class); + $catalog = []; + + foreach ($config->translationMessagePaths as $locale => $paths) { + $locale = Locale::from($locale)->value; + $catalog[$locale] ??= []; + + foreach ($paths as $path) { + $contents = Filesystem\read_file($path); + $messages = match (true) { + Str\ends_with($path, '.json') => Json\decode($contents), + Str\ends_with($path, ['.yaml', '.yml']) => Yaml::parse($contents), + }; + + foreach (Arr\undot($messages) as $key => $message) { + $catalog[$locale][$key] = $message; + } + } + } + + return new GenericCatalog($catalog); + } +} diff --git a/packages/intl/src/Catalog/GenericCatalog.php b/packages/intl/src/Catalog/GenericCatalog.php new file mode 100644 index 0000000000..9d5900472d --- /dev/null +++ b/packages/intl/src/Catalog/GenericCatalog.php @@ -0,0 +1,41 @@ + $catalog + */ + public function __construct( + private array $catalog = [], + ) {} + + public function has(Locale $locale, string $key): bool + { + return Arr\has_key($this->catalog, "{$locale->value}.{$key}"); + } + + public function get(Locale $locale, string $key): ?string + { + return Arr\get_by_key( + array: $this->catalog, + key: "{$locale->value}.{$key}", + default: Arr\get_by_key( + array: $this->catalog, + key: "{$locale->getLanguage()}.{$key}", + ), + ); + } + + public function add(Locale $locale, string $key, string $message): self + { + $this->catalog = Arr\set_by_key($this->catalog, "{$locale->value}.{$key}", $message); + + return $this; + } +} diff --git a/packages/intl/src/GenericTranslator.php b/packages/intl/src/GenericTranslator.php new file mode 100644 index 0000000000..9ee18608da --- /dev/null +++ b/packages/intl/src/GenericTranslator.php @@ -0,0 +1,52 @@ +catalog->has($locale, $key)) { + $this->eventBus?->dispatch(new TranslationMiss( + locale: $locale, + key: $key, + )); + } + + $message = $this->catalog->get($locale, $key) ?? $this->catalog->get($this->config->fallbackLocale, $key); + + if ($message === null) { + return $key; + } + + try { + return $this->formatter->format($message, ...$arguments); + } catch (\Throwable $exception) { + $this->eventBus?->dispatch(new TranslationFailure( + locale: $locale, + key: $key, + exception: $exception, + )); + + return $key; + } + } + + public function translate(string $key, mixed ...$arguments): string + { + return $this->translateForLocale($this->config->currentLocale, $key, ...$arguments); + } +} diff --git a/packages/intl/src/IntlConfig.php b/packages/intl/src/IntlConfig.php new file mode 100644 index 0000000000..2d141be772 --- /dev/null +++ b/packages/intl/src/IntlConfig.php @@ -0,0 +1,52 @@ + */ + public array $markupFormatters = []; + + /** @var array */ + public array $translationMessagePaths = []; + + public function __construct( + /** + * Defines the locale used throughout the application. + */ + public Locale $currentLocale, + + /** + * Defines the fallback locale used when a translation message does not exist in the current locale. + */ + public Locale $fallbackLocale, + ) {} + + public function addFunction(FormattingFunction|SelectorFunction $fn): void + { + $this->functions[] = $fn; + } + + public function addMarkupFormatter(MarkupFormatter|StandaloneMarkupFormatter $formatter): void + { + $this->markupFormatters[] = $formatter; + } + + public function addTranslationMessageFile(Locale $locale, string $path): void + { + $this->translationMessagePaths[$locale->value] ??= []; + + if (! in_array($path, $this->translationMessagePaths[$locale->value], strict: true)) { + $this->translationMessagePaths[$locale->value][] = $path; + } + } +} diff --git a/packages/intl/src/IntlInsightsProvider.php b/packages/intl/src/IntlInsightsProvider.php new file mode 100644 index 0000000000..7da6edf3fa --- /dev/null +++ b/packages/intl/src/IntlInsightsProvider.php @@ -0,0 +1,27 @@ + $this->intlConfig->currentLocale->getDisplayLanguage(), + 'Fallback locale' => $this->intlConfig->fallbackLocale->getDisplayLanguage(), + 'Translation files' => (string) arr($this->intlConfig->translationMessagePaths)->flatten()->count(), + 'Intl extension' => extension_loaded('intl') ? new Insight('ENABLED', Insight::SUCCESS) : new Insight('DISABLED', Insight::WARNING), + ]; + } +} diff --git a/packages/support/src/Language/Locale.php b/packages/intl/src/Locale.php similarity index 99% rename from packages/support/src/Language/Locale.php rename to packages/intl/src/Locale.php index 3a5c06b7e3..faa3bc407c 100644 --- a/packages/support/src/Language/Locale.php +++ b/packages/intl/src/Locale.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Support\Language; +namespace Tempest\Intl; use Locale as NativeLocale; diff --git a/packages/intl/src/MessageFormat/Formatter/FormattedValue.php b/packages/intl/src/MessageFormat/Formatter/FormattedValue.php new file mode 100644 index 0000000000..fc5905a762 --- /dev/null +++ b/packages/intl/src/MessageFormat/Formatter/FormattedValue.php @@ -0,0 +1,11 @@ +context; + } +} diff --git a/packages/intl/src/MessageFormat/Formatter/LocalVariable.php b/packages/intl/src/MessageFormat/Formatter/LocalVariable.php new file mode 100644 index 0000000000..bf2845b476 --- /dev/null +++ b/packages/intl/src/MessageFormat/Formatter/LocalVariable.php @@ -0,0 +1,15 @@ + $variables */ + private array $variables = []; + + public function __construct( + /** @var array */ + private readonly array $functions = [], + /** @var array */ + private readonly array $markupFormatters = [], + ) {} + + /** + * Formats a message string with the given variables. + */ + public function format(string $message, mixed ...$variables): string + { + try { + $ast = new MessageFormatParser($message)->parse(); + + $this->variables = $this->parseLocalVariables($variables); + + return $this->formatMessage($ast, $variables); + } catch (ParsingException $e) { + throw new FormattingException('Failed to parse message.', [ + 'message' => $message, + 'variables' => $variables, + 'exception' => $e, + ]); + } + } + + private function formatMessage(MessageNode $message): string + { + if ($message instanceof SimpleMessage) { + return $this->formatPattern($message->pattern); + } + + if ($message instanceof ComplexMessage) { + $localVariables = []; + + foreach ($message->declarations as $declaration) { + if ($declaration instanceof InputDeclaration) { + $expression = $declaration->expression; + $variableName = $expression->variable->name->name; + + if (! array_key_exists($variableName, $this->variables)) { + throw new FormattingException("Required input variable `{$variableName}` not provided."); + } + + if ($expression->function instanceof FunctionCall) { + $this->variables[$variableName] = new LocalVariable( + identifier: $variableName, + value: $this->variables[$variableName]->value, + function: $this->getSelectorFunction((string) $expression->function->identifier), + parameters: $this->evaluateOptions($expression->function->options), + ); + } + } elseif ($declaration instanceof LocalDeclaration) { + $variableName = $declaration->variable->name->name; + + $localVariables[$variableName] = new LocalVariable( + identifier: $variableName, + value: $this->evaluateExpression($declaration->expression)->value, + function: $this->getSelectorFunction($declaration->expression->function?->identifier), + parameters: $declaration->expression->attributes, + ); + } + } + + $originalVariables = $this->variables; + $this->variables = [...$this->variables, ...$localVariables]; + + try { + return $this->formatComplexBody($message->body); + } finally { + $this->variables = $originalVariables; + } + } + + throw new FormattingException('Unknown message type: ' . get_class($message)); + } + + private function formatComplexBody(ComplexBody $body): string + { + if ($body instanceof QuotedPattern) { + return $this->formatPattern($body->pattern); + } + + if ($body instanceof SimplePatternBody) { + return $this->formatPattern($body->pattern); + } + + if ($body instanceof Matcher) { + return $this->formatMatcher($body); + } + + throw new FormattingException('Unknown complex body type: ' . get_class($body)); + } + + private function formatMatcher(Matcher $matcher): string + { + $selectorVariables = []; + + foreach ($matcher->selectors as $selector) { + $variableName = $selector->name->name; + + if (! array_key_exists($variableName, $this->variables)) { + throw new FormattingException("Selector variable `{$variableName}` not found."); + } + + $selectorVariables[] = $this->variables[$variableName]; + } + + $bestVariant = null; + $wildcardVariant = null; + + foreach ($matcher->variants as $variant) { + if (count($variant->keys) !== count($selectorVariables)) { + continue; + } + + $matches = true; + $hasWildcard = false; + + for ($i = 0; $i < count($variant->keys); $i++) { + $keyNode = $variant->keys[$i]; + $variable = $selectorVariables[$i]; + + if ($keyNode instanceof WildcardKey) { + $hasWildcard = true; + continue; + } + + if (! ($keyNode instanceof Literal)) { + $matches = false; + break; + } + + $variantKey = $keyNode->value; + $isMatch = false; + + if ($variable->function) { + $isMatch = $variable->function->match($variantKey, $variable->value, $variable->parameters); + } else { + $isMatch = $variable->value === $variantKey; + } + + if (! $isMatch) { + $matches = false; + break; + } + } + + if ($matches) { + if (! $hasWildcard) { + $bestVariant = $variant; + break; + } elseif ($wildcardVariant === null) { + $wildcardVariant = $variant; + } + } + } + + $selectedVariant = $bestVariant ?? $wildcardVariant; + + if ($selectedVariant === null) { + $selectorValues = array_column($selectorVariables, 'value'); + + // TODO: test this + throw new FormattingException('No matching variant found for selector values: ' . json_encode($selectorValues)); + } + + return $this->formatPattern($selectedVariant->pattern->pattern); + } + + private function formatPattern(Pattern $pattern): string + { + $result = ''; + + foreach ($pattern->elements as $element) { + if ($element instanceof Text) { + $result .= $element->value; + } elseif ($element instanceof Placeholder) { + $result .= $this->formatPlaceholder($element); + } + } + + return $result; + } + + private function formatPlaceholder(Placeholder $placeholder): string + { + if ($placeholder instanceof Expression) { + $value = $this->evaluateExpression($placeholder); + + return $value->formatted; + } + + if ($placeholder instanceof Markup) { + return $this->formatMarkup($placeholder); + } + + if ($placeholder instanceof QuotedPattern) { + return $this->formatPattern($placeholder->pattern); + } + + throw new FormattingException('Unknown placeholder type: ' . get_class($placeholder)); + } + + private function evaluateExpression(Expression $expression): FormattedValue + { + $value = null; + + if ($expression instanceof LiteralExpression) { + $value = $expression->literal->value; + } elseif ($expression instanceof VariableExpression) { + $variableName = $expression->variable->name->name; + + if (! array_key_exists($variableName, $this->variables)) { + throw new FormattingException("Variable `{$variableName}` not found"); + } + + $value = $this->variables[$variableName]->value; + } elseif ($expression instanceof FunctionExpression) { + $value = null; // Function-only expressions start with null + } + + if ($expression->function !== null) { + $functionName = (string) $expression->function->identifier; + $options = $this->evaluateOptions($expression->function->options); + + if ($function = $this->getFormattingFunction($functionName)) { + return $function->format($value, $options); + } else { + throw new FormattingException("Unknown function `{$functionName}`."); + } + } + + $formatted = $value !== null ? ((string) $value) : ''; + + return new FormattedValue($value, $formatted); + } + + private function evaluateOptions(array $options): array + { + $result = []; + + foreach ($options as $option) { + $name = $option->identifier->name; + + if ($option->value instanceof Variable) { + $variableName = $option->value->name->name; + + if (! array_key_exists($variableName, $this->variables)) { + throw new FormattingException("Option variable `{$variableName}` not found."); + } + + $result[$name] = $this->variables[$variableName]->value; + } elseif ($option->value instanceof Literal) { + $result[$name] = $option->value->value; + } + } + + return $result; + } + + private function parseLocalVariables(array $variables): array + { + $result = []; + + foreach ($variables as $key => $value) { + if ($value instanceof LocalVariable) { + $result[$key] = $value; + } else { + $result[$key] = new LocalVariable( + identifier: $key, + value: $value, + ); + } + } + + return $result; + } + + private function formatMarkup(Markup $markup): string + { + $tag = (string) $markup->identifier; + $options = Arr\map_with_keys($markup->options, fn (Option $option) => yield $option->identifier->name => $option->value->value); + + if ($markup->type === MarkupType::STANDALONE) { + if (is_null($formatter = $this->getStandaloneMarkupFormatter($tag))) { + return ''; + } + + return $formatter->format($tag, $options); + } + + if (is_null($formatter = $this->getMarkupFormatter($tag))) { + return ''; + } + + return match ($markup->type) { + MarkupType::OPEN => $formatter->formatOpenTag($tag, $options), + MarkupType::CLOSE => $formatter->formatCloseTag($tag), + default => '', + }; + } + + private function getMarkupFormatter(?string $tag): ?MarkupFormatter + { + if (! $tag) { + return null; + } + + return arr($this->markupFormatters) + ->filter(fn (MarkupFormatter|StandaloneMarkupFormatter $fn) => $fn instanceof MarkupFormatter) + ->first(fn (MarkupFormatter $fn) => $fn->supportsTag($tag)); + } + + private function getStandaloneMarkupFormatter(?string $tag): ?StandaloneMarkupFormatter + { + if (! $tag) { + return null; + } + + return arr($this->markupFormatters) + ->filter(fn (MarkupFormatter|StandaloneMarkupFormatter $fn) => $fn instanceof StandaloneMarkupFormatter) + ->first(fn (StandaloneMarkupFormatter $fn) => $fn->supportsTag($tag)); + } + + private function getSelectorFunction(?string $name): ?SelectorFunction + { + if (! $name) { + return null; + } + + return arr($this->functions) + ->filter(fn (FormattingFunction|SelectorFunction $fn) => $fn instanceof SelectorFunction) + ->first(fn (SelectorFunction $fn) => $fn->name === $name); + } + + private function getFormattingFunction(?string $name): ?FormattingFunction + { + if (! $name) { + return null; + } + + return arr($this->functions) + ->filter(fn (FormattingFunction|SelectorFunction $fn) => $fn instanceof FormattingFunction) + ->first(fn (FormattingFunction $fn) => $fn->name === $name); + } +} diff --git a/packages/intl/src/MessageFormat/FormattingFunction.php b/packages/intl/src/MessageFormat/FormattingFunction.php new file mode 100644 index 0000000000..1268844083 --- /dev/null +++ b/packages/intl/src/MessageFormat/FormattingFunction.php @@ -0,0 +1,20 @@ +format(Arr\get_by_key($parameters, 'pattern')); + + return new FormattedValue($value, $formatted); + } +} diff --git a/packages/intl/src/MessageFormat/Functions/NumberFunction.php b/packages/intl/src/MessageFormat/Functions/NumberFunction.php new file mode 100644 index 0000000000..a1b6cd0475 --- /dev/null +++ b/packages/intl/src/MessageFormat/Functions/NumberFunction.php @@ -0,0 +1,55 @@ +pluralRules->getPluralCategory($this->intlConfig->currentLocale, $number)) { + return true; + } + + return false; + } + + public function format(mixed $value, array $parameters): FormattedValue + { + $number = Number\parse($value); + $formatted = match (Arr\get_by_key($parameters, 'style')) { + 'percent' => Number\to_percentage($number), + 'currency' => Number\currency($number, Currency::parse(Arr\get_by_key($parameters, 'currency'))), + default => Number\format($number), + }; + + return new FormattedValue($number, $formatted); + } +} diff --git a/packages/intl/src/MessageFormat/Functions/StringFunction.php b/packages/intl/src/MessageFormat/Functions/StringFunction.php new file mode 100644 index 0000000000..62e406c727 --- /dev/null +++ b/packages/intl/src/MessageFormat/Functions/StringFunction.php @@ -0,0 +1,36 @@ + Str\to_upper_case($string), + 'lowercase', 'lower' => Str\to_lower_case($string), + 'titlecase', 'title' => Str\to_title_case($string), + 'snakecase', 'snake' => Str\to_snake_case($string), + 'camelcase', 'camel' => Str\to_camel_case($string), + 'kebabcase', 'kebab' => Str\to_kebab_case($string), + 'sentencecase', 'sentence' => Str\to_sentence_case($string), + default => $string, + }; + + return new FormattedValue($string, $formatted); + } +} diff --git a/packages/intl/src/MessageFormat/Markup/HtmlTagFormatter.php b/packages/intl/src/MessageFormat/Markup/HtmlTagFormatter.php new file mode 100644 index 0000000000..78e210181f --- /dev/null +++ b/packages/intl/src/MessageFormat/Markup/HtmlTagFormatter.php @@ -0,0 +1,24 @@ +', $tag, Html\format_attributes($options)); + } + + public function formatCloseTag(string $tag): string + { + return sprintf('', $tag); + } +} diff --git a/packages/intl/src/MessageFormat/Markup/IconMarkupFormatter.php b/packages/intl/src/MessageFormat/Markup/IconMarkupFormatter.php new file mode 100644 index 0000000000..ec6b6f1f04 --- /dev/null +++ b/packages/intl/src/MessageFormat/Markup/IconMarkupFormatter.php @@ -0,0 +1,33 @@ +container->get(Icon::class)->render( + name: Str\after_first($tag, 'icon-'), + class: Arr\get_by_key($options, 'class'), + ); + } +} diff --git a/packages/intl/src/MessageFormat/Markup/VoidHtmlTagFormatter.php b/packages/intl/src/MessageFormat/Markup/VoidHtmlTagFormatter.php new file mode 100644 index 0000000000..60ad86385b --- /dev/null +++ b/packages/intl/src/MessageFormat/Markup/VoidHtmlTagFormatter.php @@ -0,0 +1,19 @@ +toString(); + } +} diff --git a/packages/intl/src/MessageFormat/MarkupFormatter.php b/packages/intl/src/MessageFormat/MarkupFormatter.php new file mode 100644 index 0000000000..0aa483c2ee --- /dev/null +++ b/packages/intl/src/MessageFormat/MarkupFormatter.php @@ -0,0 +1,21 @@ +input = str_replace("\r\n", "\n", $input); + $this->len = mb_strlen($this->input, 'UTF-8'); + } + + /** + * Parses the input string and returns the root MessageNode. + */ + public function parse(): MessageNode + { + $this->consumeOptionalWhitespace(); + + $peek = $this->peek(6); + + if (str_starts_with($peek, '.local') || str_starts_with($peek, '.input') || str_starts_with($peek, '.match')) { + return $this->parseComplexMessage(); + } + + if (str_starts_with($peek, '{{')) { + return $this->parseComplexMessage(); + } + + $message = $this->parseSimpleMessage(); + $this->consumeOptionalWhitespace(); + + if (! $this->isEof()) { + $this->throw("Expected end of input but found '{$this->peek()}'"); + } + + return $message; + } + + private function parseSimpleMessage(): SimpleMessage + { + $pattern = $this->parsePattern(); + + return new SimpleMessage($pattern); + } + + private function parseComplexMessage(): ComplexMessage + { + $declarations = []; + + while (! $this->isEof()) { + $snapshot = $this->pos; + $this->consumeOptionalWhitespace(); + $peek = $this->peek(6); + + if (str_starts_with($peek, '.input')) { + $declarations[] = $this->parseInputDeclaration(); + } elseif (str_starts_with($peek, '.local')) { + $declarations[] = $this->parseLocalDeclaration(); + } else { + $this->pos = $snapshot; + break; + } + } + + $this->consumeOptionalWhitespace(); + $body = $this->parseComplexBody(); + $this->consumeOptionalWhitespace(); + + if (! $this->isEof()) { + $this->throw("Expected end of input but found '{$this->peek()}'"); + } + + return new ComplexMessage($declarations, $body); + } + + private function parseInputDeclaration(): InputDeclaration + { + $this->consumeKeyword('.input'); + $this->consumeRequiredWhitespace(); + + return new InputDeclaration($this->parseVariableExpression()); + } + + private function parseLocalDeclaration(): LocalDeclaration + { + $this->consumeKeyword('.local'); + $this->consumeRequiredWhitespace(); + + $variable = $this->parseVariable(); + + $this->consumeOptionalWhitespace(); + $this->consumeChar('='); + $this->consumeOptionalWhitespace(); + + return new LocalDeclaration($variable, $this->parseExpression()); + } + + private function parseComplexBody(): ComplexBody + { + if ($this->peek(2) === '{{') { + return $this->parseQuotedPattern(); + } + + if ($this->peek(6) === '.match') { + return $this->parseMatcher(); + } + + $pattern = $this->parsePattern(); + + return new SimplePatternBody($pattern); + } + + private function parseMatcher(): Matcher + { + $this->consumeKeyword('.match'); + $selectors = []; + + do { + $this->consumeRequiredWhitespace(); + + $selectors[] = $this->parseVariable(); + $snapshot = $this->pos; + + $this->consumeOptionalWhitespace(); + + $continue = $this->peek() === '$'; + $this->pos = $snapshot; + } while ($continue); + + $this->consumeRequiredWhitespace(); + + $variants = []; + $variants[] = $this->parseVariant(); + + while (true) { + $snapshot = $this->pos; + $this->consumeOptionalWhitespace(); + $peek = $this->peek(); + + if ($peek === '' || ! ($peek === '*' || $peek === '|' || preg_match(self::NAME_CHAR_REGEX, $peek))) { + $this->pos = $snapshot; + break; + } + + $variants[] = $this->parseVariant(); + } + + return new Matcher($selectors, $variants); + } + + private function parseVariant(): Variant + { + $keys = []; + + do { + if ($this->peek() === '*') { + $this->consumeChar('*'); + $keys[] = new WildcardKey(); + } else { + $keys[] = $this->parseLiteral(); + } + $this->consumeOptionalWhitespace(); + } while ($this->peek() !== '{'); + + return new Variant($keys, $this->parseQuotedPattern()); + } + + private function parsePattern(string $terminator = ''): Pattern + { + $elements = []; + $buffer = ''; + $terminatorLen = strlen($terminator); + + while (! $this->isEof()) { + if ($terminatorLen > 0 && $this->peek($terminatorLen) === $terminator) { + break; + } + + $char = $this->peek(); + + if ($char === '{') { + if ($buffer !== '') { + $elements[] = new Text($buffer); + $buffer = ''; + } + + $elements[] = $this->parsePlaceholder(); + + continue; + } + + if ($char === '}') { + $this->throw("Unmatched '}' in pattern"); + } + + if ($char === '\\') { + $buffer .= $this->parseEscapedChar(['{', '}', '|', '\\']); + } else { + $buffer .= $this->readChar(); + } + } + + if ($buffer !== '') { + $elements[] = new Text($buffer); + } + + return new Pattern($elements); + } + + private function parseQuotedPattern(): QuotedPattern + { + $this->consumeChar('{'); + $this->consumeChar('{'); + $pattern = $this->parsePattern('}}'); + $this->consumeChar('}'); + $this->consumeChar('}'); + + return new QuotedPattern($pattern); + } + + private function parsePlaceholder(): Placeholder + { + $peek = $this->peek(2); + + if ($peek === '{#' || $peek === '{/') { + return $this->parseMarkup(); + } + + return $this->parseExpression(); + } + + private function parseExpression(): Expression + { + $this->consumeChar('{'); + $this->consumeOptionalWhitespace(); + + $node = $this->parseExpressionBody(); + + $this->consumeOptionalWhitespace(); + $this->consumeChar('}'); + + return $node; + } + + private function parseVariableExpression(): VariableExpression + { + $this->consumeChar('{'); + $this->consumeOptionalWhitespace(); + $variable = $this->parseVariable(); + $this->consumeOptionalWhitespace(); + $function = $this->peek() === ':' ? $this->parseFunction() : null; + $attributes = []; + + while ($this->peek() === '@') { + $attributes[] = $this->parseAttribute(); + $this->consumeOptionalWhitespace(); + } + + $this->consumeOptionalWhitespace(); + $this->consumeChar('}'); + + return new VariableExpression($variable, $function, $attributes); + } + + private function parseExpressionBody(): Expression + { + $subject = null; + $function = null; + + if ($this->peek() === '$') { + $subject = $this->parseVariable(); + } elseif ($this->peek() === ':') { + $function = $this->parseFunction(); + } else { + $subject = $this->parseLiteral(); + } + + $this->consumeOptionalWhitespace(); + + if ($function === null && $this->peek() === ':') { + $function = $this->parseFunction(); + } + + $attributes = []; + while ($this->peek() === '@') { + $attributes[] = $this->parseAttribute(); + $this->consumeOptionalWhitespace(); + } + + if ($subject instanceof Variable) { + return new VariableExpression($subject, $function, $attributes); + } + + if ($subject instanceof Literal) { + return new LiteralExpression($subject, $function, $attributes); + } + + if ($function !== null) { + return new FunctionExpression($function, $attributes); + } + + $this->throw('Invalid expression structure.'); + } + + private function parseMarkup(): Markup + { + $this->consumeChar('{'); + $this->consumeOptionalWhitespace(); + + $type = MarkupType::OPEN; + + if ($this->peek() === '/') { + $this->consumeChar('/'); + $type = MarkupType::CLOSE; + } else { + $this->consumeChar('#'); + } + + $identifier = $this->parseIdentifier(); + $this->consumeOptionalWhitespace(); + + $options = []; + while ($this->isEof() === false && ! in_array($this->peek(), ['@', '/', '}'], true)) { + $options[] = $this->parseOption(); + $this->consumeOptionalWhitespace(); + } + + $attributes = []; + while ($this->peek() === '@') { + $attributes[] = $this->parseAttribute(); + $this->consumeOptionalWhitespace(); + } + + if ($type === MarkupType::OPEN && $this->peek() === '/') { + $this->consumeChar('/'); + $type = MarkupType::STANDALONE; + } + + $this->consumeOptionalWhitespace(); + $this->consumeChar('}'); + + return new Markup($type, $identifier, $options, $attributes); + } + + private function parseFunction(): FunctionCall + { + $this->consumeChar(':'); + $identifier = $this->parseIdentifier(); + $this->consumeOptionalWhitespace(); + + $options = []; + while ($this->isEof() === false && ! in_array($this->peek(), ['@', '}'], true)) { + $options[] = $this->parseOption(); + $this->consumeOptionalWhitespace(); + } + + return new FunctionCall($identifier, $options); + } + + private function parseOption(): Option + { + $identifier = $this->parseIdentifier(); + + $this->consumeOptionalWhitespace(); + $this->consumeChar('='); + $this->consumeOptionalWhitespace(); + + $value = $this->peek() === '$' ? $this->parseVariable() : $this->parseLiteral(); + + return new Option($identifier, $value); + } + + private function parseAttribute(): Attribute + { + $this->consumeChar('@'); + $identifier = $this->parseIdentifier(); + $this->consumeOptionalWhitespace(); + $value = null; + + if ($this->peek() === '=') { + $this->consumeChar('='); + $this->consumeOptionalWhitespace(); + + $value = $this->parseLiteral(); + } + + return new Attribute($identifier, $value); + } + + private function parseVariable(): Variable + { + $this->consumeChar('$'); + + return new Variable($this->parseIdentifier()); + } + + private function parseLiteral(): Literal + { + if ($this->peek() === '|') { + return $this->parseQuotedLiteral(); + } + + return $this->parseUnquotedLiteral(); + } + + private function parseQuotedLiteral(): QuotedLiteral + { + $this->consumeChar('|'); + $buffer = ''; + + while (! $this->isEof() && $this->peek() !== '|') { + if ($this->peek() === '\\') { + $buffer .= $this->parseEscapedChar(['|', '\\']); + } else { + $buffer .= $this->readChar(); + } + } + + $this->consumeChar('|'); + + return new QuotedLiteral($buffer); + } + + private function parseUnquotedLiteral(): UnquotedLiteral + { + $buffer = ''; + $char = $this->peek(); + + if ($char === '' || ! preg_match(self::NAME_CHAR_REGEX, $char)) { + $this->throw("Invalid unquoted literal start character: '{$char}'"); + } + + $buffer .= $this->readChar(); + + while (! $this->isEof()) { + $char = $this->peek(); + + if (! preg_match(self::NAME_CHAR_REGEX, $char)) { + break; + } + + $buffer .= $this->readChar(); + } + + return new UnquotedLiteral($buffer); + } + + private function parseIdentifier(): Identifier + { + $name = $this->parseName(); + + if ($this->peek() === ':') { + $this->consumeChar(':'); + $namespace = $name; + $name = $this->parseName(); + + return new Identifier($name, $namespace); + } + + return new Identifier($name); + } + + private function parseName(): string + { + $start = $this->peek(); + + if (! preg_match(self::NAME_START_REGEX, $start)) { + $this->throw("Invalid identifier start character: '{$start}'"); + } + + $buffer = $this->readChar(); + + while (! $this->isEof()) { + $char = $this->peek(); + + if (! preg_match(self::NAME_CHAR_REGEX, $char)) { + break; + } + + $buffer .= $this->readChar(); + } + + return $buffer; + } + + private function parseEscapedChar(array $escapable): string + { + $this->consumeChar('\\'); + $char = $this->peek(); + + if (in_array($char, $escapable, true)) { + return $this->readChar(); + } + + $this->throw("Invalid escape sequence: \\{$char}."); + } + + private function consumeKeyword(string $keyword): void + { + if ($this->peek(strlen($keyword)) !== $keyword) { + $this->throw("Expected keyword '{$keyword}'"); + } + + $this->pos += strlen($keyword); + } + + private function consumeChar(string $expected): void + { + if ($this->isEof()) { + $this->throw("Expected `{$expected}` but reached end of input."); + } + + $char = $this->readChar(); + + if ($char !== $expected) { + $this->pos--; + $this->throw("Expected `{$expected}` but found `{$char}`."); + } + } + + private function consumeOptionalWhitespace(): void + { + $this->consumeWhitespace(false); + } + + private function consumeRequiredWhitespace(): void + { + $this->consumeWhitespace(true); + } + + private function consumeWhitespace(bool $required): void + { + $startPos = $this->pos; + + while (! $this->isEof()) { + $char = $this->peek(); + + if (! preg_match('/^[\s\x{061C}\x{200E}\x{200F}\x{2066}-\x{2069}]/u', $char)) { + break; + } + + $this->readChar(); + } + + if ($required && $this->pos === $startPos) { + $this->throw('Required whitespace not found.'); + } + } + + private function readChar(): string + { + if ($this->isEof()) { + return ''; + } + + $char = mb_substr($this->input, $this->pos, 1, 'UTF-8'); + $this->pos++; + + return $char; + } + + private function peek(int $length = 1): string + { + if ($this->isEof()) { + return ''; + } + + return mb_substr($this->input, $this->pos, $length, 'UTF-8'); + } + + private function isEof(): bool + { + return $this->pos >= $this->len; + } + + private function throw(string $message): never + { + throw new ParsingException($message, $this->pos); + } +} diff --git a/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php new file mode 100644 index 0000000000..7353253387 --- /dev/null +++ b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php @@ -0,0 +1,11 @@ +variants as $variant) { + $elements[] = $variant->pattern; + } + + return new Pattern($elements); + } +} diff --git a/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php new file mode 100644 index 0000000000..02f21d601e --- /dev/null +++ b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php @@ -0,0 +1,17 @@ +pattern; + } +} diff --git a/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php new file mode 100644 index 0000000000..24538b2a10 --- /dev/null +++ b/packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php @@ -0,0 +1,17 @@ +getPattern()); + } +} diff --git a/packages/intl/src/MessageFormat/Parser/Node/Declaration/Declaration.php b/packages/intl/src/MessageFormat/Parser/Node/Declaration/Declaration.php new file mode 100644 index 0000000000..df661703fd --- /dev/null +++ b/packages/intl/src/MessageFormat/Parser/Node/Declaration/Declaration.php @@ -0,0 +1,9 @@ +namespace ? "{$this->namespace}:{$this->name}" : $this->name; + } +} diff --git a/packages/intl/src/MessageFormat/Parser/Node/Key/Key.php b/packages/intl/src/MessageFormat/Parser/Node/Key/Key.php new file mode 100644 index 0000000000..5cb1a73784 --- /dev/null +++ b/packages/intl/src/MessageFormat/Parser/Node/Key/Key.php @@ -0,0 +1,9 @@ +pattern; + } +} diff --git a/packages/intl/src/MessageFormat/Parser/Node/Pattern/Text.php b/packages/intl/src/MessageFormat/Parser/Node/Pattern/Text.php new file mode 100644 index 0000000000..e028e8c7b8 --- /dev/null +++ b/packages/intl/src/MessageFormat/Parser/Node/Pattern/Text.php @@ -0,0 +1,12 @@ +implements(FormattingFunction::class) && ! $class->implements(SelectorFunction::class)) { + return; + } + + $this->discoveryItems->add($location, $class->getName()); + } + + public function apply(): void + { + foreach ($this->discoveryItems as $className) { + $this->config->addFunction($this->container->get($className)); + } + } +} diff --git a/packages/intl/src/MessageFormatMarkupDiscovery.php b/packages/intl/src/MessageFormatMarkupDiscovery.php new file mode 100644 index 0000000000..a112a951ad --- /dev/null +++ b/packages/intl/src/MessageFormatMarkupDiscovery.php @@ -0,0 +1,39 @@ +implements(MarkupFormatter::class) && ! $class->implements(StandaloneMarkupFormatter::class)) { + return; + } + + $this->discoveryItems->add($location, $class->getName()); + } + + public function apply(): void + { + foreach ($this->discoveryItems as $className) { + $this->config->addMarkupFormatter($this->container->get($className)); + } + } +} diff --git a/packages/intl/src/MessageFormatterInitializer.php b/packages/intl/src/MessageFormatterInitializer.php new file mode 100644 index 0000000000..0ac1c91e40 --- /dev/null +++ b/packages/intl/src/MessageFormatterInitializer.php @@ -0,0 +1,22 @@ +get(IntlConfig::class); + + return new MessageFormatter( + functions: $config->functions, + markupFormatters: $config->markupFormatters, + ); + } +} diff --git a/packages/support/src/Number/functions.php b/packages/intl/src/Number/functions.php similarity index 84% rename from packages/support/src/Number/functions.php rename to packages/intl/src/Number/functions.php index af05dedc70..ebd641c703 100644 --- a/packages/support/src/Number/functions.php +++ b/packages/intl/src/Number/functions.php @@ -1,12 +1,36 @@ = 1000000) { + return 6; + } + + if ($abs >= 1000) { + return 3; + } + + return 0; + } + + /** + * Gets the exponent for scientific notation. + */ + private static function getExponent(float|int $n): int + { + if ($n === 0 || $n === 0.0) { + return 0; + } + + return (int) floor(log10(abs($n))); + } + + /** + * Checks if number is in range. + */ + private static function inRange(int|float $value, int|float $start, int|float $end): bool + { + return $value >= $start && $value <= $end; + } + + /** + * Checks if number matches any value in comma-separated list. + */ + private static function matchesValues(int|float $value, string $values): bool + { + $parts = explode(',', $values); + + foreach ($parts as $part) { + $part = trim($part); + + if (str_contains($part, '~')) { + [$start, $end] = explode('~', $part); + + if (self::inRange($value, (float) trim($start), (float) trim($end))) { + return true; + } + } elseif (str_contains($part, '..')) { + [$start, $end] = explode('..', $part); + + if (self::inRange($value, (float) trim($start), (float) trim($end))) { + return true; + } + } elseif (((float) $part) === ((float) $value)) { + return true; + } + } + return false; + } + + /** + * Gets the plural category for the af locale. + */ + private static function getPluralCategoryAf(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ak locale. + */ + private static function getPluralCategoryAk(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the am locale. + */ + private static function getPluralCategoryAm(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the an locale. + */ + private static function getPluralCategoryAn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ar locale. + */ + private static function getPluralCategoryAr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0) { + return 'zero'; + } + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + if (self::inRange($n % 100, 3, 10)) { + return 'few'; + } + + if (self::inRange($n % 100, 11, 99)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ars locale. + */ + private static function getPluralCategoryArs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0) { + return 'zero'; + } + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + if (self::inRange($n % 100, 3, 10)) { + return 'few'; + } + + if (self::inRange($n % 100, 11, 99)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the as locale. + */ + private static function getPluralCategoryAs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the asa locale. + */ + private static function getPluralCategoryAsa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ast locale. + */ + private static function getPluralCategoryAst(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the az locale. + */ + private static function getPluralCategoryAz(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bal locale. + */ + private static function getPluralCategoryBal(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the be locale. + */ + private static function getPluralCategoryBe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (($n % 10) === 1 && ($n % 100) !== 11) { + return 'one'; + } + + if (self::inRange($n % 10, 2, 4) && ! self::inRange($n % 100, 12, 14)) { + return 'few'; + } + + if (($n % 10) === 0 || self::inRange($n % 10, 5, 9) || self::inRange($n % 100, 11, 14)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bem locale. + */ + private static function getPluralCategoryBem(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bez locale. + */ + private static function getPluralCategoryBez(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bg locale. + */ + private static function getPluralCategoryBg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bho locale. + */ + private static function getPluralCategoryBho(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the blo locale. + */ + private static function getPluralCategoryBlo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0) { + return 'zero'; + } + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bm locale. + */ + private static function getPluralCategoryBm(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the bn locale. + */ + private static function getPluralCategoryBn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bo locale. + */ + private static function getPluralCategoryBo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the br locale. + */ + private static function getPluralCategoryBr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (($n % 10) === 1 && ! (($n % 100) === 11 || ($n % 100) === 71 || ($n % 100) === 91)) { + return 'one'; + } + + if (($n % 10) === 2 && ! (($n % 100) === 12 || ($n % 100) === 72 || ($n % 100) === 92)) { + return 'two'; + } + + if ((self::inRange($n % 10, 3, 4) || ($n % 10) === 9) && ! (self::inRange($n % 100, 10, 19) || self::inRange($n % 100, 70, 79) || self::inRange($n % 100, 90, 99))) { + return 'few'; + } + + if ($n !== 0 && ($n % 1000000) === 0) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the brx locale. + */ + private static function getPluralCategoryBrx(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the bs locale. + */ + private static function getPluralCategoryBs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + return 'one'; + } + + if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ca locale. + */ + private static function getPluralCategoryCa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ce locale. + */ + private static function getPluralCategoryCe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ceb locale. + */ + private static function getPluralCategoryCeb(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ( + $v === 0 && ($i === 1 || $i === 2 || $i === 3) || + $v === 0 && ! (($i % 10) === 4 || ($i % 10) === 6 || ($i % 10) === 9) || + $v !== 0 && ! (($f % 10) === 4 || ($f % 10) === 6 || ($f % 10) === 9) + ) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the cgg locale. + */ + private static function getPluralCategoryCgg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the chr locale. + */ + private static function getPluralCategoryChr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ckb locale. + */ + private static function getPluralCategoryCkb(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the cs locale. + */ + private static function getPluralCategoryCs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if (self::inRange($i, 2, 4) && $v === 0) { + return 'few'; + } + + if ($v !== 0) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the csw locale. + */ + private static function getPluralCategoryCsw(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the cy locale. + */ + private static function getPluralCategoryCy(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0) { + return 'zero'; + } + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + if ($n === 3) { + return 'few'; + } + + if ($n === 6) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the da locale. + */ + private static function getPluralCategoryDa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1 || $t !== 0 && ($i === 0 || $i === 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the de locale. + */ + private static function getPluralCategoryDe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the doi locale. + */ + private static function getPluralCategoryDoi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the dsb locale. + */ + private static function getPluralCategoryDsb(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 100) === 1 || ($f % 100) === 1) { + return 'one'; + } + + if ($v === 0 && ($i % 100) === 2 || ($f % 100) === 2) { + return 'two'; + } + + if ($v === 0 && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the dv locale. + */ + private static function getPluralCategoryDv(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the dz locale. + */ + private static function getPluralCategoryDz(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the ee locale. + */ + private static function getPluralCategoryEe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the el locale. + */ + private static function getPluralCategoryEl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the en locale. + */ + private static function getPluralCategoryEn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the eo locale. + */ + private static function getPluralCategoryEo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the es locale. + */ + private static function getPluralCategoryEs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the et locale. + */ + private static function getPluralCategoryEt(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the eu locale. + */ + private static function getPluralCategoryEu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the fa locale. + */ + private static function getPluralCategoryFa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ff locale. + */ + private static function getPluralCategoryFf(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $i === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the fi locale. + */ + private static function getPluralCategoryFi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the fil locale. + */ + private static function getPluralCategoryFil(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ( + $v === 0 && ($i === 1 || $i === 2 || $i === 3) || + $v === 0 && ! (($i % 10) === 4 || ($i % 10) === 6 || ($i % 10) === 9) || + $v !== 0 && ! (($f % 10) === 4 || ($f % 10) === 6 || ($f % 10) === 9) + ) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the fo locale. + */ + private static function getPluralCategoryFo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the fr locale. + */ + private static function getPluralCategoryFr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $i === 1) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the fur locale. + */ + private static function getPluralCategoryFur(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the fy locale. + */ + private static function getPluralCategoryFy(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ga locale. + */ + private static function getPluralCategoryGa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + if (self::inRange($n, 3, 6)) { + return 'few'; + } + + if (self::inRange($n, 7, 10)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the gd locale. + */ + private static function getPluralCategoryGd(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1 || $n === 11) { + return 'one'; + } + + if ($n === 2 || $n === 12) { + return 'two'; + } + + if (self::inRange($n, 3, 10) || self::inRange($n, 13, 19)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the gl locale. + */ + private static function getPluralCategoryGl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the gsw locale. + */ + private static function getPluralCategoryGsw(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the gu locale. + */ + private static function getPluralCategoryGu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the guw locale. + */ + private static function getPluralCategoryGuw(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the gv locale. + */ + private static function getPluralCategoryGv(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1) { + return 'one'; + } + + if ($v === 0 && ($i % 10) === 2) { + return 'two'; + } + + if ($v === 0 && (($i % 100) === 0 || ($i % 100) === 20 || ($i % 100) === 40 || ($i % 100) === 60 || ($i % 100) === 80)) { + return 'few'; + } + + if ($v !== 0) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ha locale. + */ + private static function getPluralCategoryHa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the haw locale. + */ + private static function getPluralCategoryHaw(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the he locale. + */ + private static function getPluralCategoryHe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0 || $i === 0 && $v !== 0) { + return 'one'; + } + + if ($i === 2 && $v === 0) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the hi locale. + */ + private static function getPluralCategoryHi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the hnj locale. + */ + private static function getPluralCategoryHnj(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the hr locale. + */ + private static function getPluralCategoryHr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + return 'one'; + } + + if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the hsb locale. + */ + private static function getPluralCategoryHsb(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 100) === 1 || ($f % 100) === 1) { + return 'one'; + } + + if ($v === 0 && ($i % 100) === 2 || ($f % 100) === 2) { + return 'two'; + } + + if ($v === 0 && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the hu locale. + */ + private static function getPluralCategoryHu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the hy locale. + */ + private static function getPluralCategoryHy(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $i === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ia locale. + */ + private static function getPluralCategoryIa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the id locale. + */ + private static function getPluralCategoryId(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the ig locale. + */ + private static function getPluralCategoryIg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the ii locale. + */ + private static function getPluralCategoryIi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the io locale. + */ + private static function getPluralCategoryIo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the is locale. + */ + private static function getPluralCategoryIs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($t === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($t % 10) === 1 && ($t % 100) !== 11) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the it locale. + */ + private static function getPluralCategoryIt(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the iu locale. + */ + private static function getPluralCategoryIu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ja locale. + */ + private static function getPluralCategoryJa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the jbo locale. + */ + private static function getPluralCategoryJbo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the jgo locale. + */ + private static function getPluralCategoryJgo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the jmc locale. + */ + private static function getPluralCategoryJmc(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the jv locale. + */ + private static function getPluralCategoryJv(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the jw locale. + */ + private static function getPluralCategoryJw(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the ka locale. + */ + private static function getPluralCategoryKa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kab locale. + */ + private static function getPluralCategoryKab(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $i === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kaj locale. + */ + private static function getPluralCategoryKaj(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kcg locale. + */ + private static function getPluralCategoryKcg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kde locale. + */ + private static function getPluralCategoryKde(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the kea locale. + */ + private static function getPluralCategoryKea(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the kk locale. + */ + private static function getPluralCategoryKk(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kkj locale. + */ + private static function getPluralCategoryKkj(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kl locale. + */ + private static function getPluralCategoryKl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the km locale. + */ + private static function getPluralCategoryKm(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the kn locale. + */ + private static function getPluralCategoryKn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ko locale. + */ + private static function getPluralCategoryKo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the ks locale. + */ + private static function getPluralCategoryKs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ksb locale. + */ + private static function getPluralCategoryKsb(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ksh locale. + */ + private static function getPluralCategoryKsh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0) { + return 'zero'; + } + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ku locale. + */ + private static function getPluralCategoryKu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the kw locale. + */ + private static function getPluralCategoryKw(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0) { + return 'zero'; + } + + if ($n === 1) { + return 'one'; + } + + if ( + ($n % 100) === 2 || + ($n % 100) === 22 || + ($n % 100) === 42 || + ($n % 100) === 62 || + ($n % 100) === 82 || + ($n % 1000) === 0 && (self::inRange($n % 100000, 1000, 20000) || ($n % 100000) === 40000 || ($n % 100000) === 60000 || ($n % 100000) === 80000) || + $n !== 0 && ($n % 1000000) === 100000 + ) { + return 'two'; + } + + if (($n % 100) === 3 || ($n % 100) === 23 || ($n % 100) === 43 || ($n % 100) === 63 || ($n % 100) === 83) { + return 'few'; + } + + if ($n !== 1 && (($n % 100) === 1 || ($n % 100) === 21 || ($n % 100) === 41 || ($n % 100) === 61 || ($n % 100) === 81)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ky locale. + */ + private static function getPluralCategoryKy(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the lag locale. + */ + private static function getPluralCategoryLag(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0) { + return 'zero'; + } + + if (($i === 0 || $i === 1) && $n !== 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the lb locale. + */ + private static function getPluralCategoryLb(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the lg locale. + */ + private static function getPluralCategoryLg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the lij locale. + */ + private static function getPluralCategoryLij(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the lkt locale. + */ + private static function getPluralCategoryLkt(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the lld locale. + */ + private static function getPluralCategoryLld(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ln locale. + */ + private static function getPluralCategoryLn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the lo locale. + */ + private static function getPluralCategoryLo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the lt locale. + */ + private static function getPluralCategoryLt(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (($n % 10) === 1 && ! self::inRange($n % 100, 11, 19)) { + return 'one'; + } + + if (self::inRange($n % 10, 2, 9) && ! self::inRange($n % 100, 11, 19)) { + return 'few'; + } + + if ($f !== 0) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the lv locale. + */ + private static function getPluralCategoryLv(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (($n % 10) === 0 || self::inRange($n % 100, 11, 19) || $v === 2 && self::inRange($f % 100, 11, 19)) { + return 'zero'; + } + + if (($n % 10) === 1 && ($n % 100) !== 11 || $v === 2 && ($f % 10) === 1 && ($f % 100) !== 11 || $v !== 2 && ($f % 10) === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the mas locale. + */ + private static function getPluralCategoryMas(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the mg locale. + */ + private static function getPluralCategoryMg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the mgo locale. + */ + private static function getPluralCategoryMgo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the mk locale. + */ + private static function getPluralCategoryMk(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ml locale. + */ + private static function getPluralCategoryMl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the mn locale. + */ + private static function getPluralCategoryMn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the mo locale. + */ + private static function getPluralCategoryMo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($v !== 0 || $n === 0 || $n !== 1 && self::inRange($n % 100, 1, 19)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the mr locale. + */ + private static function getPluralCategoryMr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ms locale. + */ + private static function getPluralCategoryMs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the mt locale. + */ + private static function getPluralCategoryMt(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + if ($n === 0 || self::inRange($n % 100, 3, 10)) { + return 'few'; + } + + if (self::inRange($n % 100, 11, 19)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the my locale. + */ + private static function getPluralCategoryMy(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the nah locale. + */ + private static function getPluralCategoryNah(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the naq locale. + */ + private static function getPluralCategoryNaq(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nb locale. + */ + private static function getPluralCategoryNb(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nd locale. + */ + private static function getPluralCategoryNd(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ne locale. + */ + private static function getPluralCategoryNe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nl locale. + */ + private static function getPluralCategoryNl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nn locale. + */ + private static function getPluralCategoryNn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nnh locale. + */ + private static function getPluralCategoryNnh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the no locale. + */ + private static function getPluralCategoryNo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nqo locale. + */ + private static function getPluralCategoryNqo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the nr locale. + */ + private static function getPluralCategoryNr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nso locale. + */ + private static function getPluralCategoryNso(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ny locale. + */ + private static function getPluralCategoryNy(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the nyn locale. + */ + private static function getPluralCategoryNyn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the om locale. + */ + private static function getPluralCategoryOm(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the or locale. + */ + private static function getPluralCategoryOr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the os locale. + */ + private static function getPluralCategoryOs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the osa locale. + */ + private static function getPluralCategoryOsa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the pa locale. + */ + private static function getPluralCategoryPa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the pap locale. + */ + private static function getPluralCategoryPap(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the pcm locale. + */ + private static function getPluralCategoryPcm(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the pl locale. + */ + private static function getPluralCategoryPl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { + return 'few'; + } + + if ($v === 0 && $i !== 1 && self::inRange($i % 10, 0, 1) || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 12, 14)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the prg locale. + */ + private static function getPluralCategoryPrg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (($n % 10) === 0 || self::inRange($n % 100, 11, 19) || $v === 2 && self::inRange($f % 100, 11, 19)) { + return 'zero'; + } + + if (($n % 10) === 1 && ($n % 100) !== 11 || $v === 2 && ($f % 10) === 1 && ($f % 100) !== 11 || $v !== 2 && ($f % 10) === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ps locale. + */ + private static function getPluralCategoryPs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the pt locale. + */ + private static function getPluralCategoryPt(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($i, 0, 1)) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the pt-PT locale. + */ + private static function getPluralCategoryPt_PT(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the rm locale. + */ + private static function getPluralCategoryRm(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ro locale. + */ + private static function getPluralCategoryRo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($v !== 0 || $n === 0 || $n !== 1 && self::inRange($n % 100, 1, 19)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the rof locale. + */ + private static function getPluralCategoryRof(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ru locale. + */ + private static function getPluralCategoryRu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11) { + return 'one'; + } + + if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { + return 'few'; + } + + if ($v === 0 && ($i % 10) === 0 || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 11, 14)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the rwk locale. + */ + private static function getPluralCategoryRwk(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sah locale. + */ + private static function getPluralCategorySah(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the saq locale. + */ + private static function getPluralCategorySaq(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sat locale. + */ + private static function getPluralCategorySat(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sc locale. + */ + private static function getPluralCategorySc(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the scn locale. + */ + private static function getPluralCategoryScn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sd locale. + */ + private static function getPluralCategorySd(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sdh locale. + */ + private static function getPluralCategorySdh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the se locale. + */ + private static function getPluralCategorySe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the seh locale. + */ + private static function getPluralCategorySeh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ses locale. + */ + private static function getPluralCategorySes(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the sg locale. + */ + private static function getPluralCategorySg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the sh locale. + */ + private static function getPluralCategorySh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + return 'one'; + } + + if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the shi locale. + */ + private static function getPluralCategoryShi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + if (self::inRange($n, 2, 10)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the si locale. + */ + private static function getPluralCategorySi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 0 || $n === 1 || $i === 0 && $f === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sk locale. + */ + private static function getPluralCategorySk(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if (self::inRange($i, 2, 4) && $v === 0) { + return 'few'; + } + + if ($v !== 0) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sl locale. + */ + private static function getPluralCategorySl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 100) === 1) { + return 'one'; + } + + if ($v === 0 && ($i % 100) === 2) { + return 'two'; + } + + if ($v === 0 && self::inRange($i % 100, 3, 4) || $v !== 0) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sma locale. + */ + private static function getPluralCategorySma(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the smi locale. + */ + private static function getPluralCategorySmi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the smj locale. + */ + private static function getPluralCategorySmj(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the smn locale. + */ + private static function getPluralCategorySmn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sms locale. + */ + private static function getPluralCategorySms(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + if ($n === 2) { + return 'two'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sn locale. + */ + private static function getPluralCategorySn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the so locale. + */ + private static function getPluralCategorySo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sq locale. + */ + private static function getPluralCategorySq(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sr locale. + */ + private static function getPluralCategorySr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { + return 'one'; + } + + if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14) || self::inRange($f % 10, 2, 4) && ! self::inRange($f % 100, 12, 14)) { + return 'few'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ss locale. + */ + private static function getPluralCategorySs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ssy locale. + */ + private static function getPluralCategorySsy(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the st locale. + */ + private static function getPluralCategorySt(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the su locale. + */ + private static function getPluralCategorySu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the sv locale. + */ + private static function getPluralCategorySv(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the sw locale. + */ + private static function getPluralCategorySw(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the syr locale. + */ + private static function getPluralCategorySyr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ta locale. + */ + private static function getPluralCategoryTa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the te locale. + */ + private static function getPluralCategoryTe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the teo locale. + */ + private static function getPluralCategoryTeo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the th locale. + */ + private static function getPluralCategoryTh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the ti locale. + */ + private static function getPluralCategoryTi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the tig locale. + */ + private static function getPluralCategoryTig(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the tk locale. + */ + private static function getPluralCategoryTk(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the tl locale. + */ + private static function getPluralCategoryTl(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ( + $v === 0 && ($i === 1 || $i === 2 || $i === 3) || + $v === 0 && ! (($i % 10) === 4 || ($i % 10) === 6 || ($i % 10) === 9) || + $v !== 0 && ! (($f % 10) === 4 || ($f % 10) === 6 || ($f % 10) === 9) + ) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the tn locale. + */ + private static function getPluralCategoryTn(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the to locale. + */ + private static function getPluralCategoryTo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the tpi locale. + */ + private static function getPluralCategoryTpi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the tr locale. + */ + private static function getPluralCategoryTr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ts locale. + */ + private static function getPluralCategoryTs(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the tzm locale. + */ + private static function getPluralCategoryTzm(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1) || self::inRange($n, 11, 99)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ug locale. + */ + private static function getPluralCategoryUg(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the uk locale. + */ + private static function getPluralCategoryUk(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11) { + return 'one'; + } + + if ($v === 0 && self::inRange($i % 10, 2, 4) && ! self::inRange($i % 100, 12, 14)) { + return 'few'; + } + + if ($v === 0 && ($i % 10) === 0 || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 11, 14)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the und locale. + */ + private static function getPluralCategoryUnd(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the ur locale. + */ + private static function getPluralCategoryUr(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the uz locale. + */ + private static function getPluralCategoryUz(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the ve locale. + */ + private static function getPluralCategoryVe(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the vec locale. + */ + private static function getPluralCategoryVec(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { + return 'many'; + } + + return 'other'; + } + + /** + * Gets the plural category for the vi locale. + */ + private static function getPluralCategoryVi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the vo locale. + */ + private static function getPluralCategoryVo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the vun locale. + */ + private static function getPluralCategoryVun(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the wa locale. + */ + private static function getPluralCategoryWa(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if (self::inRange($n, 0, 1)) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the wae locale. + */ + private static function getPluralCategoryWae(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the wo locale. + */ + private static function getPluralCategoryWo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the xh locale. + */ + private static function getPluralCategoryXh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the xog locale. + */ + private static function getPluralCategoryXog(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the yi locale. + */ + private static function getPluralCategoryYi(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 1 && $v === 0) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for the yo locale. + */ + private static function getPluralCategoryYo(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the yue locale. + */ + private static function getPluralCategoryYue(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the zh locale. + */ + private static function getPluralCategoryZh(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + return 'other'; + } + + /** + * Gets the plural category for the zu locale. + */ + private static function getPluralCategoryZu(float|int $n): string + { + $i = self::getIntegerPart($n); + $v = self::getVisibleFractionalDigits($n); + $f = self::getFractionalDigits($n); + $t = self::getCompactExponent($n); + $e = self::getExponent($n); + + if ($i === 0 || $n === 1) { + return 'one'; + } + + return 'other'; + } + + /** + * Gets the plural category for a number in the specified locale. + */ + public static function getPluralCategory(Locale $locale, float|int $number): string + { + return match ($locale->getLanguage()) { + 'af' => self::getPluralCategoryAf($number), + 'ak' => self::getPluralCategoryAk($number), + 'am' => self::getPluralCategoryAm($number), + 'an' => self::getPluralCategoryAn($number), + 'ar' => self::getPluralCategoryAr($number), + 'ars' => self::getPluralCategoryArs($number), + 'as' => self::getPluralCategoryAs($number), + 'asa' => self::getPluralCategoryAsa($number), + 'ast' => self::getPluralCategoryAst($number), + 'az' => self::getPluralCategoryAz($number), + 'bal' => self::getPluralCategoryBal($number), + 'be' => self::getPluralCategoryBe($number), + 'bem' => self::getPluralCategoryBem($number), + 'bez' => self::getPluralCategoryBez($number), + 'bg' => self::getPluralCategoryBg($number), + 'bho' => self::getPluralCategoryBho($number), + 'blo' => self::getPluralCategoryBlo($number), + 'bm' => self::getPluralCategoryBm($number), + 'bn' => self::getPluralCategoryBn($number), + 'bo' => self::getPluralCategoryBo($number), + 'br' => self::getPluralCategoryBr($number), + 'brx' => self::getPluralCategoryBrx($number), + 'bs' => self::getPluralCategoryBs($number), + 'ca' => self::getPluralCategoryCa($number), + 'ce' => self::getPluralCategoryCe($number), + 'ceb' => self::getPluralCategoryCeb($number), + 'cgg' => self::getPluralCategoryCgg($number), + 'chr' => self::getPluralCategoryChr($number), + 'ckb' => self::getPluralCategoryCkb($number), + 'cs' => self::getPluralCategoryCs($number), + 'csw' => self::getPluralCategoryCsw($number), + 'cy' => self::getPluralCategoryCy($number), + 'da' => self::getPluralCategoryDa($number), + 'de' => self::getPluralCategoryDe($number), + 'doi' => self::getPluralCategoryDoi($number), + 'dsb' => self::getPluralCategoryDsb($number), + 'dv' => self::getPluralCategoryDv($number), + 'dz' => self::getPluralCategoryDz($number), + 'ee' => self::getPluralCategoryEe($number), + 'el' => self::getPluralCategoryEl($number), + 'en' => self::getPluralCategoryEn($number), + 'eo' => self::getPluralCategoryEo($number), + 'es' => self::getPluralCategoryEs($number), + 'et' => self::getPluralCategoryEt($number), + 'eu' => self::getPluralCategoryEu($number), + 'fa' => self::getPluralCategoryFa($number), + 'ff' => self::getPluralCategoryFf($number), + 'fi' => self::getPluralCategoryFi($number), + 'fil' => self::getPluralCategoryFil($number), + 'fo' => self::getPluralCategoryFo($number), + 'fr' => self::getPluralCategoryFr($number), + 'fur' => self::getPluralCategoryFur($number), + 'fy' => self::getPluralCategoryFy($number), + 'ga' => self::getPluralCategoryGa($number), + 'gd' => self::getPluralCategoryGd($number), + 'gl' => self::getPluralCategoryGl($number), + 'gsw' => self::getPluralCategoryGsw($number), + 'gu' => self::getPluralCategoryGu($number), + 'guw' => self::getPluralCategoryGuw($number), + 'gv' => self::getPluralCategoryGv($number), + 'ha' => self::getPluralCategoryHa($number), + 'haw' => self::getPluralCategoryHaw($number), + 'he' => self::getPluralCategoryHe($number), + 'hi' => self::getPluralCategoryHi($number), + 'hnj' => self::getPluralCategoryHnj($number), + 'hr' => self::getPluralCategoryHr($number), + 'hsb' => self::getPluralCategoryHsb($number), + 'hu' => self::getPluralCategoryHu($number), + 'hy' => self::getPluralCategoryHy($number), + 'ia' => self::getPluralCategoryIa($number), + 'id' => self::getPluralCategoryId($number), + 'ig' => self::getPluralCategoryIg($number), + 'ii' => self::getPluralCategoryIi($number), + 'io' => self::getPluralCategoryIo($number), + 'is' => self::getPluralCategoryIs($number), + 'it' => self::getPluralCategoryIt($number), + 'iu' => self::getPluralCategoryIu($number), + 'ja' => self::getPluralCategoryJa($number), + 'jbo' => self::getPluralCategoryJbo($number), + 'jgo' => self::getPluralCategoryJgo($number), + 'jmc' => self::getPluralCategoryJmc($number), + 'jv' => self::getPluralCategoryJv($number), + 'jw' => self::getPluralCategoryJw($number), + 'ka' => self::getPluralCategoryKa($number), + 'kab' => self::getPluralCategoryKab($number), + 'kaj' => self::getPluralCategoryKaj($number), + 'kcg' => self::getPluralCategoryKcg($number), + 'kde' => self::getPluralCategoryKde($number), + 'kea' => self::getPluralCategoryKea($number), + 'kk' => self::getPluralCategoryKk($number), + 'kkj' => self::getPluralCategoryKkj($number), + 'kl' => self::getPluralCategoryKl($number), + 'km' => self::getPluralCategoryKm($number), + 'kn' => self::getPluralCategoryKn($number), + 'ko' => self::getPluralCategoryKo($number), + 'ks' => self::getPluralCategoryKs($number), + 'ksb' => self::getPluralCategoryKsb($number), + 'ksh' => self::getPluralCategoryKsh($number), + 'ku' => self::getPluralCategoryKu($number), + 'kw' => self::getPluralCategoryKw($number), + 'ky' => self::getPluralCategoryKy($number), + 'lag' => self::getPluralCategoryLag($number), + 'lb' => self::getPluralCategoryLb($number), + 'lg' => self::getPluralCategoryLg($number), + 'lij' => self::getPluralCategoryLij($number), + 'lkt' => self::getPluralCategoryLkt($number), + 'lld' => self::getPluralCategoryLld($number), + 'ln' => self::getPluralCategoryLn($number), + 'lo' => self::getPluralCategoryLo($number), + 'lt' => self::getPluralCategoryLt($number), + 'lv' => self::getPluralCategoryLv($number), + 'mas' => self::getPluralCategoryMas($number), + 'mg' => self::getPluralCategoryMg($number), + 'mgo' => self::getPluralCategoryMgo($number), + 'mk' => self::getPluralCategoryMk($number), + 'ml' => self::getPluralCategoryMl($number), + 'mn' => self::getPluralCategoryMn($number), + 'mo' => self::getPluralCategoryMo($number), + 'mr' => self::getPluralCategoryMr($number), + 'ms' => self::getPluralCategoryMs($number), + 'mt' => self::getPluralCategoryMt($number), + 'my' => self::getPluralCategoryMy($number), + 'nah' => self::getPluralCategoryNah($number), + 'naq' => self::getPluralCategoryNaq($number), + 'nb' => self::getPluralCategoryNb($number), + 'nd' => self::getPluralCategoryNd($number), + 'ne' => self::getPluralCategoryNe($number), + 'nl' => self::getPluralCategoryNl($number), + 'nn' => self::getPluralCategoryNn($number), + 'nnh' => self::getPluralCategoryNnh($number), + 'no' => self::getPluralCategoryNo($number), + 'nqo' => self::getPluralCategoryNqo($number), + 'nr' => self::getPluralCategoryNr($number), + 'nso' => self::getPluralCategoryNso($number), + 'ny' => self::getPluralCategoryNy($number), + 'nyn' => self::getPluralCategoryNyn($number), + 'om' => self::getPluralCategoryOm($number), + 'or' => self::getPluralCategoryOr($number), + 'os' => self::getPluralCategoryOs($number), + 'osa' => self::getPluralCategoryOsa($number), + 'pa' => self::getPluralCategoryPa($number), + 'pap' => self::getPluralCategoryPap($number), + 'pcm' => self::getPluralCategoryPcm($number), + 'pl' => self::getPluralCategoryPl($number), + 'prg' => self::getPluralCategoryPrg($number), + 'ps' => self::getPluralCategoryPs($number), + 'pt' => self::getPluralCategoryPt($number), + 'pt-PT' => self::getPluralCategoryPt_PT($number), + 'rm' => self::getPluralCategoryRm($number), + 'ro' => self::getPluralCategoryRo($number), + 'rof' => self::getPluralCategoryRof($number), + 'ru' => self::getPluralCategoryRu($number), + 'rwk' => self::getPluralCategoryRwk($number), + 'sah' => self::getPluralCategorySah($number), + 'saq' => self::getPluralCategorySaq($number), + 'sat' => self::getPluralCategorySat($number), + 'sc' => self::getPluralCategorySc($number), + 'scn' => self::getPluralCategoryScn($number), + 'sd' => self::getPluralCategorySd($number), + 'sdh' => self::getPluralCategorySdh($number), + 'se' => self::getPluralCategorySe($number), + 'seh' => self::getPluralCategorySeh($number), + 'ses' => self::getPluralCategorySes($number), + 'sg' => self::getPluralCategorySg($number), + 'sh' => self::getPluralCategorySh($number), + 'shi' => self::getPluralCategoryShi($number), + 'si' => self::getPluralCategorySi($number), + 'sk' => self::getPluralCategorySk($number), + 'sl' => self::getPluralCategorySl($number), + 'sma' => self::getPluralCategorySma($number), + 'smi' => self::getPluralCategorySmi($number), + 'smj' => self::getPluralCategorySmj($number), + 'smn' => self::getPluralCategorySmn($number), + 'sms' => self::getPluralCategorySms($number), + 'sn' => self::getPluralCategorySn($number), + 'so' => self::getPluralCategorySo($number), + 'sq' => self::getPluralCategorySq($number), + 'sr' => self::getPluralCategorySr($number), + 'ss' => self::getPluralCategorySs($number), + 'ssy' => self::getPluralCategorySsy($number), + 'st' => self::getPluralCategorySt($number), + 'su' => self::getPluralCategorySu($number), + 'sv' => self::getPluralCategorySv($number), + 'sw' => self::getPluralCategorySw($number), + 'syr' => self::getPluralCategorySyr($number), + 'ta' => self::getPluralCategoryTa($number), + 'te' => self::getPluralCategoryTe($number), + 'teo' => self::getPluralCategoryTeo($number), + 'th' => self::getPluralCategoryTh($number), + 'ti' => self::getPluralCategoryTi($number), + 'tig' => self::getPluralCategoryTig($number), + 'tk' => self::getPluralCategoryTk($number), + 'tl' => self::getPluralCategoryTl($number), + 'tn' => self::getPluralCategoryTn($number), + 'to' => self::getPluralCategoryTo($number), + 'tpi' => self::getPluralCategoryTpi($number), + 'tr' => self::getPluralCategoryTr($number), + 'ts' => self::getPluralCategoryTs($number), + 'tzm' => self::getPluralCategoryTzm($number), + 'ug' => self::getPluralCategoryUg($number), + 'uk' => self::getPluralCategoryUk($number), + 'und' => self::getPluralCategoryUnd($number), + 'ur' => self::getPluralCategoryUr($number), + 'uz' => self::getPluralCategoryUz($number), + 've' => self::getPluralCategoryVe($number), + 'vec' => self::getPluralCategoryVec($number), + 'vi' => self::getPluralCategoryVi($number), + 'vo' => self::getPluralCategoryVo($number), + 'vun' => self::getPluralCategoryVun($number), + 'wa' => self::getPluralCategoryWa($number), + 'wae' => self::getPluralCategoryWae($number), + 'wo' => self::getPluralCategoryWo($number), + 'xh' => self::getPluralCategoryXh($number), + 'xog' => self::getPluralCategoryXog($number), + 'yi' => self::getPluralCategoryYi($number), + 'yo' => self::getPluralCategoryYo($number), + 'yue' => self::getPluralCategoryYue($number), + 'zh' => self::getPluralCategoryZh($number), + 'zu' => self::getPluralCategoryZu($number), + default => 'other', + }; + } + + /** + * Gets all supported locales. + */ + public static function getSupportedLocales(): array + { + return [ + 'af', + 'ak', + 'am', + 'an', + 'ar', + 'ars', + 'as', + 'asa', + 'ast', + 'az', + 'bal', + 'be', + 'bem', + 'bez', + 'bg', + 'bho', + 'blo', + 'bm', + 'bn', + 'bo', + 'br', + 'brx', + 'bs', + 'ca', + 'ce', + 'ceb', + 'cgg', + 'chr', + 'ckb', + 'cs', + 'csw', + 'cy', + 'da', + 'de', + 'doi', + 'dsb', + 'dv', + 'dz', + 'ee', + 'el', + 'en', + 'eo', + 'es', + 'et', + 'eu', + 'fa', + 'ff', + 'fi', + 'fil', + 'fo', + 'fr', + 'fur', + 'fy', + 'ga', + 'gd', + 'gl', + 'gsw', + 'gu', + 'guw', + 'gv', + 'ha', + 'haw', + 'he', + 'hi', + 'hnj', + 'hr', + 'hsb', + 'hu', + 'hy', + 'ia', + 'id', + 'ig', + 'ii', + 'io', + 'is', + 'it', + 'iu', + 'ja', + 'jbo', + 'jgo', + 'jmc', + 'jv', + 'jw', + 'ka', + 'kab', + 'kaj', + 'kcg', + 'kde', + 'kea', + 'kk', + 'kkj', + 'kl', + 'km', + 'kn', + 'ko', + 'ks', + 'ksb', + 'ksh', + 'ku', + 'kw', + 'ky', + 'lag', + 'lb', + 'lg', + 'lij', + 'lkt', + 'lld', + 'ln', + 'lo', + 'lt', + 'lv', + 'mas', + 'mg', + 'mgo', + 'mk', + 'ml', + 'mn', + 'mo', + 'mr', + 'ms', + 'mt', + 'my', + 'nah', + 'naq', + 'nb', + 'nd', + 'ne', + 'nl', + 'nn', + 'nnh', + 'no', + 'nqo', + 'nr', + 'nso', + 'ny', + 'nyn', + 'om', + 'or', + 'os', + 'osa', + 'pa', + 'pap', + 'pcm', + 'pl', + 'prg', + 'ps', + 'pt', + 'pt-PT', + 'rm', + 'ro', + 'rof', + 'ru', + 'rwk', + 'sah', + 'saq', + 'sat', + 'sc', + 'scn', + 'sd', + 'sdh', + 'se', + 'seh', + 'ses', + 'sg', + 'sh', + 'shi', + 'si', + 'sk', + 'sl', + 'sma', + 'smi', + 'smj', + 'smn', + 'sms', + 'sn', + 'so', + 'sq', + 'sr', + 'ss', + 'ssy', + 'st', + 'su', + 'sv', + 'sw', + 'syr', + 'ta', + 'te', + 'teo', + 'th', + 'ti', + 'tig', + 'tk', + 'tl', + 'tn', + 'to', + 'tpi', + 'tr', + 'ts', + 'tzm', + 'ug', + 'uk', + 'und', + 'ur', + 'uz', + 've', + 'vec', + 'vi', + 'vo', + 'vun', + 'wa', + 'wae', + 'wo', + 'xh', + 'xog', + 'yi', + 'yo', + 'yue', + 'zh', + 'zu', + ]; + } +} diff --git a/packages/support/src/Pluralizer/InflectorPluralizer.php b/packages/intl/src/Pluralizer/InflectorPluralizer.php similarity index 67% rename from packages/support/src/Pluralizer/InflectorPluralizer.php rename to packages/intl/src/Pluralizer/InflectorPluralizer.php index 1c19716588..cb8b04ae53 100644 --- a/packages/support/src/Pluralizer/InflectorPluralizer.php +++ b/packages/intl/src/Pluralizer/InflectorPluralizer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Support\Pluralizer; +namespace Tempest\Intl\Pluralizer; use Countable; use Doctrine\Inflector\Inflector; @@ -37,6 +37,24 @@ public function singularize(Stringable|string $value): string return $this->matchCase($this->inflector->singularize((string) $value), $value); } + public function singularizeLastWord(Stringable|string $value): string + { + $string = (string) $value; + $parts = preg_split('/(.)(?=[A-Z])/u', $string, flags: PREG_SPLIT_DELIM_CAPTURE); + $lastWord = array_pop($parts); + + return implode('', $parts) . $this->singularize($lastWord); + } + + public function pluralizeLastWord(Stringable|string $value, int|array|Countable $count = 2): string + { + $string = (string) $value; + $parts = preg_split('/(.)(?=[A-Z])/u', $string, flags: PREG_SPLIT_DELIM_CAPTURE); + $lastWord = array_pop($parts); + + return implode('', $parts) . $this->pluralize($lastWord, $count); + } + private function matchCase(Stringable|string $value, Stringable|string $comparison): string { $value = (string) $value; diff --git a/packages/intl/src/Pluralizer/Pluralizer.php b/packages/intl/src/Pluralizer/Pluralizer.php new file mode 100644 index 0000000000..23f694d3dd --- /dev/null +++ b/packages/intl/src/Pluralizer/Pluralizer.php @@ -0,0 +1,31 @@ +isLocale($locale = str($path)->beforeLast('.')->afterLast('.')->toString())) { + return; + } + + if (! is_file($path)) { + return; + } + + $this->discoveryItems->add($location, [$path, $locale]); + } + + public function apply(): void + { + foreach ($this->discoveryItems as [$path, $locale]) { + $this->config->addTranslationMessageFile(Locale::from($locale), $path); + } + } + + private function isLocale(string $candidate): bool + { + $locale = arr(Locale::cases()) + ->first(function (Locale $locale) use ($candidate) { + if (strtolower($locale->value) === strtolower($candidate)) { + return true; + } + + return strtolower($locale->getLanguage()) === strtolower($candidate); + }); + + return ! is_null($locale); + } +} diff --git a/packages/intl/src/TranslationMiss.php b/packages/intl/src/TranslationMiss.php new file mode 100644 index 0000000000..394a1647db --- /dev/null +++ b/packages/intl/src/TranslationMiss.php @@ -0,0 +1,13 @@ +translate('hello', name: 'Jon Doe'); // Hello, Jon Doe! + * ``` + */ + public function translate(string $key, mixed ...$arguments): string; + + /** + * Translates the given key for a specific locale with optional arguments. + * + * **Example** + * ```php + * $translator->translate(Locale::FRENCH, 'hello', name: 'Jon Doe'); // Bonjour, Jon Doe! + * ``` + */ + public function translateForLocale(Locale $locale, string $key, mixed ...$arguments): string; +} diff --git a/packages/intl/src/TranslatorInitializer.php b/packages/intl/src/TranslatorInitializer.php new file mode 100644 index 0000000000..477b7a77df --- /dev/null +++ b/packages/intl/src/TranslatorInitializer.php @@ -0,0 +1,25 @@ +get(IntlConfig::class), + catalog: $container->get(Catalog::class), + formatter: $container->get(MessageFormatter::class), + eventBus: $container->get(EventBus::class), + ); + } +} diff --git a/packages/intl/src/functions.php b/packages/intl/src/functions.php new file mode 100644 index 0000000000..af0972ff99 --- /dev/null +++ b/packages/intl/src/functions.php @@ -0,0 +1,59 @@ +translate($key, ...$arguments); +} + +/** + * Translates the given key for a specific locale with optional arguments. + */ +function translate_locale(Locale $locale, string $key, mixed ...$arguments): string +{ + return get(Translator::class)->translateForLocale($locale, $key, ...$arguments); +} + +/** + * Converts the given string to its English plural form. + */ +function pluralize(Stringable|string $value, int|array|Countable $count = 2): string +{ + return get(Pluralizer::class)->pluralize($value, $count); +} + +/** + * Converts the given string to its English singular form. + */ +function singularize(Stringable|string $value): string +{ + return get(Pluralizer::class)->singularize($value); +} + +/** + * Converts the last word of the given string to its English singular form. + */ +function singularize_last_word(Stringable|string $value): string +{ + return get(Pluralizer::class)->singularizeLastWord($value); +} + +/** + * Converts the last word of the given string to its English plural form. + */ +function pluralize_last_word(Stringable|string $value, int|array|Countable $count = 2): string +{ + return get(Pluralizer::class)->pluralizeLastWord($value, $count); +} diff --git a/packages/intl/src/i18n.config.php b/packages/intl/src/i18n.config.php new file mode 100644 index 0000000000..c80522313f --- /dev/null +++ b/packages/intl/src/i18n.config.php @@ -0,0 +1,9 @@ +here.'])] + #[TestWith(['This is {#strong}bold{/strong}.', 'This is bold.'])] + public function test_html_tag_markup(string $input, string $expected): void + { + $formatter = new MessageFormatter( + markupFormatters: [new HtmlTagFormatter()], + ); + + $this->assertSame($expected, $formatter->format($input)); + } + + #[TestWith(['Hello{#br/}World', 'Hello
World'])] + public function test_void_html_tag_markup(string $input, string $expected): void + { + $formatter = new MessageFormatter( + markupFormatters: [new VoidHtmlTagFormatter()], + ); + + $this->assertSame($expected, $formatter->format($input)); + } + + public function test_placeholder_variable(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + Hello, {$name}! + TXT, name: 'Jon'); + + $this->assertSame('Hello, Jon!', $value); + } + + public function test_format_datetime_function_and_parameters(): void + { + $formatter = new MessageFormatter([new DateTimeFunction()]); + + $value = $formatter->format(<<<'TXT' + Today is {$today :datetime pattern=|yyyy/MM/dd|}. + TXT, today: '2024-01-01'); + + $this->assertSame('Today is 2024/01/01.', $value); + } + + public function test_format_number_function(): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + + $value = $formatter->format(<<<'TXT' + The total was {31 :number style=percent}. + TXT); + + $this->assertSame('The total was 31%.', $value); + } + + #[TestWith([0, "pas d'avion"])] + #[TestWith([1, 'un avion'])] + #[TestWith([5, '5 avions'])] + public function test_match_number(int $count, string $expected): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + + $value = $formatter->format(<<<'TXT' + .input {$aircraft :number} + .match $aircraft + 0 {{pas d'avion}} + 1 {{un avion}} + * {{{$aircraft} avions}} + TXT, aircraft: $count); + + $this->assertSame($expected, $value); + } + + public function test_unquoted_text(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + Hello, {world}! + TXT); + + $this->assertSame('Hello, world!', $value); + } + + public function test_quoted_text(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + My name is {|John Doe|}. + TXT); + + $this->assertSame('My name is John Doe.', $value); + } + + public function test_number_matcher(): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + $value = $formatter->format(<<<'TXT' + .input {$count :number} + .match $count + one {{You have {$count} notification.}} + * {{You have {$count} notifications.}} + TXT, count: 1); + + $this->assertSame('You have 1 notification.', $value); + } + + public function test_number_matcher_exact(): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + $value = $formatter->format(<<<'TXT' + .input {$count :number select=exact} + .match $count + one {{You have {$count} notification.}} + * {{You have {$count} notifications.}} + TXT, count: 1); + + $this->assertSame('You have 1 notifications.', $value); + } + + public function test_local_declaration(): void + { + $formatter = new MessageFormatter([new StringFunction()]); + $value = $formatter->format(<<<'TXT' + .local $val = {foo2 :string} + .match $val + foo {{Foo}} + bar {{Bar}} + * {{No match}} + TXT); + + $this->assertSame('No match', $value); + } + + public function test_local_declarations_unquoted_literals(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + .local $x = {42} + .local $y = {number42} + .local $z = {_number} + {{{$x} {$y} {$z}}} + TXT); + + $this->assertSame('42 number42 _number', $value); + } + + public function test_local_declarations_quoted_literals(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + .local $x = {|@literal|} + .local $y = {|white space|} + .local $z = {|{{curly braces}}|} + {{{$x} {$y} {$z} {|and \\, a backslash|}}} + TXT); + + $this->assertSame('@literal white space {{curly braces}} and \, a backslash', $value); + } + + public function test_whitespace(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + .input {$num :number} + {{ This is the {$num} pattern }} + TXT, num: 5); + + $this->assertSame(' This is the 5 pattern ', $value); + } + + public function test_escape(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + Backslash: \\, left curly brace \{, right curly brace \} + TXT); + + $this->assertSame('Backslash: \, left curly brace {, right curly brace }', $value); + } + + public function test_matchers_escape(): void + { + $formatter = new MessageFormatter(); + $value = $formatter->format(<<<'TXT' + .input {$char :string} + .match $char + | | {{You entered a space character.}} + |\|| {{You entered a pipe character.}} + * {{You entered something else.}} + TXT, char: '|'); + + $this->assertSame('You entered a pipe character.', $value); + } + + public function test_matchers_number_exact_match(): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + + $value = $formatter->format(<<<'TXT' + .input {$numDays :number select=exact} + .match $numDays + 1 {{{$numDays} one}} + 2 {{{$numDays} two}} + 3 {{{$numDays} three}} + TXT, numDays: 2); + + $this->assertSame('2 two', $value); + } + + #[TestWith([1, '1 den'])] + #[TestWith([2, '2 dny'])] + #[TestWith([1.5, '1.5 dne'])] + #[TestWith([5, '5 dní'])] + public function test_matchers_czech(int|float $days, string $expected): void + { + locale_set_default(Locale::CZECH->value); + + $formatter = new MessageFormatter([$this->createNumberFunction()]); + + $value = $formatter->format(<<<'TXT' + .input {$days :number} + .match $days + one {{{$days} den}} + few {{{$days} dny}} + many {{{$days} dne}} + * {{{$days} dní}} + TXT, days: $days); + + $this->assertSame($expected, $value); + } + + public function test_string_function(): void + { + $formatter = new MessageFormatter([ + new StringFunction(), + $this->createNumberFunction(), + ]); + + $value = $formatter->format(<<<'TXT' + .input {$operand :string} + .match $operand + 1 {{Number 1}} + one {{String "one"}} + * {{Something else}} + TXT, operand: 1); + + $this->assertSame('Number 1', $value); + } + + #[TestWith(['value', 'value'])] + #[TestWith([1, '1'])] + #[TestWith([1.1, '1.1'])] + #[TestWith([['name' => 'Jon'], ''])] + public function test_string_formatting(mixed $input, string $expected): void + { + $formatter = new MessageFormatter([new StringFunction()]); + + $value = $formatter->format(<<<'TXT' + {$value :string} + TXT, value: $input); + + $this->assertSame($expected, $value); + } + + #[TestWith(['value', 'VALUE', 'upper'])] + #[TestWith(['VALUE', 'value', 'lower'])] + #[TestWith(['my value', 'my_value', 'snake'])] + #[TestWith(['my value', 'my-value', 'kebab'])] + #[TestWith(['my value', 'My value', 'sentence'])] + #[TestWith(['my value', 'myValue', 'camel'])] + #[TestWith(['my value', 'My Value', 'title'])] + public function test_string_formatting_options(mixed $input, string $expected, string $style): void + { + $formatter = new MessageFormatter([new StringFunction()]); + + $value = $formatter->format(<<assertSame($expected, $value); + } + + public function test_number_currency(): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + + $value = $formatter->format(<<<'TXT' + You have {42 :number style=currency currency=$currency}. + TXT, currency: Currency::USD); + + $this->assertSame('You have $42.00.', $value); + } + + public function test_shadowing(): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + + $value = $formatter->format(<<<'TXT' + .local $count = {42} + {{The count is: {$count}}} + TXT, count: 32); + + $this->assertSame('The count is: 42', $value); + } + + #[TestWith([0, 'No items.'])] + #[TestWith([1, '1 item.'])] + #[TestWith([5, '5 items.'])] + public function test_pluralization(int $count, string $expected): void + { + $formatter = new MessageFormatter([$this->createNumberFunction()]); + + $value = $formatter->format(<<<'TXT' + .input {$count :number} + .match $count + 0 {{No items.}} + one {{1 item.}} + * {{{$count} items.}} + TXT, count: $count); + + $this->assertSame($expected, $value); + } + + public function test_multiple_selectors(): void + { + $formatter = new MessageFormatter([ + $this->createNumberFunction(), + new DateTimeFunction(), + ]); + + $value = $formatter->format(<<<'TXT' + .input {$hostGender :string} + .input {$guestCount :number} + .match $hostGender $guestCount + female 0 {{{$hostName} does not give a party.}} + female 1 {{{$hostName} invites {$guestName} to her party.}} + female 2 {{{$hostName} invites {$guestName} and one other person to her party.}} + female * {{{$hostName} invites {$guestCount} people, including {$guestName}, to her party.}} + male 0 {{{$hostName} does not give a party.}} + male 1 {{{$hostName} invites {$guestName} to his party.}} + male 2 {{{$hostName} invites {$guestName} and one other person to his party.}} + male * {{{$hostName} invites {$guestCount} people, including {$guestName}, to his party.}} + * 0 {{{$hostName} does not give a party.}} + * 1 {{{$hostName} invites {$guestName} to their party.}} + * 2 {{{$hostName} invites {$guestName} and one other person to their party.}} + * * {{{$hostName} invites {$guestCount} people, including {$guestName}, to their party.}} + TXT, hostGender: 'female', hostName: 'Alice', guestCount: 2, guestName: 'Bob'); + + $this->assertSame('Alice invites Bob and one other person to her party.', $value); + } + + public function test_custom_function(): void + { + $formatter = new MessageFormatter([ + new class implements FormattingFunction { + public string $name = 'uppercase'; + + public function format(mixed $value, array $parameters): FormattedValue + { + return new FormattedValue($value, mb_strtoupper($value)); + } + }, + ]); + + $value = $formatter->format(<<<'TXT' + Check out {MessageFormat :uppercase}. + TXT); + + $this->assertSame('Check out MESSAGEFORMAT.', $value); + } + + private function createNumberFunction(): NumberFunction + { + return new NumberFunction( + new IntlConfig(Locale::default(), Locale::default()), + ); + } +} diff --git a/packages/support/tests/Number/FunctionsTest.php b/packages/intl/tests/FunctionsTest.php similarity index 89% rename from packages/support/tests/Number/FunctionsTest.php rename to packages/intl/tests/FunctionsTest.php index 71e90066a8..8f06d96bc4 100644 --- a/packages/support/tests/Number/FunctionsTest.php +++ b/packages/intl/tests/FunctionsTest.php @@ -1,17 +1,18 @@ assertSame('0', Number\format(0)); $this->assertSame('0', Number\format(0.0)); @@ -247,4 +248,38 @@ public function test_summarize(): void $this->assertSame('-1Q', Number\to_human_readable(-1000000000000000)); $this->assertSame('-1KQ', Number\to_human_readable(-1000000000000000000)); } + + public function test_parse_int(): void + { + $this->assertSame(1, Number\parse_int(1)); + $this->assertSame(1, Number\parse_int(1, default: 0)); + $this->assertSame(1, Number\parse_int('1')); + $this->assertSame(0, Number\parse_int('1.1')); + $this->assertSame(0, Number\parse_int(1.1)); + $this->assertSame(10, Number\parse_int(1.1, default: 10)); + $this->assertSame(Math\INT64_MAX, Number\parse_int(Math\INT64_MAX)); + } + + public function test_parse_float(): void + { + $this->assertSame(1.0, Number\parse_float(1)); + $this->assertSame(1.0, Number\parse_float(1, default: 0.0)); + $this->assertSame(1.0, Number\parse_float('1')); + $this->assertSame(1.1, Number\parse_float('1.1')); + $this->assertSame(1.1, Number\parse_float(1.1)); + $this->assertSame(10.0, Number\parse_float('abc', default: 10.0)); + $this->assertSame(Math\FLOAT32_MAX, Number\parse_float(Math\FLOAT32_MAX)); + } + + public function test_parse_number(): void + { + $this->assertSame(1, Number\parse(1)); + $this->assertSame(1, Number\parse(1, default: 0)); + $this->assertSame(1, Number\parse('1')); + $this->assertSame(1.1, Number\parse('1.1')); + $this->assertSame(1.1, Number\parse(1.1)); + $this->assertSame(10, Number\parse('abc', default: 10)); + $this->assertSame(Math\INT64_MAX, Number\parse(Math\INT64_MAX)); + $this->assertSame(Math\FLOAT32_MAX, Number\parse(Math\FLOAT32_MAX)); + } } diff --git a/packages/intl/tests/GenericCatalogTest.php b/packages/intl/tests/GenericCatalogTest.php new file mode 100644 index 0000000000..3ccbf5966d --- /dev/null +++ b/packages/intl/tests/GenericCatalogTest.php @@ -0,0 +1,27 @@ +add(Locale::FRENCH, 'hello', 'Bonjour'); + + // Has test + $this->assertTrue($catalog->has(Locale::FRENCH, 'hello')); + $this->assertFalse($catalog->has(Locale::FRENCH, 'goodbye')); + $this->assertFalse($catalog->has(Locale::ENGLISH, 'hello')); + + // Get test + $this->assertSame('Bonjour', $catalog->get(Locale::FRENCH, 'hello')); + + // Fallback test + $this->assertSame('Bonjour', $catalog->get(Locale::FRENCH_FRANCE, 'hello')); + } +} diff --git a/packages/intl/tests/GenericTranslatorTest.php b/packages/intl/tests/GenericTranslatorTest.php new file mode 100644 index 0000000000..fcc76a9f8d --- /dev/null +++ b/packages/intl/tests/GenericTranslatorTest.php @@ -0,0 +1,111 @@ +catalog = new GenericCatalog(); + $this->catalog->add(Locale::FRENCH, 'hello', 'Bonjour!'); + $this->catalog->add(Locale::ENGLISH, 'hello', 'Hello!'); + + $this->config = new IntlConfig( + currentLocale: Locale::FRENCH, + fallbackLocale: Locale::ENGLISH, + ); + + $this->translator = new GenericTranslator( + config: $this->config, + catalog: $this->catalog, + formatter: new MessageFormatter([ + new StringFunction(), + new NumberFunction($this->config), + new DateTimeFunction(), + ]), + ); + } + + public function test_translate(): void + { + // existing + $this->assertSame('Bonjour!', $this->translator->translate('hello')); + + // add to catalog + $this->catalog->add(Locale::ENGLISH, 'goodbye', 'Goodbye!'); + $this->assertSame('Goodbye!', $this->translator->translate('goodbye')); + } + + public function test_fallback(): void + { + $this->config->currentLocale = Locale::FRENCH; + $this->config->fallbackLocale = Locale::ENGLISH; + + $this->catalog->add(Locale::ENGLISH, 'aircraft_count', '{$count :number} aircraft'); + $this->assertSame('2 aircraft', $this->translator->translate('aircraft_count', count: 2)); + } + + public function test_complex_message(): void + { + $this->config->currentLocale = Locale::FRENCH; + + $this->catalog->add(Locale::FRENCH, 'aircraft_count', <<<'MF2' + .input {$aircraft :number} + .match $aircraft + 0 {{pas d'avion}} + 1 {{un avion}} + * {{{$aircraft} avions}} + MF2); + + $this->assertSame("pas d'avion", $this->translator->translate('aircraft_count', aircraft: 0)); + $this->assertSame('un avion', $this->translator->translate('aircraft_count', aircraft: 1)); + $this->assertSame('2 avions', $this->translator->translate('aircraft_count', aircraft: 2)); + } + + public function test_translate_missing(): void + { + $this->assertSame('missing_key', $this->translator->translate('missing_key')); + $this->assertSame('missing.key', $this->translator->translate('missing.key')); + } + + public function test_translate_variables(): void + { + $this->catalog->add(Locale::ENGLISH, 'goodbye_user', 'Goodbye, {$user :string}!'); + + $this->assertSame('Goodbye, Jon Doe!', $this->translator->translate('goodbye_user', user: 'Jon Doe')); + } + + public function test_change_locale(): void + { + $this->config->currentLocale = Locale::ENGLISH; + $this->assertSame('Hello!', $this->translator->translate('hello')); + + $this->config->currentLocale = Locale::FRENCH; + $this->assertSame('Bonjour!', $this->translator->translate('hello')); + } + + public function test_translate_for_locale(): void + { + $this->config->currentLocale = Locale::ENGLISH; + + $this->assertSame('Bonjour!', $this->translator->translateForLocale(Locale::FRENCH, 'hello')); + $this->assertSame('Hello!', $this->translator->translateForLocale(Locale::ENGLISH, 'hello')); + $this->assertSame('missing_key', $this->translator->translateForLocale(Locale::FRENCH, 'missing_key')); + } +} diff --git a/packages/support/tests/Pluralizer/InflectorPluralizerTest.php b/packages/intl/tests/InflectorPluralizerTest.php similarity index 91% rename from packages/support/tests/Pluralizer/InflectorPluralizerTest.php rename to packages/intl/tests/InflectorPluralizerTest.php index e5825dd100..e08727f429 100644 --- a/packages/support/tests/Pluralizer/InflectorPluralizerTest.php +++ b/packages/intl/tests/InflectorPluralizerTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Tempest\Support\Tests\Pluralizer; +namespace Tempest\Intl\Tests; use Countable; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; -use Tempest\Support\Pluralizer\InflectorPluralizer; +use Tempest\Intl\Pluralizer\InflectorPluralizer; /** * @internal diff --git a/packages/support/tests/Language/LocaleTest.php b/packages/intl/tests/LocaleTest.php similarity index 98% rename from packages/support/tests/Language/LocaleTest.php rename to packages/intl/tests/LocaleTest.php index 30040f2742..064ec2df74 100644 --- a/packages/support/tests/Language/LocaleTest.php +++ b/packages/intl/tests/LocaleTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Tempest\Support\Tests\Language; +namespace Tempest\Intl\Tests; use Generator; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use Tempest\Support\Language\Locale; +use Tempest\Intl\Locale; use function locale_get_default; use function locale_set_default; diff --git a/packages/intl/tests/ParserTest.php b/packages/intl/tests/ParserTest.php new file mode 100644 index 0000000000..8a6eaf8d82 --- /dev/null +++ b/packages/intl/tests/ParserTest.php @@ -0,0 +1,74 @@ +parse(); + + $this->assertInstanceOf(SimpleMessage::class, $ast); + $this->assertInstanceOf(Pattern::class, $ast->pattern); + $this->assertInstanceOf(Text::class, $ast->pattern->elements[0]); + $this->assertSame('Hello, world!', $ast->pattern->elements[0]->value); + } + + public function test_local_declaration(): void + { + /** @var ComplexMessage $ast */ + $ast = new MessageFormatParser(<<<'MF2' + .local $time = {$launch_date :datetime style=|medium|} + Launch time: {$time} + MF2)->parse(); + + $this->assertInstanceOf(ComplexMessage::class, $ast); + $this->assertInstanceOf(Text::class, $ast->pattern->elements[0]); + $this->assertInstanceOf(VariableExpression::class, $ast->pattern->elements[1]); + $this->assertInstanceOf(LocalDeclaration::class, $ast->declarations[0]); + $this->assertSame('time', $ast->declarations[0]->variable->name->name); + $this->assertSame('datetime', $ast->declarations[0]->expression->function->identifier->name); + $this->assertSame('style', $ast->declarations[0]->expression->function->options[0]->identifier->name); + $this->assertSame('medium', $ast->declarations[0]->expression->function->options[0]->value->value); + $this->assertSame('launch_date', $ast->declarations[0]->expression->variable->name->name); + } + + public function test_input_declaration(): void + { + /** @var ComplexMessage $ast */ + $ast = new MessageFormatParser(<<<'MF2' + .input {$numDays :number select=exact} + .match $numDays + 1 {{{$numDays} one}} + 2 {{{$numDays} two}} + 3 {{{$numDays} three}} + MF2)->parse(); + + $this->assertInstanceOf(ComplexMessage::class, $ast); + $this->assertSame('numDays', $ast->pattern->elements[0]->pattern->elements[0]->variable->name->name); + $this->assertSame(' one', $ast->pattern->elements[0]->pattern->elements[1]->value); + $this->assertSame('numDays', $ast->pattern->elements[1]->pattern->elements[0]->variable->name->name); + $this->assertSame(' two', $ast->pattern->elements[1]->pattern->elements[1]->value); + $this->assertSame('numDays', $ast->pattern->elements[2]->pattern->elements[0]->variable->name->name); + $this->assertSame(' three', $ast->pattern->elements[2]->pattern->elements[1]->value); + } + + public function test_function_with_option_quoted_literal(): void + { + /** @var ComplexMessage $ast */ + $ast = new MessageFormatParser(<<<'MF2' + Today is {$today :datetime pattern=|yyyy/MM/dd|}. + MF2)->parse(); + + $this->assertSame('yyyy/MM/dd', $ast->pattern->elements[1]->function->options[0]->value->value); + } +} diff --git a/packages/intl/tests/PluralRulesMatcherTest.php b/packages/intl/tests/PluralRulesMatcherTest.php new file mode 100644 index 0000000000..867273ce1d --- /dev/null +++ b/packages/intl/tests/PluralRulesMatcherTest.php @@ -0,0 +1,37 @@ +assertSame('one', $matcher->getPluralCategory(Locale::ENGLISH, 1)); + $this->assertSame('other', $matcher->getPluralCategory(Locale::ENGLISH, 3)); + $this->assertSame('other', $matcher->getPluralCategory(Locale::ENGLISH, 11)); + } + + public function test_ru(): void + { + $matcher = new PluralRulesMatcher(); + + $this->assertSame('one', $matcher->getPluralCategory(Locale::RUSSIAN, 1)); + $this->assertSame('few', $matcher->getPluralCategory(Locale::RUSSIAN, 3)); + $this->assertSame('many', $matcher->getPluralCategory(Locale::RUSSIAN, 11)); + } + + public function test_fr(): void + { + $matcher = new PluralRulesMatcher(); + + $this->assertSame('one', $matcher->getPluralCategory(Locale::FRENCH, 1)); + $this->assertSame('many', $matcher->getPluralCategory(Locale::FRENCH, 1_000_000)); + $this->assertSame('other', $matcher->getPluralCategory(Locale::FRENCH, 5)); + } +} diff --git a/packages/router/src/Static/StaticGenerateCommand.php b/packages/router/src/Static/StaticGenerateCommand.php index 81f383e977..a8b31e9ccf 100644 --- a/packages/router/src/Static/StaticGenerateCommand.php +++ b/packages/router/src/Static/StaticGenerateCommand.php @@ -17,6 +17,7 @@ use Tempest\Http\Method; use Tempest\Http\Status; use Tempest\HttpClient\HttpClient; +use Tempest\Intl; use Tempest\Router\DataProvider; use Tempest\Router\RouteConfig; use Tempest\Router\Router; @@ -32,7 +33,6 @@ use Tempest\Vite\Exceptions\ManifestNotFoundException; use Throwable; -use function Tempest\Support\Language\pluralize; use function Tempest\Support\path; use function Tempest\Support\str; use function Tempest\uri; @@ -83,7 +83,7 @@ public function __invoke( match (true) { $event->exception instanceof DeadLinksDetectedException => $this->keyValue( "{$event->path}", - sprintf("%s DEAD %s", count($event->exception->links), pluralize('LINK', count($event->exception->links))), + sprintf("%s DEAD %s", count($event->exception->links), Intl\pluralize('LINK', count($event->exception->links))), ), $event->exception instanceof InvalidStatusCodeException => $this->keyValue( "{$event->path}", diff --git a/packages/support/composer.json b/packages/support/composer.json index 0e99ed4e9b..15ff589bcf 100644 --- a/packages/support/composer.json +++ b/packages/support/composer.json @@ -5,7 +5,6 @@ "minimum-stability": "dev", "require": { "php": "^8.4", - "doctrine/inflector": "^2.0", "tempest/container": "dev-main", "voku/portable-ascii": "^2.0.3", "symfony/uid": "^7.1" @@ -23,14 +22,12 @@ "src/Random/functions.php", "src/Regex/functions.php", "src/Namespace/functions.php", - "src/Language/functions.php", "src/Path/functions.php", "src/Comparison/functions.php", "src/Math/constants.php", "src/Math/functions.php", "src/Json/functions.php", - "src/Filesystem/functions.php", - "src/Number/functions.php" + "src/Filesystem/functions.php" ] }, "autoload-dev": { diff --git a/packages/support/src/Currency.php b/packages/support/src/Currency.php index d598e4bc74..2d9d306b4d 100644 --- a/packages/support/src/Currency.php +++ b/packages/support/src/Currency.php @@ -5,163 +5,172 @@ /** * Represents an ISO-4217 currency. */ -enum Currency +enum Currency: string { - case AED; - case AFN; - case ALL; - case AMD; - case AOA; - case ARS; - case AUD; - case AWG; - case AZN; - case BAM; - case BBD; - case BDT; - case BGN; - case BHD; - case BIF; - case BMD; - case BND; - case BOB; - case BRL; - case BSD; - case BTN; - case BWP; - case BYN; - case BZD; - case CAD; - case CDF; - case CHF; - case CLP; - case CNY; - case COP; - case CRC; - case CUP; - case CVE; - case CZK; - case DJF; - case DKK; - case DOP; - case DZD; - case EGP; - case ERN; - case ETB; - case EUR; - case FJD; - case FKP; - case GBP; - case GEL; - case GHS; - case GIP; - case GMD; - case GNF; - case GTQ; - case GYD; - case HKD; - case HNL; - case HTG; - case HUF; - case IDR; - case ILS; - case INR; - case IQD; - case IRR; - case ISK; - case JMD; - case JOD; - case JPY; - case KES; - case KGS; - case KHR; - case KMF; - case KPW; - case KRW; - case KWD; - case KYD; - case KZT; - case LAK; - case LBP; - case LKR; - case LRD; - case LSL; - case LYD; - case MAD; - case MDL; - case MGA; - case MKD; - case MMK; - case MNT; - case MOP; - case MRU; - case MUR; - case MVR; - case MWK; - case MXN; - case MYR; - case MZN; - case NAD; - case NGN; - case NIO; - case NOK; - case NPR; - case NZD; - case OMR; - case PAB; - case PEN; - case PGK; - case PHP; - case PKR; - case PLN; - case PYG; - case QAR; - case RON; - case RSD; - case RUB; - case RWF; - case SAR; - case SBD; - case SCR; - case SDG; - case SEK; - case SGD; - case SHP; - case SLE; - case SOS; - case SRD; - case SSP; - case STN; - case SVC; - case SYP; - case SZL; - case THB; - case TJS; - case TMT; - case TND; - case TOP; - case TRY; - case TTD; - case TWD; - case TZS; - case UAH; - case UGX; - case USD; - case UYU; - case UYW; - case UZS; - case VED; - case VES; - case VND; - case VUV; - case WST; - case XAF; - case XCD; - case XCG; - case XOF; - case XPF; - case YER; - case ZAR; - case ZMW; - case ZWG; + case AED = 'AED'; + case AFN = 'AFN'; + case ALL = 'ALL'; + case AMD = 'AMD'; + case AOA = 'AOA'; + case ARS = 'ARS'; + case AUD = 'AUD'; + case AWG = 'AWG'; + case AZN = 'AZN'; + case BAM = 'BAM'; + case BBD = 'BBD'; + case BDT = 'BDT'; + case BGN = 'BGN'; + case BHD = 'BHD'; + case BIF = 'BIF'; + case BMD = 'BMD'; + case BND = 'BND'; + case BOB = 'BOB'; + case BRL = 'BRL'; + case BSD = 'BSD'; + case BTN = 'BTN'; + case BWP = 'BWP'; + case BYN = 'BYN'; + case BZD = 'BZD'; + case CAD = 'CAD'; + case CDF = 'CDF'; + case CHF = 'CHF'; + case CLP = 'CLP'; + case CNY = 'CNY'; + case COP = 'COP'; + case CRC = 'CRC'; + case CUP = 'CUP'; + case CVE = 'CVE'; + case CZK = 'CZK'; + case DJF = 'DJF'; + case DKK = 'DKK'; + case DOP = 'DOP'; + case DZD = 'DZD'; + case EGP = 'EGP'; + case ERN = 'ERN'; + case ETB = 'ETB'; + case EUR = 'EUR'; + case FJD = 'FJD'; + case FKP = 'FKP'; + case GBP = 'GBP'; + case GEL = 'GEL'; + case GHS = 'GHS'; + case GIP = 'GIP'; + case GMD = 'GMD'; + case GNF = 'GNF'; + case GTQ = 'GTQ'; + case GYD = 'GYD'; + case HKD = 'HKD'; + case HNL = 'HNL'; + case HTG = 'HTG'; + case HUF = 'HUF'; + case IDR = 'IDR'; + case ILS = 'ILS'; + case INR = 'INR'; + case IQD = 'IQD'; + case IRR = 'IRR'; + case ISK = 'ISK'; + case JMD = 'JMD'; + case JOD = 'JOD'; + case JPY = 'JPY'; + case KES = 'KES'; + case KGS = 'KGS'; + case KHR = 'KHR'; + case KMF = 'KMF'; + case KPW = 'KPW'; + case KRW = 'KRW'; + case KWD = 'KWD'; + case KYD = 'KYD'; + case KZT = 'KZT'; + case LAK = 'LAK'; + case LBP = 'LBP'; + case LKR = 'LKR'; + case LRD = 'LRD'; + case LSL = 'LSL'; + case LYD = 'LYD'; + case MAD = 'MAD'; + case MDL = 'MDL'; + case MGA = 'MGA'; + case MKD = 'MKD'; + case MMK = 'MMK'; + case MNT = 'MNT'; + case MOP = 'MOP'; + case MRU = 'MRU'; + case MUR = 'MUR'; + case MVR = 'MVR'; + case MWK = 'MWK'; + case MXN = 'MXN'; + case MYR = 'MYR'; + case MZN = 'MZN'; + case NAD = 'NAD'; + case NGN = 'NGN'; + case NIO = 'NIO'; + case NOK = 'NOK'; + case NPR = 'NPR'; + case NZD = 'NZD'; + case OMR = 'OMR'; + case PAB = 'PAB'; + case PEN = 'PEN'; + case PGK = 'PGK'; + case PHP = 'PHP'; + case PKR = 'PKR'; + case PLN = 'PLN'; + case PYG = 'PYG'; + case QAR = 'QAR'; + case RON = 'RON'; + case RSD = 'RSD'; + case RUB = 'RUB'; + case RWF = 'RWF'; + case SAR = 'SAR'; + case SBD = 'SBD'; + case SCR = 'SCR'; + case SDG = 'SDG'; + case SEK = 'SEK'; + case SGD = 'SGD'; + case SHP = 'SHP'; + case SLE = 'SLE'; + case SOS = 'SOS'; + case SRD = 'SRD'; + case SSP = 'SSP'; + case STN = 'STN'; + case SVC = 'SVC'; + case SYP = 'SYP'; + case SZL = 'SZL'; + case THB = 'THB'; + case TJS = 'TJS'; + case TMT = 'TMT'; + case TND = 'TND'; + case TOP = 'TOP'; + case TRY = 'TRY'; + case TTD = 'TTD'; + case TWD = 'TWD'; + case TZS = 'TZS'; + case UAH = 'UAH'; + case UGX = 'UGX'; + case USD = 'USD'; + case UYU = 'UYU'; + case UYW = 'UYW'; + case UZS = 'UZS'; + case VED = 'VED'; + case VES = 'VES'; + case VND = 'VND'; + case VUV = 'VUV'; + case WST = 'WST'; + case XAF = 'XAF'; + case XCD = 'XCD'; + case XCG = 'XCG'; + case XOF = 'XOF'; + case XPF = 'XPF'; + case YER = 'YER'; + case ZAR = 'ZAR'; + case ZMW = 'ZMW'; + case ZWG = 'ZWG'; + + public static function parse(mixed $currency): self + { + if ($currency instanceof self) { + return $currency; + } + + return self::from($currency); + } } diff --git a/packages/support/src/Html/functions.php b/packages/support/src/Html/functions.php index 3a6d77b993..f472e73f3c 100644 --- a/packages/support/src/Html/functions.php +++ b/packages/support/src/Html/functions.php @@ -36,11 +36,150 @@ function is_void_tag(Stringable|string $tag): bool } /** - * Creates an HTML tag with the specified optional attributes and content. + * Determines whether the specified HTML tag is known HTML tag. + * @see https://developer.mozilla.org/en-US/docs/Glossary/Tag */ - function create_tag(string $tag, array $attributes = [], ?string $content = null): HtmlString + function is_html_tag(Stringable|string $tag): bool + { + return ( + is_void_tag($tag) || + in_array( + (string) $tag, + [ + 'a', + 'abbr', + 'acronym', + 'address', + 'applet', + 'area', + 'article', + 'aside', + 'audio', + 'b', + 'base', + 'basefont', + 'bdi', + 'bdo', + 'big', + 'blockquote', + 'body', + 'br', + 'button', + 'canvas', + 'caption', + 'center', + 'cite', + 'code', + 'col', + 'colgroup', + 'data', + 'datalist', + 'dd', + 'del', + 'details', + 'dfn', + 'dialog', + 'dir', + 'div', + 'dl', + 'dt', + 'em', + 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'font', + 'footer', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'link', + 'main', + 'map', + 'mark', + 'menu', + 'meta', + 'meter', + 'nav', + 'noframes', + 'noscript', + 'object', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'param', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'script', + 'search', + 'section', + 'select', + 'small', + 'source', + 'span', + 'strike', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'svg', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'title', + 'tr', + 'track', + 'tt', + 'u', + 'ul', + 'var', + 'video', + 'wbr', + ], + strict: true, + ) + ); + } + + function format_attributes(array $attributes = []): string { - $attributes = arr($attributes) + return $attributes = arr($attributes) ->filter(fn (mixed $value) => ! in_array($value, [false, null], strict: true)) ->map(fn (mixed $value, int|string $key) => $value === true ? $key : ($key . '="' . $value . '"')) ->values() @@ -50,6 +189,14 @@ function create_tag(string $tag, array $attributes = [], ?string $content = null callback: fn ($string) => $string->prepend(' '), ) ->toString(); + } + + /** + * Creates an HTML tag with the specified optional attributes and content. + */ + function create_tag(string $tag, array $attributes = [], ?string $content = null): HtmlString + { + $attributes = namespace\format_attributes($attributes); if ($content || ! is_void_tag($tag)) { return new HtmlString(sprintf('<%s%s>%s', $tag, $attributes, $content ?? '', $tag)); diff --git a/packages/support/src/Language/functions.php b/packages/support/src/Language/functions.php deleted file mode 100644 index 735159f6d0..0000000000 --- a/packages/support/src/Language/functions.php +++ /dev/null @@ -1,27 +0,0 @@ -pluralize($value, $count); - } - - /** - * Converts the given string to its English singular form. - */ - function singularize(Stringable|string $value): string - { - return get(Pluralizer::class)->singularize($value); - } -} diff --git a/packages/support/src/Pluralizer/Pluralizer.php b/packages/support/src/Pluralizer/Pluralizer.php deleted file mode 100644 index e8054d353b..0000000000 --- a/packages/support/src/Pluralizer/Pluralizer.php +++ /dev/null @@ -1,15 +0,0 @@ -createOrModify(pluralize($this->value, $count)); + $this->ensurePluralizerInstalled(__METHOD__); + + return $this->createOrModify(Intl\pluralize($this->value, $count)); + } + + /** + * Converts the string to its English singular form. + */ + public function singularize(int|array|Countable $count = 2): self + { + $this->ensurePluralizerInstalled(__METHOD__); + + return $this->createOrModify(Intl\singularize($this->value, $count)); } /** @@ -126,7 +139,9 @@ public function pluralize(int|array|Countable $count = 2): self */ public function pluralizeLastWord(int|array|Countable $count = 2): self { - return $this->createOrModify(pluralize_last_word($this->value, $count)); + $this->ensurePluralizerInstalled(__METHOD__); + + return $this->createOrModify(Intl\pluralize_last_word($this->value, $count)); } /** @@ -134,7 +149,9 @@ public function pluralizeLastWord(int|array|Countable $count = 2): self */ public function singularizeLastWord(): self { - return $this->createOrModify(singularize_last_word($this->value)); + $this->ensurePluralizerInstalled(__METHOD__); + + return $this->createOrModify(Intl\singularize_last_word($this->value)); } /** @@ -811,6 +828,13 @@ public function length(): int return mb_strlen($this->value); } + private function ensurePluralizerInstalled(string $function): void + { + if (! interface_exists(Intl\Pluralizer\Pluralizer::class)) { + throw new \RuntimeException("The `tempest/intl` package is required to use `{$function}`."); + } + } + /** * Executes callback with the given `$value` and returns the same `$value`. * diff --git a/packages/support/src/Str/functions.php b/packages/support/src/Str/functions.php index 80a87c96db..d1d877d71b 100644 --- a/packages/support/src/Str/functions.php +++ b/packages/support/src/Str/functions.php @@ -3,10 +3,8 @@ declare(strict_types=1); namespace Tempest\Support\Str { - use Countable; use Stringable; use Tempest\Support\Arr; - use Tempest\Support\Language; use voku\helper\ASCII; use function levenshtein as php_levenshtein; @@ -150,14 +148,6 @@ function is_ascii(Stringable|string $string): bool return ASCII::is_ascii((string) $string); } - /** - * Converts the given string to its English plural form. - */ - function pluralize(Stringable|string $string, int|array|Countable $count = 2): string - { - return Language\pluralize((string) $string, $count); - } - /** * Changes the case of the first letter to uppercase. */ @@ -188,30 +178,6 @@ function deduplicate(Stringable|string $string, Stringable|string|iterable $char return $string; } - /** - * Converts the last word of the given string to its English plural form. - */ - function pluralize_last_word(Stringable|string $string, int|array|Countable $count = 2): string - { - $string = (string) $string; - $parts = preg_split('/(.)(?=[A-Z])/u', $string, -1, PREG_SPLIT_DELIM_CAPTURE); - $lastWord = array_pop($parts); - - return implode('', $parts) . pluralize($lastWord, $count); - } - - /** - * Converts the last word of the given string to its English plural form. - */ - function singularize_last_word(Stringable|string $string): string - { - $string = (string) $string; - $parts = preg_split('/(.)(?=[A-Z])/u', $string, -1, PREG_SPLIT_DELIM_CAPTURE); - $lastWord = array_pop($parts); - - return implode('', $parts) . Language\singularize($lastWord); - } - /** * Ensures the given string starts with the specified `$prefix`. */ @@ -914,6 +880,30 @@ function equals(Stringable|string $string, string|Stringable $other): bool return ((string) $string) === ((string) $other); } + /** + * Parses the given value to a string, returning the default value if it is not a string or `Stringable`. + */ + function parse(mixed $string, ?string $default = null): ?string + { + if (is_string($string)) { + return $string; + } + + if (is_int($string) || is_float($string)) { + return (string) $string; + } + + if ($string instanceof Stringable) { + return (string) $string; + } + + if (is_object($string) && method_exists($string, '__toString')) { + return (string) $string; + } + + return $default; + } + /** * Normalizes `Stringable` to string, while keeping other values the same. * diff --git a/packages/support/tests/Str/FunctionsTest.php b/packages/support/tests/Str/FunctionsTest.php new file mode 100644 index 0000000000..8bea472a72 --- /dev/null +++ b/packages/support/tests/Str/FunctionsTest.php @@ -0,0 +1,25 @@ +assertSame('foo', Str\parse('foo')); + $this->assertSame('1', Str\parse('1')); + $this->assertSame('1', Str\parse(1)); + $this->assertSame(null, Str\parse(new stdClass())); + $this->assertSame('', Str\parse(new stdClass(), default: '')); + $this->assertSame('foo', Str\parse(new stdClass(), default: 'foo')); + $this->assertSame('foo', Str\parse(new MutableString('foo'))); + $this->assertSame('foo', Str\parse(new ImmutableString('foo'))); + $this->assertSame(null, Str\parse(['a', 'b'])); + } +} diff --git a/packages/view/src/Components/Icon.php b/packages/view/src/Components/Icon.php index 92734a828d..4ec7b3c2e3 100644 --- a/packages/view/src/Components/Icon.php +++ b/packages/view/src/Components/Icon.php @@ -10,6 +10,7 @@ use Tempest\Http\Status; use Tempest\HttpClient\HttpClient; use Tempest\Support\Html\HtmlString; +use Tempest\Support\Str; use Tempest\Support\Str\ImmutableString; use Tempest\View\IconCache; use Tempest\View\IconConfig; @@ -74,7 +75,7 @@ private function download(string $prefix, string $name): ?string private function svg(string $name): ?string { try { - $parts = explode(':', $name, 2); + $parts = explode(':', Str\replace($name, '-', ':'), 2); if (count($parts) !== 2) { return null; diff --git a/tests/Integration/Intl/DiscoveryTest.php b/tests/Integration/Intl/DiscoveryTest.php new file mode 100644 index 0000000000..aee57cfd6b --- /dev/null +++ b/tests/Integration/Intl/DiscoveryTest.php @@ -0,0 +1,44 @@ +container->get(IntlConfig::class); + + $this->assertCount(3, $config->functions); + } + + public function test_markup_formatters_are_discovered(): void + { + $config = $this->container->get(IntlConfig::class); + + $this->assertCount(3, $config->markupFormatters); + } + + public function test_discovery_adds_paths_to_config(): void + { + $discovery = $this->container->get(TranslationMessageDiscovery::class); + $discovery->setItems(new DiscoveryItems([])); + $discovery->discoverPath(new DiscoveryLocation('', ''), __DIR__ . '/Fixtures/messages.json'); + $discovery->discoverPath(new DiscoveryLocation('', ''), __DIR__ . '/Fixtures/messages.abcde.json'); + $discovery->discoverPath(new DiscoveryLocation('', ''), __DIR__ . '/Fixtures/messages.fr.yaml'); + $discovery->discoverPath(new DiscoveryLocation('', ''), __DIR__ . '/Fixtures/messages.en_US.json'); + $discovery->apply(); + + $config = $this->container->get(IntlConfig::class); + + $this->assertSame([ + 'fr' => [__DIR__ . '/Fixtures/messages.fr.yaml'], + 'en_US' => [__DIR__ . '/Fixtures/messages.en_US.json'], + ], $config->translationMessagePaths); + } +} diff --git a/tests/Integration/Intl/Fixtures/messages.abcde.json b/tests/Integration/Intl/Fixtures/messages.abcde.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/Integration/Intl/Fixtures/messages.abcde.json @@ -0,0 +1 @@ +{} diff --git a/tests/Integration/Intl/Fixtures/messages.en_US.json b/tests/Integration/Intl/Fixtures/messages.en_US.json new file mode 100644 index 0000000000..b84b84efd3 --- /dev/null +++ b/tests/Integration/Intl/Fixtures/messages.en_US.json @@ -0,0 +1,7 @@ +{ + "hello": "Hello, {$name}!", + "cart": { + "checkout": "Checkout", + "items": "<\n .input {$count :number}\n .match $count\n one {{There is one item in your cart.}}\n * {{There is {$count} items in your cart.}}\n>" + } +} diff --git a/tests/Integration/Intl/Fixtures/messages.fr.yaml b/tests/Integration/Intl/Fixtures/messages.fr.yaml new file mode 100644 index 0000000000..04d00a5ea0 --- /dev/null +++ b/tests/Integration/Intl/Fixtures/messages.fr.yaml @@ -0,0 +1,8 @@ +hello: 'Bonjour, {$name}!' +cart: + checkout: Passer à la caisse + items: + .input {$count :number} + .match $count + one {{Il y a un article dans votre panier.}} + * {{Il y a {$count} articles dans votre panier.}} diff --git a/tests/Integration/Intl/Fixtures/messages.json b/tests/Integration/Intl/Fixtures/messages.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/Integration/Intl/Fixtures/messages.json @@ -0,0 +1 @@ +{} diff --git a/tests/Integration/Intl/FunctionsTest.php b/tests/Integration/Intl/FunctionsTest.php new file mode 100644 index 0000000000..689bec7b8b --- /dev/null +++ b/tests/Integration/Intl/FunctionsTest.php @@ -0,0 +1,48 @@ +assertEquals($expected, Intl\pluralize($value, $count)); + } + + #[TestWith(['Migrations', 'Migration'])] + #[TestWith(['migrations', 'migration'])] + public function test_singularize(string $value, string $expected): void + { + $this->assertEquals($expected, Intl\singularize($value)); + } + + public function test_singularize_last_word(): void + { + $this->assertEquals('Multiple Migration', Intl\singularize_last_word('Multiple Migration')); + $this->assertEquals('Multiple Migration', Intl\singularize_last_word('Multiple Migrations')); + $this->assertEquals('Multiple Aircraft', Intl\singularize_last_word('Multiple Aircraft')); + } + + public function test_pluralize_last_word(): void + { + $this->assertEquals('Multiple Migrations', Intl\pluralize_last_word('Multiple Migration')); + $this->assertEquals('Multiple Migrations', Intl\pluralize_last_word('Multiple Migrations')); + $this->assertEquals('Multiple Aircraft', Intl\pluralize_last_word('Multiple Aircraft')); + } +} diff --git a/tests/Integration/Intl/TranslatorTest.php b/tests/Integration/Intl/TranslatorTest.php new file mode 100644 index 0000000000..6c07036b24 --- /dev/null +++ b/tests/Integration/Intl/TranslatorTest.php @@ -0,0 +1,114 @@ +container->get(IntlConfig::class); + $config->addTranslationMessageFile(Locale::FRENCH, __DIR__ . '/Fixtures/messages.fr.yaml'); + $config->addTranslationMessageFile(Locale::ENGLISH, __DIR__ . '/Fixtures/messages.en_US.json'); + } + + public function test_translator(): void + { + $translator = $this->container->get(Translator::class); + + $this->assertSame('Hello, Jon Doe!', $translator->translate('hello', name: 'Jon Doe')); + $this->assertSame('Checkout', $translator->translate('cart.checkout')); + $this->assertSame('Passer à la caisse', $translator->translateForLocale(Locale::FRENCH, 'cart.checkout')); + + $this->assertSame('Il y a 3 articles dans votre panier.', $translator->translateForLocale(Locale::FRENCH, 'cart.items', count: 3)); + } + + public function test_function(): void + { + $this->assertSame('Hello, Jon Doe!', translate('hello', name: 'Jon Doe')); + $this->assertSame('Checkout', translate('cart.checkout')); + $this->assertSame('Passer à la caisse', translate_locale(Locale::FRENCH, 'cart.checkout')); + } + + public function test_default_locale(): void + { + $config = $this->container->get(IntlConfig::class); + + $this->assertSame(Locale::default(), $config->currentLocale); + } + + public function test_event_miss(): void + { + /** @var TranslationMiss|null $received */ + $received = null; + + $eventbus = $this->container->get(EventBus::class); + $eventbus->listen(function (TranslationMiss $event) use (&$received): void { + $received = $event; + }); + + $translator = $this->container->get(Translator::class); + $translator->translate('unknown'); + + $this->assertInstanceOf(TranslationMiss::class, $received); + $this->assertSame(Locale::ENGLISH_UNITED_STATES, $received->locale); + $this->assertSame('unknown', $received->key); + } + + public function test_event_fail(): void + { + /** @var TranslationFailure|null $received */ + $received = null; + + $eventbus = $this->container->get(EventBus::class); + $eventbus->listen(function (TranslationFailure $event) use (&$received): void { + $received = $event; + }); + + $catalog = $this->container->get(Catalog::class); + $catalog->add(Locale::ENGLISH_UNITED_STATES, 'failure', '{$foo'); + + $translator = $this->container->get(Translator::class); + $translator->translate('failure'); + + $this->assertInstanceOf(TranslationFailure::class, $received); + $this->assertSame(Locale::ENGLISH_UNITED_STATES, $received->locale); + $this->assertSame('failure', $received->key); + $this->assertSame('Failed to parse message.', $received->exception->getMessage()); + } + + public function test_icon_markup(): void + { + $translator = $this->container->get(Translator::class); + $catalog = $this->container->get(Catalog::class); + $catalog->add(Locale::ENGLISH, 'has_icon', '{#icon-tabler-tornado/}'); + + $this->assertStringContainsStringIgnoringCase('translate('has_icon')); + } + + #[TestWith(['Click {#a href=|https://tempestphp.com|}here{/a}.', 'Click here.'])] + #[TestWith(['This is {#strong}bold{/strong}.', 'This is bold.'])] + #[TestWith(['Hello{#br/}World', 'Hello
World'])] + public function test_html_markup(string $input, string $expected): void + { + $translator = $this->container->get(Translator::class); + $catalog = $this->container->get(Catalog::class); + $catalog->add(Locale::ENGLISH, 'test', $input); + + $this->assertSame($expected, $translator->translate('test')); + } +} diff --git a/tests/Integration/Support/LanguageTest.php b/tests/Integration/Support/LanguageTest.php deleted file mode 100644 index 1a1008774e..0000000000 --- a/tests/Integration/Support/LanguageTest.php +++ /dev/null @@ -1,35 +0,0 @@ -assertEquals($expected, pluralize($value, $count)); - } - - #[TestWith(['Migrations', 'Migration'])] - #[TestWith(['migrations', 'migration'])] - public function test_that_pluralizer_singularizes(string $value, string $expected): void - { - $this->assertEquals($expected, singularize($value)); - } -}