first commit

This commit is contained in:
Claudecio Martins
2026-06-16 10:04:10 -03:00
commit a951944997
4463 changed files with 419677 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
CHANGELOG
=========
7.1
---
* Add `DatePoint::getMicrosecond()` and `DatePoint::setMicrosecond()`
6.4
---
* Add `DatePoint`: an immutable DateTime implementation with stricter error handling and return types
* Throw `DateMalformedStringException`/`DateInvalidTimeZoneException` when appropriate
* Add `$modifier` argument to the `now()` helper
6.3
---
* Add `ClockAwareTrait` to help write time-sensitive classes
* Add `Clock` class and `now()` function
6.2
---
* Add the component
+89
View File
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
use Psr\Clock\ClockInterface as PsrClockInterface;
/**
* A global clock.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Clock implements ClockInterface
{
private static ClockInterface $globalClock;
public function __construct(
private readonly ?PsrClockInterface $clock = null,
private ?\DateTimeZone $timezone = null,
) {
}
/**
* Returns the current global clock.
*
* Note that you should prefer injecting a ClockInterface or using
* ClockAwareTrait when possible instead of using this method.
*/
public static function get(): ClockInterface
{
return self::$globalClock ??= new NativeClock();
}
public static function set(PsrClockInterface $clock): void
{
self::$globalClock = $clock instanceof ClockInterface ? $clock : new self($clock);
}
public function now(): DatePoint
{
$now = ($this->clock ?? self::get())->now();
if (!$now instanceof DatePoint) {
$now = DatePoint::createFromInterface($now);
}
return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now;
}
public function sleep(float|int $seconds): void
{
$clock = $this->clock ?? self::get();
if ($clock instanceof ClockInterface) {
$clock->sleep($seconds);
} else {
(new NativeClock())->sleep($seconds);
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
use Psr\Clock\ClockInterface;
use Symfony\Contracts\Service\Attribute\Required;
/**
* A trait to help write time-sensitive classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ClockAwareTrait
{
private readonly ClockInterface $clock;
#[Required]
public function setClock(ClockInterface $clock): void
{
$this->clock = $clock;
}
protected function now(): DatePoint
{
$now = ($this->clock ??= new Clock())->now();
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
use Psr\Clock\ClockInterface as PsrClockInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ClockInterface extends PsrClockInterface
{
public function sleep(float|int $seconds): void;
public function withTimeZone(\DateTimeZone|string $timezone): static;
}
+169
View File
@@ -0,0 +1,169 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* An immmutable DateTime with stricter error handling and return types than the native one.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class DatePoint extends \DateTimeImmutable
{
/**
* @throws \DateMalformedStringException When $datetime is invalid
*/
public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null, ?parent $reference = null)
{
$now = $reference ?? Clock::get()->now();
if ('now' !== $datetime) {
if (!$now instanceof static) {
$now = static::createFromInterface($now);
}
if (\PHP_VERSION_ID < 80300) {
try {
$builtInDate = new parent($datetime, $timezone ?? $now->getTimezone());
$timezone = $builtInDate->getTimezone();
} catch (\Exception $e) {
throw new \DateMalformedStringException($e->getMessage(), $e->getCode(), $e);
}
} else {
$builtInDate = new parent($datetime, $timezone ?? $now->getTimezone());
$timezone = $builtInDate->getTimezone();
}
$now = $now->setTimezone($timezone)->modify($datetime);
if ('00:00:00.000000' === $builtInDate->format('H:i:s.u')) {
$now = $now->setTime(0, 0);
}
} elseif (null !== $timezone) {
$now = $now->setTimezone($timezone);
}
$this->__unserialize((array) $now);
}
/**
* @throws \DateMalformedStringException When $format or $datetime are invalid
*/
public static function createFromFormat(string $format, string $datetime, ?\DateTimeZone $timezone = null): static
{
return parent::createFromFormat($format, $datetime, $timezone) ?: throw new \DateMalformedStringException(static::getLastErrors()['errors'][0] ?? 'Invalid date string or format.');
}
public static function createFromInterface(\DateTimeInterface $object): static
{
return parent::createFromInterface($object);
}
public static function createFromMutable(\DateTime $object): static
{
return parent::createFromMutable($object);
}
public static function createFromTimestamp(int|float $timestamp): static
{
if (\PHP_VERSION_ID >= 80400) {
return parent::createFromTimestamp($timestamp);
}
if (\is_int($timestamp) || !$ms = (int) $timestamp - $timestamp) {
return static::createFromFormat('U', (string) $timestamp);
}
if (!is_finite($timestamp) || \PHP_INT_MAX + 1.0 <= $timestamp || \PHP_INT_MIN > $timestamp) {
throw new \DateRangeError(\sprintf('DateTimeImmutable::createFromTimestamp(): Argument #1 ($timestamp) must be a finite number between %s and %s.999999, %s given', \PHP_INT_MIN, \PHP_INT_MAX, $timestamp));
}
if ($timestamp < 0) {
$timestamp = (int) $timestamp - 2.0 + $ms;
}
return static::createFromFormat('U.u', \sprintf('%.6F', $timestamp));
}
public function add(\DateInterval $interval): static
{
return parent::add($interval);
}
public function sub(\DateInterval $interval): static
{
return parent::sub($interval);
}
/**
* @throws \DateMalformedStringException When $modifier is invalid
*/
public function modify(string $modifier): static
{
if (\PHP_VERSION_ID < 80300) {
return @parent::modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? \sprintf('Invalid modifier: "%s".', $modifier));
}
return parent::modify($modifier);
}
public function setTimestamp(int $value): static
{
return parent::setTimestamp($value);
}
public function setDate(int $year, int $month, int $day): static
{
return parent::setDate($year, $month, $day);
}
public function setISODate(int $year, int $week, int $day = 1): static
{
return parent::setISODate($year, $week, $day);
}
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
{
return parent::setTime($hour, $minute, $second, $microsecond);
}
public function setTimezone(\DateTimeZone $timezone): static
{
return parent::setTimezone($timezone);
}
public function getTimezone(): \DateTimeZone
{
return parent::getTimezone() ?: throw new \DateInvalidTimeZoneException('The DatePoint object has no timezone.');
}
public function setMicrosecond(int $microsecond): static
{
if ($microsecond < 0 || $microsecond > 999999) {
throw new \DateRangeError('DatePoint::setMicrosecond(): Argument #1 ($microsecond) must be between 0 and 999999, '.$microsecond.' given');
}
if (\PHP_VERSION_ID < 80400) {
return $this->setTime(...explode('.', $this->format('H.i.s.'.$microsecond)));
}
return parent::setMicrosecond($microsecond);
}
public function getMicrosecond(): int
{
if (\PHP_VERSION_ID >= 80400) {
return parent::getMicrosecond();
}
return $this->format('u');
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2022-present Fabien Potencier
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.
+102
View File
@@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* A clock that always returns the same date, suitable for testing time-sensitive logic.
*
* Consider using ClockSensitiveTrait in your test cases instead of using this class directly.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MockClock implements ClockInterface
{
private DatePoint $now;
/**
* @throws \DateMalformedStringException When $now is invalid
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZone|string|null $timezone = null)
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
if (\is_string($now)) {
$now = new DatePoint($now, $timezone ?? new \DateTimeZone('UTC'));
} elseif (!$now instanceof DatePoint) {
$now = DatePoint::createFromInterface($now);
}
$this->now = null !== $timezone ? $now->setTimezone($timezone) : $now;
}
public function now(): DatePoint
{
return clone $this->now;
}
public function sleep(float|int $seconds): void
{
if (0 >= $seconds) {
return;
}
$now = (float) $this->now->format('Uu') + $seconds * 1e6;
$now = substr_replace(\sprintf('@%07.0F', $now), '.', -6, 0);
$timezone = $this->now->getTimezone();
$this->now = DatePoint::createFromInterface(new \DateTimeImmutable($now, $timezone))->setTimezone($timezone);
}
/**
* @throws \DateMalformedStringException When $modifier is invalid
*/
public function modify(string $modifier): void
{
if (\PHP_VERSION_ID < 80300) {
$this->now = @$this->now->modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? \sprintf('Invalid modifier: "%s". Could not modify MockClock.', $modifier));
return;
}
$this->now = $this->now->modify($modifier);
}
/**
* @throws \DateInvalidTimeZoneException When the timezone name is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->now = $clone->now->setTimezone($timezone);
return $clone;
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* A monotonic clock suitable for performance profiling.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MonotonicClock implements ClockInterface
{
private int $sOffset;
private int $usOffset;
private \DateTimeZone $timezone;
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeZone|string|null $timezone = null)
{
if (false === $offset = hrtime()) {
throw new \RuntimeException('hrtime() returned false: the runtime environment does not provide access to a monotonic timer.');
}
$time = explode(' ', microtime(), 2);
$this->sOffset = $time[1] - $offset[0];
$this->usOffset = (int) ($time[0] * 1000000) - (int) ($offset[1] / 1000);
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
}
public function now(): DatePoint
{
[$s, $us] = hrtime();
if (1000000 <= $us = (int) ($us / 1000) + $this->usOffset) {
++$s;
$us -= 1000000;
} elseif (0 > $us) {
--$s;
$us += 1000000;
}
if (6 !== \strlen($now = (string) $us)) {
$now = str_pad($now, 6, '0', \STR_PAD_LEFT);
}
$now = '@'.($s + $this->sOffset).'.'.$now;
return DatePoint::createFromInterface(new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone);
}
public function sleep(float|int $seconds): void
{
if (0 < $s = (int) $seconds) {
sleep($s);
}
if (0 < $us = $seconds - $s) {
usleep((int) ($us * 1E6));
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* A clock that relies the system time.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativeClock implements ClockInterface
{
private \DateTimeZone $timezone;
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeZone|string|null $timezone = null)
{
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
}
public function now(): DatePoint
{
return DatePoint::createFromInterface(new \DateTimeImmutable('now', $this->timezone));
}
public function sleep(float|int $seconds): void
{
if (0 < $s = (int) $seconds) {
sleep($s);
}
if (0 < $us = $seconds - $s) {
usleep((int) ($us * 1E6));
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}
+47
View File
@@ -0,0 +1,47 @@
Clock Component
===============
Symfony Clock decouples applications from the system clock.
Getting Started
---------------
```bash
composer require symfony/clock
```
```php
use Symfony\Component\Clock\NativeClock;
use Symfony\Component\Clock\ClockInterface;
class MyClockSensitiveClass
{
public function __construct(
private ClockInterface $clock,
) {
// Only if you need to force a timezone:
//$this->clock = $clock->withTimeZone('UTC');
}
public function doSomething()
{
$now = $this->clock->now();
// [...] do something with $now, which is a \DateTimeImmutable object
$this->clock->sleep(2.5); // Pause execution for 2.5 seconds
}
}
$clock = new NativeClock();
$service = new MyClockSensitiveClass($clock);
$service->doSomething();
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/clock.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
+28
View File
@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
if (!\function_exists(now::class)) {
/**
* @throws \DateMalformedStringException When the modifier is invalid
*/
function now(string $modifier = 'now'): DatePoint
{
if ('now' !== $modifier) {
return new DatePoint($modifier);
}
$now = Clock::get()->now();
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock\Test;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\BeforeClass;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
use function Symfony\Component\Clock\now;
/**
* Helps with mocking the time in your test cases.
*
* This trait provides one self::mockTime() method that freezes the time.
* It restores the global clock after each test case.
* self::mockTime() accepts either a string (eg '+1 days' or '2022-12-22'),
* a DateTimeImmutable, or a boolean (to freeze/restore the global clock).
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ClockSensitiveTrait
{
public static function mockTime(string|\DateTimeImmutable|bool $when = true): ClockInterface
{
Clock::set(match (true) {
false === $when => self::saveClockBeforeTest(false),
true === $when => new MockClock(),
$when instanceof \DateTimeImmutable => new MockClock($when),
default => new MockClock(now($when)),
});
return Clock::get();
}
/**
* @beforeClass
*
* @before
*
* @internal
*/
#[Before]
#[BeforeClass]
public static function saveClockBeforeTest(bool $save = true): ClockInterface
{
static $originalClock;
if ($save && $originalClock) {
self::restoreClockAfterTest();
}
return $save ? $originalClock = Clock::get() : $originalClock;
}
/**
* @after
*
* @internal
*/
#[After]
protected static function restoreClockAfterTest(): void
{
Clock::set(self::saveClockBeforeTest(false));
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"name": "symfony/clock",
"type": "library",
"description": "Decouples applications from the system clock",
"keywords": ["clock", "time", "psr20"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"provide": {
"psr/clock-implementation": "1.0"
},
"require": {
"php": ">=8.2",
"psr/clock": "^1.0",
"symfony/polyfill-php83": "^1.28"
},
"autoload": {
"files": [ "Resources/now.php" ],
"psr-4": { "Symfony\\Component\\Clock\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}
+5
View File
@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2020-present Fabien Potencier
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.
+26
View File
@@ -0,0 +1,26 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.
+35
View File
@@ -0,0 +1,35 @@
{
"name": "symfony/deprecation-contracts",
"type": "library",
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1"
},
"autoload": {
"files": [
"function.php"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.7-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}
+232
View File
@@ -0,0 +1,232 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Ctype;
/**
* Ctype implementation through regex.
*
* @internal
*
* @author Gert de Pagter <BackEndTea@gmail.com>
*/
final class Ctype
{
/**
* Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise.
*
* @see https://php.net/ctype-alnum
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_alnum($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text);
}
/**
* Returns TRUE if every character in text is a letter, FALSE otherwise.
*
* @see https://php.net/ctype-alpha
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_alpha($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text);
}
/**
* Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise.
*
* @see https://php.net/ctype-cntrl
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_cntrl($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text);
}
/**
* Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
*
* @see https://php.net/ctype-digit
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_digit($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text);
}
/**
* Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise.
*
* @see https://php.net/ctype-graph
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_graph($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text);
}
/**
* Returns TRUE if every character in text is a lowercase letter.
*
* @see https://php.net/ctype-lower
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_lower($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text);
}
/**
* Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all.
*
* @see https://php.net/ctype-print
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_print($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text);
}
/**
* Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise.
*
* @see https://php.net/ctype-punct
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_punct($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text);
}
/**
* Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters.
*
* @see https://php.net/ctype-space
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_space($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text);
}
/**
* Returns TRUE if every character in text is an uppercase letter.
*
* @see https://php.net/ctype-upper
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_upper($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text);
}
/**
* Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise.
*
* @see https://php.net/ctype-xdigit
*
* @param mixed $text
*
* @return bool
*/
public static function ctype_xdigit($text)
{
$text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);
return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text);
}
/**
* Converts integers to their char versions according to normal ctype behaviour, if needed.
*
* If an integer between -128 and 255 inclusive is provided,
* it is interpreted as the ASCII value of a single character
* (negative values have 256 added in order to allow characters in the Extended ASCII range).
* Any other integer is interpreted as a string containing the decimal digits of the integer.
*
* @param mixed $int
* @param string $function
*
* @return mixed
*/
private static function convert_int_to_char_for_ctype($int, $function)
{
if (\PHP_VERSION_ID >= 80100 && !\is_string($int)) {
@trigger_error($function.'(): Argument of type '.get_debug_type($int).' will be interpreted as string in the future', \E_USER_DEPRECATED);
}
if (!\is_int($int)) {
return $int;
}
if ($int < -128 || $int > 255) {
return (string) $int;
}
if ($int < 0) {
$int += 256;
}
return \chr($int);
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
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.
+12
View File
@@ -0,0 +1,12 @@
Symfony Polyfill / Ctype
========================
This component provides `ctype_*` functions to users who run php versions without the ctype extension.
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).
+50
View File
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Ctype as p;
if (\PHP_VERSION_ID >= 80000) {
return require __DIR__.'/bootstrap80.php';
}
if (!function_exists('ctype_alnum')) {
function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }
}
if (!function_exists('ctype_alpha')) {
function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }
}
if (!function_exists('ctype_cntrl')) {
function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); }
}
if (!function_exists('ctype_digit')) {
function ctype_digit($text) { return p\Ctype::ctype_digit($text); }
}
if (!function_exists('ctype_graph')) {
function ctype_graph($text) { return p\Ctype::ctype_graph($text); }
}
if (!function_exists('ctype_lower')) {
function ctype_lower($text) { return p\Ctype::ctype_lower($text); }
}
if (!function_exists('ctype_print')) {
function ctype_print($text) { return p\Ctype::ctype_print($text); }
}
if (!function_exists('ctype_punct')) {
function ctype_punct($text) { return p\Ctype::ctype_punct($text); }
}
if (!function_exists('ctype_space')) {
function ctype_space($text) { return p\Ctype::ctype_space($text); }
}
if (!function_exists('ctype_upper')) {
function ctype_upper($text) { return p\Ctype::ctype_upper($text); }
}
if (!function_exists('ctype_xdigit')) {
function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); }
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Ctype as p;
if (!function_exists('ctype_alnum')) {
function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); }
}
if (!function_exists('ctype_alpha')) {
function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); }
}
if (!function_exists('ctype_cntrl')) {
function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); }
}
if (!function_exists('ctype_digit')) {
function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); }
}
if (!function_exists('ctype_graph')) {
function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); }
}
if (!function_exists('ctype_lower')) {
function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); }
}
if (!function_exists('ctype_print')) {
function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); }
}
if (!function_exists('ctype_punct')) {
function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); }
}
if (!function_exists('ctype_space')) {
function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); }
}
if (!function_exists('ctype_upper')) {
function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); }
}
if (!function_exists('ctype_xdigit')) {
function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); }
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "symfony/polyfill-ctype",
"type": "library",
"description": "Symfony polyfill for ctype functions",
"keywords": ["polyfill", "compatibility", "portable", "ctype"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Ctype\\": "" },
"files": [ "bootstrap.php" ]
},
"suggest": {
"ext-ctype": "For best performance"
},
"minimum-stability": "dev",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2015-present Fabien Potencier
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.
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
Symfony Polyfill / Mbstring
===========================
This component provides a partial, native PHP implementation for the
[Mbstring](https://php.net/mbstring) extension.
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).
@@ -0,0 +1,119 @@
<?php
return [
'İ' => 'i̇',
'µ' => 'μ',
'ſ' => 's',
'ͅ' => 'ι',
'ς' => 'σ',
'ϐ' => 'β',
'ϑ' => 'θ',
'ϕ' => 'φ',
'ϖ' => 'π',
'ϰ' => 'κ',
'ϱ' => 'ρ',
'ϵ' => 'ε',
'ẛ' => 'ṡ',
'' => 'ι',
'ß' => 'ss',
'ʼn' => 'ʼn',
'ǰ' => 'ǰ',
'ΐ' => 'ΐ',
'ΰ' => 'ΰ',
'և' => 'եւ',
'ẖ' => 'ẖ',
'ẗ' => 'ẗ',
'ẘ' => 'ẘ',
'ẙ' => 'ẙ',
'ẚ' => 'aʾ',
'ẞ' => 'ss',
'ὐ' => 'ὐ',
'ὒ' => 'ὒ',
'ὔ' => 'ὔ',
'ὖ' => 'ὖ',
'ᾀ' => 'ἀι',
'ᾁ' => 'ἁι',
'ᾂ' => 'ἂι',
'ᾃ' => 'ἃι',
'ᾄ' => 'ἄι',
'ᾅ' => 'ἅι',
'ᾆ' => 'ἆι',
'ᾇ' => 'ἇι',
'ᾈ' => 'ἀι',
'ᾉ' => 'ἁι',
'ᾊ' => 'ἂι',
'ᾋ' => 'ἃι',
'ᾌ' => 'ἄι',
'ᾍ' => 'ἅι',
'ᾎ' => 'ἆι',
'ᾏ' => 'ἇι',
'ᾐ' => 'ἠι',
'ᾑ' => 'ἡι',
'ᾒ' => 'ἢι',
'ᾓ' => 'ἣι',
'ᾔ' => 'ἤι',
'ᾕ' => 'ἥι',
'ᾖ' => 'ἦι',
'ᾗ' => 'ἧι',
'ᾘ' => 'ἠι',
'ᾙ' => 'ἡι',
'ᾚ' => 'ἢι',
'ᾛ' => 'ἣι',
'ᾜ' => 'ἤι',
'ᾝ' => 'ἥι',
'ᾞ' => 'ἦι',
'ᾟ' => 'ἧι',
'ᾠ' => 'ὠι',
'ᾡ' => 'ὡι',
'ᾢ' => 'ὢι',
'ᾣ' => 'ὣι',
'ᾤ' => 'ὤι',
'ᾥ' => 'ὥι',
'ᾦ' => 'ὦι',
'ᾧ' => 'ὧι',
'ᾨ' => 'ὠι',
'ᾩ' => 'ὡι',
'ᾪ' => 'ὢι',
'ᾫ' => 'ὣι',
'ᾬ' => 'ὤι',
'ᾭ' => 'ὥι',
'ᾮ' => 'ὦι',
'ᾯ' => 'ὧι',
'ᾲ' => 'ὰι',
'ᾳ' => 'αι',
'ᾴ' => 'άι',
'ᾶ' => 'ᾶ',
'ᾷ' => 'ᾶι',
'ᾼ' => 'αι',
'ῂ' => 'ὴι',
'ῃ' => 'ηι',
'ῄ' => 'ήι',
'ῆ' => 'ῆ',
'ῇ' => 'ῆι',
'ῌ' => 'ηι',
'ῒ' => 'ῒ',
'ῖ' => 'ῖ',
'ῗ' => 'ῗ',
'ῢ' => 'ῢ',
'ῤ' => 'ῤ',
'ῦ' => 'ῦ',
'ῧ' => 'ῧ',
'ῲ' => 'ὼι',
'ῳ' => 'ωι',
'ῴ' => 'ώι',
'ῶ' => 'ῶ',
'ῷ' => 'ῶι',
'ῼ' => 'ωι',
'ff' => 'ff',
'fi' => 'fi',
'fl' => 'fl',
'ffi' => 'ffi',
'ffl' => 'ffl',
'ſt' => 'st',
'st' => 'st',
'ﬓ' => 'մն',
'ﬔ' => 'մե',
'ﬕ' => 'մի',
'ﬖ' => 'վն',
'ﬗ' => 'մխ',
];
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID >= 80000) {
return require __DIR__.'/bootstrap80.php';
}
return require __DIR__.'/bootstrap72.php';
+173
View File
@@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Mbstring as p;
if (!function_exists('mb_convert_encoding')) {
function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); }
}
if (!function_exists('mb_decode_mimeheader')) {
function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); }
}
if (!function_exists('mb_encode_mimeheader')) {
function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); }
}
if (!function_exists('mb_decode_numericentity')) {
function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); }
}
if (!function_exists('mb_encode_numericentity')) {
function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); }
}
if (!function_exists('mb_convert_case')) {
function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); }
}
if (!function_exists('mb_internal_encoding')) {
function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); }
}
if (!function_exists('mb_language')) {
function mb_language($language = null) { return p\Mbstring::mb_language($language); }
}
if (!function_exists('mb_list_encodings')) {
function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
}
if (!function_exists('mb_encoding_aliases')) {
function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
}
if (!function_exists('mb_check_encoding')) {
function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); }
}
if (!function_exists('mb_detect_encoding')) {
function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); }
}
if (!function_exists('mb_detect_order')) {
function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); }
}
if (!function_exists('mb_parse_str')) {
function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; }
}
if (!function_exists('mb_strlen')) {
function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); }
}
if (!function_exists('mb_strpos')) {
function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strtolower')) {
function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); }
}
if (!function_exists('mb_strtoupper')) {
function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); }
}
if (!function_exists('mb_substitute_character')) {
function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); }
}
if (!function_exists('mb_substr')) {
function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); }
}
if (!function_exists('mb_stripos')) {
function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_stristr')) {
function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrchr')) {
function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrichr')) {
function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strripos')) {
function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strrpos')) {
function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strstr')) {
function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_get_info')) {
function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
}
if (!function_exists('mb_http_output')) {
function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); }
}
if (!function_exists('mb_strwidth')) {
function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); }
}
if (!function_exists('mb_substr_count')) {
function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); }
}
if (!function_exists('mb_output_handler')) {
function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); }
}
if (!function_exists('mb_http_input')) {
function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); }
}
if (!function_exists('mb_convert_variables')) {
function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); }
}
if (!function_exists('mb_ord')) {
function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); }
}
if (!function_exists('mb_chr')) {
function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); }
}
if (!function_exists('mb_scrub')) {
function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); }
}
if (!function_exists('mb_str_split')) {
function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); }
}
if (!function_exists('mb_str_pad')) {
/** @return string|false */
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null) { return p\Mbstring::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
if (!function_exists('mb_ucfirst')) {
/** @return string|false */
function mb_ucfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_ucfirst((string) $string, $encoding); }
}
if (!function_exists('mb_lcfirst')) {
/** @return string|false */
function mb_lcfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_lcfirst((string) $string, $encoding); }
}
if (!function_exists('mb_trim')) {
/** @return string|false */
function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_trim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_ltrim')) {
/** @return string|false */
function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_ltrim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_rtrim')) {
/** @return string|false */
function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_rtrim((string) $string, $characters, $encoding); }
}
if (extension_loaded('mbstring')) {
return;
}
if (!defined('MB_CASE_UPPER')) {
define('MB_CASE_UPPER', 0);
}
if (!defined('MB_CASE_LOWER')) {
define('MB_CASE_LOWER', 1);
}
if (!defined('MB_CASE_TITLE')) {
define('MB_CASE_TITLE', 2);
}
+167
View File
@@ -0,0 +1,167 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Mbstring as p;
if (!function_exists('mb_convert_encoding')) {
function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); }
}
if (!function_exists('mb_decode_mimeheader')) {
function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); }
}
if (!function_exists('mb_encode_mimeheader')) {
function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); }
}
if (!function_exists('mb_decode_numericentity')) {
function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); }
}
if (!function_exists('mb_encode_numericentity')) {
function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); }
}
if (!function_exists('mb_convert_case')) {
function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); }
}
if (!function_exists('mb_internal_encoding')) {
function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); }
}
if (!function_exists('mb_language')) {
function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); }
}
if (!function_exists('mb_list_encodings')) {
function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); }
}
if (!function_exists('mb_encoding_aliases')) {
function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); }
}
if (!function_exists('mb_check_encoding')) {
function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); }
}
if (!function_exists('mb_detect_encoding')) {
function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); }
}
if (!function_exists('mb_detect_order')) {
function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); }
}
if (!function_exists('mb_parse_str')) {
function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; }
}
if (!function_exists('mb_strlen')) {
function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); }
}
if (!function_exists('mb_strpos')) {
function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_strtolower')) {
function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); }
}
if (!function_exists('mb_strtoupper')) {
function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); }
}
if (!function_exists('mb_substitute_character')) {
function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); }
}
if (!function_exists('mb_substr')) {
function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); }
}
if (!function_exists('mb_stripos')) {
function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_stristr')) {
function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_strrchr')) {
function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_strrichr')) {
function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_strripos')) {
function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_strrpos')) {
function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_strstr')) {
function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_get_info')) {
function mb_get_info(?string $type = 'all'): array|string|int|false|null { return p\Mbstring::mb_get_info((string) $type); }
}
if (!function_exists('mb_http_output')) {
function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); }
}
if (!function_exists('mb_strwidth')) {
function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); }
}
if (!function_exists('mb_substr_count')) {
function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); }
}
if (!function_exists('mb_output_handler')) {
function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); }
}
if (!function_exists('mb_http_input')) {
function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); }
}
if (!function_exists('mb_convert_variables')) {
function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); }
}
if (!function_exists('mb_ord')) {
function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); }
}
if (!function_exists('mb_chr')) {
function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); }
}
if (!function_exists('mb_scrub')) {
function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); }
}
if (!function_exists('mb_str_split')) {
function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); }
}
if (!function_exists('mb_str_pad')) {
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
if (!function_exists('mb_ucfirst')) {
function mb_ucfirst(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst((string) $string, $encoding); }
}
if (!function_exists('mb_lcfirst')) {
function mb_lcfirst(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst((string) $string, $encoding); }
}
if (!function_exists('mb_trim')) {
function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_ltrim')) {
function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_rtrim')) {
function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim((string) $string, $characters, $encoding); }
}
if (extension_loaded('mbstring')) {
return;
}
if (!defined('MB_CASE_UPPER')) {
define('MB_CASE_UPPER', 0);
}
if (!defined('MB_CASE_LOWER')) {
define('MB_CASE_LOWER', 1);
}
if (!defined('MB_CASE_TITLE')) {
define('MB_CASE_TITLE', 2);
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "symfony/polyfill-mbstring",
"type": "library",
"description": "Symfony polyfill for the Mbstring extension",
"keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2",
"ext-iconv": "*"
},
"provide": {
"ext-mbstring": "*"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" },
"files": [ "bootstrap.php" ]
},
"suggest": {
"ext-mbstring": "For best performance"
},
"minimum-stability": "dev",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2020-present Fabien Potencier
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.
+115
View File
@@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php80;
/**
* @author Ion Bazan <ion.bazan@gmail.com>
* @author Nico Oelgart <nicoswd@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class Php80
{
public static function fdiv(float $dividend, float $divisor): float
{
return @($dividend / $divisor);
}
public static function get_debug_type($value): string
{
switch (true) {
case null === $value: return 'null';
case \is_bool($value): return 'bool';
case \is_string($value): return 'string';
case \is_array($value): return 'array';
case \is_int($value): return 'int';
case \is_float($value): return 'float';
case \is_object($value): break;
case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
default:
if (null === $type = @get_resource_type($value)) {
return 'unknown';
}
if ('Unknown' === $type) {
$type = 'closed';
}
return "resource ($type)";
}
$class = \get_class($value);
if (false === strpos($class, '@')) {
return $class;
}
return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
}
public static function get_resource_id($res): int
{
if (!\is_resource($res) && null === @get_resource_type($res)) {
throw new \TypeError(\sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
}
return (int) $res;
}
public static function preg_last_error_msg(): string
{
switch (preg_last_error()) {
case \PREG_INTERNAL_ERROR:
return 'Internal error';
case \PREG_BAD_UTF8_ERROR:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
case \PREG_BAD_UTF8_OFFSET_ERROR:
return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
case \PREG_BACKTRACK_LIMIT_ERROR:
return 'Backtrack limit exhausted';
case \PREG_RECURSION_LIMIT_ERROR:
return 'Recursion limit exhausted';
case \PREG_JIT_STACKLIMIT_ERROR:
return 'JIT stack limit exhausted';
case \PREG_NO_ERROR:
return 'No error';
default:
return 'Unknown error';
}
}
public static function str_contains(string $haystack, string $needle): bool
{
return '' === $needle || false !== strpos($haystack, $needle);
}
public static function str_starts_with(string $haystack, string $needle): bool
{
return 0 === strncmp($haystack, $needle, \strlen($needle));
}
public static function str_ends_with(string $haystack, string $needle): bool
{
if ('' === $needle || $needle === $haystack) {
return true;
}
if ('' === $haystack) {
return false;
}
$needleLength = \strlen($needle);
return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php80;
/**
* @author Fedonyuk Anton <info@ensostudio.ru>
*
* @internal
*/
class PhpToken implements \Stringable
{
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $text;
/**
* @var -1|positive-int
*/
public $line;
/**
* @var int
*/
public $pos;
/**
* @param -1|positive-int $line
*/
public function __construct(int $id, string $text, int $line = -1, int $position = -1)
{
$this->id = $id;
$this->text = $text;
$this->line = $line;
$this->pos = $position;
}
public function getTokenName(): ?string
{
if ('UNKNOWN' === $name = token_name($this->id)) {
$name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text;
}
return $name;
}
/**
* @param int|string|array $kind
*/
public function is($kind): bool
{
foreach ((array) $kind as $value) {
if (\in_array($value, [$this->id, $this->text], true)) {
return true;
}
}
return false;
}
public function isIgnorable(): bool
{
return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true);
}
public function __toString(): string
{
return (string) $this->text;
}
/**
* @return list<static>
*/
public static function tokenize(string $code, int $flags = 0): array
{
$line = 1;
$position = 0;
$tokens = token_get_all($code, $flags);
foreach ($tokens as $index => $token) {
if (\is_string($token)) {
$id = \ord($token);
$text = $token;
} else {
[$id, $text, $line] = $token;
}
$tokens[$index] = new static($id, $text, $line, $position);
$position += \strlen($text);
}
return $tokens;
}
}
+25
View File
@@ -0,0 +1,25 @@
Symfony Polyfill / Php80
========================
This component provides features added to PHP 8.0 core:
- [`Stringable`](https://php.net/stringable) interface
- [`fdiv`](https://php.net/fdiv)
- [`ValueError`](https://php.net/valueerror) class
- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class
- `FILTER_VALIDATE_BOOL` constant
- [`get_debug_type`](https://php.net/get_debug_type)
- [`PhpToken`](https://php.net/phptoken) class
- [`preg_last_error_msg`](https://php.net/preg_last_error_msg)
- [`str_contains`](https://php.net/str_contains)
- [`str_starts_with`](https://php.net/str_starts_with)
- [`str_ends_with`](https://php.net/str_ends_with)
- [`get_resource_id`](https://php.net/get_resource_id)
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class Attribute
{
public const TARGET_CLASS = 1;
public const TARGET_FUNCTION = 2;
public const TARGET_METHOD = 4;
public const TARGET_PROPERTY = 8;
public const TARGET_CLASS_CONSTANT = 16;
public const TARGET_PARAMETER = 32;
public const TARGET_ALL = 63;
public const IS_REPEATABLE = 64;
/** @var int */
public $flags;
public function __construct(int $flags = self::TARGET_ALL)
{
$this->flags = $flags;
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) {
class PhpToken extends Symfony\Polyfill\Php80\PhpToken
{
}
}
@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80000) {
interface Stringable
{
/**
* @return string
*/
public function __toString();
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80000) {
class UnhandledMatchError extends Error
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80000) {
class ValueError extends Error
{
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php80 as p;
if (\PHP_VERSION_ID >= 80000) {
return;
}
if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) {
define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN);
}
if (!function_exists('fdiv')) {
function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); }
}
if (!function_exists('preg_last_error_msg')) {
function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); }
}
if (!function_exists('str_contains')) {
function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('str_starts_with')) {
function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('str_ends_with')) {
function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('get_debug_type')) {
function get_debug_type($value): string { return p\Php80::get_debug_type($value); }
}
if (!function_exists('get_resource_id')) {
function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); }
}
+37
View File
@@ -0,0 +1,37 @@
{
"name": "symfony/polyfill-php80",
"type": "library",
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"keywords": ["polyfill", "shim", "compatibility", "portable"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Php80\\": "" },
"files": [ "bootstrap.php" ],
"classmap": [ "Resources/stubs" ]
},
"minimum-stability": "dev",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2022-present Fabien Potencier
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.
+205
View File
@@ -0,0 +1,205 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php83;
/**
* @author Ion Bazan <ion.bazan@gmail.com>
* @author Pierre Ambroise <pierre27.ambroise@gmail.com>
*
* @internal
*/
final class Php83
{
private const JSON_MAX_DEPTH = 0x7FFFFFFF; // see https://www.php.net/manual/en/function.json-decode.php
public static function json_validate(string $json, int $depth = 512, int $flags = 0): bool
{
if (0 !== $flags && \defined('JSON_INVALID_UTF8_IGNORE') && \JSON_INVALID_UTF8_IGNORE !== $flags) {
throw new \ValueError('json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)');
}
if ($depth <= 0) {
throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0');
}
if ($depth > self::JSON_MAX_DEPTH) {
throw new \ValueError(\sprintf('json_validate(): Argument #2 ($depth) must be less than %d', self::JSON_MAX_DEPTH));
}
json_decode($json, true, $depth, $flags);
return \JSON_ERROR_NONE === json_last_error();
}
/** @return string|false */
public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
{
if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
}
if (null === $encoding) {
$encoding = mb_internal_encoding();
}
$errorToTrigger = null;
try {
if (!@mb_check_encoding('', $encoding)) {
$errorToTrigger = \sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding);
}
} catch (\ValueError $e) {
$errorToTrigger = \sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding);
}
if (null === $errorToTrigger && mb_strlen($pad_string, $encoding) <= 0) {
$errorToTrigger = 'mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string';
}
if (null !== $errorToTrigger) {
if (80000 > \PHP_VERSION_ID) {
trigger_error($errorToTrigger, \E_USER_WARNING);
return false;
}
throw new \ValueError($errorToTrigger);
}
$paddingRequired = $length - mb_strlen($string, $encoding);
if ($paddingRequired < 1) {
return $string;
}
switch ($pad_type) {
case \STR_PAD_LEFT:
return mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string;
case \STR_PAD_RIGHT:
return $string.mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding);
default:
$leftPaddingLength = floor($paddingRequired / 2);
$rightPaddingLength = $paddingRequired - $leftPaddingLength;
return mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding);
}
}
public static function str_increment(string $string): string
{
if ('' === $string) {
throw new \ValueError('str_increment(): Argument #1 ($string) cannot be empty');
}
if (!preg_match('/^[a-zA-Z0-9]+$/', $string)) {
throw new \ValueError('str_increment(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters');
}
for ($i = \strlen($string) - 1; $i >= 0; --$i) {
$char = $string[$i];
if ('z' === $char) {
$string[$i] = 'a';
continue;
}
if ('Z' === $char) {
$string[$i] = 'A';
continue;
}
if ('9' === $char) {
$string[$i] = '0';
continue;
}
$string[$i] = \chr(\ord($char) + 1);
return $string;
}
switch ($string[0]) {
case 'a': return 'a'.$string;
case 'A': return 'A'.$string;
}
return '1'.$string;
}
public static function str_decrement(string $string): string
{
if ('' === $string) {
throw new \ValueError('str_decrement(): Argument #1 ($string) cannot be empty');
}
if (!preg_match('/^[a-zA-Z0-9]+$/', $string)) {
throw new \ValueError('str_decrement(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters');
}
if (preg_match('/\A(?:0[aA0]?|[aA])\z/', $string)) {
throw new \ValueError(\sprintf('str_decrement(): Argument #1 ($string) "%s" is out of decrement range', $string));
}
if (!\in_array(substr($string, -1), ['A', 'a', '0'], true)) {
return implode('', \array_slice(str_split($string), 0, -1)).\chr(\ord(substr($string, -1)) - 1);
}
$carry = '';
$decremented = '';
for ($i = \strlen($string) - 1; $i >= 0; --$i) {
$char = $string[$i];
switch ($char) {
case 'A':
if ('' !== $carry) {
$decremented = $carry.$decremented;
$carry = '';
}
$carry = 'Z';
break;
case 'a':
if ('' !== $carry) {
$decremented = $carry.$decremented;
$carry = '';
}
$carry = 'z';
break;
case '0':
if ('' !== $carry) {
$decremented = $carry.$decremented;
$carry = '';
}
$carry = '9';
break;
case '1':
if ('' !== $carry) {
$decremented = $carry.$decremented;
$carry = '';
}
break;
default:
if ('' !== $carry) {
$decremented = $carry.$decremented;
$carry = '';
}
if (!\in_array($char, ['A', 'a', '0'], true)) {
$decremented = \chr(\ord($char) - 1).$decremented;
}
}
}
return $decremented;
}
}
+22
View File
@@ -0,0 +1,22 @@
Symfony Polyfill / Php83
========================
This component provides features added to PHP 8.3 core:
- [`json_validate`](https://wiki.php.net/rfc/json_validate)
- [`Override`](https://wiki.php.net/rfc/marking_overriden_methods)
- [`mb_str_pad`](https://wiki.php.net/rfc/mb_str_pad)
- [`ldap_exop_sync`](https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures)
- [`ldap_connect_wallet`](https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures)
- [`stream_context_set_options`](https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures)
- [`str_increment` and `str_decrement`](https://wiki.php.net/rfc/saner-inc-dec-operators)
- [`Date*Exception/Error classes`](https://wiki.php.net/rfc/datetime-exceptions)
- [`SQLite3Exception`](https://wiki.php.net/rfc/sqlite3_exceptions)
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateError extends Error
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateException extends Exception
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateInvalidOperationException extends DateException
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateInvalidTimeZoneException extends DateException
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateMalformedIntervalStringException extends DateException
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateMalformedPeriodStringException extends DateException
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateMalformedStringException extends DateException
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateObjectError extends DateError
{
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class DateRangeError extends DateError
{
}
}
@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
#[Attribute(Attribute::TARGET_METHOD)]
final class Override
{
public function __construct()
{
}
}
}
@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80300) {
class SQLite3Exception extends Exception
{
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php83 as p;
if (\PHP_VERSION_ID >= 80300) {
return;
}
if (!function_exists('json_validate')) {
function json_validate(string $json, int $depth = 512, int $flags = 0): bool { return p\Php83::json_validate($json, $depth, $flags); }
}
if (!function_exists('stream_context_set_options')) {
function stream_context_set_options($context, array $options): bool { return stream_context_set_option($context, $options); }
}
if (!function_exists('str_increment')) {
function str_increment(string $string): string { return p\Php83::str_increment($string); }
}
if (!function_exists('str_decrement')) {
function str_decrement(string $string): string { return p\Php83::str_decrement($string); }
}
if (\PHP_VERSION_ID < 80000) {
require __DIR__.'/bootstrap72.php';
}
if (extension_loaded('mbstring')) {
if (!function_exists('mb_str_pad')) {
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Php83::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
}
if (\PHP_VERSION_ID >= 80100) {
return require __DIR__.'/bootstrap81.php';
}
if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) {
function ldap_exop_sync($ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $response_data, $response_oid); }
}
if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) {
function ldap_connect_wallet(?string $uri, string $wallet, string $password, int $auth_mode = \GSLC_SSL_NO_AUTH) { return ldap_connect($uri, $wallet, $password, $auth_mode); }
}
+19
View File
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php83 as p;
if (extension_loaded('mbstring')) {
if (!function_exists('mb_str_pad')) {
/** @return string|false */
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null) { return p\Php83::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php83 as p;
if (\PHP_VERSION_ID >= 80300) {
return;
}
if (!function_exists('json_validate')) {
function json_validate(string $json, int $depth = 512, int $flags = 0): bool { return p\Php83::json_validate($json, $depth, $flags); }
}
if (!function_exists('str_increment')) {
function str_increment(string $string): string { return p\Php83::str_increment($string); }
}
if (!function_exists('str_decrement')) {
function str_decrement(string $string): string { return p\Php83::str_decrement($string); }
}
if (extension_loaded('mbstring') && !function_exists('mb_str_pad')) {
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Php83::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) {
function ldap_exop_sync(\LDAP\Connection $ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $response_data, $response_oid); }
}
if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) {
function ldap_connect_wallet(?string $uri, string $wallet, #[\SensitiveParameter] string $password, int $auth_mode = \GSLC_SSL_NO_AUTH): \LDAP\Connection|false { return ldap_connect($uri, $wallet, $password, $auth_mode); }
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "symfony/polyfill-php83",
"type": "library",
"description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
"keywords": ["polyfill", "shim", "compatibility", "portable"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Php83\\": "" },
"files": [ "bootstrap.php" ],
"classmap": [ "Resources/stubs" ]
},
"minimum-stability": "dev",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
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.
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation;
interface LocaleAwareInterface
{
/**
* Sets the current locale.
*
* @return void
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function setLocale(string $locale);
/**
* Returns the current locale.
*/
public function getLocale(): string;
}
+9
View File
@@ -0,0 +1,9 @@
Symfony Translation Contracts
=============================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful and
that already have battle tested implementations.
See https://github.com/symfony/contracts/blob/main/README.md for more information.
@@ -0,0 +1,401 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation\Test;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
/**
* Test should cover all languages mentioned on http://translate.sourceforge.net/wiki/l10n/pluralforms
* and Plural forms mentioned on http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms.
*
* See also https://developer.mozilla.org/en/Localization_and_Plurals which mentions 15 rules having a maximum of 6 forms.
* The mozilla code is also interesting to check for.
*
* As mentioned by chx http://drupal.org/node/1273968 we can cover all by testing number from 0 to 199
*
* The goal to cover all languages is too far fetched so this test case is smaller.
*
* @author Clemens Tolboom clemens@build2be.nl
*/
class TranslatorTest extends TestCase
{
private string $defaultLocale;
protected function setUp(): void
{
$this->defaultLocale = \Locale::getDefault();
\Locale::setDefault('en');
}
protected function tearDown(): void
{
\Locale::setDefault($this->defaultLocale);
}
public function getTranslator(): TranslatorInterface
{
return new class implements TranslatorInterface {
use TranslatorTrait;
};
}
/**
* @dataProvider getTransTests
*/
#[DataProvider('getTransTests')]
public function testTrans($expected, $id, $parameters)
{
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->trans($id, $parameters));
}
/**
* @dataProvider getTransChoiceTests
*/
#[DataProvider('getTransChoiceTests')]
public function testTransChoiceWithExplicitLocale($expected, $id, $number)
{
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
}
/**
* @requires extension intl
*
* @dataProvider getTransChoiceTests
*/
#[DataProvider('getTransChoiceTests')]
#[RequiresPhpExtension('intl')]
public function testTransChoiceWithDefaultLocale($expected, $id, $number)
{
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
}
/**
* @dataProvider getTransChoiceTests
*/
#[DataProvider('getTransChoiceTests')]
public function testTransChoiceWithEnUsPosix($expected, $id, $number)
{
$translator = $this->getTranslator();
$translator->setLocale('en_US_POSIX');
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
}
public function testGetSetLocale()
{
$translator = $this->getTranslator();
$this->assertEquals('en', $translator->getLocale());
}
/**
* @requires extension intl
*/
#[RequiresPhpExtension('intl')]
public function testGetLocaleReturnsDefaultLocaleIfNotSet()
{
$translator = $this->getTranslator();
\Locale::setDefault('pt_BR');
$this->assertEquals('pt_BR', $translator->getLocale());
\Locale::setDefault('en');
$this->assertEquals('en', $translator->getLocale());
}
public static function getTransTests()
{
yield ['Symfony is great!', 'Symfony is great!', []];
yield ['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']];
if (class_exists(TranslatableMessage::class)) {
yield ['He said "Symfony is awesome!".', 'He said "%what%".', ['%what%' => new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'])]];
}
}
public static function getTransChoiceTests()
{
return [
['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1],
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
['There are 0 apples', 'There is 1 apple|There are %count% apples', 0],
['There is 1 apple', 'There is 1 apple|There are %count% apples', 1],
['There are 10 apples', 'There is 1 apple|There are %count% apples', 10],
// custom validation messages may be coded with a fixed value
['There are 2 apples', 'There are 2 apples', 2],
];
}
/**
* @dataProvider getInterval
*/
#[DataProvider('getInterval')]
public function testInterval($expected, $number, $interval)
{
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->trans($interval.' foo|[1,Inf[ bar', ['%count%' => $number]));
}
public static function getInterval()
{
return [
['foo', 3, '{1,2, 3 ,4}'],
['bar', 10, '{1,2, 3 ,4}'],
['bar', 3, '[1,2]'],
['foo', 1, '[1,2]'],
['foo', 2, '[1,2]'],
['bar', 1, ']1,2['],
['bar', 2, ']1,2['],
['foo', log(0), '[-Inf,2['],
['foo', -log(0), '[-2,+Inf]'],
];
}
/**
* @dataProvider getChooseTests
*/
#[DataProvider('getChooseTests')]
public function testChoose($expected, $id, $number, $locale = null)
{
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number], null, $locale));
}
public function testReturnMessageIfExactlyOneStandardRuleIsGiven()
{
$translator = $this->getTranslator();
$this->assertEquals('There are two apples', $translator->trans('There are two apples', ['%count%' => 2]));
}
/**
* @dataProvider getNonMatchingMessages
*/
#[DataProvider('getNonMatchingMessages')]
public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number)
{
$translator = $this->getTranslator();
$this->expectException(\InvalidArgumentException::class);
$translator->trans($id, ['%count%' => $number]);
}
public static function getNonMatchingMessages()
{
return [
['{0} There are no apples|{1} There is one apple', 2],
['{1} There is one apple|]1,Inf] There are %count% apples', 0],
['{1} There is one apple|]2,Inf] There are %count% apples', 2],
['{0} There are no apples|There is one apple', 2],
];
}
public static function getChooseTests()
{
return [
['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
['There are no apples', '{0}There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1],
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10],
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
['There are 0 apples', 'There is one apple|There are %count% apples', 0],
['There is one apple', 'There is one apple|There are %count% apples', 1],
['There are 10 apples', 'There is one apple|There are %count% apples', 10],
['There are 0 apples', 'one: There is one apple|more: There are %count% apples', 0],
['There is one apple', 'one: There is one apple|more: There are %count% apples', 1],
['There are 10 apples', 'one: There is one apple|more: There are %count% apples', 10],
['There are no apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 0],
['There is one apple', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 1],
['There are 10 apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 10],
['', '{0}|{1} There is one apple|]1,Inf] There are %count% apples', 0],
['', '{0} There are no apples|{1}|]1,Inf] There are %count% apples', 1],
// Indexed only tests which are Gettext PoFile* compatible strings.
['There are 0 apples', 'There is one apple|There are %count% apples', 0],
['There is one apple', 'There is one apple|There are %count% apples', 1],
['There are 2 apples', 'There is one apple|There are %count% apples', 2],
// Tests for float numbers
['There is almost one apple', '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7],
['There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1],
['There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7],
['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0],
['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0],
['There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0],
// Test texts with new-lines
// with double-quotes and \n in id & double-quotes and actual newlines in text
["This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a
new-line in it. Selector = 0.|{1}This is a text with a
new-line in it. Selector = 1.|[1,Inf]This is a text with a
new-line in it. Selector > 1.', 0],
// with double-quotes and \n in id and single-quotes and actual newlines in text
["This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a
new-line in it. Selector = 0.|{1}This is a text with a
new-line in it. Selector = 1.|[1,Inf]This is a text with a
new-line in it. Selector > 1.', 1],
["This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a
new-line in it. Selector = 0.|{1}This is a text with a
new-line in it. Selector = 1.|[1,Inf]This is a text with a
new-line in it. Selector > 1.', 5],
// with double-quotes and id split across lines
['This is a text with a
new-line in it. Selector = 1.', '{0}This is a text with a
new-line in it. Selector = 0.|{1}This is a text with a
new-line in it. Selector = 1.|[1,Inf]This is a text with a
new-line in it. Selector > 1.', 1],
// with single-quotes and id split across lines
['This is a text with a
new-line in it. Selector > 1.', '{0}This is a text with a
new-line in it. Selector = 0.|{1}This is a text with a
new-line in it. Selector = 1.|[1,Inf]This is a text with a
new-line in it. Selector > 1.', 5],
// with single-quotes and \n in text
['This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0],
// with double-quotes and id split across lines
["This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1],
// escape pipe
['This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0],
// Empty plural set (2 plural forms) from a .PO file
['', '|', 1],
// Empty plural set (3 plural forms) from a .PO file
['', '||', 1],
// Floating values
['1.5 liters', '%count% liter|%count% liters', 1.5],
['1.5 litre', '%count% litre|%count% litres', 1.5, 'fr'],
// Negative values
['-1 degree', '%count% degree|%count% degrees', -1],
['-1 degré', '%count% degré|%count% degrés', -1],
['-1.5 degrees', '%count% degree|%count% degrees', -1.5],
['-1.5 degré', '%count% degré|%count% degrés', -1.5, 'fr'],
['-2 degrees', '%count% degree|%count% degrees', -2],
['-2 degrés', '%count% degré|%count% degrés', -2],
];
}
/**
* @dataProvider failingLangcodes
*/
#[DataProvider('failingLangcodes')]
public function testFailedLangcodes($nplural, $langCodes)
{
$matrix = $this->generateTestData($langCodes);
$this->validateMatrix($nplural, $matrix, false);
}
/**
* @dataProvider successLangcodes
*/
#[DataProvider('successLangcodes')]
public function testLangcodes($nplural, $langCodes)
{
$matrix = $this->generateTestData($langCodes);
$this->validateMatrix($nplural, $matrix);
}
/**
* This array should contain all currently known langcodes.
*
* As it is impossible to have this ever complete we should try as hard as possible to have it almost complete.
*/
public static function successLangcodes(): array
{
return [
['1', ['ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky']],
['2', ['nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM', 'en_US_POSIX']],
['3', ['be', 'bs', 'cs', 'hr']],
['4', ['cy', 'mt', 'sl']],
['6', ['ar']],
];
}
/**
* This array should be at least empty within the near future.
*
* This both depends on a complete list trying to add above as understanding
* the plural rules of the current failing languages.
*
* @return array With nplural together with langcodes
*/
public static function failingLangcodes(): array
{
return [
['1', ['fa']],
['2', ['jbo']],
['3', ['cbs']],
['4', ['gd', 'kw']],
['5', ['ga']],
];
}
/**
* We validate only on the plural coverage. Thus the real rules is not tested.
*
* @param string $nplural Plural expected
* @param array $matrix Containing langcodes and their plural index values
*/
protected function validateMatrix(string $nplural, array $matrix, bool $expectSuccess = true)
{
foreach ($matrix as $langCode => $data) {
$indexes = array_flip($data);
if ($expectSuccess) {
$this->assertCount($nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms.");
} else {
$this->assertNotCount($nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms.");
}
}
}
protected function generateTestData($langCodes)
{
$translator = new class {
use TranslatorTrait {
getPluralizationRule as public;
}
};
$matrix = [];
foreach ($langCodes as $langCode) {
for ($count = 0; $count < 200; ++$count) {
$plural = $translator->getPluralizationRule($count, $langCode);
$matrix[$langCode][$count] = $plural;
}
}
return $matrix;
}
}
@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface TranslatableInterface
{
public function trans(TranslatorInterface $translator, ?string $locale = null): string;
}
@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TranslatorInterface
{
/**
* Translates the given message.
*
* When a number is provided as a parameter named "%count%", the message is parsed for plural
* forms and a translation is chosen according to this number using the following rules:
*
* Given a message with different plural translations separated by a
* pipe (|), this method returns the correct portion of the message based
* on the given number, locale and the pluralization rules in the message
* itself.
*
* The message supports two different types of pluralization rules:
*
* interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
* indexed: There is one apple|There are %count% apples
*
* The indexed solution can also contain labels (e.g. one: There is one apple).
* This is purely for making the translations more clear - it does not
* affect the functionality.
*
* The two methods can also be mixed:
* {0} There are no apples|one: There is one apple|more: There are %count% apples
*
* An interval can represent a finite set of numbers:
* {1,2,3,4}
*
* An interval can represent numbers between two numbers:
* [1, +Inf]
* ]-1,2[
*
* The left delimiter can be [ (inclusive) or ] (exclusive).
* The right delimiter can be [ (exclusive) or ] (inclusive).
* Beside numbers, you can use -Inf and +Inf for the infinite.
*
* @see https://en.wikipedia.org/wiki/ISO_31-11
*
* @param string $id The message id (may also be an object that can be cast to string)
* @param array $parameters An array of parameters for the message
* @param string|null $domain The domain for the message or null to use the default
* @param string|null $locale The locale or null to use the default
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string;
/**
* Returns the default locale.
*/
public function getLocale(): string;
}
+231
View File
@@ -0,0 +1,231 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
/**
* A trait to help implement TranslatorInterface and LocaleAwareInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
trait TranslatorTrait
{
private ?string $locale = null;
/**
* @return void
*/
public function setLocale(string $locale)
{
$this->locale = $locale;
}
public function getLocale(): string
{
return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en');
}
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
if (null === $id || '' === $id) {
return '';
}
foreach ($parameters as $k => $v) {
if ($v instanceof TranslatableInterface) {
$parameters[$k] = $v->trans($this, $locale);
}
}
if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) {
return strtr($id, $parameters);
}
$number = (float) $parameters['%count%'];
$locale = $locale ?: $this->getLocale();
$parts = [];
if (preg_match('/^\|++$/', $id)) {
$parts = explode('|', $id);
} elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) {
$parts = $matches[0];
}
$intervalRegexp = <<<'EOF'
/^(?P<interval>
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
|
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
)\s*(?P<message>.*?)$/xs
EOF;
$standardRules = [];
foreach ($parts as $part) {
$part = trim(str_replace('||', '|', $part));
// try to match an explicit rule, then fallback to the standard ones
if (preg_match($intervalRegexp, $part, $matches)) {
if ($matches[2]) {
foreach (explode(',', $matches[3]) as $n) {
if ($number == $n) {
return strtr($matches['message'], $parameters);
}
}
} else {
$leftNumber = '-Inf' === $matches['left'] ? -\INF : (float) $matches['left'];
$rightNumber = is_numeric($matches['right']) ? (float) $matches['right'] : \INF;
if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber)
&& (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber)
) {
return strtr($matches['message'], $parameters);
}
}
} elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) {
$standardRules[] = $matches[1];
} else {
$standardRules[] = $part;
}
}
$position = $this->getPluralizationRule($number, $locale);
if (!isset($standardRules[$position])) {
// when there's exactly one rule given, and that rule is a standard
// rule, use this rule
if (1 === \count($parts) && isset($standardRules[0])) {
return strtr($standardRules[0], $parameters);
}
$message = \sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number);
if (class_exists(InvalidArgumentException::class)) {
throw new InvalidArgumentException($message);
}
throw new \InvalidArgumentException($message);
}
return strtr($standardRules[$position], $parameters);
}
/**
* Returns the plural position to use for the given locale and number.
*
* The plural rules are derived from code of the Zend Framework (2010-09-25),
* which is subject to the new BSD license (http://framework.zend.com/license/new-bsd).
* Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
*/
private function getPluralizationRule(float $number, string $locale): int
{
$number = abs($number);
return match ('pt_BR' !== $locale && 'en_US_POSIX' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) {
'af',
'bn',
'bg',
'ca',
'da',
'de',
'el',
'en',
'en_US_POSIX',
'eo',
'es',
'et',
'eu',
'fa',
'fi',
'fo',
'fur',
'fy',
'gl',
'gu',
'ha',
'he',
'hu',
'is',
'it',
'ku',
'lb',
'ml',
'mn',
'mr',
'nah',
'nb',
'ne',
'nl',
'nn',
'no',
'oc',
'om',
'or',
'pa',
'pap',
'ps',
'pt',
'so',
'sq',
'sv',
'sw',
'ta',
'te',
'tk',
'ur',
'zu' => (1 == $number) ? 0 : 1,
'am',
'bh',
'fil',
'fr',
'gun',
'hi',
'hy',
'ln',
'mg',
'nso',
'pt_BR',
'ti',
'wa' => ($number < 2) ? 0 : 1,
'be',
'bs',
'hr',
'ru',
'sh',
'sr',
'uk' => ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2),
'cs',
'sk' => (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2),
'ga' => (1 == $number) ? 0 : ((2 == $number) ? 1 : 2),
'lt' => ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2),
'sl' => (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)),
'mk' => (1 == $number % 10) ? 0 : 1,
'mt' => (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)),
'lv' => (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2),
'pl' => (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2),
'cy' => (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)),
'ro' => (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2),
'ar' => (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))),
default => 0,
};
}
}
+37
View File
@@ -0,0 +1,37 @@
{
"name": "symfony/translation-contracts",
"type": "library",
"description": "Generic abstractions related to translation",
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1"
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\Translation\\": "" },
"exclude-from-classmap": [
"/Test/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.7-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}
+242
View File
@@ -0,0 +1,242 @@
CHANGELOG
=========
7.4
---
* Make the extractor alias optional
* Deprecate `TranslatableMessage::__toString`
* Add `Symfony\Component\Translation\StaticMessage`
7.3
---
* Add `Translator::addGlobalParameter()` to allow defining global translation parameters
7.2
---
* Deprecate `ProviderFactoryTestCase`, extend `AbstractProviderFactoryTestCase` instead
The `testIncompleteDsnException()` test is no longer provided by default. If you make use of it by implementing the `incompleteDsnProvider()` data providers,
you now need to use the `IncompleteDsnTestTrait`.
* Make `ProviderFactoryTestCase` and `ProviderTestCase` compatible with PHPUnit 10+
* Add `lint:translations` command
* Deprecate passing an escape character to `CsvFileLoader::setCsvControl()`
* Make Xliff 2.0 attributes in segment element available as `segment-attributes`
metadata returned by `XliffFileLoader` and make `XliffFileDumper` write them to the file
7.1
---
* Mark class `DataCollectorTranslator` as `final`
7.0
---
* Remove `PhpStringTokenParser`
* Remove `PhpExtractor` in favor of `PhpAstExtractor`
6.4
---
* Give current locale to `LocaleSwitcher::runWithLocale()`'s callback
* Add `--as-tree` option to `translation:pull` command to write YAML messages as a tree-like structure
* [BC BREAK] Add argument `$buildDir` to `DataCollectorTranslator::warmUp()`
* Add `DataCollectorTranslatorPass` and `LoggingTranslatorPass` (moved from `FrameworkBundle`)
* Add `PhraseTranslationProvider`
6.2.7
-----
* [BC BREAK] The following data providers for `ProviderFactoryTestCase` are now static:
`supportsProvider()`, `createProvider()`, `unsupportedSchemeProvider()`and `incompleteDsnProvider()`
* [BC BREAK] `ProviderTestCase::toStringProvider()` is now static
6.2
---
* Deprecate `PhpStringTokenParser`
* Deprecate `PhpExtractor` in favor of `PhpAstExtractor`
* Add `PhpAstExtractor` (requires [nikic/php-parser](https://github.com/nikic/php-parser) to be installed)
6.1
---
* Parameters implementing `TranslatableInterface` are processed
* Add the file extension to the `XliffFileDumper` constructor
5.4
---
* Add `github` format & autodetection to render errors as annotations when
running the XLIFF linter command in a Github Actions environment.
* Translation providers are not experimental anymore
5.3
---
* Add `translation:pull` and `translation:push` commands to manage translations with third-party providers
* Add `TranslatorBagInterface::getCatalogues` method
* Add support to load XLIFF string in `XliffFileLoader`
5.2.0
-----
* added support for calling `trans` with ICU formatted messages
* added `PseudoLocalizationTranslator`
* added `TranslatableMessage` objects that represent a message that can be translated
* added the `t()` function to easily create `TranslatableMessage` objects
* Added support for extracting messages from `TranslatableMessage` objects
5.1.0
-----
* added support for `name` attribute on `unit` element from xliff2 to be used as a translation key instead of always the `source` element
5.0.0
-----
* removed support for using `null` as the locale in `Translator`
* removed `TranslatorInterface`
* removed `MessageSelector`
* removed `ChoiceMessageFormatterInterface`
* removed `PluralizationRule`
* removed `Interval`
* removed `transChoice()` methods, use the trans() method instead with a %count% parameter
* removed `FileDumper::setBackup()` and `TranslationWriter::disableBackup()`
* removed `MessageFormatter::choiceFormat()`
* added argument `$filename` to `PhpExtractor::parseTokens()`
* removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit.
4.4.0
-----
* deprecated support for using `null` as the locale in `Translator`
* deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit.
* Marked the `TranslationDataCollector` class as `@final`.
4.3.0
-----
* Improved Xliff 1.2 loader to load the original file's metadata
* Added `TranslatorPathsPass`
4.2.0
-----
* Started using ICU parent locales as fallback locales.
* allow using the ICU message format using domains with the "+intl-icu" suffix
* deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter
* deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface`
* deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead
* Added `IntlFormatter` and `IntlFormatterInterface`
* added support for multiple files and directories in `XliffLintCommand`
* Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal
4.1.0
-----
* The `FileDumper::setBackup()` method is deprecated.
* The `TranslationWriter::disableBackup()` method is deprecated.
* The `XliffFileDumper` will write "name" on the "unit" node when dumping XLIFF 2.0.
4.0.0
-----
* removed the backup feature of the `FileDumper` class
* removed `TranslationWriter::writeTranslations()` method
* removed support for passing `MessageSelector` instances to the constructor of the `Translator` class
3.4.0
-----
* Added `TranslationDumperPass`
* Added `TranslationExtractorPass`
* Added `TranslatorPass`
* Added `TranslationReader` and `TranslationReaderInterface`
* Added `<notes>` section to the Xliff 2.0 dumper.
* Improved Xliff 2.0 loader to load `<notes>` section.
* Added `TranslationWriterInterface`
* Deprecated `TranslationWriter::writeTranslations` in favor of `TranslationWriter::write`
* added support for adding custom message formatter and decoupling the default one.
* Added `PhpExtractor`
* Added `PhpStringTokenParser`
3.2.0
-----
* Added support for escaping `|` in plural translations with double pipe.
3.1.0
-----
* Deprecated the backup feature of the file dumper classes.
3.0.0
-----
* removed `FileDumper::format()` method.
* Changed the visibility of the locale property in `Translator` from protected to private.
2.8.0
-----
* deprecated FileDumper::format(), overwrite FileDumper::formatCatalogue() instead.
* deprecated Translator::getMessages(), rely on TranslatorBagInterface::getCatalogue() instead.
* added `FileDumper::formatCatalogue` which allows format the catalogue without dumping it into file.
* added option `json_encoding` to JsonFileDumper
* added options `as_tree`, `inline` to YamlFileDumper
* added support for XLIFF 2.0.
* added support for XLIFF target and tool attributes.
* added message parameters to DataCollectorTranslator.
* [DEPRECATION] The `DiffOperation` class has been deprecated and
will be removed in Symfony 3.0, since its operation has nothing to do with 'diff',
so the class name is misleading. The `TargetOperation` class should be used for
this use-case instead.
2.7.0
-----
* added DataCollectorTranslator for collecting the translated messages.
2.6.0
-----
* added possibility to cache catalogues
* added TranslatorBagInterface
* added LoggingTranslator
* added Translator::getMessages() for retrieving the message catalogue as an array
2.5.0
-----
* added relative file path template to the file dumpers
* added optional backup to the file dumpers
* changed IcuResFileDumper to extend FileDumper
2.3.0
-----
* added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues)
* added Translator::getFallbackLocales()
* deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method
2.2.0
-----
* QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3.
* [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now
throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found
and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid.
* changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException
(IcuDatFileLoader, IcuResFileLoader and QtFileLoader)
2.1.0
-----
* added support for more than one fallback locale
* added support for extracting translation messages from templates (Twig and PHP)
* added dumpers for translation catalogs
* added support for QT, gettext, and ResourceBundles
@@ -0,0 +1,183 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\LogicException;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Base catalogues binary operation class.
*
* A catalogue binary operation performs operation on
* source (the left argument) and target (the right argument) catalogues.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
abstract class AbstractOperation implements OperationInterface
{
public const OBSOLETE_BATCH = 'obsolete';
public const NEW_BATCH = 'new';
public const ALL_BATCH = 'all';
protected MessageCatalogue $result;
/**
* This array stores 'all', 'new' and 'obsolete' messages for all valid domains.
*
* The data structure of this array is as follows:
*
* [
* 'domain 1' => [
* 'all' => [...],
* 'new' => [...],
* 'obsolete' => [...]
* ],
* 'domain 2' => [
* 'all' => [...],
* 'new' => [...],
* 'obsolete' => [...]
* ],
* ...
* ]
*
* @var array The array that stores 'all', 'new' and 'obsolete' messages
*/
protected array $messages;
private array $domains;
/**
* @throws LogicException
*/
public function __construct(
protected MessageCatalogueInterface $source,
protected MessageCatalogueInterface $target,
) {
if ($source->getLocale() !== $target->getLocale()) {
throw new LogicException('Operated catalogues must belong to the same locale.');
}
$this->result = new MessageCatalogue($source->getLocale());
$this->messages = [];
}
public function getDomains(): array
{
if (!isset($this->domains)) {
$domains = [];
foreach ([$this->source, $this->target] as $catalogue) {
foreach ($catalogue->getDomains() as $domain) {
$domains[$domain] = $domain;
if ($catalogue->all($domainIcu = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)) {
$domains[$domainIcu] = $domainIcu;
}
}
}
$this->domains = array_values($domains);
}
return $this->domains;
}
public function getMessages(string $domain): array
{
if (!\in_array($domain, $this->getDomains(), true)) {
throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::ALL_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain][self::ALL_BATCH];
}
public function getNewMessages(string $domain): array
{
if (!\in_array($domain, $this->getDomains(), true)) {
throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::NEW_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain][self::NEW_BATCH];
}
public function getObsoleteMessages(string $domain): array
{
if (!\in_array($domain, $this->getDomains(), true)) {
throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain][self::OBSOLETE_BATCH];
}
public function getResult(): MessageCatalogueInterface
{
foreach ($this->getDomains() as $domain) {
if (!isset($this->messages[$domain])) {
$this->processDomain($domain);
}
}
return $this->result;
}
/**
* @param self::*_BATCH $batch
*/
public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void
{
// If MessageFormatter class does not exists, intl domains are not supported.
if (!class_exists(\MessageFormatter::class)) {
return;
}
foreach ($this->getDomains() as $domain) {
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
$messages = match ($batch) {
self::OBSOLETE_BATCH => $this->getObsoleteMessages($domain),
self::NEW_BATCH => $this->getNewMessages($domain),
self::ALL_BATCH => $this->getMessages($domain),
default => throw new \InvalidArgumentException(\sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH)),
};
if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) {
continue;
}
$result = $this->getResult();
$allIntlMessages = $result->all($intlDomain);
$currentMessages = array_diff_key($messages, $result->all($domain));
$result->replace($currentMessages, $domain);
$result->replace($allIntlMessages + $messages, $intlDomain);
}
}
/**
* Performs operation on source and target catalogues for the given domain and
* stores the results.
*
* @param string $domain The domain which the operation will be performed for
*/
abstract protected function processDomain(string $domain): void;
}
+69
View File
@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Merge operation between two catalogues as follows:
* all = source target = {x: x ∈ source x ∈ target}
* new = all source = {x: x ∈ target ∧ x ∉ source}
* obsolete = source all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅
* Basically, the result contains messages from both catalogues.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class MergeOperation extends AbstractOperation
{
protected function processDomain(string $domain): void
{
$this->messages[$domain] = [
'all' => [],
'new' => [],
'obsolete' => [],
];
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
foreach ($this->target->getCatalogueMetadata('', $domain) ?? [] as $key => $value) {
if (null === $this->result->getCatalogueMetadata($key, $domain)) {
$this->result->setCatalogueMetadata($key, $value, $domain);
}
}
foreach ($this->target->getCatalogueMetadata('', $intlDomain) ?? [] as $key => $value) {
if (null === $this->result->getCatalogueMetadata($key, $intlDomain)) {
$this->result->setCatalogueMetadata($key, $value, $intlDomain);
}
}
foreach ($this->source->all($domain) as $id => $message) {
$this->messages[$domain]['all'][$id] = $message;
$d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
}
foreach ($this->target->all($domain) as $id => $message) {
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
}
}
}
}
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Represents an operation on catalogue(s).
*
* An instance of this interface performs an operation on one or more catalogues and
* stores intermediate and final results of the operation.
*
* The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and
* the following results are stored:
*
* Messages: also called 'all', are valid messages for the given domain after the operation is performed.
*
* New Messages: also called 'new' (new = all source = {x: x ∈ all ∧ x ∉ source}).
*
* Obsolete Messages: also called 'obsolete' (obsolete = source all = {x: x ∈ source ∧ x ∉ all}).
*
* Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
interface OperationInterface
{
/**
* Returns domains affected by operation.
*/
public function getDomains(): array;
/**
* Returns all valid messages ('all') after operation.
*/
public function getMessages(string $domain): array;
/**
* Returns new messages ('new') after operation.
*/
public function getNewMessages(string $domain): array;
/**
* Returns obsolete messages ('obsolete') after operation.
*/
public function getObsoleteMessages(string $domain): array;
/**
* Returns resulting catalogue ('result').
*/
public function getResult(): MessageCatalogueInterface;
}
@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Target operation between two catalogues:
* intersection = source ∩ target = {x: x ∈ source ∧ x ∈ target}
* all = intersection (target intersection) = target
* new = all source = {x: x ∈ target ∧ x ∉ source}
* obsolete = source all = source target = {x: x ∈ source ∧ x ∉ target}
* Basically, the result contains messages from the target catalogue.
*
* @author Michael Lee <michael.lee@zerustech.com>
*/
class TargetOperation extends AbstractOperation
{
protected function processDomain(string $domain): void
{
$this->messages[$domain] = [
'all' => [],
'new' => [],
'obsolete' => [],
];
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
foreach ($this->target->getCatalogueMetadata('', $domain) ?? [] as $key => $value) {
if (null === $this->result->getCatalogueMetadata($key, $domain)) {
$this->result->setCatalogueMetadata($key, $value, $domain);
}
}
foreach ($this->target->getCatalogueMetadata('', $intlDomain) ?? [] as $key => $value) {
if (null === $this->result->getCatalogueMetadata($key, $intlDomain)) {
$this->result->setCatalogueMetadata($key, $value, $intlDomain);
}
}
// For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``,
// because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
//
// For 'new' messages, the code can't be simplified as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));``
// because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback}
//
// For 'obsolete' messages, the code can't be simplified as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))``
// because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
foreach ($this->source->all($domain) as $id => $message) {
if ($this->target->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
} else {
$this->messages[$domain]['obsolete'][$id] = $message;
}
}
foreach ($this->target->all($domain) as $id => $message) {
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
}
}
}
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
/**
* This interface is used to get, set, and delete metadata about the Catalogue.
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
interface CatalogueMetadataAwareInterface
{
/**
* Gets catalogue metadata for the given domain and key.
*
* Passing an empty domain will return an array with all catalogue metadata indexed by
* domain and then by key. Passing an empty key will return an array with all
* catalogue metadata for the given domain.
*
* @return mixed The value that was set or an array with the domains/keys or null
*/
public function getCatalogueMetadata(string $key = '', string $domain = 'messages'): mixed;
/**
* Adds catalogue metadata to a message domain.
*/
public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages'): void;
/**
* Deletes catalogue metadata for the given key and domain.
*
* Passing an empty domain will delete all catalogue metadata. Passing an empty key will
* delete all metadata for the given domain.
*/
public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages'): void;
}
@@ -0,0 +1,129 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Exception\ExceptionInterface;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Lint translations files syntax and outputs encountered errors.
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
#[AsCommand(name: 'lint:translations', description: 'Lint translations files syntax and outputs encountered errors')]
class TranslationLintCommand extends Command
{
private SymfonyStyle $io;
public function __construct(
private TranslatorInterface&TranslatorBagInterface $translator,
private array $enabledLocales = [],
) {
parent::__construct();
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('locale')) {
$suggestions->suggestValues($this->enabledLocales);
}
}
protected function configure(): void
{
$this
->setDefinition([
new InputOption('locale', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to lint.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command lint translations.
<info>php %command.full_name%</>
EOF
);
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$locales = $input->getOption('locale');
/** @var array<string, array<string, array<string, \Throwable>>> $errors */
$errors = [];
$domainsByLocales = [];
foreach ($locales as $locale) {
$messageCatalogue = $this->translator->getCatalogue($locale);
foreach ($domainsByLocales[$locale] = $messageCatalogue->getDomains() as $domain) {
foreach ($messageCatalogue->all($domain) as $id => $translation) {
try {
$this->translator->trans($id, [], $domain, $messageCatalogue->getLocale());
} catch (ExceptionInterface $e) {
$errors[$locale][$domain][$id] = $e;
}
}
}
}
if (!$domainsByLocales) {
$this->io->error('No translation files were found.');
return Command::SUCCESS;
}
$this->io->table(
['Locale', 'Domains', 'Valid?'],
array_map(
static fn (string $locale, array $domains) => [
$locale,
implode(', ', $domains),
!\array_key_exists($locale, $errors) ? '<info>Yes</>' : '<error>No</>',
],
array_keys($domainsByLocales),
$domainsByLocales
),
);
if ($errors) {
foreach ($errors as $locale => $domains) {
foreach ($domains as $domain => $domainsErrors) {
$this->io->section(\sprintf('Errors for locale "%s" and domain "%s"', $locale, $domain));
foreach ($domainsErrors as $id => $error) {
$this->io->text(\sprintf('Translation key "%s" is invalid:', $id));
$this->io->error($error->getMessage());
}
}
}
return Command::FAILURE;
}
$this->io->success('All translations are valid.');
return Command::SUCCESS;
}
}
@@ -0,0 +1,177 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Catalogue\TargetOperation;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
#[AsCommand(name: 'translation:pull', description: 'Pull translations from a given provider.')]
final class TranslationPullCommand extends Command
{
use TranslationTrait;
public function __construct(
private TranslationProviderCollection $providerCollection,
private TranslationWriterInterface $writer,
private TranslationReaderInterface $reader,
private string $defaultLocale,
private array $transPaths = [],
private array $enabledLocales = [],
) {
parent::__construct();
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('provider')) {
$suggestions->suggestValues($this->providerCollection->keys());
return;
}
if ($input->mustSuggestOptionValuesFor('domains')) {
$provider = $this->providerCollection->get($input->getArgument('provider'));
if (method_exists($provider, 'getDomains')) {
$suggestions->suggestValues($provider->getDomains());
}
return;
}
if ($input->mustSuggestOptionValuesFor('locales')) {
$suggestions->suggestValues($this->enabledLocales);
return;
}
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']);
}
}
protected function configure(): void
{
$keys = $this->providerCollection->keys();
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
$this
->setDefinition([
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format.', 'xlf12'),
new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pulls translations from the given provider. Only
new translations are pulled, existing ones are not overwritten.
You can overwrite existing translations (and remove the missing ones on local side) by using the <info>--force</> flag:
<info>php %command.full_name% --force provider</>
Full example:
<info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</>
This command pulls all translations associated with the <info>messages</> and <info>validators</> domains for the <info>en</> locale.
Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case.
Local translations for others domains and locales are ignored.
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$provider = $this->providerCollection->get($input->getArgument('provider'));
$force = $input->getOption('force');
$intlIcu = $input->getOption('intl-icu');
$locales = $input->getOption('locales') ?: $this->enabledLocales;
$domains = $input->getOption('domains');
$format = $input->getOption('format');
$asTree = (int) $input->getOption('as-tree');
$xliffVersion = '1.2';
if ($intlIcu && !$force) {
$io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.');
}
switch ($format) {
case 'xlf20': $xliffVersion = '2.0';
// no break
case 'xlf12': $format = 'xlf';
}
$writeOptions = [
'path' => end($this->transPaths),
'xliff_version' => $xliffVersion,
'default_locale' => $this->defaultLocale,
'as_tree' => (bool) $asTree,
'inline' => $asTree,
];
if (!$domains) {
$domains = $provider->getDomains();
}
$providerTranslations = $provider->read($domains, $locales);
if ($force) {
foreach ($providerTranslations->getCatalogues() as $catalogue) {
$operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue);
if ($intlIcu) {
$operation->moveMessagesToIntlDomainsIfPossible();
}
$this->writer->write($operation->getResult(), $format, $writeOptions);
}
$io->success(\sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
// Append pulled translations to local ones.
$localTranslations->addBag($providerTranslations->diff($localTranslations));
foreach ($localTranslations->getCatalogues() as $catalogue) {
$this->writer->write($catalogue, $format, $writeOptions);
}
$io->success(\sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
}
@@ -0,0 +1,177 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Provider\FilteringProvider;
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\TranslatorBag;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
#[AsCommand(name: 'translation:push', description: 'Push translations to a given provider.')]
final class TranslationPushCommand extends Command
{
use TranslationTrait;
public function __construct(
private TranslationProviderCollection $providers,
private TranslationReaderInterface $reader,
private array $transPaths = [],
private array $enabledLocales = [],
) {
parent::__construct();
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('provider')) {
$suggestions->suggestValues($this->providers->keys());
return;
}
if ($input->mustSuggestOptionValuesFor('domains')) {
$provider = $this->providers->get($input->getArgument('provider'));
if (method_exists($provider, 'getDomains')) {
$domains = $provider->getDomains();
$suggestions->suggestValues($domains);
}
return;
}
if ($input->mustSuggestOptionValuesFor('locales')) {
$suggestions->suggestValues($this->enabledLocales);
}
}
protected function configure(): void
{
$keys = $this->providers->keys();
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
$this
->setDefinition([
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pushes translations to the given provider. Only new
translations are pushed, existing ones are not overwritten.
You can overwrite existing translations by using the <info>--force</> flag:
<info>php %command.full_name% --force provider</>
You can delete provider translations which are not present locally by using the <info>--delete-missing</> flag:
<info>php %command.full_name% --delete-missing provider</>
Full example:
<info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</>
This command pushes all translations associated with the <info>messages</> and <info>validators</> domains for the <info>en</> locale.
Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
Provider translations for others domains and locales are ignored.
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$provider = $this->providers->get($input->getArgument('provider'));
if (!$this->enabledLocales) {
throw new InvalidArgumentException(\sprintf('You must define "framework.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME)));
}
$io = new SymfonyStyle($input, $output);
$domains = $input->getOption('domains');
$locales = $input->getOption('locales');
$force = $input->getOption('force');
$deleteMissing = $input->getOption('delete-missing');
if (!$domains && $provider instanceof FilteringProvider) {
$domains = $provider->getDomains();
}
// Reading local translations must be done after retrieving the domains from the provider
// in order to manage only translations from configured domains
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
if (!$domains) {
$domains = $this->getDomainsFromTranslatorBag($localTranslations);
}
if (!$deleteMissing && $force) {
$provider->write($localTranslations);
$io->success(\sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
$providerTranslations = $provider->read($domains, $locales);
if ($deleteMissing) {
$provider->delete($providerTranslations->diff($localTranslations));
$io->success(\sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
// Read provider translations again, after missing translations deletion,
// to avoid push freshly deleted translations.
$providerTranslations = $provider->read($domains, $locales);
}
$translationsToWrite = $localTranslations->diff($providerTranslations);
if ($force) {
$translationsToWrite->addBag($localTranslations->intersect($providerTranslations));
}
$provider->write($translationsToWrite);
$io->success(\sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array
{
$domains = [];
foreach ($translatorBag->getCatalogues() as $catalogue) {
$domains = array_merge($domains, $catalogue->getDomains());
}
return array_unique($domains);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\TranslatorBag;
/**
* @internal
*/
trait TranslationTrait
{
private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag
{
$bag = new TranslatorBag();
foreach ($locales as $locale) {
$catalogue = new MessageCatalogue($locale);
foreach ($transPaths as $path) {
$this->reader->read($path, $catalogue);
}
if ($domains) {
foreach ($domains as $domain) {
$bag->addCatalogue($this->filterCatalogue($catalogue, $domain));
}
} else {
$bag->addCatalogue($catalogue);
}
}
return $bag;
}
private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
{
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
// extract intl-icu messages only
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
if ($intlMessages = $catalogue->all($intlDomain)) {
$filteredCatalogue->add($intlMessages, $intlDomain);
}
// extract all messages and subtract intl-icu messages
if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
$filteredCatalogue->add($messages, $domain);
}
foreach ($catalogue->getResources() as $resource) {
$filteredCatalogue->addResource($resource);
}
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
foreach ($metadata as $k => $v) {
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
}
}
if ($metadata = $catalogue->getMetadata('', $domain)) {
foreach ($metadata as $k => $v) {
$filteredCatalogue->setMetadata($k, $v, $domain);
}
}
return $filteredCatalogue;
}
}
+288
View File
@@ -0,0 +1,288 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Util\XliffUtils;
/**
* Validates XLIFF files syntax and outputs encountered errors.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
#[AsCommand(name: 'lint:xliff', description: 'Lint an XLIFF file and outputs encountered errors')]
class XliffLintCommand extends Command
{
private string $format;
private bool $displayCorrectFiles;
private ?\Closure $directoryIteratorProvider;
private ?\Closure $isReadableProvider;
public function __construct(
?string $name = null,
?callable $directoryIteratorProvider = null,
?callable $isReadableProvider = null,
private bool $requireStrictFileNames = true,
) {
parent::__construct($name);
$this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...);
$this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...);
}
protected function configure(): void
{
$this
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())))
->setHelp(<<<EOF
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
You can validates XLIFF contents passed from STDIN:
<info>cat filename | php %command.full_name% -</info>
You can also validate the syntax of a file:
<info>php %command.full_name% filename</info>
Or of a whole directory:
<info>php %command.full_name% dirname</info>
The <info>--format</info> option specifies the format of the command output:
<info>php %command.full_name% dirname --format=json</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$filenames = (array) $input->getArgument('filename');
$this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt');
$this->displayCorrectFiles = $output->isVerbose();
if (['-'] === $filenames) {
return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]);
}
if (!$filenames) {
throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
}
$filesInfo = [];
foreach ($filenames as $filename) {
if (!$this->isReadable($filename)) {
throw new RuntimeException(\sprintf('File or directory "%s" is not readable.', $filename));
}
foreach ($this->getFiles($filename) as $file) {
$filesInfo[] = $this->validate(file_get_contents($file), $file);
}
}
return $this->display($io, $filesInfo);
}
private function validate(string $content, ?string $file = null): array
{
$errors = [];
// Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input
if ('' === trim($content)) {
return ['file' => $file, 'valid' => true];
}
$internal = libxml_use_internal_errors(true);
$document = new \DOMDocument();
$document->loadXML($content);
if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) {
$normalizedLocalePattern = \sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/'));
// strict file names require translation files to be named '____.locale.xlf'
// otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed
// also, the regexp matching must be case-insensitive, as defined for 'target-language' values
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
$expectedFilenamePattern = $this->requireStrictFileNames ? \sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : \sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
if (0 === preg_match($expectedFilenamePattern, basename($file))) {
$errors[] = [
'line' => -1,
'column' => -1,
'message' => \sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
];
}
}
foreach (XliffUtils::validateSchema($document) as $xmlError) {
$errors[] = [
'line' => $xmlError['line'],
'column' => $xmlError['column'],
'message' => $xmlError['message'],
];
}
libxml_clear_errors();
libxml_use_internal_errors($internal);
return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors];
}
private function display(SymfonyStyle $io, array $files): int
{
return match ($this->format) {
'txt' => $this->displayTxt($io, $files),
'json' => $this->displayJson($io, $files),
'github' => $this->displayTxt($io, $files, true),
default => throw new InvalidArgumentException(\sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
};
}
private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int
{
$countFiles = \count($filesInfo);
$erroredFiles = 0;
$githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null;
foreach ($filesInfo as $info) {
if ($info['valid'] && $this->displayCorrectFiles) {
$io->comment('<info>OK</info>'.($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
} elseif (!$info['valid']) {
++$erroredFiles;
$io->text('<error> ERROR </error>'.($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
$io->listing(array_map(static function ($error) use ($info, $githubReporter) {
// general document errors have a '-1' line number
$line = -1 === $error['line'] ? null : $error['line'];
$githubReporter?->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null);
return null === $line ? $error['message'] : \sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']);
}, $info['messages']));
}
}
if (0 === $erroredFiles) {
$io->success(\sprintf('All %d XLIFF files contain valid syntax.', $countFiles));
} else {
$io->warning(\sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
}
return min($erroredFiles, 1);
}
private function displayJson(SymfonyStyle $io, array $filesInfo): int
{
$errors = 0;
array_walk($filesInfo, static function (&$v) use (&$errors) {
$v['file'] = (string) $v['file'];
if (!$v['valid']) {
++$errors;
}
});
$io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
return min($errors, 1);
}
/**
* @return iterable<\SplFileInfo>
*/
private function getFiles(string $fileOrDirectory): iterable
{
if (is_file($fileOrDirectory)) {
yield new \SplFileInfo($fileOrDirectory);
return;
}
foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
if (!\in_array($file->getExtension(), ['xlf', 'xliff'], true)) {
continue;
}
yield $file;
}
}
/**
* @return iterable<\SplFileInfo>
*/
private function getDirectoryIterator(string $directory): iterable
{
$default = static fn ($directory) => new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
if (null !== $this->directoryIteratorProvider) {
return ($this->directoryIteratorProvider)($directory, $default);
}
return $default($directory);
}
private function isReadable(string $fileOrDirectory): bool
{
$default = static fn ($fileOrDirectory) => is_readable($fileOrDirectory);
if (null !== $this->isReadableProvider) {
return ($this->isReadableProvider)($fileOrDirectory, $default);
}
return $default($fileOrDirectory);
}
private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string
{
foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) {
if ('target-language' === $attribute->nodeName) {
return $attribute->nodeValue;
}
}
return null;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues($this->getAvailableFormatOptions());
}
}
/** @return string[] */
private function getAvailableFormatOptions(): array
{
return ['txt', 'json', 'github'];
}
}
@@ -0,0 +1,155 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Translation\DataCollectorTranslator;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @final
*/
class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface
{
public function __construct(
private DataCollectorTranslator $translator,
) {
}
public function lateCollect(): void
{
$messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages());
$this->data += $this->computeCount($messages);
$this->data['messages'] = $messages;
$this->data = $this->cloneVar($this->data);
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->data['locale'] = $this->translator->getLocale();
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
$this->data['global_parameters'] = $this->translator->getGlobalParameters();
}
public function reset(): void
{
$this->data = [];
}
public function getMessages(): array|Data
{
return $this->data['messages'] ?? [];
}
public function getCountMissings(): int
{
return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0;
}
public function getCountFallbacks(): int
{
return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0;
}
public function getCountDefines(): int
{
return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0;
}
public function getLocale(): ?string
{
return !empty($this->data['locale']) ? $this->data['locale'] : null;
}
/**
* @internal
*/
public function getFallbackLocales(): Data|array
{
return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : [];
}
/**
* @internal
*/
public function getGlobalParameters(): Data|array
{
return $this->data['global_parameters'] ?? [];
}
public function getName(): string
{
return 'translation';
}
private function sanitizeCollectedMessages(array $messages): array
{
$result = [];
foreach ($messages as $key => $message) {
$messageId = $message['locale'].$message['domain'].$message['id'];
if (!isset($result[$messageId])) {
$message['count'] = 1;
$message['parameters'] = !empty($message['parameters']) ? [$message['parameters']] : [];
$messages[$key]['translation'] = $this->sanitizeString($message['translation']);
$result[$messageId] = $message;
} else {
if (!empty($message['parameters'])) {
$result[$messageId]['parameters'][] = $message['parameters'];
}
++$result[$messageId]['count'];
}
unset($messages[$key]);
}
return $result;
}
private function computeCount(array $messages): array
{
$count = [
DataCollectorTranslator::MESSAGE_DEFINED => 0,
DataCollectorTranslator::MESSAGE_MISSING => 0,
DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0,
];
foreach ($messages as $message) {
++$count[$message['state']];
}
return $count;
}
private function sanitizeString(string $string, int $length = 80): string
{
$string = trim(preg_replace('/\s+/', ' ', $string));
if (false !== $encoding = mb_detect_encoding($string, null, true)) {
if (mb_strlen($string, $encoding) > $length) {
return mb_substr($string, 0, $length - 3, $encoding).'...';
}
} elseif (\strlen($string) > $length) {
return substr($string, 0, $length - 3).'...';
}
return $string;
}
}
+145
View File
@@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Contracts\Service\ResetInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @final since Symfony 7.1
*/
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface, ResetInterface
{
public const MESSAGE_DEFINED = 0;
public const MESSAGE_MISSING = 1;
public const MESSAGE_EQUALS_FALLBACK = 2;
private array $messages = [];
public function __construct(
private TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator,
) {
}
public function reset(): void
{
$this->messages = [];
}
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters);
return $trans;
}
public function setLocale(string $locale): void
{
$this->translator->setLocale($locale);
}
public function getLocale(): string
{
return $this->translator->getLocale();
}
public function getCatalogue(?string $locale = null): MessageCatalogueInterface
{
return $this->translator->getCatalogue($locale);
}
public function getCatalogues(): array
{
return $this->translator->getCatalogues();
}
public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
if ($this->translator instanceof WarmableInterface) {
return $this->translator->warmUp($cacheDir, $buildDir);
}
return [];
}
/**
* Gets the fallback locales.
*/
public function getFallbackLocales(): array
{
if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) {
return $this->translator->getFallbackLocales();
}
return [];
}
public function getGlobalParameters(): array
{
if ($this->translator instanceof Translator || method_exists($this->translator, 'getGlobalParameters')) {
return $this->translator->getGlobalParameters();
}
return [];
}
public function __call(string $method, array $args): mixed
{
return $this->translator->{$method}(...$args);
}
public function getCollectedMessages(): array
{
return $this->messages;
}
private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = []): void
{
$domain ??= 'messages';
$catalogue = $this->translator->getCatalogue($locale);
$locale = $catalogue->getLocale();
$fallbackLocale = null;
if ($catalogue->defines($id, $domain)) {
$state = self::MESSAGE_DEFINED;
} elseif ($catalogue->has($id, $domain)) {
$state = self::MESSAGE_EQUALS_FALLBACK;
$fallbackCatalogue = $catalogue->getFallbackCatalogue();
while ($fallbackCatalogue) {
if ($fallbackCatalogue->defines($id, $domain)) {
$fallbackLocale = $fallbackCatalogue->getLocale();
break;
}
$fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
}
} else {
$state = self::MESSAGE_MISSING;
}
$this->messages[] = [
'locale' => $locale,
'fallbackLocale' => $fallbackLocale,
'domain' => $domain,
'id' => $id,
'translation' => $translation,
'parameters' => $parameters,
'state' => $state,
'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null,
];
}
}
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Translation\TranslatorBagInterface;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class DataCollectorTranslatorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has('translator')) {
return;
}
$translatorClass = $container->getParameterBag()->resolveValue($container->findDefinition('translator')->getClass());
if (!is_subclass_of($translatorClass, TranslatorBagInterface::class)) {
$container->removeDefinition('translator.data_collector');
$container->removeDefinition('data_collector.translation');
}
}
}
@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class LoggingTranslatorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasAlias('logger') || !$container->hasAlias('translator')) {
return;
}
if (!$container->hasParameter('translator.logging') || !$container->getParameter('translator.logging')) {
return;
}
$translatorAlias = $container->getAlias('translator');
$definition = $container->getDefinition((string) $translatorAlias);
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $translatorAlias));
}
if (!$r->isSubclassOf(TranslatorInterface::class) || !$r->isSubclassOf(TranslatorBagInterface::class)) {
return;
}
$container->getDefinition('translator.logging')->setDecoratedService('translator');
$warmer = $container->getDefinition('translation.warmer');
$subscriberAttributes = $warmer->getTag('container.service_subscriber');
$warmer->clearTag('container.service_subscriber');
foreach ($subscriberAttributes as $k => $v) {
if ((!isset($v['id']) || 'translator' !== $v['id']) && (!isset($v['key']) || 'translator' !== $v['key'])) {
$warmer->addTag('container.service_subscriber', $v);
}
}
$warmer->addTag('container.service_subscriber', ['key' => 'translator', 'id' => 'translator.logging.inner']);
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds tagged translation.formatter services to translation writer.
*/
class TranslationDumperPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translation.writer')) {
return;
}
$definition = $container->getDefinition('translation.writer');
foreach ($container->findTaggedServiceIds('translation.dumper', true) as $id => $attributes) {
$definition->addMethodCall('addDumper', [$attributes[0]['alias'], new Reference($id)]);
}
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds tagged translation.extractor services to translation extractor.
*/
class TranslationExtractorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translation.extractor')) {
return;
}
$definition = $container->getDefinition('translation.extractor');
foreach ($container->findTaggedServiceIds('translation.extractor', true) as $id => $attributes) {
$definition->addMethodCall('addExtractor', [$attributes[0]['alias'] ?? $id, new Reference($id)]);
}
}
}
@@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class TranslatorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translator.default')) {
return;
}
$loaders = [];
$loaderRefs = [];
foreach ($container->findTaggedServiceIds('translation.loader', true) as $id => $attributes) {
$loaderRefs[$id] = new Reference($id);
$loaders[$id][] = $attributes[0]['alias'];
if (isset($attributes[0]['legacy-alias'])) {
$loaders[$id][] = $attributes[0]['legacy-alias'];
}
}
if ($container->hasDefinition('translation.reader')) {
$definition = $container->getDefinition('translation.reader');
foreach ($loaders as $id => $formats) {
foreach ($formats as $format) {
$definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]);
}
}
}
$container
->findDefinition('translator.default')
->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs))
->replaceArgument(3, $loaders)
;
if ($container->hasDefinition('validator') && $container->hasDefinition('translation.extractor.visitor.constraint')) {
$constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint');
$constraintClassNames = [];
foreach ($container->getDefinitions() as $definition) {
if (!$definition->hasTag('validator.constraint_validator')) {
continue;
}
// Resolve constraint validator FQCN even if defined as %foo.validator.class% parameter
$className = $container->getParameterBag()->resolveValue($definition->getClass());
// Extraction of the constraint class name from the Constraint Validator FQCN
$constraintClassNames[] = str_replace('Validator', '', substr(strrchr($className, '\\'), 1));
}
$constraintVisitorDefinition->setArgument(0, $constraintClassNames);
}
if (!$container->hasParameter('twig.default_path')) {
return;
}
$paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1));
if ($container->hasDefinition('console.command.translation_debug')) {
$definition = $container->getDefinition('console.command.translation_debug');
$definition->replaceArgument(4, $container->getParameter('twig.default_path'));
if (\count($definition->getArguments()) > 6) {
$definition->replaceArgument(6, $paths);
}
}
if ($container->hasDefinition('console.command.translation_extract')) {
$definition = $container->getDefinition('console.command.translation_extract');
$definition->replaceArgument(5, $container->getParameter('twig.default_path'));
if (\count($definition->getArguments()) > 7) {
$definition->replaceArgument(7, $paths);
}
}
}
}
@@ -0,0 +1,142 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class TranslatorPathsPass extends AbstractRecursivePass
{
protected bool $skipScalars = true;
private int $level = 0;
/**
* @var array<string, bool>
*/
private array $paths = [];
/**
* @var array<int, Definition>
*/
private array $definitions = [];
/**
* @var array<string, array<string, bool>>
*/
private array $controllers = [];
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('translator')) {
return;
}
foreach ($this->findControllerArguments($container) as $controller => $argument) {
$id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller));
if ($container->hasDefinition($id)) {
[$locatorRef] = $argument->getValues();
$this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true;
}
}
try {
parent::process($container);
$paths = [];
foreach ($this->paths as $class => $_) {
if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) {
$paths[] = $r->getFileName();
foreach ($r->getTraits() as $trait) {
$paths[] = $trait->getFileName();
}
}
}
if ($paths) {
if ($container->hasDefinition('console.command.translation_debug')) {
$definition = $container->getDefinition('console.command.translation_debug');
$definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths));
}
if ($container->hasDefinition('console.command.translation_extract')) {
$definition = $container->getDefinition('console.command.translation_extract');
$definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths));
}
}
} finally {
$this->level = 0;
$this->paths = [];
$this->definitions = [];
}
}
protected function processValue(mixed $value, bool $isRoot = false): mixed
{
if ($value instanceof Reference) {
if ('translator' === (string) $value) {
for ($i = $this->level - 1; $i >= 0; --$i) {
$class = $this->definitions[$i]->getClass();
if (ServiceLocator::class === $class) {
if (!isset($this->controllers[$this->currentId ?? ''])) {
continue;
}
foreach ($this->controllers[$this->currentId ?? ''] as $class => $_) {
$this->paths[$class] = true;
}
} else {
$this->paths[$class] = true;
}
break;
}
}
return $value;
}
if ($value instanceof Definition) {
$this->definitions[$this->level++] = $value;
$value = parent::processValue($value, $isRoot);
unset($this->definitions[--$this->level]);
return $value;
}
return parent::processValue($value, $isRoot);
}
private function findControllerArguments(ContainerBuilder $container): array
{
if (!$container->has('argument_resolver.service')) {
return [];
}
$resolverDef = $container->findDefinition('argument_resolver.service');
if (TraceableValueResolver::class === $resolverDef->getClass()) {
$resolverDef = $container->getDefinition($resolverDef->getArgument(0));
}
$argument = $resolverDef->getArgument(0);
if ($argument instanceof Reference) {
$argument = $container->getDefinition($argument);
}
return $argument->getArgument(0);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* CsvFileDumper generates a csv formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class CsvFileDumper extends FileDumper
{
private string $delimiter = ';';
private string $enclosure = '"';
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
{
$handle = fopen('php://memory', 'r+');
foreach ($messages->all($domain) as $source => $target) {
fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure, '\\');
}
rewind($handle);
$output = stream_get_contents($handle);
fclose($handle);
return $output;
}
/**
* Sets the delimiter and escape character for CSV.
*/
public function setCsvControl(string $delimiter = ';', string $enclosure = '"'): void
{
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
}
protected function getExtension(): string
{
return 'csv';
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* DumperInterface is the interface implemented by all translation dumpers.
* There is no common option.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
interface DumperInterface
{
/**
* Dumps the message catalogue.
*
* @param array $options Options that are used by the dumper
*/
public function dump(MessageCatalogue $messages, array $options = []): void;
}
+101
View File
@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\RuntimeException;
use Symfony\Component\Translation\MessageCatalogue;
/**
* FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s).
*
* Options:
* - path (mandatory): the directory where the files should be saved
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
abstract class FileDumper implements DumperInterface
{
/**
* A template for the relative paths to files.
*/
protected string $relativePathTemplate = '%domain%.%locale%.%extension%';
/**
* Sets the template for the relative paths to files.
*/
public function setRelativePathTemplate(string $relativePathTemplate): void
{
$this->relativePathTemplate = $relativePathTemplate;
}
public function dump(MessageCatalogue $messages, array $options = []): void
{
if (!\array_key_exists('path', $options)) {
throw new InvalidArgumentException('The file dumper needs a path option.');
}
// save a file for each domain
foreach ($messages->getDomains() as $domain) {
$fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale());
if (!file_exists($fullpath)) {
$directory = \dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0o777, true)) {
throw new RuntimeException(\sprintf('Unable to create directory "%s".', $directory));
}
}
$intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX;
$intlMessages = $messages->all($intlDomain);
if ($intlMessages) {
$intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale());
file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options));
$messages->replace([], $intlDomain);
try {
if ($messages->all($domain)) {
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
continue;
} finally {
$messages->replace($intlMessages, $intlDomain);
}
}
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
}
/**
* Transforms a domain of a message catalogue to its string representation.
*/
abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string;
/**
* Gets the file extension of the dumper.
*/
abstract protected function getExtension(): string;
/**
* Gets the relative file path using the template.
*/
private function getRelativePath(string $domain, string $locale): string
{
return strtr($this->relativePathTemplate, [
'%domain%' => $domain,
'%locale%' => $locale,
'%extension%' => $this->getExtension(),
]);
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class IcuResFileDumper extends FileDumper
{
protected string $relativePathTemplate = '%domain%/%locale%.%extension%';
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
{
$data = $indexes = $resources = '';
foreach ($messages->all($domain) as $source => $target) {
$indexes .= pack('v', \strlen($data) + 28);
$data .= $source."\0";
}
$data .= $this->writePadding($data);
$keyTop = $this->getPosition($data);
foreach ($messages->all($domain) as $source => $target) {
$resources .= pack('V', $this->getPosition($data));
$data .= pack('V', \strlen($target))
.mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8')
.$this->writePadding($data)
;
}
$resOffset = $this->getPosition($data);
$data .= pack('v', \count($messages->all($domain)))
.$indexes
.$this->writePadding($data)
.$resources
;
$bundleTop = $this->getPosition($data);
$root = pack('V7',
$resOffset + (2 << 28), // Resource Offset + Resource Type
6, // Index length
$keyTop, // Index keys top
$bundleTop, // Index resources top
$bundleTop, // Index bundle top
\count($messages->all($domain)), // Index max table length
0 // Index attributes
);
$header = pack('vC2v4C12@32',
32, // Header size
0xDA, 0x27, // Magic number 1 and 2
20, 0, 0, 2, // Rest of the header, ..., Size of a char
0x52, 0x65, 0x73, 0x42, // Data format identifier
1, 2, 0, 0, // Data version
1, 4, 0, 0 // Unicode version
);
return $header.$root.$data;
}
private function writePadding(string $data): ?string
{
$padding = \strlen($data) % 4;
return $padding ? str_repeat("\xAA", 4 - $padding) : null;
}
private function getPosition(string $data): float|int
{
return (\strlen($data) + 28) / 4;
}
protected function getExtension(): string
{
return 'res';
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* IniFileDumper generates an ini formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class IniFileDumper extends FileDumper
{
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
{
$output = '';
foreach ($messages->all($domain) as $source => $target) {
$escapeTarget = str_replace('"', '\"', $target);
$output .= $source.'="'.$escapeTarget."\"\n";
}
return $output;
}
protected function getExtension(): string
{
return 'ini';
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* JsonFileDumper generates an json formatted string representation of a message catalogue.
*
* @author singles
*/
class JsonFileDumper extends FileDumper
{
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
{
$flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT;
return json_encode($messages->all($domain), $flags);
}
protected function getExtension(): string
{
return 'json';
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\Loader\MoFileLoader;
use Symfony\Component\Translation\MessageCatalogue;
/**
* MoFileDumper generates a gettext formatted string representation of a message catalogue.
*
* @author Stealth35
*/
class MoFileDumper extends FileDumper
{
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
{
$sources = $targets = $sourceOffsets = $targetOffsets = '';
$offsets = [];
$size = 0;
foreach ($messages->all($domain) as $source => $target) {
$offsets[] = array_map('strlen', [$sources, $source, $targets, $target]);
$sources .= "\0".$source;
$targets .= "\0".$target;
++$size;
}
$header = [
'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC,
'formatRevision' => 0,
'count' => $size,
'offsetId' => MoFileLoader::MO_HEADER_SIZE,
'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size),
'sizeHashes' => 0,
'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size),
];
$sourcesSize = \strlen($sources);
$sourcesStart = $header['offsetHashes'] + 1;
foreach ($offsets as $offset) {
$sourceOffsets .= $this->writeLong($offset[1])
.$this->writeLong($offset[0] + $sourcesStart);
$targetOffsets .= $this->writeLong($offset[3])
.$this->writeLong($offset[2] + $sourcesStart + $sourcesSize);
}
return implode('', array_map($this->writeLong(...), $header))
.$sourceOffsets
.$targetOffsets
.$sources
.$targets;
}
protected function getExtension(): string
{
return 'mo';
}
private function writeLong(mixed $str): string
{
return pack('V*', $str);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Dumper;
use Symfony\Component\Translation\MessageCatalogue;
/**
* PhpFileDumper generates PHP files from a message catalogue.
*
* @author Michel Salib <michelsalib@hotmail.com>
*/
class PhpFileDumper extends FileDumper
{
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string
{
return "<?php\n\nreturn ".var_export($messages->all($domain), true).";\n";
}
protected function getExtension(): string
{
return 'php';
}
}

Some files were not shown because too many files have changed in this diff Show More