Skip to content

Commit 86a078a

Browse files
committed
[Translation] Add DateIntervalFormatter
1 parent d679ac5 commit 86a078a

File tree

10 files changed

+249
-1
lines changed

10 files changed

+249
-1
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Extension;
13+
14+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatter;
15+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatterInterface;
16+
use Twig\Extension\AbstractExtension;
17+
use Twig\TwigFilter;
18+
19+
class IntlExtension extends AbstractExtension
20+
{
21+
private $dateIntervalFormatter;
22+
23+
public function __construct(DateIntervalFormatterInterface $dateIntervalFormatter = null)
24+
{
25+
$this->dateIntervalFormatter = $dateIntervalFormatter ?? new DateIntervalFormatter();
26+
}
27+
28+
public function getFilters(): array
29+
{
30+
return [
31+
new TwigFilter('date_interval', [$this->dateIntervalFormatter, 'formatInterval']),
32+
new TwigFilter('date_relative', [$this->dateIntervalFormatter, 'formatRelative']),
33+
];
34+
}
35+
}

src/Symfony/Bridge/Twig/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"symfony/form": "^5.3|^6.0",
3232
"symfony/http-foundation": "^5.3|^6.0",
3333
"symfony/http-kernel": "^4.4|^5.0|^6.0",
34-
"symfony/intl": "^4.4|^5.0|^6.0",
34+
"symfony/intl": "^5.4|^6.0",
3535
"symfony/mime": "^5.2|^6.0",
3636
"symfony/polyfill-intl-icu": "~1.0",
3737
"symfony/property-info": "^4.4|^5.1|^6.0",

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
8080
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
8181
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
82+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatterInterface;
8283
use Symfony\Component\Lock\Lock;
8384
use Symfony\Component\Lock\LockFactory;
8485
use Symfony\Component\Lock\LockInterface;
@@ -574,6 +575,10 @@ public function load(array $configs, ContainerBuilder $container)
574575
'kernel.locale_aware',
575576
'kernel.reset',
576577
]);
578+
579+
if (class_exists(DateIntervalFormatterInterface::class)) {
580+
$loader->load('intl.php');
581+
}
577582
}
578583

579584
/**
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatter;
15+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatterInterface;
16+
17+
return static function (ContainerConfigurator $container) {
18+
$container->services()
19+
->set('intl.date_interval_formatter', DateIntervalFormatter::class)
20+
->alias(DateIntervalFormatterInterface::class, 'intl.date_interval_formatter')
21+
;
22+
};

src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Bundle\FrameworkBundle\CacheWarmer\TranslationsCacheWarmer;
1616
use Symfony\Bundle\FrameworkBundle\Translation\Translator;
17+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatter;
18+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatterInterface;
1719
use Symfony\Component\Translation\Dumper\CsvFileDumper;
1820
use Symfony\Component\Translation\Dumper\IcuResFileDumper;
1921
use Symfony\Component\Translation\Dumper\IniFileDumper;
@@ -158,5 +160,8 @@
158160
->args([service(ContainerInterface::class)])
159161
->tag('container.service_subscriber', ['id' => 'translator'])
160162
->tag('kernel.cache_warmer')
163+
164+
->set('translation.formatter.date_interval', DateIntervalFormatter::class)
165+
->alias(DateIntervalFormatterInterface::class, 'translation.formatter.date_interval')
161166
;
162167
};

src/Symfony/Bundle/TwigBundle/Resources/config/twig.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Bridge\Twig\Extension\HttpFoundationExtension;
2222
use Symfony\Bridge\Twig\Extension\HttpKernelExtension;
2323
use Symfony\Bridge\Twig\Extension\HttpKernelRuntime;
24+
use Symfony\Bridge\Twig\Extension\IntlExtension;
2425
use Symfony\Bridge\Twig\Extension\ProfilerExtension;
2526
use Symfony\Bridge\Twig\Extension\RoutingExtension;
2627
use Symfony\Bridge\Twig\Extension\SerializerExtension;
@@ -167,5 +168,9 @@
167168
->args([service('serializer')])
168169

169170
->set('twig.extension.serializer', SerializerExtension::class)
171+
172+
->set('twig.extension.intl', IntlExtension::class)
173+
->args([service('intl.date_interval_formatter')->nullOnInvalid()])
174+
->tag('twig.extension')
170175
;
171176
};

src/Symfony/Component/Intl/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.4
5+
---
6+
7+
* Add `DateIntervalFormatter` to humanize a `DateInterval` or a difference between two `DateTimeInterface` objects
8+
49
5.3
510
---
611

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Intl\DateIntervalFormatter;
13+
14+
class DateIntervalFormatter implements DateIntervalFormatterInterface
15+
{
16+
private const VALUE_NAMES = [
17+
'y' => 'year',
18+
'm' => 'month',
19+
'd' => 'day',
20+
'h' => 'hour',
21+
'i' => 'minute',
22+
's' => 'second',
23+
];
24+
25+
public function formatInterval($interval, int $precision = 0): string
26+
{
27+
if (!$interval instanceof \DateInterval) {
28+
$interval = new \DateInterval($interval);
29+
}
30+
31+
$elements = [];
32+
$currentPrecision = 0;
33+
34+
foreach (self::VALUE_NAMES as $value => $name) {
35+
if ($elements) {
36+
++$currentPrecision;
37+
}
38+
39+
if ((!$precision || $currentPrecision < $precision) && $interval->{$value}) {
40+
$elements[] = $this->format($interval->{$value}, $name);
41+
}
42+
}
43+
44+
$lastElement = array_pop($elements);
45+
46+
return null !== $lastElement ? ($elements ? implode(', ', $elements).' and '.$lastElement : $lastElement) : 'now';
47+
}
48+
49+
/**
50+
* @param \DateTimeInterface|string $dateTime
51+
* @param \DateTimeInterface|string|null $currentDateTime
52+
*/
53+
public function formatRelative($dateTime, $currentDateTime = null, int $precision = 0): string
54+
{
55+
if (!$dateTime instanceof \DateTimeInterface) {
56+
$dateTime = new \DateTimeImmutable($dateTime);
57+
}
58+
59+
if (!$currentDateTime instanceof \DateTimeInterface) {
60+
$currentDateTime = new \DateTimeImmutable($currentDateTime ?? 'now');
61+
}
62+
63+
if ($dateTime > $currentDateTime) {
64+
return 'in '.$this->formatInterval($currentDateTime->diff($dateTime));
65+
}
66+
67+
if ($dateTime->getTimestamp() === $currentDateTime->getTimestamp()) {
68+
return 'now';
69+
}
70+
71+
return $this->formatInterval($currentDateTime->diff($dateTime), $precision).' ago';
72+
}
73+
74+
private function format(int $number, string $singular): string
75+
{
76+
return $number > 1 ? "{$number} {$singular}s" : "{$number} {$singular}";
77+
}
78+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Intl\DateIntervalFormatter;
13+
14+
interface DateIntervalFormatterInterface
15+
{
16+
/**
17+
* @param \DateInterval|string $interval
18+
*/
19+
public function formatInterval($interval, int $precision = 0): string;
20+
21+
/**
22+
* @param \DateTimeInterface|string $dateTime
23+
* @param \DateTimeInterface|string|null $currentDateTime
24+
*/
25+
public function formatRelative($dateTime, $currentDateTime = null, int $precision = 0): string;
26+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Intl\Tests\DateIntervalFormatter;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Intl\DateIntervalFormatter\DateIntervalFormatter;
16+
17+
class DateIntervalFormatterTest extends TestCase
18+
{
19+
/**
20+
* @dataProvider provideIntervals
21+
*/
22+
public function testFormatInterval($interval, string $expected, int $precision = 0)
23+
{
24+
$formatter = new DateIntervalFormatter();
25+
$this->assertSame($expected, $formatter->formatInterval($interval, $precision));
26+
}
27+
28+
public function provideIntervals(): \Generator
29+
{
30+
yield [new \DateInterval('PT0S'), 'now'];
31+
yield [new \DateInterval('PT1S'), '1 second'];
32+
yield [new \DateInterval('PT10S'), '10 seconds'];
33+
yield [new \DateInterval('PT1M10S'), '1 minute and 10 seconds'];
34+
yield [new \DateInterval('PT10M10S'), '10 minutes and 10 seconds'];
35+
yield [new \DateInterval('PT1H10M10S'), '1 hour, 10 minutes and 10 seconds'];
36+
yield [new \DateInterval('PT10H10M10S'), '10 hours, 10 minutes and 10 seconds'];
37+
yield [new \DateInterval('P1DT10H10M10S'), '1 day, 10 hours, 10 minutes and 10 seconds'];
38+
yield [new \DateInterval('P10DT10H10M10S'), '10 days, 10 hours, 10 minutes and 10 seconds'];
39+
yield [new \DateInterval('P1M10DT10H10M10S'), '1 month, 10 days, 10 hours, 10 minutes and 10 seconds'];
40+
yield [new \DateInterval('P10M10DT10H10M10S'), '10 months, 10 days, 10 hours, 10 minutes and 10 seconds'];
41+
yield [new \DateInterval('P1Y10M10DT10H10M10S'), '1 year, 10 months, 10 days, 10 hours, 10 minutes and 10 seconds'];
42+
yield [new \DateInterval('P10Y10M10DT10H10M10S'), '10 years, 10 months, 10 days, 10 hours, 10 minutes and 10 seconds'];
43+
yield [new \DateInterval('P10Y10M10DT10H10M10S'), '10 years, 10 months and 10 days', 3];
44+
yield [new \DateInterval('P1Y1DT1S'), '1 year and 1 day', 3];
45+
yield ['PT1M10S', '1 minute and 10 seconds'];
46+
}
47+
48+
/**
49+
* @dataProvider provideDates
50+
*/
51+
public function testFormatDates($dateTime, $currentDateTime, string $expected, int $precision = 0)
52+
{
53+
$formatter = new DateIntervalFormatter();
54+
$this->assertSame($expected, $formatter->formatRelative($dateTime, $currentDateTime, $precision));
55+
}
56+
57+
public function provideDates(): \Generator
58+
{
59+
yield [new \DateTime('2021-01-01'), new \DateTime('2020-01-01'), 'in 1 year'];
60+
yield [new \DateTime('2020-01-01'), new \DateTime('2021-01-01'), '1 year ago'];
61+
yield [new \DateTime('2021-01-01'), new \DateTime('2021-01-01'), 'now'];
62+
yield [new \DateTime('2020-01-01'), new \DateTime('2021-02-02'), '1 year and 1 month ago', 2];
63+
yield ['2021-01-01', new \DateTime('2020-01-01'), 'in 1 year'];
64+
yield ['2021-01-01', '2020-01-01', 'in 1 year'];
65+
yield ['-1 hour', null, '1 hour ago'];
66+
}
67+
}

0 commit comments

Comments
 (0)