From 1e4137b3bbf61419807582c5308238e336eff09b Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 16:22:10 +0200 Subject: [PATCH 01/20] feat: parser and formatter --- bin/release | 13 +- composer.json | 4 + packages/i18n/.gitattributes | 14 + packages/i18n/LICENCE.md | 9 + packages/i18n/bin/plural-rules.php | 380 ++ packages/i18n/composer.json | 23 + packages/i18n/phpunit.xml | 13 + .../i18n/src/InternationalizationConfig.php | 11 + .../Formatter/FormattedValue.php | 11 + .../Formatter/FormattingException.php | 20 + .../Formatter/MessageFormatFunction.php | 18 + .../Formatter/MessageFormatter.php | 313 ++ .../Functions/DateTimeFunction.php | 21 + .../Functions/NumberFunction.php | 26 + .../Functions/StringFunction.php | 30 + .../Parser/Node/ComplexBody/ComplexBody.php | 11 + .../Parser/Node/ComplexBody/Matcher.php | 28 + .../Node/ComplexBody/SimplePatternBody.php | 17 + .../Parser/Node/ComplexBody/Variant.php | 17 + .../Parser/Node/ComplexMessage.php | 16 + .../Parser/Node/Declaration/Declaration.php | 9 + .../Node/Declaration/InputDeclaration.php | 12 + .../Node/Declaration/LocalDeclaration.php | 14 + .../Parser/Node/Expression/Attribute.php | 15 + .../Parser/Node/Expression/Expression.php | 16 + .../Parser/Node/Expression/FunctionCall.php | 17 + .../Node/Expression/FunctionExpression.php | 13 + .../Node/Expression/LiteralExpression.php | 16 + .../Parser/Node/Expression/Option.php | 16 + .../Node/Expression/VariableExpression.php | 16 + .../MessageFormat/Parser/Node/Identifier.php | 16 + .../src/MessageFormat/Parser/Node/Key/Key.php | 9 + .../Parser/Node/Key/WildcardKey.php | 7 + .../Parser/Node/Literal/Literal.php | 13 + .../Parser/Node/Literal/QuotedLiteral.php | 7 + .../Parser/Node/Literal/UnquotedLiteral.php | 7 + .../Parser/Node/Markup/Markup.php | 20 + .../Parser/Node/Markup/MarkupType.php | 10 + .../MessageFormat/Parser/Node/MessageNode.php | 12 + .../src/MessageFormat/Parser/Node/Node.php | 7 + .../Parser/Node/ParsingException.php | 13 + .../Parser/Node/Pattern/Pattern.php | 15 + .../Parser/Node/Pattern/Placeholder.php | 9 + .../Parser/Node/Pattern/QuotedPattern.php | 17 + .../Parser/Node/Pattern/Text.php | 12 + .../Parser/Node/SimpleMessage.php | 7 + .../MessageFormat/Parser/Node/Variable.php | 10 + .../i18n/src/MessageFormat/Parser/Parser.php | 627 +++ .../src/PluralRules/PluralRulesMatcher.php | 4486 +++++++++++++++++ packages/i18n/tests/FormatterTest.php | 317 ++ packages/i18n/tests/ParserTest.php | 74 + .../i18n/tests/PluralRulesMatcherTest.php | 37 + packages/support/src/Currency.php | 325 +- packages/support/src/Number/functions.php | 24 + packages/support/src/Str/functions.php | 24 + .../support/tests/Number/FunctionsTest.php | 35 + packages/support/tests/Str/FunctionsTest.php | 25 + 57 files changed, 7140 insertions(+), 164 deletions(-) create mode 100644 packages/i18n/.gitattributes create mode 100644 packages/i18n/LICENCE.md create mode 100755 packages/i18n/bin/plural-rules.php create mode 100644 packages/i18n/composer.json create mode 100644 packages/i18n/phpunit.xml create mode 100644 packages/i18n/src/InternationalizationConfig.php create mode 100644 packages/i18n/src/MessageFormat/Formatter/FormattedValue.php create mode 100644 packages/i18n/src/MessageFormat/Formatter/FormattingException.php create mode 100644 packages/i18n/src/MessageFormat/Formatter/MessageFormatFunction.php create mode 100644 packages/i18n/src/MessageFormat/Formatter/MessageFormatter.php create mode 100644 packages/i18n/src/MessageFormat/Functions/DateTimeFunction.php create mode 100644 packages/i18n/src/MessageFormat/Functions/NumberFunction.php create mode 100644 packages/i18n/src/MessageFormat/Functions/StringFunction.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ComplexMessage.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Declaration/Declaration.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Declaration/InputDeclaration.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Declaration/LocalDeclaration.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/Attribute.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/Expression.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/FunctionCall.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/FunctionExpression.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/LiteralExpression.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/Option.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/VariableExpression.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Identifier.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Key/Key.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Key/WildcardKey.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Literal/Literal.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Literal/QuotedLiteral.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Literal/UnquotedLiteral.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Markup/Markup.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Markup/MarkupType.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/MessageNode.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Node.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ParsingException.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Pattern/Pattern.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Pattern/Placeholder.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Pattern/QuotedPattern.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/SimpleMessage.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Variable.php create mode 100644 packages/i18n/src/MessageFormat/Parser/Parser.php create mode 100644 packages/i18n/src/PluralRules/PluralRulesMatcher.php create mode 100644 packages/i18n/tests/FormatterTest.php create mode 100644 packages/i18n/tests/ParserTest.php create mode 100644 packages/i18n/tests/PluralRulesMatcherTest.php create mode 100644 packages/support/tests/Str/FunctionsTest.php 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 dad61a485f..8e84c2538b 100644 --- a/composer.json +++ b/composer.json @@ -83,6 +83,7 @@ "tempest/generation": "self.version", "tempest/http": "self.version", "tempest/http-client": "self.version", + "tempest/i18n": "self.version", "tempest/log": "self.version", "tempest/mapper": "self.version", "tempest/reflection": "self.version", @@ -117,6 +118,7 @@ "Tempest\\Generation\\": "packages/generation/src", "Tempest\\HttpClient\\": "packages/http-client/src", "Tempest\\Http\\": "packages/http/src", + "Tempest\\Internationalization\\": "packages/i18n/src", "Tempest\\Log\\": "packages/log/src", "Tempest\\Mapper\\": "packages/mapper/src", "Tempest\\Reflection\\": "packages/reflection/src", @@ -174,6 +176,7 @@ "Tempest\\Generation\\Tests\\": "packages/generation/tests", "Tempest\\HttpClient\\Tests\\": "packages/http-client/tests", "Tempest\\Http\\Tests\\": "packages/http/tests", + "Tempest\\Internationalization\\Tests\\": "packages/i18n/tests", "Tempest\\Log\\Tests\\": "packages/log/tests", "Tempest\\Mapper\\Tests\\": "packages/mapper/tests", "Tempest\\Reflection\\Tests\\": "packages/reflection/tests", @@ -202,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", + "i18n:plural": "./packages/i18n/bin/plural-rules.php", "release": [ "composer qa", "./bin/release" diff --git a/packages/i18n/.gitattributes b/packages/i18n/.gitattributes new file mode 100644 index 0000000000..3f7775660b --- /dev/null +++ b/packages/i18n/.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/i18n/LICENCE.md b/packages/i18n/LICENCE.md new file mode 100644 index 0000000000..e403836c23 --- /dev/null +++ b/packages/i18n/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/i18n/bin/plural-rules.php b/packages/i18n/bin/plural-rules.php new file mode 100755 index 0000000000..f46cd0a779 --- /dev/null +++ b/packages/i18n/bin/plural-rules.php @@ -0,0 +1,380 @@ +#!/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) { + 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) { + 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; + } + + $condition = $this->parseRule($rule); + if ($condition) { + $output .= " if ({$condition}) {\n"; + $output .= " return '{$category}';\n"; + $output .= " }\n\n"; + } + } + + $output .= " }\n\n"; + + return $output; + } + + private function parseRule(string $rule): string + { + // Extract the rule condition (before @integer/@decimal examples) + $rulePart = trim(explode('@', $rule)[0]); + + if (empty($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); + + // Modulo operations "n % 100 = 3..10" or "n % 10 = 3..4,9" + 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); + } + + // Direct comparisons "n = 1" or "n = 0..1" + 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 === '!=='; + + // Handle single number + if (preg_match('/^\d+(?:\.\d+)?$/', $values)) { + return "{$varExpression} {$operator} {$values}"; + } + + // Handle single range like "3..10" + 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; + } + + // Handle complex values with commas and ranges like "3..4,9" or "2,22,42,62,82" + if (str_contains($values, ',')) { + $parts = array_map('trim', explode(',', $values)); + $conditions = []; + + foreach ($parts as $part) { + if (str_contains($part, '..')) { + // Range like "3..4" + 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, '~')) { + // Range like "3~10" + if (preg_match('/^(\d+(?:\.\d+)?)~(\d+(?:\.\d+)?)$/', $part, $matches)) { + $start = $matches[1]; + $end = $matches[2]; + $conditions[] = "self::inRange({$varExpression}, {$start}, {$end})"; + } + } else { + // Single value + $conditions[] = "{$varExpression} === {$part}"; + } + } + + if (empty($conditions)) { + return 'false'; + } + + $combined = '(' . implode(' || ', $conditions) . ')'; + return $isNegative ? "!{$combined}" : $combined; + } + + // Handle tilde ranges like "3~10" + 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; + } + } + + // Fallback: use matchesValues for complex patterns + $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/i18n/composer.json b/packages/i18n/composer.json new file mode 100644 index 0000000000..8bdbad2d7a --- /dev/null +++ b/packages/i18n/composer.json @@ -0,0 +1,23 @@ +{ + "name": "tempest/i18n", + "description": "A component for working with internationalization.", + "license": "MIT", + "minimum-stability": "dev", + "require": { + "php": "^8.4", + "tempest/container": "dev-main" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.17" + }, + "autoload": { + "psr-4": { + "Tempest\\Internationalization\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tempest\\Internationalization\\Tests\\": "tests" + } + } +} diff --git a/packages/i18n/phpunit.xml b/packages/i18n/phpunit.xml new file mode 100644 index 0000000000..f41bab92e8 --- /dev/null +++ b/packages/i18n/phpunit.xml @@ -0,0 +1,13 @@ + + + + + tests + + + + + src + + + diff --git a/packages/i18n/src/InternationalizationConfig.php b/packages/i18n/src/InternationalizationConfig.php new file mode 100644 index 0000000000..ee0a84cd3c --- /dev/null +++ b/packages/i18n/src/InternationalizationConfig.php @@ -0,0 +1,11 @@ +context; + } +} diff --git a/packages/i18n/src/MessageFormat/Formatter/MessageFormatFunction.php b/packages/i18n/src/MessageFormat/Formatter/MessageFormatFunction.php new file mode 100644 index 0000000000..2fc79ae1e5 --- /dev/null +++ b/packages/i18n/src/MessageFormat/Formatter/MessageFormatFunction.php @@ -0,0 +1,18 @@ + $variables */ + private array $variables = []; + + public function __construct( + /** @var MessageFormatFunction[] */ + private readonly array $functions = [], + private readonly PluralRulesMatcher $pluralRules = new PluralRulesMatcher(), + ) {} + + /** + * Formats a message string with the given variables. + */ + public function format(string $message, mixed ...$variables): string + { + try { + $ast = new Parser($message)->parse(); + + $this->variables = $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) { + $variableName = $declaration->expression->variable->name->name; + + if (! array_key_exists($variableName, $this->variables)) { + throw new FormattingException("Required input variable '{$variableName}' not provided."); + } + } elseif ($declaration instanceof LocalDeclaration) { + $variableName = $declaration->variable->name->name; + $value = $this->evaluateExpression($declaration->expression); + $localVariables[$variableName] = $value->value; + } + } + + $originalVariables = $this->variables; + $this->variables = [...$this->variables, ...$localVariables]; + + try { + $result = $this->formatComplexBody($message->body); + $this->variables = $originalVariables; + + return $result; + } catch (Exception $e) { + $this->variables = $originalVariables; + + throw $e; + } + } + + 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 + { + $selectorValues = []; + + foreach ($matcher->selectors as $selector) { + $variableName = $selector->name->name; + + if (! array_key_exists($variableName, $this->variables)) { + throw new FormattingException("Selector variable '{$variableName}' not found."); + } + + $selectorValues[] = $this->variables[$variableName]; + } + + // Find the best matching variant + $bestVariant = null; + $wildcardVariant = null; + + foreach ($matcher->variants as $variant) { + if (count($variant->keys) !== count($selectorValues)) { + continue; // Key count mismatch + } + + $matches = true; + $hasWildcard = false; + + for ($i = 0; $i < count($variant->keys); $i++) { + $key = $variant->keys[$i]; + $selectorValue = $selectorValues[$i]; + + if ($key instanceof WildcardKey) { + $hasWildcard = true; + continue; + } + + if ($key instanceof Literal) { + if (! $this->matchesKey($selectorValue, $key->value)) { + $matches = false; + break; + } + } + } + + if ($matches) { + if (! $hasWildcard) { + $bestVariant = $variant; + break; + } elseif ($wildcardVariant === null) { + $wildcardVariant = $variant; + } + } + } + + $selectedVariant = $bestVariant ?? $wildcardVariant; + + if ($selectedVariant === null) { + throw new FormattingException('No matching variant found for selector values: ' . json_encode($selectorValues)); + } + + return $this->formatPattern($selectedVariant->pattern->pattern); + } + + private function matchesKey(mixed $value, string $keyValue): bool + { + if (is_numeric($value)) { + $number = (float) $value; + + if ($keyValue === ((string) $number) || $keyValue === ((string) ((int) $number))) { + return true; + } + + if ($keyValue === $this->pluralRules->getPluralCategory(Locale::default(), $number)) { + return true; + } + } + + return ((string) $value) === $keyValue; + } + + 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]; + } 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->getFunction($functionName)) { + return $function->evaluate($value, $options); + } else { + throw new FormattingException("Unknown function `{$functionName}`."); + } + } + + $formatted = $value !== null ? ((string) $value) : ''; + + return new FormattedValue($value, $formatted); + } + + private function getFunction(string $name): ?MessageFormatFunction + { + return array_find( + array: $this->functions, + callback: fn (MessageFormatFunction $fn) => $fn->name === $name, + ); + } + + 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]; + } elseif ($option->value instanceof Literal) { + $result[$name] = $option->value->value; + } + } + + return $result; + } + + private function formatMarkup(Markup $markup): string + { + // TODO: more advanced with options + // built-in HtmlMarkup + $tag = (string) $markup->identifier; + + return match ($markup->type) { + MarkupType::OPEN => "<$tag>", + MarkupType::CLOSE => "", + MarkupType::STANDALONE => "<$tag/>", + default => '', + }; + } +} diff --git a/packages/i18n/src/MessageFormat/Functions/DateTimeFunction.php b/packages/i18n/src/MessageFormat/Functions/DateTimeFunction.php new file mode 100644 index 0000000000..9add7f40ae --- /dev/null +++ b/packages/i18n/src/MessageFormat/Functions/DateTimeFunction.php @@ -0,0 +1,21 @@ +format(Arr\get_by_key($parameters, 'pattern')); + + return new FormattedValue($value, $formatted); + } +} diff --git a/packages/i18n/src/MessageFormat/Functions/NumberFunction.php b/packages/i18n/src/MessageFormat/Functions/NumberFunction.php new file mode 100644 index 0000000000..3df46c020d --- /dev/null +++ b/packages/i18n/src/MessageFormat/Functions/NumberFunction.php @@ -0,0 +1,26 @@ + 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/i18n/src/MessageFormat/Functions/StringFunction.php b/packages/i18n/src/MessageFormat/Functions/StringFunction.php new file mode 100644 index 0000000000..8436f46720 --- /dev/null +++ b/packages/i18n/src/MessageFormat/Functions/StringFunction.php @@ -0,0 +1,30 @@ + 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/i18n/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php new file mode 100644 index 0000000000..bb7e877067 --- /dev/null +++ b/packages/i18n/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/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php new file mode 100644 index 0000000000..e33f54e252 --- /dev/null +++ b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php @@ -0,0 +1,17 @@ +pattern; + } +} diff --git a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php new file mode 100644 index 0000000000..ff4412955c --- /dev/null +++ b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php @@ -0,0 +1,17 @@ +getPattern()); + } +} diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Declaration/Declaration.php b/packages/i18n/src/MessageFormat/Parser/Node/Declaration/Declaration.php new file mode 100644 index 0000000000..b7ee1d039e --- /dev/null +++ b/packages/i18n/src/MessageFormat/Parser/Node/Declaration/Declaration.php @@ -0,0 +1,9 @@ +namespace ? "{$this->namespace}:{$this->name}" : $this->name; + } +} diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Key/Key.php b/packages/i18n/src/MessageFormat/Parser/Node/Key/Key.php new file mode 100644 index 0000000000..4cca55b987 --- /dev/null +++ b/packages/i18n/src/MessageFormat/Parser/Node/Key/Key.php @@ -0,0 +1,9 @@ +pattern; + } +} diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php new file mode 100644 index 0000000000..a0bb20b2b6 --- /dev/null +++ b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php @@ -0,0 +1,12 @@ +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/i18n/src/PluralRules/PluralRulesMatcher.php b/packages/i18n/src/PluralRules/PluralRulesMatcher.php new file mode 100644 index 0000000000..1d8041a0d2 --- /dev/null +++ b/packages/i18n/src/PluralRules/PluralRulesMatcher.php @@ -0,0 +1,4486 @@ += 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) { + 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/i18n/tests/FormatterTest.php b/packages/i18n/tests/FormatterTest.php new file mode 100644 index 0000000000..3b16a6f759 --- /dev/null +++ b/packages/i18n/tests/FormatterTest.php @@ -0,0 +1,317 @@ +format(<<<'TXT' + This is {#bold}bold{/bold}. + TXT); + + // TODO: offer custom markup + + $this->assertSame('This is bold.', $value); + } + + 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(): void + { + $formatter = new MessageFormatter([new DateTimeFunction()]); + + $value = $formatter->format(<<<'TXT' + Today is {$today :datetime}. + TXT, today: '2024-01-01'); + + $this->assertSame("Today is Jan 1, 2024, 12:00:00\u{202F}AM.", $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([new NumberFunction()]); + + $value = $formatter->format(<<<'TXT' + The total was {31 :number style=percent}. + TXT); + + $this->assertSame('The total was 31%.', $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_matchers(): void + { + $formatter = new MessageFormatter(); + $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_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([new NumberFunction()]); + + $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([new NumberFunction()]); + + $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 NumberFunction()]); + + $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([new NumberFunction()]); + + $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([new NumberFunction()]); + + $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([ + new NumberFunction(), + ]); + + $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([ + new NumberFunction(), + 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 MessageFormatFunction { + public string $name = 'uppercase'; + + public function evaluate(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); + } +} diff --git a/packages/i18n/tests/ParserTest.php b/packages/i18n/tests/ParserTest.php new file mode 100644 index 0000000000..4e0450bab1 --- /dev/null +++ b/packages/i18n/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 Parser(<<<'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 Parser(<<<'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 Parser(<<<'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/i18n/tests/PluralRulesMatcherTest.php b/packages/i18n/tests/PluralRulesMatcherTest.php new file mode 100644 index 0000000000..3d09ae5ec1 --- /dev/null +++ b/packages/i18n/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/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/Number/functions.php b/packages/support/src/Number/functions.php index af05dedc70..ca5ecdea29 100644 --- a/packages/support/src/Number/functions.php +++ b/packages/support/src/Number/functions.php @@ -7,6 +7,30 @@ use Tempest\Support\Language\Locale; use Tempest\Support\Math; +/** + * Returns the numeric value of the given `$number`, defaulting to `$default` if the input is not a valid number. + */ +function parse(mixed $number, int|float $default = 0): int|float +{ + return filter_var($number, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) ?? filter_var($number, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE) ?? $default; +} + +/** + * Returns the int value of the given `$number`, defaulting to `$default` if the input is not a valid integer. + */ +function parseInt(mixed $number, int $default = 0): int +{ + return filter_var($number, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) ?? $default; +} + +/** + * Returns the float value of the given `$number`, defaulting to `$default` if the input is not a valid float. + */ +function parseFloat(mixed $number, float $default = 0.0): float +{ + return filter_var($number, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE) ?? $default; +} + /** * Formats the given number. * diff --git a/packages/support/src/Str/functions.php b/packages/support/src/Str/functions.php index 80a87c96db..6aa2ea789a 100644 --- a/packages/support/src/Str/functions.php +++ b/packages/support/src/Str/functions.php @@ -914,6 +914,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/Number/FunctionsTest.php b/packages/support/tests/Number/FunctionsTest.php index 71e90066a8..da1d67a914 100644 --- a/packages/support/tests/Number/FunctionsTest.php +++ b/packages/support/tests/Number/FunctionsTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Tempest\Support\Currency; use Tempest\Support\Language\Locale; +use Tempest\Support\Math; use Tempest\Support\Number; final class FunctionsTest extends TestCase @@ -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\parseInt(1)); + $this->assertSame(1, Number\parseInt(1, default: 0)); + $this->assertSame(1, Number\parseInt('1')); + $this->assertSame(0, Number\parseInt('1.1')); + $this->assertSame(0, Number\parseInt(1.1)); + $this->assertSame(10, Number\parseInt(1.1, default: 10)); + $this->assertSame(Math\INT64_MAX, Number\parseInt(Math\INT64_MAX)); + } + + public function test_parse_float(): void + { + $this->assertSame(1.0, Number\parseFloat(1)); + $this->assertSame(1.0, Number\parseFloat(1, default: 0.0)); + $this->assertSame(1.0, Number\parseFloat('1')); + $this->assertSame(1.1, Number\parseFloat('1.1')); + $this->assertSame(1.1, Number\parseFloat(1.1)); + $this->assertSame(10.0, Number\parseFloat('abc', default: 10.0)); + $this->assertSame(Math\FLOAT32_MAX, Number\parseFloat(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/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'])); + } +} From 181e55ebd6a6573ad6e5a70a75507af2ee3b0018 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 19:44:11 +0200 Subject: [PATCH 02/20] feat: config, catalog and translator --- composer.json | 3 +- packages/i18n/composer.json | 7 +- packages/i18n/src/Catalog/Catalog.php | 23 ++++ .../i18n/src/Catalog/CatalogInitializer.php | 36 ++++++ packages/i18n/src/Catalog/GenericCatalog.php | 41 +++++++ .../i18n/src/InternationalizationConfig.php | 34 +++++- .../src/MessageFormatFunctionDiscovery.php | 38 ++++++ .../i18n/src/MessageFormatterInitializer.php | 21 ++++ .../i18n/src/TranslationMessageDiscovery.php | 68 +++++++++++ .../i18n/src/Translator/GenericTranslator.php | 55 +++++++++ .../src/Translator/TranslationFailure.php | 14 +++ .../i18n/src/Translator/TranslationMiss.php | 13 ++ packages/i18n/src/Translator/Translator.php | 28 +++++ .../src/Translator/TranslatorInitializer.php | 21 ++++ packages/i18n/src/functions.php | 24 ++++ packages/i18n/src/i18n.config.php | 9 ++ packages/i18n/tests/FormatterTest.php | 18 +++ packages/i18n/tests/GenericCatalogTest.php | 27 +++++ packages/i18n/tests/GenericTranslatorTest.php | 111 ++++++++++++++++++ .../Internationalization/DiscoveryTest.php | 37 ++++++ .../Fixtures/messages.abcde.json | 1 + .../Fixtures/messages.en_US.json | 13 ++ .../Fixtures/messages.fr.json | 13 ++ .../Fixtures/messages.json | 1 + .../Internationalization/TranslatorTest.php | 46 ++++++++ 25 files changed, 698 insertions(+), 4 deletions(-) create mode 100644 packages/i18n/src/Catalog/Catalog.php create mode 100644 packages/i18n/src/Catalog/CatalogInitializer.php create mode 100644 packages/i18n/src/Catalog/GenericCatalog.php create mode 100644 packages/i18n/src/MessageFormatFunctionDiscovery.php create mode 100644 packages/i18n/src/MessageFormatterInitializer.php create mode 100644 packages/i18n/src/TranslationMessageDiscovery.php create mode 100644 packages/i18n/src/Translator/GenericTranslator.php create mode 100644 packages/i18n/src/Translator/TranslationFailure.php create mode 100644 packages/i18n/src/Translator/TranslationMiss.php create mode 100644 packages/i18n/src/Translator/Translator.php create mode 100644 packages/i18n/src/Translator/TranslatorInitializer.php create mode 100644 packages/i18n/src/functions.php create mode 100644 packages/i18n/src/i18n.config.php create mode 100644 packages/i18n/tests/GenericCatalogTest.php create mode 100644 packages/i18n/tests/GenericTranslatorTest.php create mode 100644 tests/Integration/Internationalization/DiscoveryTest.php create mode 100644 tests/Integration/Internationalization/Fixtures/messages.abcde.json create mode 100644 tests/Integration/Internationalization/Fixtures/messages.en_US.json create mode 100644 tests/Integration/Internationalization/Fixtures/messages.fr.json create mode 100644 tests/Integration/Internationalization/Fixtures/messages.json create mode 100644 tests/Integration/Internationalization/TranslatorTest.php diff --git a/composer.json b/composer.json index 8e84c2538b..d372fd0dbc 100644 --- a/composer.json +++ b/composer.json @@ -139,6 +139,7 @@ "packages/datetime/src/functions.php", "packages/debug/src/functions.php", "packages/event-bus/src/functions.php", + "packages/i18n/src/functions.php", "packages/mapper/src/functions.php", "packages/reflection/src/functions.php", "packages/router/src/functions.php", @@ -205,7 +206,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", - "i18n:plural": "./packages/i18n/bin/plural-rules.php", + "i18n:plural": "./packages/i18n/bin/plural-rules.php", "release": [ "composer qa", "./bin/release" diff --git a/packages/i18n/composer.json b/packages/i18n/composer.json index 8bdbad2d7a..b978ebe840 100644 --- a/packages/i18n/composer.json +++ b/packages/i18n/composer.json @@ -5,12 +5,17 @@ "minimum-stability": "dev", "require": { "php": "^8.4", - "tempest/container": "dev-main" + "tempest/core": "dev-main", + "tempest/container": "dev-main", + "tempest/datetime": "dev-main" }, "require-dev": { "phpunit/phpunit": "^11.5.17" }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { "Tempest\\Internationalization\\": "src" } diff --git a/packages/i18n/src/Catalog/Catalog.php b/packages/i18n/src/Catalog/Catalog.php new file mode 100644 index 0000000000..65bac3eea9 --- /dev/null +++ b/packages/i18n/src/Catalog/Catalog.php @@ -0,0 +1,23 @@ +get(InternationalizationConfig::class); + $catalog = []; + + foreach ($config->translationMessagePaths as $locale => $paths) { + $locale = Locale::from($locale)->value; + $catalog[$locale] ??= []; + + foreach ($paths as $path) { + $messages = Json\decode(Filesystem\read_file($path)); + $messages = Arr\undot($messages); + + foreach ($messages as $key => $message) { + $catalog[$locale][$key] = $message; + } + } + } + + return new GenericCatalog($catalog); + } +} diff --git a/packages/i18n/src/Catalog/GenericCatalog.php b/packages/i18n/src/Catalog/GenericCatalog.php new file mode 100644 index 0000000000..55875f70cd --- /dev/null +++ b/packages/i18n/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($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/i18n/src/InternationalizationConfig.php b/packages/i18n/src/InternationalizationConfig.php index ee0a84cd3c..cf01f2de4f 100644 --- a/packages/i18n/src/InternationalizationConfig.php +++ b/packages/i18n/src/InternationalizationConfig.php @@ -2,10 +2,40 @@ namespace Tempest\Internationalization; +use Tempest\Internationalization\MessageFormat\Formatter\MessageFormatFunction; +use Tempest\Support\Language\Locale; + final class InternationalizationConfig { + /** @var MessageFormatFunction[] */ + public array $functions = []; + + /** @var array */ + public array $translationMessagePaths = []; + public function __construct( - /** @var MessageFormatFunction[] */ - public array $functions = [], + /** + * 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 addMessageFormatFunction(MessageFormatFunction $fn): void + { + $this->functions[] = $fn; + } + + 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/i18n/src/MessageFormatFunctionDiscovery.php b/packages/i18n/src/MessageFormatFunctionDiscovery.php new file mode 100644 index 0000000000..d86b0ee7ea --- /dev/null +++ b/packages/i18n/src/MessageFormatFunctionDiscovery.php @@ -0,0 +1,38 @@ +implements(MessageFormatFunction::class)) { + return; + } + + $this->discoveryItems->add($location, $class->getName()); + } + + public function apply(): void + { + foreach ($this->discoveryItems as $className) { + $this->config->addMessageFormatFunction($this->container->get($className)); + } + } +} diff --git a/packages/i18n/src/MessageFormatterInitializer.php b/packages/i18n/src/MessageFormatterInitializer.php new file mode 100644 index 0000000000..ae52629f70 --- /dev/null +++ b/packages/i18n/src/MessageFormatterInitializer.php @@ -0,0 +1,21 @@ +get(InternationalizationConfig::class); + + return new MessageFormatter( + functions: $config->functions, + pluralRules: new PluralRulesMatcher(), + ); + } +} diff --git a/packages/i18n/src/TranslationMessageDiscovery.php b/packages/i18n/src/TranslationMessageDiscovery.php new file mode 100644 index 0000000000..f9d8a6732f --- /dev/null +++ b/packages/i18n/src/TranslationMessageDiscovery.php @@ -0,0 +1,68 @@ +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/i18n/src/Translator/GenericTranslator.php b/packages/i18n/src/Translator/GenericTranslator.php new file mode 100644 index 0000000000..b824c3ec28 --- /dev/null +++ b/packages/i18n/src/Translator/GenericTranslator.php @@ -0,0 +1,55 @@ +catalog->get($locale, $key); + + if (! $message) { + $message = $this->catalog->get($this->config->fallbackLocale, $key); + } + + if (! $message) { + $this->eventBus?->dispatch(new TranslationMiss( + locale: $locale, + key: $key, + )); + + 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/i18n/src/Translator/TranslationFailure.php b/packages/i18n/src/Translator/TranslationFailure.php new file mode 100644 index 0000000000..574896d7d1 --- /dev/null +++ b/packages/i18n/src/Translator/TranslationFailure.php @@ -0,0 +1,14 @@ +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/i18n/src/Translator/TranslatorInitializer.php b/packages/i18n/src/Translator/TranslatorInitializer.php new file mode 100644 index 0000000000..394b7bf666 --- /dev/null +++ b/packages/i18n/src/Translator/TranslatorInitializer.php @@ -0,0 +1,21 @@ +get(InternationalizationConfig::class), + catalog: $container->get(Catalog::class), + formatter: $container->get(MessageFormatter::class), + ); + } +} diff --git a/packages/i18n/src/functions.php b/packages/i18n/src/functions.php new file mode 100644 index 0000000000..4ddc6a30f4 --- /dev/null +++ b/packages/i18n/src/functions.php @@ -0,0 +1,24 @@ +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); +} diff --git a/packages/i18n/src/i18n.config.php b/packages/i18n/src/i18n.config.php new file mode 100644 index 0000000000..4eadcdd168 --- /dev/null +++ b/packages/i18n/src/i18n.config.php @@ -0,0 +1,9 @@ +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([new NumberFunction()]); + + $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(); diff --git a/packages/i18n/tests/GenericCatalogTest.php b/packages/i18n/tests/GenericCatalogTest.php new file mode 100644 index 0000000000..453cf32c22 --- /dev/null +++ b/packages/i18n/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/i18n/tests/GenericTranslatorTest.php b/packages/i18n/tests/GenericTranslatorTest.php new file mode 100644 index 0000000000..3da4972905 --- /dev/null +++ b/packages/i18n/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 InternationalizationConfig( + currentLocale: Locale::FRENCH, + fallbackLocale: Locale::ENGLISH, + ); + + $this->translator = new GenericTranslator( + config: $this->config, + catalog: $this->catalog, + formatter: new MessageFormatter([ + new StringFunction(), + new NumberFunction(), + 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/tests/Integration/Internationalization/DiscoveryTest.php b/tests/Integration/Internationalization/DiscoveryTest.php new file mode 100644 index 0000000000..2c152c3a98 --- /dev/null +++ b/tests/Integration/Internationalization/DiscoveryTest.php @@ -0,0 +1,37 @@ +container->get(InternationalizationConfig::class); + + $this->assertCount(3, $config->functions); + } + + 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.json'); + $discovery->discoverPath(new DiscoveryLocation('', ''), __DIR__ . '/Fixtures/messages.en_US.json'); + $discovery->apply(); + + $config = $this->container->get(InternationalizationConfig::class); + + $this->assertSame([ + 'fr' => [__DIR__ . '/Fixtures/messages.fr.json'], + 'en_US' => [__DIR__ . '/Fixtures/messages.en_US.json'], + ], $config->translationMessagePaths); + } +} diff --git a/tests/Integration/Internationalization/Fixtures/messages.abcde.json b/tests/Integration/Internationalization/Fixtures/messages.abcde.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/Integration/Internationalization/Fixtures/messages.abcde.json @@ -0,0 +1 @@ +{} diff --git a/tests/Integration/Internationalization/Fixtures/messages.en_US.json b/tests/Integration/Internationalization/Fixtures/messages.en_US.json new file mode 100644 index 0000000000..eb46ca47e5 --- /dev/null +++ b/tests/Integration/Internationalization/Fixtures/messages.en_US.json @@ -0,0 +1,13 @@ +{ + "hello": "Hello, {$name}!", + "ui": { + "sidebar": { + "project": "Project", + "title": "Name of the project." + }, + "statusbar": { + "statusbar_empty": "No entries added yet.", + "statusbar_ok": "Ok." + } + } +} diff --git a/tests/Integration/Internationalization/Fixtures/messages.fr.json b/tests/Integration/Internationalization/Fixtures/messages.fr.json new file mode 100644 index 0000000000..7617923f57 --- /dev/null +++ b/tests/Integration/Internationalization/Fixtures/messages.fr.json @@ -0,0 +1,13 @@ +{ + "hello": "Bonjour, {$name}!", + "ui": { + "sidebar": { + "project": "Projet", + "title": "Nom du projet" + }, + "statusbar": { + "statusbar_empty": "Aucun item.", + "statusbar_ok": "Ok." + } + } +} diff --git a/tests/Integration/Internationalization/Fixtures/messages.json b/tests/Integration/Internationalization/Fixtures/messages.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/Integration/Internationalization/Fixtures/messages.json @@ -0,0 +1 @@ +{} diff --git a/tests/Integration/Internationalization/TranslatorTest.php b/tests/Integration/Internationalization/TranslatorTest.php new file mode 100644 index 0000000000..81d0329f30 --- /dev/null +++ b/tests/Integration/Internationalization/TranslatorTest.php @@ -0,0 +1,46 @@ +container->get(InternationalizationConfig::class); + $config->addTranslationMessageFile(Locale::FRENCH, __DIR__ . '/Fixtures/messages.fr.json'); + $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('Project', $translator->translate('ui.sidebar.project')); + $this->assertSame('Projet', $translator->translateForLocale(Locale::FRENCH, 'ui.sidebar.project')); + } + + public function test_function(): void + { + $this->assertSame('Hello, Jon Doe!', translate('hello', name: 'Jon Doe')); + $this->assertSame('Project', translate('ui.sidebar.project')); + $this->assertSame('Projet', translate_locale(Locale::FRENCH, 'ui.sidebar.project')); + } +} From cb291094accaa580ea1ad52d481eab3d622eb1e5 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 20:13:42 +0200 Subject: [PATCH 03/20] style: apply fixes from mago --- packages/i18n/bin/plural-rules.php | 19 +- .../{Translator => }/GenericTranslator.php | 13 +- .../Formatter/FormattedValue.php | 6 +- .../Formatter/MessageFormatter.php | 9 +- .../Parser/Node/ComplexBody/Matcher.php | 6 +- .../Node/ComplexBody/SimplePatternBody.php | 4 +- .../Parser/Node/ComplexBody/Variant.php | 6 +- .../Node/Declaration/InputDeclaration.php | 4 +- .../Node/Declaration/LocalDeclaration.php | 6 +- .../Parser/Node/Expression/Attribute.php | 6 +- .../Parser/Node/Expression/FunctionCall.php | 6 +- .../Parser/Node/Expression/Option.php | 6 +- .../MessageFormat/Parser/Node/Identifier.php | 6 +- .../Parser/Node/Markup/Markup.php | 10 +- .../Parser/Node/ParsingException.php | 2 +- .../Parser/Node/Pattern/Pattern.php | 4 +- .../Parser/Node/Pattern/QuotedPattern.php | 4 +- .../Parser/Node/Pattern/Text.php | 4 +- .../MessageFormat/Parser/Node/Variable.php | 4 +- .../i18n/src/MessageFormat/Parser/Parser.php | 10 +- .../src/PluralRules/PluralRulesMatcher.php | 498 +++++++++++++----- .../{Translator => }/TranslationFailure.php | 2 +- .../src/{Translator => }/TranslationMiss.php | 2 +- .../i18n/src/{Translator => }/Translator.php | 2 +- .../TranslatorInitializer.php | 2 +- packages/i18n/src/functions.php | 2 +- packages/i18n/src/i18n.config.php | 4 +- packages/i18n/tests/FormatterTest.php | 13 +- packages/i18n/tests/GenericTranslatorTest.php | 4 +- packages/support/src/Number/functions.php | 4 +- .../support/tests/Number/FunctionsTest.php | 28 +- .../Commands/ContainerShowCommandTest.php | 4 +- .../Internationalization/TranslatorTest.php | 9 +- 33 files changed, 466 insertions(+), 243 deletions(-) rename packages/i18n/src/{Translator => }/GenericTranslator.php (78%) rename packages/i18n/src/{Translator => }/TranslationFailure.php (81%) rename packages/i18n/src/{Translator => }/TranslationMiss.php (78%) rename packages/i18n/src/{Translator => }/Translator.php (93%) rename packages/i18n/src/{Translator => }/TranslatorInitializer.php (92%) diff --git a/packages/i18n/bin/plural-rules.php b/packages/i18n/bin/plural-rules.php index f46cd0a779..e5c07904c1 100755 --- a/packages/i18n/bin/plural-rules.php +++ b/packages/i18n/bin/plural-rules.php @@ -185,8 +185,7 @@ private function generateLanguageMethod(string $locale, array $rules): string break; } - $condition = $this->parseRule($rule); - if ($condition) { + if ($condition = $this->parseRule($rule)) { $output .= " if ({$condition}) {\n"; $output .= " return '{$category}';\n"; $output .= " }\n\n"; @@ -200,10 +199,9 @@ private function generateLanguageMethod(string $locale, array $rules): string private function parseRule(string $rule): string { - // Extract the rule condition (before @integer/@decimal examples) $rulePart = trim(explode('@', $rule)[0]); - if (empty($rulePart)) { + if (! $rulePart) { return ''; } @@ -233,7 +231,6 @@ private function parseSingleCondition(string $condition): string { $condition = trim($condition); - // Modulo operations "n % 100 = 3..10" or "n % 10 = 3..4,9" if (preg_match('/^([nifvet])\s*%\s*(\d+)\s*(=|!=)\s*(.+)$/', $condition, $matches)) { $var = $this->getVariable($matches[1]); $mod = $matches[2]; @@ -243,7 +240,6 @@ private function parseSingleCondition(string $condition): string return $this->parseValueCondition("({$var} % {$mod})", $op, $values); } - // Direct comparisons "n = 1" or "n = 0..1" if (preg_match('/^([nifvet])\s*(=|!=)\s*(.+)$/', $condition, $matches)) { $var = $this->getVariable($matches[1]); $op = $matches[2] === '=' ? '===' : '!=='; @@ -260,12 +256,10 @@ private function parseValueCondition(string $varExpression, string $operator, st $values = trim($values); $isNegative = $operator === '!=='; - // Handle single number if (preg_match('/^\d+(?:\.\d+)?$/', $values)) { return "{$varExpression} {$operator} {$values}"; } - // Handle single range like "3..10" if (preg_match('/^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/', $values, $matches)) { $start = $matches[1]; $end = $matches[2]; @@ -273,33 +267,29 @@ private function parseValueCondition(string $varExpression, string $operator, st return $isNegative ? "!{$condition}" : $condition; } - // Handle complex values with commas and ranges like "3..4,9" or "2,22,42,62,82" if (str_contains($values, ',')) { $parts = array_map('trim', explode(',', $values)); $conditions = []; foreach ($parts as $part) { if (str_contains($part, '..')) { - // Range like "3..4" 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, '~')) { - // Range like "3~10" if (preg_match('/^(\d+(?:\.\d+)?)~(\d+(?:\.\d+)?)$/', $part, $matches)) { $start = $matches[1]; $end = $matches[2]; $conditions[] = "self::inRange({$varExpression}, {$start}, {$end})"; } } else { - // Single value $conditions[] = "{$varExpression} === {$part}"; } } - if (empty($conditions)) { + if (! $conditions) { return 'false'; } @@ -307,7 +297,6 @@ private function parseValueCondition(string $varExpression, string $operator, st return $isNegative ? "!{$combined}" : $combined; } - // Handle tilde ranges like "3~10" if (str_contains($values, '~')) { if (preg_match('/^(\d+(?:\.\d+)?)~(\d+(?:\.\d+)?)$/', $values, $matches)) { $start = $matches[1]; @@ -317,8 +306,8 @@ private function parseValueCondition(string $varExpression, string $operator, st } } - // Fallback: use matchesValues for complex patterns $condition = "self::matchesValues({$varExpression}, '{$values}')"; + return $isNegative ? "!{$condition}" : $condition; } diff --git a/packages/i18n/src/Translator/GenericTranslator.php b/packages/i18n/src/GenericTranslator.php similarity index 78% rename from packages/i18n/src/Translator/GenericTranslator.php rename to packages/i18n/src/GenericTranslator.php index b824c3ec28..ca136e21b4 100644 --- a/packages/i18n/src/Translator/GenericTranslator.php +++ b/packages/i18n/src/GenericTranslator.php @@ -1,21 +1,20 @@ variable->name->name; if (! array_key_exists($variableName, $this->variables)) { - throw new FormattingException("Variable `$variableName` not found"); + throw new FormattingException("Variable `{$variableName}` not found"); } $value = $this->variables[$variableName]; @@ -304,9 +303,9 @@ private function formatMarkup(Markup $markup): string $tag = (string) $markup->identifier; return match ($markup->type) { - MarkupType::OPEN => "<$tag>", - MarkupType::CLOSE => "", - MarkupType::STANDALONE => "<$tag/>", + MarkupType::OPEN => "<{$tag}>", + MarkupType::CLOSE => "", + MarkupType::STANDALONE => "<{$tag}/>", default => '', }; } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php index cbf6e9604f..efaf7ca229 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php @@ -4,15 +4,15 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Pattern\Pattern; -final class Matcher implements ComplexBody +final readonly class Matcher implements ComplexBody { /** * @param Variable[] $selectors * @param Variant[] $variants */ public function __construct( - public readonly array $selectors, - public readonly array $variants, + public array $selectors, + public array $variants, ) {} public function getPattern(): Pattern diff --git a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php index e33f54e252..a6226d3584 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php @@ -4,10 +4,10 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Pattern\Pattern; -final class SimplePatternBody implements ComplexBody +final readonly class SimplePatternBody implements ComplexBody { public function __construct( - public readonly Pattern $pattern, + public Pattern $pattern, ) {} public function getPattern(): Pattern diff --git a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php index ff4412955c..3d98837023 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php @@ -5,13 +5,13 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Node; use Tempest\Internationalization\MessageFormat\Parser\Node\Pattern\QuotedPattern; -final class Variant implements Node +final readonly class Variant implements Node { /** * @param Key[] $keys */ public function __construct( - public readonly array $keys, - public readonly QuotedPattern $pattern, + public array $keys, + public QuotedPattern $pattern, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Declaration/InputDeclaration.php b/packages/i18n/src/MessageFormat/Parser/Node/Declaration/InputDeclaration.php index 761a2d5e5c..3154cfd3cc 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Declaration/InputDeclaration.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Declaration/InputDeclaration.php @@ -4,9 +4,9 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Expression\VariableExpression; -final class InputDeclaration implements Declaration +final readonly class InputDeclaration implements Declaration { public function __construct( - public readonly VariableExpression $expression, + public VariableExpression $expression, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Declaration/LocalDeclaration.php b/packages/i18n/src/MessageFormat/Parser/Node/Declaration/LocalDeclaration.php index 9390a37521..cf61f527c5 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Declaration/LocalDeclaration.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Declaration/LocalDeclaration.php @@ -5,10 +5,10 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Expression\Expression; use Tempest\Internationalization\MessageFormat\Parser\Node\Variable; -final class LocalDeclaration implements Declaration +final readonly class LocalDeclaration implements Declaration { public function __construct( - public readonly Variable $variable, - public readonly Expression $expression, + public Variable $variable, + public Expression $expression, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Expression/Attribute.php b/packages/i18n/src/MessageFormat/Parser/Node/Expression/Attribute.php index d82c22d7af..e292c144f4 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Expression/Attribute.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Expression/Attribute.php @@ -6,10 +6,10 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Literal\Literal; use Tempest\Internationalization\MessageFormat\Parser\Node\Node; -final class Attribute implements Node +final readonly class Attribute implements Node { public function __construct( - public readonly Identifier $identifier, - public readonly ?Literal $value, + public Identifier $identifier, + public ?Literal $value, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Expression/FunctionCall.php b/packages/i18n/src/MessageFormat/Parser/Node/Expression/FunctionCall.php index e17bbcba38..3b6a5ff6bb 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Expression/FunctionCall.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Expression/FunctionCall.php @@ -5,13 +5,13 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Identifier; use Tempest\Internationalization\MessageFormat\Parser\Node\Node; -final class FunctionCall implements Node +final readonly class FunctionCall implements Node { /** * @param (Option)[] $options */ public function __construct( - public readonly Identifier $identifier, - public readonly array $options, + public Identifier $identifier, + public array $options, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Expression/Option.php b/packages/i18n/src/MessageFormat/Parser/Node/Expression/Option.php index ebc99c0b4f..3a7590d3fe 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Expression/Option.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Expression/Option.php @@ -7,10 +7,10 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Node; use Tempest\Internationalization\MessageFormat\Parser\Node\Variable; -final class Option implements Node +final readonly class Option implements Node { public function __construct( - public readonly Identifier $identifier, - public readonly Literal|Variable $value, + public Identifier $identifier, + public Literal|Variable $value, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Identifier.php b/packages/i18n/src/MessageFormat/Parser/Node/Identifier.php index 21d6804316..9bb0220f7c 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Identifier.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Identifier.php @@ -2,11 +2,11 @@ namespace Tempest\Internationalization\MessageFormat\Parser\Node; -final class Identifier implements Node +final readonly class Identifier implements Node { public function __construct( - public readonly string $name, - public readonly ?string $namespace = null, + public string $name, + public ?string $namespace = null, ) {} public function __toString(): string diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Markup/Markup.php b/packages/i18n/src/MessageFormat/Parser/Node/Markup/Markup.php index 92dd1599bd..50dcae7d3b 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Markup/Markup.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Markup/Markup.php @@ -5,16 +5,16 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Identifier; use Tempest\Internationalization\MessageFormat\Parser\Node\Pattern\Placeholder; -final class Markup implements Placeholder +final readonly class Markup implements Placeholder { /** * @param (Option)[] $options * @param (Attribute)[] $attributes */ public function __construct( - public readonly MarkupType $type, - public readonly Identifier $identifier, - public readonly array $options, - public readonly array $attributes, + public MarkupType $type, + public Identifier $identifier, + public array $options, + public array $attributes, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/ParsingException.php b/packages/i18n/src/MessageFormat/Parser/Node/ParsingException.php index a6f4b5d057..b2a135bf46 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/ParsingException.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/ParsingException.php @@ -8,6 +8,6 @@ public function __construct( string $message, public readonly int $position, ) { - parent::__construct("$message at position $position"); + parent::__construct("{$message} at position {$position}"); } } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Pattern.php b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Pattern.php index e597eaf991..3b37145470 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Pattern.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Pattern.php @@ -4,12 +4,12 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Node; -final class Pattern implements Node +final readonly class Pattern implements Node { /** * @param (Text|Placeholder|QuotedPattern)[] $elements */ public function __construct( - public readonly array $elements, + public array $elements, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Pattern/QuotedPattern.php b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/QuotedPattern.php index bb40c890bf..45fc91e72b 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Pattern/QuotedPattern.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/QuotedPattern.php @@ -4,10 +4,10 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\ComplexBody\ComplexBody; -final class QuotedPattern implements ComplexBody, Placeholder +final readonly class QuotedPattern implements ComplexBody, Placeholder { public function __construct( - public readonly Pattern $pattern, + public Pattern $pattern, ) {} public function getPattern(): Pattern diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php index a0bb20b2b6..bf18b8826e 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php @@ -4,9 +4,9 @@ use Tempest\Internationalization\MessageFormat\Parser\Node\Node; -final class Text implements Node +final readonly class Text implements Node { public function __construct( - public readonly string $value, + public string $value, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Node/Variable.php b/packages/i18n/src/MessageFormat/Parser/Node/Variable.php index 4ad2d21464..657f2f39b3 100644 --- a/packages/i18n/src/MessageFormat/Parser/Node/Variable.php +++ b/packages/i18n/src/MessageFormat/Parser/Node/Variable.php @@ -2,9 +2,9 @@ namespace Tempest\Internationalization\MessageFormat\Parser\Node; -final class Variable implements Node +final readonly class Variable implements Node { public function __construct( - public readonly Identifier $name, + public Identifier $name, ) {} } diff --git a/packages/i18n/src/MessageFormat/Parser/Parser.php b/packages/i18n/src/MessageFormat/Parser/Parser.php index a122d86fd0..ac444f5819 100644 --- a/packages/i18n/src/MessageFormat/Parser/Parser.php +++ b/packages/i18n/src/MessageFormat/Parser/Parser.php @@ -42,13 +42,13 @@ final class Parser * Regex for a valid name-start character, as per ABNF. * `\p{L}`: any Unicode letter. */ - private const NAME_START_REGEX = '/[a-zA-Z_+]|\p{L}/u'; + private const string NAME_START_REGEX = '/[a-zA-Z_+]|\p{L}/u'; /** * Regex for valid subsequent name characters. * `\p{N}`: any Unicode number. */ - private const NAME_CHAR_REGEX = '/[a-zA-Z0-9_.-]|\p{L}|\p{N}/u'; + private const string NAME_CHAR_REGEX = '/[a-zA-Z0-9_.-]|\p{L}|\p{N}/u'; public function __construct(string $input) { @@ -474,7 +474,7 @@ private function parseUnquotedLiteral(): UnquotedLiteral $char = $this->peek(); if ($char === '' || ! preg_match(self::NAME_CHAR_REGEX, $char)) { - $this->throw("Invalid unquoted literal start character: '$char'"); + $this->throw("Invalid unquoted literal start character: '{$char}'"); } $buffer .= $this->readChar(); @@ -512,7 +512,7 @@ private function parseName(): string $start = $this->peek(); if (! preg_match(self::NAME_START_REGEX, $start)) { - $this->throw("Invalid identifier start character: '$start'"); + $this->throw("Invalid identifier start character: '{$start}'"); } $buffer = $this->readChar(); @@ -545,7 +545,7 @@ private function parseEscapedChar(array $escapable): string private function consumeKeyword(string $keyword): void { if ($this->peek(strlen($keyword)) !== $keyword) { - $this->throw("Expected keyword '$keyword'"); + $this->throw("Expected keyword '{$keyword}'"); } $this->pos += strlen($keyword); diff --git a/packages/i18n/src/PluralRules/PluralRulesMatcher.php b/packages/i18n/src/PluralRules/PluralRulesMatcher.php index 1d8041a0d2..200e1490ab 100644 --- a/packages/i18n/src/PluralRules/PluralRulesMatcher.php +++ b/packages/i18n/src/PluralRules/PluralRulesMatcher.php @@ -25,7 +25,7 @@ private static function getVisibleFractionalDigits(float|int $n): int { $str = (string) $n; - if (!str_contains($str, '.')) { + if (! str_contains($str, '.')) { return 0; } @@ -39,11 +39,11 @@ private static function getFractionalDigits(float|int $n): int { $str = (string) $n; - if (!str_contains($str, '.')) { + if (! str_contains($str, '.')) { return 0; } - return (int) rtrim(explode('.', $str)[1], '0') ?: 0; + return ((int) rtrim(explode('.', $str)[1], '0')) ?: 0; } /** @@ -51,7 +51,7 @@ private static function getFractionalDigits(float|int $n): int */ private static function getCompactExponent(float|int $n): int { - if ($n == 0) { + if ($n === 0 || $n === 0.0) { return 0; } @@ -73,7 +73,7 @@ private static function getCompactExponent(float|int $n): int */ private static function getExponent(float|int $n): int { - if ($n == 0) { + if ($n === 0 || $n === 0.0) { return 0; } @@ -110,12 +110,13 @@ private static function matchesValues(int|float $value, string $values): bool if (self::inRange($value, (float) trim($start), (float) trim($end))) { return true; } - } elseif ((float) $part === (float) $value) { + } elseif (((float) $part) === ((float) $value)) { return true; } } return false; } + /** * Gets the plural category for the af locale. */ @@ -163,7 +164,7 @@ private static function getPluralCategoryAm(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -211,11 +212,11 @@ private static function getPluralCategoryAr(float|int $n): string return 'two'; } - if (self::inRange(($n % 100), 3, 10)) { + if (self::inRange($n % 100, 3, 10)) { return 'few'; } - if (self::inRange(($n % 100), 11, 99)) { + if (self::inRange($n % 100, 11, 99)) { return 'many'; } @@ -245,11 +246,11 @@ private static function getPluralCategoryArs(float|int $n): string return 'two'; } - if (self::inRange(($n % 100), 3, 10)) { + if (self::inRange($n % 100, 3, 10)) { return 'few'; } - if (self::inRange(($n % 100), 11, 99)) { + if (self::inRange($n % 100, 11, 99)) { return 'many'; } @@ -267,7 +268,7 @@ private static function getPluralCategoryAs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -303,7 +304,7 @@ private static function getPluralCategoryAst(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -357,15 +358,15 @@ private static function getPluralCategoryBe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($n % 10) === 1) && (($n % 100) !== 11)) { + if (($n % 10) === 1 && ($n % 100) !== 11) { return 'one'; } - if ((self::inRange(($n % 10), 2, 4)) && (!self::inRange(($n % 100), 12, 14))) { + 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))) { + if (($n % 10) === 0 || self::inRange($n % 10, 5, 9) || self::inRange($n % 100, 11, 14)) { return 'many'; } @@ -491,7 +492,7 @@ private static function getPluralCategoryBn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -523,19 +524,19 @@ private static function getPluralCategoryBr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($n % 10) === 1) && (!(($n % 100) === 11 || ($n % 100) === 71 || ($n % 100) === 91))) { + 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))) { + 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)))) { + 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)) { + if ($n !== 0 && ($n % 1000000) === 0) { return 'many'; } @@ -571,11 +572,11 @@ private static function getPluralCategoryBs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($v === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) || ((($f % 10) === 1) && (($f % 100) !== 11))) { + 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)))) { + 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'; } @@ -593,11 +594,11 @@ private static function getPluralCategoryCa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -633,7 +634,11 @@ private static function getPluralCategoryCeb(float|int $n): string $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)))) { + 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'; } @@ -705,11 +710,11 @@ private static function getPluralCategoryCs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((self::inRange($i, 2, 4)) && ($v === 0)) { + if (self::inRange($i, 2, 4) && $v === 0) { return 'few'; } @@ -783,7 +788,7 @@ private static function getPluralCategoryDa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($n === 1) || (($t !== 0) && (($i === 0 || $i === 1)))) { + if ($n === 1 || $t !== 0 && ($i === 0 || $i === 1)) { return 'one'; } @@ -801,7 +806,7 @@ private static function getPluralCategoryDe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -819,7 +824,7 @@ private static function getPluralCategoryDoi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -837,15 +842,15 @@ private static function getPluralCategoryDsb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($v === 0) && (($i % 100) === 1)) || (($f % 100) === 1)) { + if ($v === 0 && ($i % 100) === 1 || ($f % 100) === 1) { return 'one'; } - if ((($v === 0) && (($i % 100) === 2)) || (($f % 100) === 2)) { + 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))) { + if ($v === 0 && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { return 'few'; } @@ -931,7 +936,7 @@ private static function getPluralCategoryEn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -971,7 +976,7 @@ private static function getPluralCategoryEs(float|int $n): string return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -989,7 +994,7 @@ private static function getPluralCategoryEt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -1025,7 +1030,7 @@ private static function getPluralCategoryFa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -1043,7 +1048,7 @@ private static function getPluralCategoryFf(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0 || $i === 1)) { + if ($i === 0 || $i === 1) { return 'one'; } @@ -1061,7 +1066,7 @@ private static function getPluralCategoryFi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -1079,7 +1084,11 @@ private static function getPluralCategoryFil(float|int $n): string $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)))) { + 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'; } @@ -1115,11 +1124,11 @@ private static function getPluralCategoryFr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0 || $i === 1)) { + if ($i === 0 || $i === 1) { return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -1155,7 +1164,7 @@ private static function getPluralCategoryFy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -1203,15 +1212,15 @@ private static function getPluralCategoryGd(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($n === 1 || $n === 11)) { + if ($n === 1 || $n === 11) { return 'one'; } - if (($n === 2 || $n === 12)) { + if ($n === 2 || $n === 12) { return 'two'; } - if ((self::inRange($n, 3, 10) || self::inRange($n, 13, 19))) { + if (self::inRange($n, 3, 10) || self::inRange($n, 13, 19)) { return 'few'; } @@ -1229,7 +1238,7 @@ private static function getPluralCategoryGl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -1265,7 +1274,7 @@ private static function getPluralCategoryGu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -1301,15 +1310,15 @@ private static function getPluralCategoryGv(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($v === 0) && (($i % 10) === 1)) { + if ($v === 0 && ($i % 10) === 1) { return 'one'; } - if (($v === 0) && (($i % 10) === 2)) { + 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))) { + if ($v === 0 && (($i % 100) === 0 || ($i % 100) === 20 || ($i % 100) === 40 || ($i % 100) === 60 || ($i % 100) === 80)) { return 'few'; } @@ -1367,11 +1376,11 @@ private static function getPluralCategoryHe(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($i === 1) && ($v === 0)) || (($i === 0) && ($v !== 0))) { + if ($i === 1 && $v === 0 || $i === 0 && $v !== 0) { return 'one'; } - if (($i === 2) && ($v === 0)) { + if ($i === 2 && $v === 0) { return 'two'; } @@ -1389,7 +1398,7 @@ private static function getPluralCategoryHi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -1421,11 +1430,11 @@ private static function getPluralCategoryHr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($v === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) || ((($f % 10) === 1) && (($f % 100) !== 11))) { + 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)))) { + 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'; } @@ -1443,15 +1452,15 @@ private static function getPluralCategoryHsb(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($v === 0) && (($i % 100) === 1)) || (($f % 100) === 1)) { + if ($v === 0 && ($i % 100) === 1 || ($f % 100) === 1) { return 'one'; } - if ((($v === 0) && (($i % 100) === 2)) || (($f % 100) === 2)) { + 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))) { + if ($v === 0 && self::inRange($i % 100, 3, 4) || self::inRange($f % 100, 3, 4)) { return 'few'; } @@ -1487,7 +1496,7 @@ private static function getPluralCategoryHy(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0 || $i === 1)) { + if ($i === 0 || $i === 1) { return 'one'; } @@ -1505,7 +1514,7 @@ private static function getPluralCategoryIa(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -1565,7 +1574,7 @@ private static function getPluralCategoryIo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -1583,7 +1592,7 @@ private static function getPluralCategoryIs(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($t === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) || ((($t % 10) === 1) && (($t % 100) !== 11))) { + if ($t === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($t % 10) === 1 && ($t % 100) !== 11) { return 'one'; } @@ -1601,11 +1610,11 @@ private static function getPluralCategoryIt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -1755,7 +1764,7 @@ private static function getPluralCategoryKab(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0 || $i === 1)) { + if ($i === 0 || $i === 1) { return 'one'; } @@ -1905,7 +1914,7 @@ private static function getPluralCategoryKn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -2021,15 +2030,23 @@ private static function getPluralCategoryKw(float|int $n): string 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))) { + 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)) { + 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))) { + if ($n !== 1 && (($n % 100) === 1 || ($n % 100) === 21 || ($n % 100) === 41 || ($n % 100) === 61 || ($n % 100) === 81)) { return 'many'; } @@ -2069,7 +2086,7 @@ private static function getPluralCategoryLag(float|int $n): string return 'zero'; } - if ((($i === 0 || $i === 1)) && ($n !== 0)) { + if (($i === 0 || $i === 1) && $n !== 0) { return 'one'; } @@ -2123,7 +2140,7 @@ private static function getPluralCategoryLij(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -2155,11 +2172,11 @@ private static function getPluralCategoryLld(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -2209,11 +2226,11 @@ private static function getPluralCategoryLt(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($n % 10) === 1) && (!self::inRange(($n % 100), 11, 19))) { + 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))) { + if (self::inRange($n % 10, 2, 9) && ! self::inRange($n % 100, 11, 19)) { return 'few'; } @@ -2235,11 +2252,11 @@ private static function getPluralCategoryLv(float|int $n): string $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)))) { + 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))) { + if (($n % 10) === 1 && ($n % 100) !== 11 || $v === 2 && ($f % 10) === 1 && ($f % 100) !== 11 || $v !== 2 && ($f % 10) === 1) { return 'one'; } @@ -2311,7 +2328,7 @@ private static function getPluralCategoryMk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($v === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) || ((($f % 10) === 1) && (($f % 100) !== 11))) { + if ($v === 0 && ($i % 10) === 1 && ($i % 100) !== 11 || ($f % 10) === 1 && ($f % 100) !== 11) { return 'one'; } @@ -2365,11 +2382,11 @@ private static function getPluralCategoryMo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if (($v !== 0) || ($n === 0) || (($n !== 1) && (self::inRange(($n % 100), 1, 19)))) { + if ($v !== 0 || $n === 0 || $n !== 1 && self::inRange($n % 100, 1, 19)) { return 'few'; } @@ -2427,11 +2444,11 @@ private static function getPluralCategoryMt(float|int $n): string return 'two'; } - if (($n === 0) || (self::inRange(($n % 100), 3, 10))) { + if ($n === 0 || self::inRange($n % 100, 3, 10)) { return 'few'; } - if (self::inRange(($n % 100), 11, 19)) { + if (self::inRange($n % 100, 11, 19)) { return 'many'; } @@ -2557,7 +2574,7 @@ private static function getPluralCategoryNl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -2819,7 +2836,7 @@ private static function getPluralCategoryPcm(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -2837,15 +2854,15 @@ private static function getPluralCategoryPl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if (($v === 0) && (self::inRange(($i % 10), 2, 4)) && (!self::inRange(($i % 100), 12, 14))) { + 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)))) { + 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'; } @@ -2863,11 +2880,11 @@ private static function getPluralCategoryPrg(float|int $n): string $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)))) { + 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))) { + if (($n % 10) === 1 && ($n % 100) !== 11 || $v === 2 && ($f % 10) === 1 && ($f % 100) !== 11 || $v !== 2 && ($f % 10) === 1) { return 'one'; } @@ -2907,7 +2924,7 @@ private static function getPluralCategoryPt(float|int $n): string return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -2925,11 +2942,11 @@ private static function getPluralCategoryPt_PT(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -2965,11 +2982,11 @@ private static function getPluralCategoryRo(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if (($v !== 0) || ($n === 0) || (($n !== 1) && (self::inRange(($n % 100), 1, 19)))) { + if ($v !== 0 || $n === 0 || $n !== 1 && self::inRange($n % 100, 1, 19)) { return 'few'; } @@ -3005,15 +3022,15 @@ private static function getPluralCategoryRu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($v === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) { + 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))) { + 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)))) { + if ($v === 0 && ($i % 10) === 0 || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 11, 14)) { return 'many'; } @@ -3103,7 +3120,7 @@ private static function getPluralCategorySc(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -3121,11 +3138,11 @@ private static function getPluralCategoryScn(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -3247,11 +3264,11 @@ private static function getPluralCategorySh(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($v === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) || ((($f % 10) === 1) && (($f % 100) !== 11))) { + 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)))) { + 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'; } @@ -3269,7 +3286,7 @@ private static function getPluralCategoryShi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -3291,7 +3308,7 @@ private static function getPluralCategorySi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($n === 0 || $n === 1)) || (($i === 0) && ($f === 1))) { + if ($n === 0 || $n === 1 || $i === 0 && $f === 1) { return 'one'; } @@ -3309,11 +3326,11 @@ private static function getPluralCategorySk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((self::inRange($i, 2, 4)) && ($v === 0)) { + if (self::inRange($i, 2, 4) && $v === 0) { return 'few'; } @@ -3335,15 +3352,15 @@ private static function getPluralCategorySl(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($v === 0) && (($i % 100) === 1)) { + if ($v === 0 && ($i % 100) === 1) { return 'one'; } - if (($v === 0) && (($i % 100) === 2)) { + if ($v === 0 && ($i % 100) === 2) { return 'two'; } - if ((($v === 0) && (self::inRange(($i % 100), 3, 4))) || ($v !== 0)) { + if ($v === 0 && self::inRange($i % 100, 3, 4) || $v !== 0) { return 'few'; } @@ -3525,11 +3542,11 @@ private static function getPluralCategorySr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((($v === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) || ((($f % 10) === 1) && (($f % 100) !== 11))) { + 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)))) { + 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'; } @@ -3615,7 +3632,7 @@ private static function getPluralCategorySv(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -3633,7 +3650,7 @@ private static function getPluralCategorySw(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -3791,7 +3808,11 @@ private static function getPluralCategoryTl(float|int $n): string $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)))) { + 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'; } @@ -3891,7 +3912,7 @@ private static function getPluralCategoryTzm(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if ((self::inRange($n, 0, 1)) || (self::inRange($n, 11, 99))) { + if (self::inRange($n, 0, 1) || self::inRange($n, 11, 99)) { return 'one'; } @@ -3927,15 +3948,15 @@ private static function getPluralCategoryUk(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($v === 0) && (($i % 10) === 1) && (($i % 100) !== 11)) { + 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))) { + 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)))) { + if ($v === 0 && ($i % 10) === 0 || $v === 0 && self::inRange($i % 10, 5, 9) || $v === 0 && self::inRange($i % 100, 11, 14)) { return 'many'; } @@ -3967,7 +3988,7 @@ private static function getPluralCategoryUr(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -4021,11 +4042,11 @@ private static function getPluralCategoryVec(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } - if ((($e === 0) && ($i !== 0) && (($i % 1000000) === 0) && ($v === 0)) || (!self::inRange($e, 0, 5))) { + if ($e === 0 && $i !== 0 && ($i % 1000000) === 0 && $v === 0 || ! self::inRange($e, 0, 5)) { return 'many'; } @@ -4179,7 +4200,7 @@ private static function getPluralCategoryYi(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 1) && ($v === 0)) { + if ($i === 1 && $v === 0) { return 'one'; } @@ -4239,7 +4260,7 @@ private static function getPluralCategoryZu(float|int $n): string $t = self::getCompactExponent($n); $e = self::getExponent($n); - if (($i === 0) || ($n === 1)) { + if ($i === 0 || $n === 1) { return 'one'; } @@ -4251,7 +4272,7 @@ private static function getPluralCategoryZu(float|int $n): string */ public static function getPluralCategory(Locale $locale, float|int $number): string { - return match($locale->getLanguage()) { + return match ($locale->getLanguage()) { 'af' => self::getPluralCategoryAf($number), 'ak' => self::getPluralCategoryAk($number), 'am' => self::getPluralCategoryAm($number), @@ -4471,7 +4492,7 @@ public static function getPluralCategory(Locale $locale, float|int $number): str 'yue' => self::getPluralCategoryYue($number), 'zh' => self::getPluralCategoryZh($number), 'zu' => self::getPluralCategoryZu($number), - default => 'other' + default => 'other', }; } @@ -4480,7 +4501,226 @@ public static function getPluralCategory(Locale $locale, float|int $number): str */ 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']; + 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/i18n/src/Translator/TranslationFailure.php b/packages/i18n/src/TranslationFailure.php similarity index 81% rename from packages/i18n/src/Translator/TranslationFailure.php rename to packages/i18n/src/TranslationFailure.php index 574896d7d1..5d77d329a4 100644 --- a/packages/i18n/src/Translator/TranslationFailure.php +++ b/packages/i18n/src/TranslationFailure.php @@ -1,6 +1,6 @@ assertSame('Hello, Jon!', $value); } - public function test_format_datetime_function(): void - { - $formatter = new MessageFormatter([new DateTimeFunction()]); - - $value = $formatter->format(<<<'TXT' - Today is {$today :datetime}. - TXT, today: '2024-01-01'); - - $this->assertSame("Today is Jan 1, 2024, 12:00:00\u{202F}AM.", $value); - } - public function test_format_datetime_function_and_parameters(): void { $formatter = new MessageFormatter([new DateTimeFunction()]); @@ -236,7 +225,7 @@ public function test_string_formatting_options(mixed $input, string $expected, s $formatter = new MessageFormatter([new StringFunction()]); $value = $formatter->format(<<assertSame($expected, $value); diff --git a/packages/i18n/tests/GenericTranslatorTest.php b/packages/i18n/tests/GenericTranslatorTest.php index 3da4972905..68ae8aede9 100644 --- a/packages/i18n/tests/GenericTranslatorTest.php +++ b/packages/i18n/tests/GenericTranslatorTest.php @@ -5,13 +5,13 @@ use PHPUnit\Framework\TestCase; use Tempest\Internationalization\Catalog\Catalog; use Tempest\Internationalization\Catalog\GenericCatalog; +use Tempest\Internationalization\GenericTranslator; use Tempest\Internationalization\InternationalizationConfig; use Tempest\Internationalization\MessageFormat\Formatter\MessageFormatter; use Tempest\Internationalization\MessageFormat\Functions\DateTimeFunction; use Tempest\Internationalization\MessageFormat\Functions\NumberFunction; use Tempest\Internationalization\MessageFormat\Functions\StringFunction; -use Tempest\Internationalization\Translator\GenericTranslator; -use Tempest\Internationalization\Translator\Translator; +use Tempest\Internationalization\Translator; use Tempest\Support\Language\Locale; final class GenericTranslatorTest extends TestCase diff --git a/packages/support/src/Number/functions.php b/packages/support/src/Number/functions.php index ca5ecdea29..91af8e9562 100644 --- a/packages/support/src/Number/functions.php +++ b/packages/support/src/Number/functions.php @@ -18,7 +18,7 @@ function parse(mixed $number, int|float $default = 0): int|float /** * Returns the int value of the given `$number`, defaulting to `$default` if the input is not a valid integer. */ -function parseInt(mixed $number, int $default = 0): int +function parse_int(mixed $number, int $default = 0): int { return filter_var($number, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) ?? $default; } @@ -26,7 +26,7 @@ function parseInt(mixed $number, int $default = 0): int /** * Returns the float value of the given `$number`, defaulting to `$default` if the input is not a valid float. */ -function parseFloat(mixed $number, float $default = 0.0): float +function parse_float(mixed $number, float $default = 0.0): float { return filter_var($number, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE) ?? $default; } diff --git a/packages/support/tests/Number/FunctionsTest.php b/packages/support/tests/Number/FunctionsTest.php index da1d67a914..20982b6bc0 100644 --- a/packages/support/tests/Number/FunctionsTest.php +++ b/packages/support/tests/Number/FunctionsTest.php @@ -251,24 +251,24 @@ public function test_summarize(): void public function test_parse_int(): void { - $this->assertSame(1, Number\parseInt(1)); - $this->assertSame(1, Number\parseInt(1, default: 0)); - $this->assertSame(1, Number\parseInt('1')); - $this->assertSame(0, Number\parseInt('1.1')); - $this->assertSame(0, Number\parseInt(1.1)); - $this->assertSame(10, Number\parseInt(1.1, default: 10)); - $this->assertSame(Math\INT64_MAX, Number\parseInt(Math\INT64_MAX)); + $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\parseFloat(1)); - $this->assertSame(1.0, Number\parseFloat(1, default: 0.0)); - $this->assertSame(1.0, Number\parseFloat('1')); - $this->assertSame(1.1, Number\parseFloat('1.1')); - $this->assertSame(1.1, Number\parseFloat(1.1)); - $this->assertSame(10.0, Number\parseFloat('abc', default: 10.0)); - $this->assertSame(Math\FLOAT32_MAX, Number\parseFloat(Math\FLOAT32_MAX)); + $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 diff --git a/tests/Integration/Container/Commands/ContainerShowCommandTest.php b/tests/Integration/Container/Commands/ContainerShowCommandTest.php index 420000b172..bef2344d9a 100644 --- a/tests/Integration/Container/Commands/ContainerShowCommandTest.php +++ b/tests/Integration/Container/Commands/ContainerShowCommandTest.php @@ -23,9 +23,9 @@ public function test_with_another_container(): void { $this->container->singleton( Container::class, - new class(clone $this->container) implements Container { + new readonly class(clone $this->container) implements Container { public function __construct( - private readonly Container $container, + private Container $container, ) {} public function register(string $className, callable $definition): self diff --git a/tests/Integration/Internationalization/TranslatorTest.php b/tests/Integration/Internationalization/TranslatorTest.php index 81d0329f30..24270872a1 100644 --- a/tests/Integration/Internationalization/TranslatorTest.php +++ b/tests/Integration/Internationalization/TranslatorTest.php @@ -9,7 +9,7 @@ use Tempest\Discovery\DiscoveryLocation; use Tempest\Internationalization\Catalog\Catalog; use Tempest\Internationalization\InternationalizationConfig; -use Tempest\Internationalization\Translator\Translator; +use Tempest\Internationalization\Translator; use Tempest\Support\Language\Locale; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -43,4 +43,11 @@ public function test_function(): void $this->assertSame('Project', translate('ui.sidebar.project')); $this->assertSame('Projet', translate_locale(Locale::FRENCH, 'ui.sidebar.project')); } + + public function test_default_locale(): void + { + $config = $this->container->get(InternationalizationConfig::class); + + $this->assertSame(Locale::default(), $config->currentLocale); + } } From 64aa360654af8923e1d1bb10bf31aed5ae7a4709 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 21:36:42 +0200 Subject: [PATCH 04/20] refactor: move `Locale`, `Number` and `Pluralizer` to `tempest/intl` --- composer.json | 13 ++-- packages/database/composer.json | 1 + .../PluralizedSnakeCaseStrategyTest.php | 2 +- packages/datetime/composer.json | 1 + packages/datetime/src/DateTime.php | 2 +- .../src/DateTimeConvenienceMethods.php | 2 +- packages/datetime/src/DateTimeInterface.php | 2 +- .../src/TemporalConvenienceMethods.php | 2 +- packages/datetime/src/TemporalInterface.php | 2 +- packages/datetime/src/Timestamp.php | 2 +- packages/datetime/src/functions.php | 2 +- packages/datetime/tests/DateTimeTest.php | 2 +- packages/datetime/tests/TimestampTest.php | 2 +- packages/i18n/composer.json | 28 --------- .../Parser/Node/ComplexBody/ComplexBody.php | 11 ---- .../Parser/Node/ComplexBody/Variant.php | 17 ----- .../Parser/Node/Declaration/Declaration.php | 9 --- .../Node/Declaration/InputDeclaration.php | 12 ---- .../Node/Declaration/LocalDeclaration.php | 14 ----- .../Parser/Node/Expression/Attribute.php | 15 ----- .../Parser/Node/Expression/Option.php | 16 ----- .../src/MessageFormat/Parser/Node/Key/Key.php | 9 --- .../Parser/Node/Key/WildcardKey.php | 7 --- .../Parser/Node/Literal/Literal.php | 13 ---- .../Parser/Node/Literal/QuotedLiteral.php | 7 --- .../Parser/Node/Literal/UnquotedLiteral.php | 7 --- .../MessageFormat/Parser/Node/MessageNode.php | 12 ---- .../src/MessageFormat/Parser/Node/Node.php | 7 --- .../Parser/Node/Pattern/Placeholder.php | 9 --- .../Parser/Node/Pattern/Text.php | 12 ---- .../Parser/Node/SimpleMessage.php | 7 --- packages/i18n/src/functions.php | 24 ------- packages/{i18n => intl}/.gitattributes | 0 packages/{i18n => intl}/LICENCE.md | 0 packages/{i18n => intl}/bin/plural-rules.php | 8 +-- packages/intl/composer.json | 31 ++++++++++ packages/{i18n => intl}/phpunit.xml | 0 .../{i18n => intl}/src/Catalog/Catalog.php | 4 +- .../src/Catalog/CatalogInitializer.php | 6 +- .../src/Catalog/GenericCatalog.php | 6 +- .../{i18n => intl}/src/GenericTranslator.php | 10 +-- .../src/InternationalizationConfig.php | 6 +- .../src/Language => intl/src}/Locale.php | 2 +- .../Formatter/FormattedValue.php | 2 +- .../Formatter/FormattingException.php | 2 +- .../Formatter/MessageFormatFunction.php | 2 +- .../Formatter/MessageFormatter.php | 52 ++++++++-------- .../Functions/DateTimeFunction.php | 6 +- .../Functions/NumberFunction.php | 8 +-- .../Functions/StringFunction.php | 6 +- .../Parser/Node/ComplexBody/ComplexBody.php | 11 ++++ .../Parser/Node/ComplexBody/Matcher.php | 4 +- .../Node/ComplexBody/SimplePatternBody.php | 4 +- .../Parser/Node/ComplexBody/Variant.php | 17 +++++ .../Parser/Node/ComplexMessage.php | 4 +- .../Parser/Node/Declaration/Declaration.php | 9 +++ .../Node/Declaration/InputDeclaration.php | 12 ++++ .../Node/Declaration/LocalDeclaration.php | 14 +++++ .../Parser/Node/Expression/Attribute.php | 15 +++++ .../Parser/Node/Expression/Expression.php | 4 +- .../Parser/Node/Expression/FunctionCall.php | 6 +- .../Node/Expression/FunctionExpression.php | 2 +- .../Node/Expression/LiteralExpression.php | 4 +- .../Parser/Node/Expression/Option.php | 16 +++++ .../Node/Expression/VariableExpression.php | 4 +- .../MessageFormat/Parser/Node/Identifier.php | 2 +- .../src/MessageFormat/Parser/Node/Key/Key.php | 9 +++ .../Parser/Node/Key/WildcardKey.php | 7 +++ .../Parser/Node/Literal/Literal.php | 13 ++++ .../Parser/Node/Literal/QuotedLiteral.php | 7 +++ .../Parser/Node/Literal/UnquotedLiteral.php | 7 +++ .../Parser/Node/Markup/Markup.php | 6 +- .../Parser/Node/Markup/MarkupType.php | 2 +- .../MessageFormat/Parser/Node/MessageNode.php | 12 ++++ .../src/MessageFormat/Parser/Node/Node.php | 7 +++ .../Parser/Node/ParsingException.php | 2 +- .../Parser/Node/Pattern/Pattern.php | 4 +- .../Parser/Node/Pattern/Placeholder.php | 9 +++ .../Parser/Node/Pattern/QuotedPattern.php | 4 +- .../Parser/Node/Pattern/Text.php | 12 ++++ .../Parser/Node/SimpleMessage.php | 7 +++ .../MessageFormat/Parser/Node/Variable.php | 2 +- .../src/MessageFormat/Parser/Parser.php | 62 +++++++++---------- .../src/MessageFormatFunctionDiscovery.php | 4 +- .../src/MessageFormatterInitializer.php | 6 +- .../src/Number/functions.php | 4 +- .../src/PluralRules/PluralRulesMatcher.php | 4 +- .../src/Pluralizer/InflectorPluralizer.php | 20 +++++- packages/intl/src/Pluralizer/Pluralizer.php | 31 ++++++++++ .../src/Pluralizer/PluralizerInitializer.php | 2 +- .../{i18n => intl}/src/TranslationFailure.php | 4 +- .../src/TranslationMessageDiscovery.php | 4 +- .../{i18n => intl}/src/TranslationMiss.php | 4 +- packages/{i18n => intl}/src/Translator.php | 4 +- .../src/TranslatorInitializer.php | 8 +-- packages/intl/src/functions.php | 59 ++++++++++++++++++ packages/{i18n => intl}/src/i18n.config.php | 4 +- .../{i18n => intl}/tests/FormatterTest.php | 18 +++--- .../Number => intl/tests}/FunctionsTest.php | 9 +-- .../tests/GenericCatalogTest.php | 6 +- .../tests/GenericTranslatorTest.php | 22 +++---- .../tests}/InflectorPluralizerTest.php | 4 +- .../Language => intl/tests}/LocaleTest.php | 4 +- packages/{i18n => intl}/tests/ParserTest.php | 16 ++--- .../tests/PluralRulesMatcherTest.php | 6 +- .../src/Static/StaticGenerateCommand.php | 4 +- packages/support/composer.json | 5 +- packages/support/src/Language/functions.php | 27 -------- .../support/src/Pluralizer/Pluralizer.php | 15 ----- .../support/src/Str/ManipulatesString.php | 30 ++++++++- packages/support/src/Str/functions.php | 36 +---------- .../DiscoveryTest.php | 6 +- .../Fixtures/messages.abcde.json | 0 .../Fixtures/messages.en_US.json | 0 .../Fixtures/messages.fr.json | 0 .../Fixtures/messages.json | 0 tests/Integration/Intl/FunctionsTest.php | 48 ++++++++++++++ .../TranslatorTest.php | 14 ++--- tests/Integration/Support/LanguageTest.php | 35 ----------- 119 files changed, 616 insertions(+), 569 deletions(-) delete mode 100644 packages/i18n/composer.json delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/Variant.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Declaration/Declaration.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Declaration/InputDeclaration.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Declaration/LocalDeclaration.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/Attribute.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Expression/Option.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Key/Key.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Key/WildcardKey.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Literal/Literal.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Literal/QuotedLiteral.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Literal/UnquotedLiteral.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/MessageNode.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Node.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Pattern/Placeholder.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/Pattern/Text.php delete mode 100644 packages/i18n/src/MessageFormat/Parser/Node/SimpleMessage.php delete mode 100644 packages/i18n/src/functions.php rename packages/{i18n => intl}/.gitattributes (100%) rename packages/{i18n => intl}/LICENCE.md (100%) rename packages/{i18n => intl}/bin/plural-rules.php (98%) create mode 100644 packages/intl/composer.json rename packages/{i18n => intl}/phpunit.xml (100%) rename packages/{i18n => intl}/src/Catalog/Catalog.php (85%) rename packages/{i18n => intl}/src/Catalog/CatalogInitializer.php (86%) rename packages/{i18n => intl}/src/Catalog/GenericCatalog.php (86%) rename packages/{i18n => intl}/src/GenericTranslator.php (83%) rename packages/{i18n => intl}/src/InternationalizationConfig.php (86%) rename packages/{support/src/Language => intl/src}/Locale.php (99%) rename packages/{i18n => intl}/src/MessageFormat/Formatter/FormattedValue.php (70%) rename packages/{i18n => intl}/src/MessageFormat/Formatter/FormattingException.php (84%) rename packages/{i18n => intl}/src/MessageFormat/Formatter/MessageFormatFunction.php (83%) rename packages/{i18n => intl}/src/MessageFormat/Formatter/MessageFormatter.php (82%) rename packages/{i18n => intl}/src/MessageFormat/Functions/DateTimeFunction.php (67%) rename packages/{i18n => intl}/src/MessageFormat/Functions/NumberFunction.php (72%) rename packages/{i18n => intl}/src/MessageFormat/Functions/StringFunction.php (80%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/ComplexBody/Matcher.php (75%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/ComplexBody/SimplePatternBody.php (60%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/ComplexBody/Variant.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/ComplexMessage.php (65%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Declaration/Declaration.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Declaration/InputDeclaration.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Declaration/LocalDeclaration.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Expression/Attribute.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Expression/Expression.php (61%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Expression/FunctionCall.php (51%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Expression/FunctionExpression.php (74%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Expression/LiteralExpression.php (63%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Expression/Option.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Expression/VariableExpression.php (64%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Identifier.php (82%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Key/Key.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Key/WildcardKey.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Literal/Literal.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Literal/QuotedLiteral.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Literal/UnquotedLiteral.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Markup/Markup.php (59%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Markup/MarkupType.php (52%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/MessageNode.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Node.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/ParsingException.php (78%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Pattern/Pattern.php (59%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Pattern/Placeholder.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Pattern/QuotedPattern.php (60%) create mode 100644 packages/intl/src/MessageFormat/Parser/Node/Pattern/Text.php create mode 100644 packages/intl/src/MessageFormat/Parser/Node/SimpleMessage.php rename packages/{i18n => intl}/src/MessageFormat/Parser/Node/Variable.php (66%) rename packages/{i18n => intl}/src/MessageFormat/Parser/Parser.php (86%) rename packages/{i18n => intl}/src/MessageFormatFunctionDiscovery.php (88%) rename packages/{i18n => intl}/src/MessageFormatterInitializer.php (70%) rename packages/{support => intl}/src/Number/functions.php (98%) rename packages/{i18n => intl}/src/PluralRules/PluralRulesMatcher.php (99%) rename packages/{support => intl}/src/Pluralizer/InflectorPluralizer.php (67%) create mode 100644 packages/intl/src/Pluralizer/Pluralizer.php rename packages/{support => intl}/src/Pluralizer/PluralizerInitializer.php (88%) rename packages/{i18n => intl}/src/TranslationFailure.php (71%) rename packages/{i18n => intl}/src/TranslationMessageDiscovery.php (95%) rename packages/{i18n => intl}/src/TranslationMiss.php (66%) rename packages/{i18n => intl}/src/Translator.php (89%) rename packages/{i18n => intl}/src/TranslatorInitializer.php (66%) create mode 100644 packages/intl/src/functions.php rename packages/{i18n => intl}/src/i18n.config.php (56%) rename packages/{i18n => intl}/tests/FormatterTest.php (94%) rename packages/{support/tests/Number => intl/tests}/FunctionsTest.php (99%) rename packages/{i18n => intl}/tests/GenericCatalogTest.php (82%) rename packages/{i18n => intl}/tests/GenericTranslatorTest.php (84%) rename packages/{support/tests/Pluralizer => intl/tests}/InflectorPluralizerTest.php (91%) rename packages/{support/tests/Language => intl/tests}/LocaleTest.php (98%) rename packages/{i18n => intl}/tests/ParserTest.php (83%) rename packages/{i18n => intl}/tests/PluralRulesMatcherTest.php (88%) delete mode 100644 packages/support/src/Language/functions.php delete mode 100644 packages/support/src/Pluralizer/Pluralizer.php rename tests/Integration/{Internationalization => Intl}/DiscoveryTest.php (88%) rename tests/Integration/{Internationalization => Intl}/Fixtures/messages.abcde.json (100%) rename tests/Integration/{Internationalization => Intl}/Fixtures/messages.en_US.json (100%) rename tests/Integration/{Internationalization => Intl}/Fixtures/messages.fr.json (100%) rename tests/Integration/{Internationalization => Intl}/Fixtures/messages.json (100%) create mode 100644 tests/Integration/Intl/FunctionsTest.php rename tests/Integration/{Internationalization => Intl}/TranslatorTest.php (82%) delete mode 100644 tests/Integration/Support/LanguageTest.php diff --git a/composer.json b/composer.json index d372fd0dbc..b5fa2463ea 100644 --- a/composer.json +++ b/composer.json @@ -83,7 +83,7 @@ "tempest/generation": "self.version", "tempest/http": "self.version", "tempest/http-client": "self.version", - "tempest/i18n": "self.version", + "tempest/intl": "self.version", "tempest/log": "self.version", "tempest/mapper": "self.version", "tempest/reflection": "self.version", @@ -118,7 +118,7 @@ "Tempest\\Generation\\": "packages/generation/src", "Tempest\\HttpClient\\": "packages/http-client/src", "Tempest\\Http\\": "packages/http/src", - "Tempest\\Internationalization\\": "packages/i18n/src", + "Tempest\\Intl\\": "packages/intl/src", "Tempest\\Log\\": "packages/log/src", "Tempest\\Mapper\\": "packages/mapper/src", "Tempest\\Reflection\\": "packages/reflection/src", @@ -139,7 +139,8 @@ "packages/datetime/src/functions.php", "packages/debug/src/functions.php", "packages/event-bus/src/functions.php", - "packages/i18n/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", @@ -148,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", @@ -177,7 +176,7 @@ "Tempest\\Generation\\Tests\\": "packages/generation/tests", "Tempest\\HttpClient\\Tests\\": "packages/http-client/tests", "Tempest\\Http\\Tests\\": "packages/http/tests", - "Tempest\\Internationalization\\Tests\\": "packages/i18n/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", @@ -206,7 +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", - "i18n:plural": "./packages/i18n/bin/plural-rules.php", + "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/i18n/composer.json b/packages/i18n/composer.json deleted file mode 100644 index b978ebe840..0000000000 --- a/packages/i18n/composer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "tempest/i18n", - "description": "A component for working with internationalization.", - "license": "MIT", - "minimum-stability": "dev", - "require": { - "php": "^8.4", - "tempest/core": "dev-main", - "tempest/container": "dev-main", - "tempest/datetime": "dev-main" - }, - "require-dev": { - "phpunit/phpunit": "^11.5.17" - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Tempest\\Internationalization\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Tempest\\Internationalization\\Tests\\": "tests" - } - } -} diff --git a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php b/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php deleted file mode 100644 index bb7e877067..0000000000 --- a/packages/i18n/src/MessageFormat/Parser/Node/ComplexBody/ComplexBody.php +++ /dev/null @@ -1,11 +0,0 @@ -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); -} diff --git a/packages/i18n/.gitattributes b/packages/intl/.gitattributes similarity index 100% rename from packages/i18n/.gitattributes rename to packages/intl/.gitattributes diff --git a/packages/i18n/LICENCE.md b/packages/intl/LICENCE.md similarity index 100% rename from packages/i18n/LICENCE.md rename to packages/intl/LICENCE.md diff --git a/packages/i18n/bin/plural-rules.php b/packages/intl/bin/plural-rules.php similarity index 98% rename from packages/i18n/bin/plural-rules.php rename to packages/intl/bin/plural-rules.php index e5c07904c1..68d1b5bb78 100755 --- a/packages/i18n/bin/plural-rules.php +++ b/packages/intl/bin/plural-rules.php @@ -20,8 +20,8 @@ public function __construct( public function generate(): string { $output = "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 @@ +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/i18n/src/i18n.config.php b/packages/intl/src/i18n.config.php similarity index 56% rename from packages/i18n/src/i18n.config.php rename to packages/intl/src/i18n.config.php index 03faa6e74e..9ae9b295dc 100644 --- a/packages/i18n/src/i18n.config.php +++ b/packages/intl/src/i18n.config.php @@ -1,7 +1,7 @@ assertSame('0', Number\format(0)); $this->assertSame('0', Number\format(0.0)); diff --git a/packages/i18n/tests/GenericCatalogTest.php b/packages/intl/tests/GenericCatalogTest.php similarity index 82% rename from packages/i18n/tests/GenericCatalogTest.php rename to packages/intl/tests/GenericCatalogTest.php index 453cf32c22..3ccbf5966d 100644 --- a/packages/i18n/tests/GenericCatalogTest.php +++ b/packages/intl/tests/GenericCatalogTest.php @@ -1,10 +1,10 @@ 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/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 6aa2ea789a..11263010f7 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`. */ @@ -847,7 +813,7 @@ function pad_left(string $string, int $totalLength, string $padString = ' '): st * => 'Yeet' * * pad_right('مرحبا', 8, 'ا') - * => 'مرحباااا' + * => 'مرح��اااا' * * @param non-empty-string $padString * @param int<0, max> $totalLength diff --git a/tests/Integration/Internationalization/DiscoveryTest.php b/tests/Integration/Intl/DiscoveryTest.php similarity index 88% rename from tests/Integration/Internationalization/DiscoveryTest.php rename to tests/Integration/Intl/DiscoveryTest.php index 2c152c3a98..efc69da5d3 100644 --- a/tests/Integration/Internationalization/DiscoveryTest.php +++ b/tests/Integration/Intl/DiscoveryTest.php @@ -1,11 +1,11 @@ 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/Internationalization/TranslatorTest.php b/tests/Integration/Intl/TranslatorTest.php similarity index 82% rename from tests/Integration/Internationalization/TranslatorTest.php rename to tests/Integration/Intl/TranslatorTest.php index 24270872a1..4d1a6349c5 100644 --- a/tests/Integration/Internationalization/TranslatorTest.php +++ b/tests/Integration/Intl/TranslatorTest.php @@ -1,20 +1,20 @@ 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)); - } -} From 294fe5195fe687eb284794355160f1569cd08c86 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 21:40:36 +0200 Subject: [PATCH 05/20] refactor: rename `IntlConfig` --- packages/intl/src/Catalog/CatalogInitializer.php | 4 ++-- packages/intl/src/Catalog/GenericCatalog.php | 2 +- packages/intl/src/GenericTranslator.php | 4 ++-- .../src/{InternationalizationConfig.php => IntlConfig.php} | 2 +- packages/intl/src/MessageFormatFunctionDiscovery.php | 2 +- packages/intl/src/MessageFormatterInitializer.php | 2 +- packages/intl/src/TranslationMessageDiscovery.php | 2 +- packages/intl/src/TranslatorInitializer.php | 4 ++-- packages/intl/src/i18n.config.php | 4 ++-- packages/intl/tests/FormatterTest.php | 2 +- packages/intl/tests/GenericTranslatorTest.php | 6 +++--- tests/Integration/Intl/DiscoveryTest.php | 6 +++--- tests/Integration/Intl/TranslatorTest.php | 6 +++--- 13 files changed, 23 insertions(+), 23 deletions(-) rename packages/intl/src/{InternationalizationConfig.php => IntlConfig.php} (96%) diff --git a/packages/intl/src/Catalog/CatalogInitializer.php b/packages/intl/src/Catalog/CatalogInitializer.php index ced998eb42..f5361b7dbe 100644 --- a/packages/intl/src/Catalog/CatalogInitializer.php +++ b/packages/intl/src/Catalog/CatalogInitializer.php @@ -4,7 +4,7 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; -use Tempest\Intl\InternationalizationConfig; +use Tempest\Intl\IntlConfig; use Tempest\Intl\Locale; use Tempest\Support\Arr; use Tempest\Support\Filesystem; @@ -14,7 +14,7 @@ final class CatalogInitializer implements Initializer { public function initialize(Container $container): Catalog { - $config = $container->get(InternationalizationConfig::class); + $config = $container->get(IntlConfig::class); $catalog = []; foreach ($config->translationMessagePaths as $locale => $paths) { diff --git a/packages/intl/src/Catalog/GenericCatalog.php b/packages/intl/src/Catalog/GenericCatalog.php index 84771560aa..ec14697153 100644 --- a/packages/intl/src/Catalog/GenericCatalog.php +++ b/packages/intl/src/Catalog/GenericCatalog.php @@ -2,7 +2,7 @@ namespace Tempest\Intl\Catalog; -use Tempest\Intl\InternationalizationConfig; +use Tempest\Intl\IntlConfig; use Tempest\Intl\Locale; use Tempest\Support\Arr; diff --git a/packages/intl/src/GenericTranslator.php b/packages/intl/src/GenericTranslator.php index 19dfc6bcb8..55ef0f9593 100644 --- a/packages/intl/src/GenericTranslator.php +++ b/packages/intl/src/GenericTranslator.php @@ -4,14 +4,14 @@ use Tempest\EventBus\EventBus; use Tempest\Intl\Catalog\Catalog; -use Tempest\Intl\InternationalizationConfig; +use Tempest\Intl\IntlConfig; use Tempest\Intl\Locale; use Tempest\Intl\MessageFormat\Formatter\MessageFormatter; final readonly class GenericTranslator implements Translator { public function __construct( - private InternationalizationConfig $config, + private IntlConfig $config, private Catalog $catalog, private MessageFormatter $formatter, private ?EventBus $eventBus = null, diff --git a/packages/intl/src/InternationalizationConfig.php b/packages/intl/src/IntlConfig.php similarity index 96% rename from packages/intl/src/InternationalizationConfig.php rename to packages/intl/src/IntlConfig.php index e07fa5b7ea..7fdd4fa0bc 100644 --- a/packages/intl/src/InternationalizationConfig.php +++ b/packages/intl/src/IntlConfig.php @@ -5,7 +5,7 @@ use Tempest\Intl\Locale; use Tempest\Intl\MessageFormat\Formatter\MessageFormatFunction; -final class InternationalizationConfig +final class IntlConfig { /** @var MessageFormatFunction[] */ public array $functions = []; diff --git a/packages/intl/src/MessageFormatFunctionDiscovery.php b/packages/intl/src/MessageFormatFunctionDiscovery.php index f5ae517bf2..f2ec535788 100644 --- a/packages/intl/src/MessageFormatFunctionDiscovery.php +++ b/packages/intl/src/MessageFormatFunctionDiscovery.php @@ -17,7 +17,7 @@ final class MessageFormatFunctionDiscovery implements Discovery public function __construct( private readonly Container $container, - private readonly InternationalizationConfig $config, + private readonly IntlConfig $config, ) {} public function discover(DiscoveryLocation $location, ClassReflector $class): void diff --git a/packages/intl/src/MessageFormatterInitializer.php b/packages/intl/src/MessageFormatterInitializer.php index ad22f7e416..a7186f8e54 100644 --- a/packages/intl/src/MessageFormatterInitializer.php +++ b/packages/intl/src/MessageFormatterInitializer.php @@ -11,7 +11,7 @@ final class MessageFormatterInitializer implements Initializer { public function initialize(Container $container): mixed { - $config = $container->get(InternationalizationConfig::class); + $config = $container->get(IntlConfig::class); return new MessageFormatter( functions: $config->functions, diff --git a/packages/intl/src/TranslationMessageDiscovery.php b/packages/intl/src/TranslationMessageDiscovery.php index 449362bf88..1ccf605290 100644 --- a/packages/intl/src/TranslationMessageDiscovery.php +++ b/packages/intl/src/TranslationMessageDiscovery.php @@ -20,7 +20,7 @@ final class TranslationMessageDiscovery implements Discovery, DiscoversPath use IsDiscovery; public function __construct( - private readonly InternationalizationConfig $config, + private readonly IntlConfig $config, ) {} public function discover(DiscoveryLocation $location, ClassReflector $class): void diff --git a/packages/intl/src/TranslatorInitializer.php b/packages/intl/src/TranslatorInitializer.php index 8173990f54..3b8679f7a4 100644 --- a/packages/intl/src/TranslatorInitializer.php +++ b/packages/intl/src/TranslatorInitializer.php @@ -5,7 +5,7 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Intl\Catalog\Catalog; -use Tempest\Intl\InternationalizationConfig; +use Tempest\Intl\IntlConfig; use Tempest\Intl\MessageFormat\Formatter\MessageFormatter; final class TranslatorInitializer implements Initializer @@ -13,7 +13,7 @@ final class TranslatorInitializer implements Initializer public function initialize(Container $container): Translator { return new GenericTranslator( - config: $container->get(InternationalizationConfig::class), + config: $container->get(IntlConfig::class), catalog: $container->get(Catalog::class), formatter: $container->get(MessageFormatter::class), ); diff --git a/packages/intl/src/i18n.config.php b/packages/intl/src/i18n.config.php index 9ae9b295dc..c80522313f 100644 --- a/packages/intl/src/i18n.config.php +++ b/packages/intl/src/i18n.config.php @@ -1,9 +1,9 @@ catalog->add(Locale::FRENCH, 'hello', 'Bonjour!'); $this->catalog->add(Locale::ENGLISH, 'hello', 'Hello!'); - $this->config = new InternationalizationConfig( + $this->config = new IntlConfig( currentLocale: Locale::FRENCH, fallbackLocale: Locale::ENGLISH, ); diff --git a/tests/Integration/Intl/DiscoveryTest.php b/tests/Integration/Intl/DiscoveryTest.php index efc69da5d3..c4444b243e 100644 --- a/tests/Integration/Intl/DiscoveryTest.php +++ b/tests/Integration/Intl/DiscoveryTest.php @@ -4,7 +4,7 @@ use Tempest\Discovery\DiscoveryItems; use Tempest\Discovery\DiscoveryLocation; -use Tempest\Intl\InternationalizationConfig; +use Tempest\Intl\IntlConfig; use Tempest\Intl\TranslationMessageDiscovery; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -12,7 +12,7 @@ final class DiscoveryTest extends FrameworkIntegrationTestCase { public function test_functions_are_discovered(): void { - $config = $this->container->get(InternationalizationConfig::class); + $config = $this->container->get(IntlConfig::class); $this->assertCount(3, $config->functions); } @@ -27,7 +27,7 @@ public function test_discovery_adds_paths_to_config(): void $discovery->discoverPath(new DiscoveryLocation('', ''), __DIR__ . '/Fixtures/messages.en_US.json'); $discovery->apply(); - $config = $this->container->get(InternationalizationConfig::class); + $config = $this->container->get(IntlConfig::class); $this->assertSame([ 'fr' => [__DIR__ . '/Fixtures/messages.fr.json'], diff --git a/tests/Integration/Intl/TranslatorTest.php b/tests/Integration/Intl/TranslatorTest.php index 4d1a6349c5..0b56626422 100644 --- a/tests/Integration/Intl/TranslatorTest.php +++ b/tests/Integration/Intl/TranslatorTest.php @@ -8,7 +8,7 @@ use Tempest\Core\Kernel\LoadDiscoveryClasses; use Tempest\Discovery\DiscoveryLocation; use Tempest\Intl\Catalog\Catalog; -use Tempest\Intl\InternationalizationConfig; +use Tempest\Intl\IntlConfig; use Tempest\Intl\Locale; use Tempest\Intl\Translator; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -23,7 +23,7 @@ protected function setUp(): void { parent::setUp(); - $config = $this->container->get(InternationalizationConfig::class); + $config = $this->container->get(IntlConfig::class); $config->addTranslationMessageFile(Locale::FRENCH, __DIR__ . '/Fixtures/messages.fr.json'); $config->addTranslationMessageFile(Locale::ENGLISH, __DIR__ . '/Fixtures/messages.en_US.json'); } @@ -46,7 +46,7 @@ public function test_function(): void public function test_default_locale(): void { - $config = $this->container->get(InternationalizationConfig::class); + $config = $this->container->get(IntlConfig::class); $this->assertSame(Locale::default(), $config->currentLocale); } From c37f193b357b4332d13504dfa40101a4f5b587c0 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 21:45:08 +0200 Subject: [PATCH 06/20] chore: remove individual package dependency on phpunit --- packages/generation/composer.json | 3 +-- packages/http-client/composer.json | 3 +-- packages/intl/composer.json | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/generation/composer.json b/packages/generation/composer.json index 2c748d4fa8..c6289aca50 100644 --- a/packages/generation/composer.json +++ b/packages/generation/composer.json @@ -20,7 +20,6 @@ } }, "require-dev": { - "spatie/phpunit-snapshot-assertions": "^5.1.8", - "phpunit/phpunit": "^11.5.17" + "spatie/phpunit-snapshot-assertions": "^5.1.8" } } diff --git a/packages/http-client/composer.json b/packages/http-client/composer.json index fe43c14993..48c9addae9 100644 --- a/packages/http-client/composer.json +++ b/packages/http-client/composer.json @@ -15,8 +15,7 @@ }, "require-dev": { "aidan-casey/mock-client": "dev-master", - "guzzlehttp/psr7": "^2.6.1", - "phpunit/phpunit": "^11.5.17" + "guzzlehttp/psr7": "^2.6.1" }, "autoload": { "psr-4": { diff --git a/packages/intl/composer.json b/packages/intl/composer.json index 20e1b51481..0da9d1f8da 100644 --- a/packages/intl/composer.json +++ b/packages/intl/composer.json @@ -11,9 +11,6 @@ "tempest/datetime": "dev-main", "tempest/support": "dev-main" }, - "require-dev": { - "phpunit/phpunit": "^11.5.17" - }, "autoload": { "files": [ "src/functions.php", From 5c1ddbe5936912843cb086e392b5009b1e4b6d79 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 22:08:05 +0200 Subject: [PATCH 07/20] fix: add event support to translator --- .../intl/src/Catalog/CatalogInitializer.php | 2 + packages/intl/src/GenericTranslator.php | 2 +- .../intl/src/MessageFormatterInitializer.php | 2 + packages/intl/src/TranslatorInitializer.php | 4 ++ tests/Integration/Intl/TranslatorTest.php | 49 ++++++++++++++++--- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/intl/src/Catalog/CatalogInitializer.php b/packages/intl/src/Catalog/CatalogInitializer.php index f5361b7dbe..3744d402d3 100644 --- a/packages/intl/src/Catalog/CatalogInitializer.php +++ b/packages/intl/src/Catalog/CatalogInitializer.php @@ -4,6 +4,7 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; +use Tempest\Container\Singleton; use Tempest\Intl\IntlConfig; use Tempest\Intl\Locale; use Tempest\Support\Arr; @@ -12,6 +13,7 @@ final class CatalogInitializer implements Initializer { + #[Singleton] public function initialize(Container $container): Catalog { $config = $container->get(IntlConfig::class); diff --git a/packages/intl/src/GenericTranslator.php b/packages/intl/src/GenericTranslator.php index 55ef0f9593..5d34f5fb81 100644 --- a/packages/intl/src/GenericTranslator.php +++ b/packages/intl/src/GenericTranslator.php @@ -12,7 +12,7 @@ { public function __construct( private IntlConfig $config, - private Catalog $catalog, + private(set) Catalog $catalog, private MessageFormatter $formatter, private ?EventBus $eventBus = null, ) {} diff --git a/packages/intl/src/MessageFormatterInitializer.php b/packages/intl/src/MessageFormatterInitializer.php index a7186f8e54..b46cd7c5ba 100644 --- a/packages/intl/src/MessageFormatterInitializer.php +++ b/packages/intl/src/MessageFormatterInitializer.php @@ -4,11 +4,13 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; +use Tempest\Container\Singleton; use Tempest\Intl\MessageFormat\Formatter\MessageFormatter; use Tempest\Intl\PluralRules\PluralRulesMatcher; final class MessageFormatterInitializer implements Initializer { + #[Singleton] public function initialize(Container $container): mixed { $config = $container->get(IntlConfig::class); diff --git a/packages/intl/src/TranslatorInitializer.php b/packages/intl/src/TranslatorInitializer.php index 3b8679f7a4..477b7a77df 100644 --- a/packages/intl/src/TranslatorInitializer.php +++ b/packages/intl/src/TranslatorInitializer.php @@ -4,18 +4,22 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; +use Tempest\Container\Singleton; +use Tempest\EventBus\EventBus; use Tempest\Intl\Catalog\Catalog; use Tempest\Intl\IntlConfig; use Tempest\Intl\MessageFormat\Formatter\MessageFormatter; final class TranslatorInitializer implements Initializer { + #[Singleton] public function initialize(Container $container): Translator { return new GenericTranslator( config: $container->get(IntlConfig::class), catalog: $container->get(Catalog::class), formatter: $container->get(MessageFormatter::class), + eventBus: $container->get(EventBus::class), ); } } diff --git a/tests/Integration/Intl/TranslatorTest.php b/tests/Integration/Intl/TranslatorTest.php index 0b56626422..6bf402b7e0 100644 --- a/tests/Integration/Intl/TranslatorTest.php +++ b/tests/Integration/Intl/TranslatorTest.php @@ -2,20 +2,17 @@ namespace Tests\Tempest\Integration\Intl; -use Tempest\Core\Commands\DiscoveryClearCommand; -use Tempest\Core\DiscoveryCache; -use Tempest\Core\FrameworkKernel; -use Tempest\Core\Kernel\LoadDiscoveryClasses; -use Tempest\Discovery\DiscoveryLocation; +use Tempest\EventBus\EventBus; use Tempest\Intl\Catalog\Catalog; use Tempest\Intl\IntlConfig; use Tempest\Intl\Locale; +use Tempest\Intl\TranslationFailure; +use Tempest\Intl\TranslationMiss; use Tempest\Intl\Translator; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; use function Tempest\Intl\translate; use function Tempest\Intl\translate_locale; -use function Tempest\Support\Path\normalize; final class TranslatorTest extends FrameworkIntegrationTestCase { @@ -50,4 +47,44 @@ public function test_default_locale(): void $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(TranslationMiss::class, 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(TranslationFailure::class, 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()); + } } From 691fc637005a42c55539d9d45d5f10cc83490d5c Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 21 Jun 2025 23:06:38 +0200 Subject: [PATCH 08/20] feat: add intl insights --- packages/intl/src/IntlInsightsProvider.php | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/intl/src/IntlInsightsProvider.php 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), + ]; + } +} From 709a1aa7ecd83b9af6beaa28049dac9c7f9e104d Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sun, 22 Jun 2025 01:33:28 +0200 Subject: [PATCH 09/20] feat: support yaml translation files --- composer.json | 1 + packages/intl/composer.json | 3 ++- packages/intl/src/Catalog/CatalogInitializer.php | 11 ++++++++--- packages/intl/src/TranslationMessageDiscovery.php | 2 +- tests/Integration/Intl/DiscoveryTest.php | 4 ++-- tests/Integration/Intl/Fixtures/messages.en_US.json | 12 +++--------- tests/Integration/Intl/Fixtures/messages.fr.json | 13 ------------- tests/Integration/Intl/Fixtures/messages.fr.yaml | 8 ++++++++ tests/Integration/Intl/TranslatorTest.php | 12 +++++++----- 9 files changed, 32 insertions(+), 34 deletions(-) delete mode 100644 tests/Integration/Intl/Fixtures/messages.fr.json create mode 100644 tests/Integration/Intl/Fixtures/messages.fr.yaml diff --git a/composer.json b/composer.json index b5fa2463ea..2e52acc127 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" diff --git a/packages/intl/composer.json b/packages/intl/composer.json index 0da9d1f8da..28250f76f8 100644 --- a/packages/intl/composer.json +++ b/packages/intl/composer.json @@ -5,7 +5,8 @@ "minimum-stability": "dev", "require": { "php": "^8.4", - "doctrine/inflector": "^2.0", + "doctrine/inflector": "^2.0", + "symfony/yaml": "^7.3", "tempest/core": "dev-main", "tempest/container": "dev-main", "tempest/datetime": "dev-main", diff --git a/packages/intl/src/Catalog/CatalogInitializer.php b/packages/intl/src/Catalog/CatalogInitializer.php index 3744d402d3..19fcb3139f 100644 --- a/packages/intl/src/Catalog/CatalogInitializer.php +++ b/packages/intl/src/Catalog/CatalogInitializer.php @@ -2,6 +2,7 @@ namespace Tempest\Intl\Catalog; +use Symfony\Component\Yaml\Yaml; use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; @@ -10,6 +11,7 @@ use Tempest\Support\Arr; use Tempest\Support\Filesystem; use Tempest\Support\Json; +use Tempest\Support\Str; final class CatalogInitializer implements Initializer { @@ -24,10 +26,13 @@ public function initialize(Container $container): Catalog $catalog[$locale] ??= []; foreach ($paths as $path) { - $messages = Json\decode(Filesystem\read_file($path)); - $messages = Arr\undot($messages); + $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 ($messages as $key => $message) { + foreach (Arr\undot($messages) as $key => $message) { $catalog[$locale][$key] = $message; } } diff --git a/packages/intl/src/TranslationMessageDiscovery.php b/packages/intl/src/TranslationMessageDiscovery.php index 1ccf605290..9f3a884667 100644 --- a/packages/intl/src/TranslationMessageDiscovery.php +++ b/packages/intl/src/TranslationMessageDiscovery.php @@ -30,7 +30,7 @@ public function discover(DiscoveryLocation $location, ClassReflector $class): vo public function discoverPath(DiscoveryLocation $location, string $path): void { - if (! ends_with($path, '.json')) { + if (! ends_with($path, ['.json', '.yml', '.yaml'])) { return; } diff --git a/tests/Integration/Intl/DiscoveryTest.php b/tests/Integration/Intl/DiscoveryTest.php index c4444b243e..11057bdd74 100644 --- a/tests/Integration/Intl/DiscoveryTest.php +++ b/tests/Integration/Intl/DiscoveryTest.php @@ -23,14 +23,14 @@ public function test_discovery_adds_paths_to_config(): void $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.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.json'], + 'fr' => [__DIR__ . '/Fixtures/messages.fr.yaml'], 'en_US' => [__DIR__ . '/Fixtures/messages.en_US.json'], ], $config->translationMessagePaths); } diff --git a/tests/Integration/Intl/Fixtures/messages.en_US.json b/tests/Integration/Intl/Fixtures/messages.en_US.json index eb46ca47e5..b84b84efd3 100644 --- a/tests/Integration/Intl/Fixtures/messages.en_US.json +++ b/tests/Integration/Intl/Fixtures/messages.en_US.json @@ -1,13 +1,7 @@ { "hello": "Hello, {$name}!", - "ui": { - "sidebar": { - "project": "Project", - "title": "Name of the project." - }, - "statusbar": { - "statusbar_empty": "No entries added yet.", - "statusbar_ok": "Ok." - } + "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.json b/tests/Integration/Intl/Fixtures/messages.fr.json deleted file mode 100644 index 7617923f57..0000000000 --- a/tests/Integration/Intl/Fixtures/messages.fr.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "hello": "Bonjour, {$name}!", - "ui": { - "sidebar": { - "project": "Projet", - "title": "Nom du projet" - }, - "statusbar": { - "statusbar_empty": "Aucun item.", - "statusbar_ok": "Ok." - } - } -} 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/TranslatorTest.php b/tests/Integration/Intl/TranslatorTest.php index 6bf402b7e0..4188baff65 100644 --- a/tests/Integration/Intl/TranslatorTest.php +++ b/tests/Integration/Intl/TranslatorTest.php @@ -21,7 +21,7 @@ protected function setUp(): void parent::setUp(); $config = $this->container->get(IntlConfig::class); - $config->addTranslationMessageFile(Locale::FRENCH, __DIR__ . '/Fixtures/messages.fr.json'); + $config->addTranslationMessageFile(Locale::FRENCH, __DIR__ . '/Fixtures/messages.fr.yaml'); $config->addTranslationMessageFile(Locale::ENGLISH, __DIR__ . '/Fixtures/messages.en_US.json'); } @@ -30,15 +30,17 @@ public function test_translator(): void $translator = $this->container->get(Translator::class); $this->assertSame('Hello, Jon Doe!', $translator->translate('hello', name: 'Jon Doe')); - $this->assertSame('Project', $translator->translate('ui.sidebar.project')); - $this->assertSame('Projet', $translator->translateForLocale(Locale::FRENCH, 'ui.sidebar.project')); + $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('Project', translate('ui.sidebar.project')); - $this->assertSame('Projet', translate_locale(Locale::FRENCH, 'ui.sidebar.project')); + $this->assertSame('Checkout', translate('cart.checkout')); + $this->assertSame('Passer à la caisse', translate_locale(Locale::FRENCH, 'cart.checkout')); } public function test_default_locale(): void From 339acdc2fb334c10555264455be3ab6a354e19ef Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sun, 22 Jun 2025 01:55:20 +0200 Subject: [PATCH 10/20] refactor: use more specific name for formatting functions --- packages/intl/src/IntlConfig.php | 6 +++--- .../Formatter/MessageFormatFunction.php | 18 ----------------- .../Formatter/MessageFormatter.php | 9 +++++---- .../src/MessageFormat/FormattingFunction.php | 20 +++++++++++++++++++ .../Functions/DateTimeFunction.php | 6 +++--- .../Functions/NumberFunction.php | 6 +++--- .../Functions/StringFunction.php | 6 +++--- .../src/MessageFormatFunctionDiscovery.php | 6 +++--- packages/intl/tests/FormatterTest.php | 6 +++--- 9 files changed, 43 insertions(+), 40 deletions(-) delete mode 100644 packages/intl/src/MessageFormat/Formatter/MessageFormatFunction.php create mode 100644 packages/intl/src/MessageFormat/FormattingFunction.php diff --git a/packages/intl/src/IntlConfig.php b/packages/intl/src/IntlConfig.php index 7fdd4fa0bc..b3fb7af918 100644 --- a/packages/intl/src/IntlConfig.php +++ b/packages/intl/src/IntlConfig.php @@ -3,11 +3,11 @@ namespace Tempest\Intl; use Tempest\Intl\Locale; -use Tempest\Intl\MessageFormat\Formatter\MessageFormatFunction; +use Tempest\Intl\MessageFormat\FormattingFunction; final class IntlConfig { - /** @var MessageFormatFunction[] */ + /** @var FormattingFunction[] */ public array $functions = []; /** @var array */ @@ -25,7 +25,7 @@ public function __construct( public Locale $fallbackLocale, ) {} - public function addMessageFormatFunction(MessageFormatFunction $fn): void + public function addFormattingFunction(FormattingFunction $fn): void { $this->functions[] = $fn; } diff --git a/packages/intl/src/MessageFormat/Formatter/MessageFormatFunction.php b/packages/intl/src/MessageFormat/Formatter/MessageFormatFunction.php deleted file mode 100644 index 96b6fb1b87..0000000000 --- a/packages/intl/src/MessageFormat/Formatter/MessageFormatFunction.php +++ /dev/null @@ -1,18 +0,0 @@ -evaluateOptions($expression->function->options); if ($function = $this->getFunction($functionName)) { - return $function->evaluate($value, $options); + return $function->format($value, $options); } else { throw new FormattingException("Unknown function `{$functionName}`."); } @@ -265,11 +266,11 @@ private function evaluateExpression(Expression $expression): FormattedValue return new FormattedValue($value, $formatted); } - private function getFunction(string $name): ?MessageFormatFunction + private function getFunction(string $name): ?FormattingFunction { return array_find( array: $this->functions, - callback: fn (MessageFormatFunction $fn) => $fn->name === $name, + callback: 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')); diff --git a/packages/intl/src/MessageFormat/Functions/NumberFunction.php b/packages/intl/src/MessageFormat/Functions/NumberFunction.php index 58e6c6afbc..7a4241fc33 100644 --- a/packages/intl/src/MessageFormat/Functions/NumberFunction.php +++ b/packages/intl/src/MessageFormat/Functions/NumberFunction.php @@ -3,16 +3,16 @@ namespace Tempest\Intl\MessageFormat\Functions; use Tempest\Intl\MessageFormat\Formatter\FormattedValue; -use Tempest\Intl\MessageFormat\Formatter\MessageFormatFunction; +use Tempest\Intl\MessageFormat\FormattingFunction; use Tempest\Intl\Number; use Tempest\Support\Arr; use Tempest\Support\Currency; -final class NumberFunction implements MessageFormatFunction +final class NumberFunction implements FormattingFunction { public string $name = 'number'; - public function evaluate(mixed $value, array $parameters): FormattedValue + public function format(mixed $value, array $parameters): FormattedValue { $number = Number\parse($value); $formatted = match (Arr\get_by_key($parameters, 'style')) { diff --git a/packages/intl/src/MessageFormat/Functions/StringFunction.php b/packages/intl/src/MessageFormat/Functions/StringFunction.php index 72a1f53eda..1bd7d1baff 100644 --- a/packages/intl/src/MessageFormat/Functions/StringFunction.php +++ b/packages/intl/src/MessageFormat/Functions/StringFunction.php @@ -3,15 +3,15 @@ namespace Tempest\Intl\MessageFormat\Functions; use Tempest\Intl\MessageFormat\Formatter\FormattedValue; -use Tempest\Intl\MessageFormat\Formatter\MessageFormatFunction; +use Tempest\Intl\MessageFormat\FormattingFunction; use Tempest\Support\Arr; use Tempest\Support\Str; -final class StringFunction implements MessageFormatFunction +final class StringFunction implements FormattingFunction { public string $name = 'string'; - public function evaluate(mixed $value, array $parameters): FormattedValue + public function format(mixed $value, array $parameters): FormattedValue { $string = Str\parse($value, default: ''); $formatted = match (Arr\get_by_key($parameters, 'style')) { diff --git a/packages/intl/src/MessageFormatFunctionDiscovery.php b/packages/intl/src/MessageFormatFunctionDiscovery.php index f2ec535788..39d595b5aa 100644 --- a/packages/intl/src/MessageFormatFunctionDiscovery.php +++ b/packages/intl/src/MessageFormatFunctionDiscovery.php @@ -8,7 +8,7 @@ use Tempest\Discovery\Discovery; use Tempest\Discovery\DiscoveryLocation; use Tempest\Discovery\IsDiscovery; -use Tempest\Intl\MessageFormat\Formatter\MessageFormatFunction; +use Tempest\Intl\MessageFormat\FormattingFunction; use Tempest\Reflection\ClassReflector; final class MessageFormatFunctionDiscovery implements Discovery @@ -22,7 +22,7 @@ public function __construct( public function discover(DiscoveryLocation $location, ClassReflector $class): void { - if (! $class->implements(MessageFormatFunction::class)) { + if (! $class->implements(FormattingFunction::class)) { return; } @@ -32,7 +32,7 @@ public function discover(DiscoveryLocation $location, ClassReflector $class): vo public function apply(): void { foreach ($this->discoveryItems as $className) { - $this->config->addMessageFormatFunction($this->container->get($className)); + $this->config->addFormattingFunction($this->container->get($className)); } } } diff --git a/packages/intl/tests/FormatterTest.php b/packages/intl/tests/FormatterTest.php index c018f74c4c..b17902d56d 100644 --- a/packages/intl/tests/FormatterTest.php +++ b/packages/intl/tests/FormatterTest.php @@ -7,8 +7,8 @@ use Tempest\Intl\IntlConfig; use Tempest\Intl\Locale; use Tempest\Intl\MessageFormat\Formatter\FormattedValue; -use Tempest\Intl\MessageFormat\Formatter\MessageFormatFunction; use Tempest\Intl\MessageFormat\Formatter\MessageFormatter; +use Tempest\Intl\MessageFormat\FormattingFunction; use Tempest\Intl\MessageFormat\Functions\DateTimeFunction; use Tempest\Intl\MessageFormat\Functions\NumberFunction; use Tempest\Intl\MessageFormat\Functions\StringFunction; @@ -305,10 +305,10 @@ public function test_multiple_selectors(): void public function test_custom_function(): void { $formatter = new MessageFormatter([ - new class implements MessageFormatFunction { + new class implements FormattingFunction { public string $name = 'uppercase'; - public function evaluate(mixed $value, array $parameters): FormattedValue + public function format(mixed $value, array $parameters): FormattedValue { return new FormattedValue($value, mb_strtoupper($value)); } From b7ff1c6eda89a1e1b7d4bba3342033b0bb378c0e Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sun, 22 Jun 2025 03:46:31 +0200 Subject: [PATCH 11/20] feat: support selector functions --- packages/intl/src/IntlConfig.php | 3 +- .../MessageFormat/Formatter/LocalVariable.php | 15 ++ .../Formatter/MessageFormatter.php | 151 +++++++++++------- .../Functions/NumberFunction.php | 31 +++- .../Functions/StringFunction.php | 8 +- .../src/MessageFormat/SelectorFunction.php | 18 +++ .../src/MessageFormatFunctionDiscovery.php | 5 +- packages/intl/tests/FormatterTest.php | 87 ++++++++-- packages/intl/tests/GenericTranslatorTest.php | 2 +- 9 files changed, 245 insertions(+), 75 deletions(-) create mode 100644 packages/intl/src/MessageFormat/Formatter/LocalVariable.php create mode 100644 packages/intl/src/MessageFormat/SelectorFunction.php diff --git a/packages/intl/src/IntlConfig.php b/packages/intl/src/IntlConfig.php index b3fb7af918..167102fb11 100644 --- a/packages/intl/src/IntlConfig.php +++ b/packages/intl/src/IntlConfig.php @@ -4,6 +4,7 @@ use Tempest\Intl\Locale; use Tempest\Intl\MessageFormat\FormattingFunction; +use Tempest\Intl\MessageFormat\SelectorFunction; final class IntlConfig { @@ -25,7 +26,7 @@ public function __construct( public Locale $fallbackLocale, ) {} - public function addFormattingFunction(FormattingFunction $fn): void + public function addFunction(FormattingFunction|SelectorFunction $fn): void { $this->functions[] = $fn; } diff --git a/packages/intl/src/MessageFormat/Formatter/LocalVariable.php b/packages/intl/src/MessageFormat/Formatter/LocalVariable.php new file mode 100644 index 0000000000..cf5b4e4a0b --- /dev/null +++ b/packages/intl/src/MessageFormat/Formatter/LocalVariable.php @@ -0,0 +1,15 @@ + $variables */ + /** @var array $variables */ private array $variables = []; public function __construct( /** @var FormattingFunction[] */ private readonly array $functions = [], - private readonly PluralRulesMatcher $pluralRules = new PluralRulesMatcher(), ) {} /** @@ -49,7 +49,7 @@ public function format(string $message, mixed ...$variables): string try { $ast = new Parser($message)->parse(); - $this->variables = $variables; + $this->variables = $this->parseLocalVariables($variables); return $this->formatMessage($ast, $variables); } catch (ParsingException $e) { @@ -72,15 +72,30 @@ private function formatMessage(MessageNode $message): string foreach ($message->declarations as $declaration) { if ($declaration instanceof InputDeclaration) { - $variableName = $declaration->expression->variable->name->name; + $expression = $declaration->expression; + $variableName = $expression->variable->name->name; if (! array_key_exists($variableName, $this->variables)) { - throw new FormattingException("Required input variable '{$variableName}' not provided."); + 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; - $value = $this->evaluateExpression($declaration->expression); - $localVariables[$variableName] = $value->value; + + $localVariables[$variableName] = new LocalVariable( + identifier: $variableName, + value: $this->evaluateExpression($declaration->expression)->value, + function: $this->getSelectorFunction($declaration->expression->function?->identifier), + parameters: $declaration->expression->attributes, + ); } } @@ -88,14 +103,9 @@ private function formatMessage(MessageNode $message): string $this->variables = [...$this->variables, ...$localVariables]; try { - $result = $this->formatComplexBody($message->body); - $this->variables = $originalVariables; - - return $result; - } catch (Exception $e) { + return $this->formatComplexBody($message->body); + } finally { $this->variables = $originalVariables; - - throw $e; } } @@ -121,44 +131,55 @@ private function formatComplexBody(ComplexBody $body): string private function formatMatcher(Matcher $matcher): string { - $selectorValues = []; + $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."); + throw new FormattingException("Selector variable `{$variableName}` not found."); } - $selectorValues[] = $this->variables[$variableName]; + $selectorVariables[] = $this->variables[$variableName]; } - // Find the best matching variant $bestVariant = null; $wildcardVariant = null; foreach ($matcher->variants as $variant) { - if (count($variant->keys) !== count($selectorValues)) { - continue; // Key count mismatch + if (count($variant->keys) !== count($selectorVariables)) { + continue; } $matches = true; $hasWildcard = false; for ($i = 0; $i < count($variant->keys); $i++) { - $key = $variant->keys[$i]; - $selectorValue = $selectorValues[$i]; + $keyNode = $variant->keys[$i]; + $variable = $selectorVariables[$i]; - if ($key instanceof WildcardKey) { + if ($keyNode instanceof WildcardKey) { $hasWildcard = true; continue; } - if ($key instanceof Literal) { - if (! $this->matchesKey($selectorValue, $key->value)) { - $matches = false; - break; - } + 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; } } @@ -175,29 +196,15 @@ private function formatMatcher(Matcher $matcher): string $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 matchesKey(mixed $value, string $keyValue): bool - { - if (is_numeric($value)) { - $number = (float) $value; - - if ($keyValue === ((string) $number) || $keyValue === ((string) ((int) $number))) { - return true; - } - - if ($keyValue === $this->pluralRules->getPluralCategory(Locale::default(), $number)) { - return true; - } - } - - return ((string) $value) === $keyValue; - } - private function formatPattern(Pattern $pattern): string { $result = ''; @@ -245,7 +252,7 @@ private function evaluateExpression(Expression $expression): FormattedValue throw new FormattingException("Variable `{$variableName}` not found"); } - $value = $this->variables[$variableName]; + $value = $this->variables[$variableName]->value; } elseif ($expression instanceof FunctionExpression) { $value = null; // Function-only expressions start with null } @@ -254,7 +261,7 @@ private function evaluateExpression(Expression $expression): FormattedValue $functionName = (string) $expression->function->identifier; $options = $this->evaluateOptions($expression->function->options); - if ($function = $this->getFunction($functionName)) { + if ($function = $this->getFormattingFunction($functionName)) { return $function->format($value, $options); } else { throw new FormattingException("Unknown function `{$functionName}`."); @@ -266,12 +273,26 @@ private function evaluateExpression(Expression $expression): FormattedValue return new FormattedValue($value, $formatted); } - private function getFunction(string $name): ?FormattingFunction + private function getSelectorFunction(?string $name): ?SelectorFunction { - return array_find( - array: $this->functions, - callback: fn (FormattingFunction $fn) => $fn->name === $name, - ); + 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); } private function evaluateOptions(array $options): array @@ -288,7 +309,7 @@ private function evaluateOptions(array $options): array throw new FormattingException("Option variable `{$variableName}` not found."); } - $result[$name] = $this->variables[$variableName]; + $result[$name] = $this->variables[$variableName]->value; } elseif ($option->value instanceof Literal) { $result[$name] = $option->value->value; } @@ -297,6 +318,24 @@ private function evaluateOptions(array $options): array 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 { // TODO: more advanced with options diff --git a/packages/intl/src/MessageFormat/Functions/NumberFunction.php b/packages/intl/src/MessageFormat/Functions/NumberFunction.php index 7a4241fc33..a1b6cd0475 100644 --- a/packages/intl/src/MessageFormat/Functions/NumberFunction.php +++ b/packages/intl/src/MessageFormat/Functions/NumberFunction.php @@ -2,16 +2,45 @@ namespace Tempest\Intl\MessageFormat\Functions; +use Tempest\Intl\IntlConfig; +use Tempest\Intl\Locale; use Tempest\Intl\MessageFormat\Formatter\FormattedValue; use Tempest\Intl\MessageFormat\FormattingFunction; +use Tempest\Intl\MessageFormat\SelectorFunction; use Tempest\Intl\Number; +use Tempest\Intl\PluralRules\PluralRulesMatcher; use Tempest\Support\Arr; use Tempest\Support\Currency; +use Tempest\Support\Str; -final class NumberFunction implements FormattingFunction +final class NumberFunction implements FormattingFunction, SelectorFunction { public string $name = 'number'; + public function __construct( + private readonly IntlConfig $intlConfig, + private readonly PluralRulesMatcher $pluralRules = new PluralRulesMatcher(), + ) {} + + public function match(string $key, mixed $value, array $parameters): bool + { + $number = Number\parse($value); + + if (Arr\get_by_key($parameters, 'select') === 'exact') { + return $key === Str\parse($value); + } + + if (Number\parse($key) === $number || $key === $value) { + return true; + } + + if ($key === $this->pluralRules->getPluralCategory($this->intlConfig->currentLocale, $number)) { + return true; + } + + return false; + } + public function format(mixed $value, array $parameters): FormattedValue { $number = Number\parse($value); diff --git a/packages/intl/src/MessageFormat/Functions/StringFunction.php b/packages/intl/src/MessageFormat/Functions/StringFunction.php index 1bd7d1baff..62e406c727 100644 --- a/packages/intl/src/MessageFormat/Functions/StringFunction.php +++ b/packages/intl/src/MessageFormat/Functions/StringFunction.php @@ -4,13 +4,19 @@ use Tempest\Intl\MessageFormat\Formatter\FormattedValue; use Tempest\Intl\MessageFormat\FormattingFunction; +use Tempest\Intl\MessageFormat\SelectorFunction; use Tempest\Support\Arr; use Tempest\Support\Str; -final class StringFunction implements FormattingFunction +final class StringFunction implements FormattingFunction, SelectorFunction { public string $name = 'string'; + public function match(string $key, mixed $value, array $parameters): bool + { + return Str\parse($value, default: '') === $key; + } + public function format(mixed $value, array $parameters): FormattedValue { $string = Str\parse($value, default: ''); diff --git a/packages/intl/src/MessageFormat/SelectorFunction.php b/packages/intl/src/MessageFormat/SelectorFunction.php new file mode 100644 index 0000000000..0d2a312079 --- /dev/null +++ b/packages/intl/src/MessageFormat/SelectorFunction.php @@ -0,0 +1,18 @@ +implements(FormattingFunction::class)) { + if (! $class->implements(FormattingFunction::class) && ! $class->implements(SelectorFunction::class)) { return; } @@ -32,7 +33,7 @@ public function discover(DiscoveryLocation $location, ClassReflector $class): vo public function apply(): void { foreach ($this->discoveryItems as $className) { - $this->config->addFormattingFunction($this->container->get($className)); + $this->config->addFunction($this->container->get($className)); } } } diff --git a/packages/intl/tests/FormatterTest.php b/packages/intl/tests/FormatterTest.php index b17902d56d..3cec9347e8 100644 --- a/packages/intl/tests/FormatterTest.php +++ b/packages/intl/tests/FormatterTest.php @@ -51,7 +51,7 @@ public function test_format_datetime_function_and_parameters(): void public function test_format_number_function(): void { - $formatter = new MessageFormatter([new NumberFunction()]); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' The total was {31 :number style=percent}. @@ -65,7 +65,7 @@ public function test_format_number_function(): void #[TestWith([5, '5 avions'])] public function test_match_number(int $count, string $expected): void { - $formatter = new MessageFormatter([new NumberFunction()]); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' .input {$aircraft :number} @@ -98,9 +98,9 @@ public function test_quoted_text(): void $this->assertSame('My name is John Doe.', $value); } - public function test_matchers(): void + public function test_number_matcher(): void { - $formatter = new MessageFormatter(); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' .input {$count :number} .match $count @@ -111,6 +111,59 @@ public function test_matchers(): void $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(); @@ -148,7 +201,7 @@ public function test_matchers_escape(): void public function test_matchers_number_exact_match(): void { - $formatter = new MessageFormatter([new NumberFunction()]); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' .input {$numDays :number select=exact} @@ -169,7 +222,7 @@ public function test_matchers_czech(int|float $days, string $expected): void { locale_set_default(Locale::CZECH->value); - $formatter = new MessageFormatter([new NumberFunction()]); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' .input {$days :number} @@ -185,7 +238,10 @@ public function test_matchers_czech(int|float $days, string $expected): void public function test_string_function(): void { - $formatter = new MessageFormatter([new NumberFunction()]); + $formatter = new MessageFormatter([ + new StringFunction(), + $this->createNumberFunction(), + ]); $value = $formatter->format(<<<'TXT' .input {$operand :string} @@ -233,7 +289,7 @@ public function test_string_formatting_options(mixed $input, string $expected, s public function test_number_currency(): void { - $formatter = new MessageFormatter([new NumberFunction()]); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' You have {42 :number style=currency currency=$currency}. @@ -244,7 +300,7 @@ public function test_number_currency(): void public function test_shadowing(): void { - $formatter = new MessageFormatter([new NumberFunction()]); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' .local $count = {42} @@ -259,9 +315,7 @@ public function test_shadowing(): void #[TestWith([5, '5 items.'])] public function test_pluralization(int $count, string $expected): void { - $formatter = new MessageFormatter([ - new NumberFunction(), - ]); + $formatter = new MessageFormatter([$this->createNumberFunction()]); $value = $formatter->format(<<<'TXT' .input {$count :number} @@ -277,7 +331,7 @@ public function test_pluralization(int $count, string $expected): void public function test_multiple_selectors(): void { $formatter = new MessageFormatter([ - new NumberFunction(), + $this->createNumberFunction(), new DateTimeFunction(), ]); @@ -321,4 +375,11 @@ public function format(mixed $value, array $parameters): FormattedValue $this->assertSame('Check out MESSAGEFORMAT.', $value); } + + private function createNumberFunction(): NumberFunction + { + return new NumberFunction( + new IntlConfig(Locale::default(), Locale::default()), + ); + } } diff --git a/packages/intl/tests/GenericTranslatorTest.php b/packages/intl/tests/GenericTranslatorTest.php index 8490c1ce5e..fcc76a9f8d 100644 --- a/packages/intl/tests/GenericTranslatorTest.php +++ b/packages/intl/tests/GenericTranslatorTest.php @@ -36,7 +36,7 @@ protected function setUp(): void catalog: $this->catalog, formatter: new MessageFormatter([ new StringFunction(), - new NumberFunction(), + new NumberFunction($this->config), new DateTimeFunction(), ]), ); From 12ea1aa5f47f4bb833365d9e2b11a65a3db51ce8 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sun, 22 Jun 2025 16:58:14 +0200 Subject: [PATCH 12/20] feat: support html, icon and custom markup --- composer.json | 2 +- packages/intl/src/GenericTranslator.php | 2 +- packages/intl/src/IntlConfig.php | 10 ++ .../MessageFormat/Formatter/LocalVariable.php | 10 +- .../Formatter/MessageFormatter.php | 94 +++++++---- .../MessageFormat/Markup/HtmlTagFormatter.php | 24 +++ .../Markup/IconMarkupFormatter.php | 33 ++++ .../Markup/VoidHtmlTagFormatter.php | 19 +++ .../src/MessageFormat/MarkupFormatter.php | 21 +++ .../StandaloneMarkupFormatter.php | 16 ++ .../intl/src/MessageFormatMarkupDiscovery.php | 39 +++++ .../intl/src/MessageFormatterInitializer.php | 5 +- packages/intl/tests/FormatterTest.php | 25 ++- packages/support/src/Html/functions.php | 153 +++++++++++++++++- packages/view/composer.json | 1 + packages/view/src/Components/Icon.php | 3 +- tests/Integration/Intl/DiscoveryTest.php | 7 + tests/Integration/Intl/TranslatorTest.php | 22 +++ 18 files changed, 437 insertions(+), 49 deletions(-) create mode 100644 packages/intl/src/MessageFormat/Markup/HtmlTagFormatter.php create mode 100644 packages/intl/src/MessageFormat/Markup/IconMarkupFormatter.php create mode 100644 packages/intl/src/MessageFormat/Markup/VoidHtmlTagFormatter.php create mode 100644 packages/intl/src/MessageFormat/MarkupFormatter.php create mode 100644 packages/intl/src/MessageFormat/StandaloneMarkupFormatter.php create mode 100644 packages/intl/src/MessageFormatMarkupDiscovery.php diff --git a/composer.json b/composer.json index 2e52acc127..7d9beccb00 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "phpat/phpat": "^0.11.0", "phpbench/phpbench": "84.x-dev", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^11.5.17", + "phpunit/phpunit": "^11.5.24", "rector/rector": "^2.0-rc2", "spatie/phpunit-snapshot-assertions": "^5.1.8", "spaze/phpstan-disallowed-calls": "^4.0", diff --git a/packages/intl/src/GenericTranslator.php b/packages/intl/src/GenericTranslator.php index 5d34f5fb81..ed66d54176 100644 --- a/packages/intl/src/GenericTranslator.php +++ b/packages/intl/src/GenericTranslator.php @@ -11,7 +11,7 @@ final readonly class GenericTranslator implements Translator { public function __construct( - private IntlConfig $config, + private(set) IntlConfig $config, private(set) Catalog $catalog, private MessageFormatter $formatter, private ?EventBus $eventBus = null, diff --git a/packages/intl/src/IntlConfig.php b/packages/intl/src/IntlConfig.php index 167102fb11..2d141be772 100644 --- a/packages/intl/src/IntlConfig.php +++ b/packages/intl/src/IntlConfig.php @@ -4,13 +4,18 @@ use Tempest\Intl\Locale; use Tempest\Intl\MessageFormat\FormattingFunction; +use Tempest\Intl\MessageFormat\MarkupFormatter; use Tempest\Intl\MessageFormat\SelectorFunction; +use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter; final class IntlConfig { /** @var FormattingFunction[] */ public array $functions = []; + /** @var array */ + public array $markupFormatters = []; + /** @var array */ public array $translationMessagePaths = []; @@ -31,6 +36,11 @@ 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] ??= []; diff --git a/packages/intl/src/MessageFormat/Formatter/LocalVariable.php b/packages/intl/src/MessageFormat/Formatter/LocalVariable.php index cf5b4e4a0b..bf2845b476 100644 --- a/packages/intl/src/MessageFormat/Formatter/LocalVariable.php +++ b/packages/intl/src/MessageFormat/Formatter/LocalVariable.php @@ -4,12 +4,12 @@ use Tempest\Intl\MessageFormat\SelectorFunction; -final class LocalVariable +final readonly class LocalVariable { public function __construct( - public readonly string $identifier, - public readonly mixed $value, - public readonly ?SelectorFunction $function = null, - public readonly array $parameters = [], + public string $identifier, + public mixed $value, + public ?SelectorFunction $function = null, + public array $parameters = [], ) {} } diff --git a/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php b/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php index e2d1862d8d..780148ce6f 100644 --- a/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php +++ b/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php @@ -3,6 +3,7 @@ namespace Tempest\Intl\MessageFormat\Formatter; use Tempest\Intl\MessageFormat\FormattingFunction; +use Tempest\Intl\MessageFormat\MarkupFormatter; use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\ComplexBody; use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\Matcher; use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\SimplePatternBody; @@ -13,6 +14,7 @@ use Tempest\Intl\MessageFormat\Parser\Node\Expression\FunctionCall; use Tempest\Intl\MessageFormat\Parser\Node\Expression\FunctionExpression; use Tempest\Intl\MessageFormat\Parser\Node\Expression\LiteralExpression; +use Tempest\Intl\MessageFormat\Parser\Node\Expression\Option; use Tempest\Intl\MessageFormat\Parser\Node\Expression\VariableExpression; use Tempest\Intl\MessageFormat\Parser\Node\Key\WildcardKey; use Tempest\Intl\MessageFormat\Parser\Node\Literal\Literal; @@ -28,6 +30,8 @@ use Tempest\Intl\MessageFormat\Parser\Node\Variable; use Tempest\Intl\MessageFormat\Parser\Parser; use Tempest\Intl\MessageFormat\SelectorFunction; +use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter; +use Tempest\Support\Arr; use function Tempest\Support\arr; @@ -37,8 +41,10 @@ final class MessageFormatter private array $variables = []; public function __construct( - /** @var FormattingFunction[] */ + /** @var array */ private readonly array $functions = [], + /** @var array */ + private readonly array $markupFormatters = [], ) {} /** @@ -273,28 +279,6 @@ private function evaluateExpression(Expression $expression): FormattedValue return new FormattedValue($value, $formatted); } - 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); - } - private function evaluateOptions(array $options): array { $result = []; @@ -338,15 +322,69 @@ private function parseLocalVariables(array $variables): array private function formatMarkup(Markup $markup): string { - // TODO: more advanced with options - // built-in HtmlMarkup $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 => "<{$tag}>", - MarkupType::CLOSE => "", - MarkupType::STANDALONE => "<{$tag}/>", + 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/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 @@ +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 index b46cd7c5ba..0ac1c91e40 100644 --- a/packages/intl/src/MessageFormatterInitializer.php +++ b/packages/intl/src/MessageFormatterInitializer.php @@ -6,18 +6,17 @@ use Tempest\Container\Initializer; use Tempest\Container\Singleton; use Tempest\Intl\MessageFormat\Formatter\MessageFormatter; -use Tempest\Intl\PluralRules\PluralRulesMatcher; final class MessageFormatterInitializer implements Initializer { #[Singleton] - public function initialize(Container $container): mixed + public function initialize(Container $container): MessageFormatter { $config = $container->get(IntlConfig::class); return new MessageFormatter( functions: $config->functions, - pluralRules: new PluralRulesMatcher(), + markupFormatters: $config->markupFormatters, ); } } diff --git a/packages/intl/tests/FormatterTest.php b/packages/intl/tests/FormatterTest.php index 3cec9347e8..beeb57a500 100644 --- a/packages/intl/tests/FormatterTest.php +++ b/packages/intl/tests/FormatterTest.php @@ -12,20 +12,31 @@ use Tempest\Intl\MessageFormat\Functions\DateTimeFunction; use Tempest\Intl\MessageFormat\Functions\NumberFunction; use Tempest\Intl\MessageFormat\Functions\StringFunction; +use Tempest\Intl\MessageFormat\Markup\HtmlTagFormatter; +use Tempest\Intl\MessageFormat\Markup\VoidHtmlTagFormatter; use Tempest\Support\Currency; final class FormatterTest extends TestCase { - public function test_format_markup(): void + #[TestWith(['Click {#a href=|https://tempestphp.com|}here{/a}.', 'Click here.'])] + #[TestWith(['This is {#strong}bold{/strong}.', 'This is bold.'])] + public function test_html_tag_markup(string $input, string $expected): void { - $formatter = new MessageFormatter(); - $value = $formatter->format(<<<'TXT' - This is {#bold}bold{/bold}. - TXT); + $formatter = new MessageFormatter( + markupFormatters: [new HtmlTagFormatter()], + ); - // TODO: offer custom markup + $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('This is bold.', $value); + $this->assertSame($expected, $formatter->format($input)); } public function test_placeholder_variable(): void 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/view/composer.json b/packages/view/composer.json index 4a72896be8..4d8b485b37 100644 --- a/packages/view/composer.json +++ b/packages/view/composer.json @@ -6,6 +6,7 @@ "require": { "php": "^8.4", "tempest/core": "dev-main", + "tempest/http-client": "dev-main", "tempest/container": "dev-main", "tempest/validation": "dev-main", "tempest/clock": "dev-main", 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 index 11057bdd74..aee57cfd6b 100644 --- a/tests/Integration/Intl/DiscoveryTest.php +++ b/tests/Integration/Intl/DiscoveryTest.php @@ -17,6 +17,13 @@ public function test_functions_are_discovered(): void $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); diff --git a/tests/Integration/Intl/TranslatorTest.php b/tests/Integration/Intl/TranslatorTest.php index 4188baff65..0e67877bd4 100644 --- a/tests/Integration/Intl/TranslatorTest.php +++ b/tests/Integration/Intl/TranslatorTest.php @@ -2,6 +2,7 @@ namespace Tests\Tempest\Integration\Intl; +use PHPUnit\Framework\Attributes\TestWith; use Tempest\EventBus\EventBus; use Tempest\Intl\Catalog\Catalog; use Tempest\Intl\IntlConfig; @@ -89,4 +90,25 @@ public function test_event_fail(): void $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')); + } } From 74737b8916dd526352fa5d8dbd59e62b7f156019 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sun, 22 Jun 2025 18:32:26 +0200 Subject: [PATCH 13/20] refactor: clean up translator --- packages/intl/src/GenericTranslator.php | 12 +++++------- packages/intl/tests/FunctionsTest.php | 1 - packages/support/src/Str/functions.php | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/intl/src/GenericTranslator.php b/packages/intl/src/GenericTranslator.php index ed66d54176..9ee18608da 100644 --- a/packages/intl/src/GenericTranslator.php +++ b/packages/intl/src/GenericTranslator.php @@ -19,18 +19,16 @@ public function __construct( public function translateForLocale(Locale $locale, string $key, mixed ...$arguments): string { - $message = $this->catalog->get($locale, $key); - - if (! $message) { - $message = $this->catalog->get($this->config->fallbackLocale, $key); - } - - if (! $message) { + if (! $this->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; } diff --git a/packages/intl/tests/FunctionsTest.php b/packages/intl/tests/FunctionsTest.php index b39b8ad038..8f06d96bc4 100644 --- a/packages/intl/tests/FunctionsTest.php +++ b/packages/intl/tests/FunctionsTest.php @@ -4,7 +4,6 @@ use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; -use Tempest\Intl; use Tempest\Intl\Locale; use Tempest\Intl\Number; use Tempest\Support\Currency; diff --git a/packages/support/src/Str/functions.php b/packages/support/src/Str/functions.php index 11263010f7..d1d877d71b 100644 --- a/packages/support/src/Str/functions.php +++ b/packages/support/src/Str/functions.php @@ -813,7 +813,7 @@ function pad_left(string $string, int $totalLength, string $padString = ' '): st * => 'Yeet' * * pad_right('مرحبا', 8, 'ا') - * => 'مرح��اااا' + * => 'مرحباااا' * * @param non-empty-string $padString * @param int<0, max> $totalLength From 180c73fe9d0fef8105b5d01dbce6d1fd31df8f34 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sun, 22 Jun 2025 19:42:00 +0200 Subject: [PATCH 14/20] chore: upgrade phpunit --- .github/workflows/isolated-tests.yml | 2 +- composer.json | 2 +- tests/Integration/Log/GenericLoggerTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/isolated-tests.yml b/.github/workflows/isolated-tests.yml index ef33fb3e2a..2f6911a295 100644 --- a/.github/workflows/isolated-tests.yml +++ b/.github/workflows/isolated-tests.yml @@ -59,7 +59,7 @@ jobs: coverage: pcov - name: Install PHPUnit - run: composer global require phpunit/phpunit:^11.5.17 + run: composer global require phpunit/phpunit:^12.2.3 - name: Setup problem matchers run: | diff --git a/composer.json b/composer.json index 7d9beccb00..a77c637808 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "phpat/phpat": "^0.11.0", "phpbench/phpbench": "84.x-dev", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^11.5.24", + "phpunit/phpunit": "^12.2.3", "rector/rector": "^2.0-rc2", "spatie/phpunit-snapshot-assertions": "^5.1.8", "spaze/phpstan-disallowed-calls": "^4.0", diff --git a/tests/Integration/Log/GenericLoggerTest.php b/tests/Integration/Log/GenericLoggerTest.php index 2e5c85b6a2..b03bce9d43 100644 --- a/tests/Integration/Log/GenericLoggerTest.php +++ b/tests/Integration/Log/GenericLoggerTest.php @@ -134,7 +134,7 @@ public function test_log_levels(mixed $level, string $expected): void } #[DataProvider('tempestLevelProvider')] - public function test_message_logged_emitted(LogLevel $level): void + public function test_message_logged_emitted(LogLevel $level, string $_): void { $eventBus = $this->container->get(EventBus::class); From 34214935b77cc9ecddc5fbd91dc1e9e1fb61c307 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Mon, 23 Jun 2025 10:03:17 +0200 Subject: [PATCH 15/20] refactor: rename parser to `MessageFormatParser` --- .../src/MessageFormat/Formatter/MessageFormatter.php | 4 ++-- .../Parser/{Parser.php => MessageFormatParser.php} | 2 +- packages/intl/tests/ParserTest.php | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) rename packages/intl/src/MessageFormat/Parser/{Parser.php => MessageFormatParser.php} (99%) diff --git a/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php b/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php index 780148ce6f..13546bf601 100644 --- a/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php +++ b/packages/intl/src/MessageFormat/Formatter/MessageFormatter.php @@ -4,6 +4,7 @@ use Tempest\Intl\MessageFormat\FormattingFunction; use Tempest\Intl\MessageFormat\MarkupFormatter; +use Tempest\Intl\MessageFormat\Parser\MessageFormatParser; use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\ComplexBody; use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\Matcher; use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\SimplePatternBody; @@ -28,7 +29,6 @@ use Tempest\Intl\MessageFormat\Parser\Node\Pattern\Text; use Tempest\Intl\MessageFormat\Parser\Node\SimpleMessage; use Tempest\Intl\MessageFormat\Parser\Node\Variable; -use Tempest\Intl\MessageFormat\Parser\Parser; use Tempest\Intl\MessageFormat\SelectorFunction; use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter; use Tempest\Support\Arr; @@ -53,7 +53,7 @@ public function __construct( public function format(string $message, mixed ...$variables): string { try { - $ast = new Parser($message)->parse(); + $ast = new MessageFormatParser($message)->parse(); $this->variables = $this->parseLocalVariables($variables); diff --git a/packages/intl/src/MessageFormat/Parser/Parser.php b/packages/intl/src/MessageFormat/Parser/MessageFormatParser.php similarity index 99% rename from packages/intl/src/MessageFormat/Parser/Parser.php rename to packages/intl/src/MessageFormat/Parser/MessageFormatParser.php index b24006717c..75475161e8 100644 --- a/packages/intl/src/MessageFormat/Parser/Parser.php +++ b/packages/intl/src/MessageFormat/Parser/MessageFormatParser.php @@ -32,7 +32,7 @@ use Tempest\Intl\MessageFormat\Parser\Node\SimpleMessage; use Tempest\Intl\MessageFormat\Parser\Node\Variable; -final class Parser +final class MessageFormatParser { private string $input; private int $pos = 0; diff --git a/packages/intl/tests/ParserTest.php b/packages/intl/tests/ParserTest.php index e4247be9cf..8a6eaf8d82 100644 --- a/packages/intl/tests/ParserTest.php +++ b/packages/intl/tests/ParserTest.php @@ -3,19 +3,19 @@ namespace Tempest\Intl\Tests; use PHPUnit\Framework\TestCase; +use Tempest\Intl\MessageFormat\Parser\MessageFormatParser; use Tempest\Intl\MessageFormat\Parser\Node\ComplexMessage; use Tempest\Intl\MessageFormat\Parser\Node\Declaration\LocalDeclaration; use Tempest\Intl\MessageFormat\Parser\Node\Expression\VariableExpression; use Tempest\Intl\MessageFormat\Parser\Node\Pattern\Pattern; use Tempest\Intl\MessageFormat\Parser\Node\Pattern\Text; use Tempest\Intl\MessageFormat\Parser\Node\SimpleMessage; -use Tempest\Intl\MessageFormat\Parser\Parser; final class ParserTest extends TestCase { public function test_simple(): void { - $ast = new Parser('Hello, world!')->parse(); + $ast = new MessageFormatParser('Hello, world!')->parse(); $this->assertInstanceOf(SimpleMessage::class, $ast); $this->assertInstanceOf(Pattern::class, $ast->pattern); @@ -26,7 +26,7 @@ public function test_simple(): void public function test_local_declaration(): void { /** @var ComplexMessage $ast */ - $ast = new Parser(<<<'MF2' + $ast = new MessageFormatParser(<<<'MF2' .local $time = {$launch_date :datetime style=|medium|} Launch time: {$time} MF2)->parse(); @@ -45,7 +45,7 @@ public function test_local_declaration(): void public function test_input_declaration(): void { /** @var ComplexMessage $ast */ - $ast = new Parser(<<<'MF2' + $ast = new MessageFormatParser(<<<'MF2' .input {$numDays :number select=exact} .match $numDays 1 {{{$numDays} one}} @@ -65,7 +65,7 @@ public function test_input_declaration(): void public function test_function_with_option_quoted_literal(): void { /** @var ComplexMessage $ast */ - $ast = new Parser(<<<'MF2' + $ast = new MessageFormatParser(<<<'MF2' Today is {$today :datetime pattern=|yyyy/MM/dd|}. MF2)->parse(); From 34536d75296f236a63e35e3f464df6ea25edc119 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Mon, 23 Jun 2025 10:03:28 +0200 Subject: [PATCH 16/20] refactor: fix event listening syntax --- tests/Integration/Intl/TranslatorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Intl/TranslatorTest.php b/tests/Integration/Intl/TranslatorTest.php index 0e67877bd4..6c07036b24 100644 --- a/tests/Integration/Intl/TranslatorTest.php +++ b/tests/Integration/Intl/TranslatorTest.php @@ -57,7 +57,7 @@ public function test_event_miss(): void $received = null; $eventbus = $this->container->get(EventBus::class); - $eventbus->listen(TranslationMiss::class, function (TranslationMiss $event) use (&$received): void { + $eventbus->listen(function (TranslationMiss $event) use (&$received): void { $received = $event; }); @@ -75,7 +75,7 @@ public function test_event_fail(): void $received = null; $eventbus = $this->container->get(EventBus::class); - $eventbus->listen(TranslationFailure::class, function (TranslationFailure $event) use (&$received): void { + $eventbus->listen(function (TranslationFailure $event) use (&$received): void { $received = $event; }); From 013436e04d9ecb3ffb3d8de8279108078530fd90 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Mon, 23 Jun 2025 10:03:49 +0200 Subject: [PATCH 17/20] chore: comment what `plural-rules` does --- packages/intl/bin/plural-rules.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/intl/bin/plural-rules.php b/packages/intl/bin/plural-rules.php index 68d1b5bb78..f413c49043 100755 --- a/packages/intl/bin/plural-rules.php +++ b/packages/intl/bin/plural-rules.php @@ -1,6 +1,11 @@ #!/usr/bin/env php Date: Mon, 23 Jun 2025 13:30:36 +0200 Subject: [PATCH 18/20] chore: remove http client dependency from tempest/view --- packages/view/composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/view/composer.json b/packages/view/composer.json index 4d8b485b37..4a72896be8 100644 --- a/packages/view/composer.json +++ b/packages/view/composer.json @@ -6,7 +6,6 @@ "require": { "php": "^8.4", "tempest/core": "dev-main", - "tempest/http-client": "dev-main", "tempest/container": "dev-main", "tempest/validation": "dev-main", "tempest/clock": "dev-main", From 0779ea93cf1ca7b2bfefc4213d5c327fded018cf Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Mon, 23 Jun 2025 13:31:28 +0200 Subject: [PATCH 19/20] fix: rename intl test suite --- packages/intl/phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/intl/phpunit.xml b/packages/intl/phpunit.xml index f41bab92e8..1c13eda788 100644 --- a/packages/intl/phpunit.xml +++ b/packages/intl/phpunit.xml @@ -1,7 +1,7 @@ - + tests From ed433c74b42ac9a3c29496399ceffc850f665a94 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Mon, 23 Jun 2025 15:36:53 +0200 Subject: [PATCH 20/20] fix: update `has` call --- packages/intl/src/Catalog/GenericCatalog.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/intl/src/Catalog/GenericCatalog.php b/packages/intl/src/Catalog/GenericCatalog.php index ec14697153..9d5900472d 100644 --- a/packages/intl/src/Catalog/GenericCatalog.php +++ b/packages/intl/src/Catalog/GenericCatalog.php @@ -17,7 +17,7 @@ public function __construct( public function has(Locale $locale, string $key): bool { - return Arr\has($this->catalog, "{$locale->value}.{$key}"); + return Arr\has_key($this->catalog, "{$locale->value}.{$key}"); } public function get(Locale $locale, string $key): ?string