From d4a46cf25739a85030433c67f47e47ef3c30a486 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Mon, 25 Jun 2018 16:41:12 +0200 Subject: [PATCH] language specific money and date display (#180) * improved date translation * language specific date and money display --- config/packages/kimai.yaml | 10 +- config/services.yaml | 7 +- src/DependencyInjection/AppExtension.php | 2 + src/DependencyInjection/Configuration.php | 7 ++ src/Twig/DateExtensions.php | 88 +++++++++++++++++ src/Twig/Extensions.php | 45 ++++++++- templates/admin/timesheet.html.twig | 2 +- templates/invoice/renderer/print.html.twig | 8 +- .../invoice/renderer/timesheet.html.twig | 4 +- templates/invoice/templates.html.twig | 1 + templates/timesheet/index.html.twig | 2 +- tests/Twig/DateExtensionsTest.php | 96 +++++++++++++++++++ tests/Twig/ExtensionsTest.php | 68 +++++++++---- var/docs/configurations.md | 5 - 14 files changed, 308 insertions(+), 37 deletions(-) create mode 100644 src/Twig/DateExtensions.php create mode 100644 tests/Twig/DateExtensionsTest.php diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index acf09a38..0f6ddde0 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -35,10 +35,18 @@ kimai: number_generator: default: 'App\Invoice\DateNumberGenerator' + # Language specific settings, like the date formats + languages: + de: + date_short: "d.m.Y" + en: + date_short: "Y-m-d" + ru: + date_short: "d.m.Y" + twig: globals: kimai_context: - date_1: "d.m.Y" # used for display in timesheets box_color: "green" # a color for ??? active_warning: 3 # display a warning color if the user has at least X active recordings control_sidebar: # all tabs in the control sidebar diff --git a/config/services.yaml b/config/services.yaml index d8d5d74e..38427e9c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -82,7 +82,12 @@ services: # ================================================================================ App\Twig\Extensions: - arguments: ['%app_locales%'] + arguments: + $locales: "%app_locales%" + + App\Twig\DateExtensions: + arguments: + $dateSettings: "%kimai.languages%" # ================================================================================ # TIMESHEET RECORD CALCULATOR diff --git a/src/DependencyInjection/AppExtension.php b/src/DependencyInjection/AppExtension.php index 192f86df..e1cb02ee 100644 --- a/src/DependencyInjection/AppExtension.php +++ b/src/DependencyInjection/AppExtension.php @@ -33,6 +33,8 @@ class AppExtension extends Extension implements PrependExtensionInterface $config = []; } + $container->setParameter('kimai.languages', $config['languages']); + $this->createTimesheetParameter($config, $container); $this->createInvoiceParameter($config, $container); } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 12a25b83..76394e29 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -111,6 +111,13 @@ class Configuration implements ConfigurationInterface ->end() ->end() ->end() + ->arrayNode('languages') + ->arrayPrototype() + ->children() + ->scalarNode('date_short')->end() + ->end() + ->end() + ->end() ->end() ->end(); diff --git a/src/Twig/DateExtensions.php b/src/Twig/DateExtensions.php new file mode 100644 index 00000000..7382fdcb --- /dev/null +++ b/src/Twig/DateExtensions.php @@ -0,0 +1,88 @@ +requestStack = $requestStack; + $this->dateSettings = $dateSettings; + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return [ + new TwigFilter('month_name', [$this, 'monthName']), + new TwigFilter('date_short', [$this, 'dateShort']), + ]; + } + + /** + * @return string + */ + protected function getLocale() + { + return $this->requestStack->getCurrentRequest()->getLocale(); + } + + /** + * @param array $context + * @param DateTime $date + * @return string + */ + public function dateShort(DateTime $date) + { + $locale = $this->getLocale(); + $format = self::FALLBACK_SHORT; + + if (isset($this->dateSettings[$locale]['date_short'])) { + $format = $this->dateSettings[$locale]['date_short']; + } + + return date_format($date, $format); + } + + /** + * @param \DateTime $date + * @return string + */ + public function monthName(\DateTime $date) + { + return 'month.' . $date->format('n'); + } +} diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php index badab677..4042d6bf 100644 --- a/src/Twig/Extensions.php +++ b/src/Twig/Extensions.php @@ -11,6 +11,8 @@ namespace App\Twig; use App\Entity\Timesheet; use App\Utils\Duration; +use NumberFormatter; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Intl\Intl; use Twig\TwigFilter; @@ -22,19 +24,36 @@ class Extensions extends \Twig_Extension /** * @var string[] */ - private $locales; + protected $locales; + + /** + * @var string + */ + protected $locale; /** * @var Duration */ protected $durationFormatter; + /** + * @var NumberFormatter + */ + protected $numberFormatter; + + /** + * @var RequestStack + */ + protected $requestStack; + /** * Extensions constructor. * @param string $locales + * @param string $locale */ - public function __construct($locales) + public function __construct(RequestStack $requestStack, $locales) { + $this->requestStack = $requestStack; $this->locales = explode('|', $locales); $this->durationFormatter = new Duration(); } @@ -107,14 +126,32 @@ class Extensions extends \Twig_Extension */ public function money($amount, $currency = null) { - $result = number_format(round($amount, 2), 2); + $locale = $this->getLocale(); + + if ($this->locale !== $locale) { + $this->locale = $locale; + $this->numberFormatter = new NumberFormatter($locale, NumberFormatter::DECIMAL); + } + + $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency); + $amount = round($amount, $fractionDigits); + $result = $this->numberFormatter->format($amount); + if (null !== $currency) { - $result .= ' ' . Intl::getCurrencyBundle()->getCurrencySymbol($currency); + $result .= ' ' . Intl::getCurrencyBundle()->getCurrencySymbol($currency, $locale); } return $result; } + /** + * @return string + */ + protected function getLocale() + { + return $this->requestStack->getCurrentRequest()->getLocale(); + } + /** * Takes the list of codes of the locales (languages) enabled in the * application and returns an array with the name of each locale written diff --git a/templates/admin/timesheet.html.twig b/templates/admin/timesheet.html.twig index dd238b62..cc63b705 100644 --- a/templates/admin/timesheet.html.twig +++ b/templates/admin/timesheet.html.twig @@ -33,7 +33,7 @@ {% for entry in entries %} - {{ entry.begin|date(kimai_context.date_1) }} + {{ entry.begin|date_short }} {% if not duration_only %} {{ entry.begin|date("H:i") }} diff --git a/templates/invoice/renderer/print.html.twig b/templates/invoice/renderer/print.html.twig index 73105d94..edeb98ab 100644 --- a/templates/invoice/renderer/print.html.twig +++ b/templates/invoice/renderer/print.html.twig @@ -2,7 +2,7 @@
@@ -36,7 +36,7 @@ {{ 'invoice.number'|trans }}: {{ model.numberGenerator.invoiceNumber }}

- {{ 'invoice.due_days'|trans }}: {{ model.dueDate|date(kimai_context.date_1) }} + {{ 'invoice.due_days'|trans }}: {{ model.dueDate|date_short }} {% if model.customer.number is not empty %}
{{ 'label.customer_number'|trans }}: {{ model.customer.number }} {% endif %} @@ -61,7 +61,7 @@ {% for entry in model.calculator.entries %} - {{ entry.begin|date(kimai_context.date_1) }} + {{ entry.begin|date_short }} {{ entry.activity.name }} / {{ entry.activity.project.name }} {{ entry.duration|duration }} {{ entry.rate|money(model.calculator.currency) }} @@ -84,7 +84,7 @@

-

{{ 'invoice.due_days'|trans }} {{ model.dueDate|date(kimai_context.date_1) }}

+

{{ 'invoice.due_days'|trans }} {{ model.dueDate|date_short }}

diff --git a/templates/invoice/renderer/timesheet.html.twig b/templates/invoice/renderer/timesheet.html.twig index 137cc6e6..1c78a7ba 100644 --- a/templates/invoice/renderer/timesheet.html.twig +++ b/templates/invoice/renderer/timesheet.html.twig @@ -23,7 +23,7 @@ - + @@ -57,7 +57,7 @@ {% for entry in model.calculator.entries %} - + diff --git a/templates/invoice/templates.html.twig b/templates/invoice/templates.html.twig index 80edd091..2ec58bfe 100644 --- a/templates/invoice/templates.html.twig +++ b/templates/invoice/templates.html.twig @@ -4,6 +4,7 @@ {% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %} {% block page_subtitle %}{{ 'admin_invoice_template.subtitle'|trans }}{% endblock %} +{% block page_actions %}{{ widgets.page_actions({'plus-square': path('admin_invoice_template_create'), 'print': path('invoice')}) }}{% endblock %} {% block main %} {% if entries.count == 0 %} diff --git a/templates/timesheet/index.html.twig b/templates/timesheet/index.html.twig index def3ff1e..3c5055db 100644 --- a/templates/timesheet/index.html.twig +++ b/templates/timesheet/index.html.twig @@ -35,7 +35,7 @@ {% for entry in entries %} - + {% if not duration_only %} diff --git a/tests/Twig/DateExtensionsTest.php b/tests/Twig/DateExtensionsTest.php new file mode 100644 index 00000000..2e823792 --- /dev/null +++ b/tests/Twig/DateExtensionsTest.php @@ -0,0 +1,96 @@ +setLocale($locale); + $requestStack = new RequestStack(); + $requestStack->push($request); + + return new DateExtensions($requestStack, $dateSettings); + } + + public function testGetFilters() + { + $filters = ['month_name', 'date_short']; + $sut = $this->getSut('de', []); + $twigFilters = $sut->getFilters(); + $this->assertCount(count($filters), $twigFilters); + $i = 0; + foreach ($twigFilters as $filter) { + $this->assertInstanceOf(TwigFilter::class, $filter); + $this->assertEquals($filters[$i++], $filter->getName()); + } + } + + /** + * @param string $locale + * @param \DateTime $date + * @param string $result + * @dataProvider getDateShortData + */ + public function testDateShort($locale, \DateTime $date, $result) + { + $sut = $this->getSut($locale, [ + 'de' => ['date_short' => 'd.m.Y'], + 'en' => ['date_short' => 'Y-m-d'], + 'ru' => ['date_short' => 'd.m.Y'], + ]); + $this->assertEquals($result, $sut->dateShort($date)); + } + + public function getDateShortData() + { + return [ + ['en', new \DateTime('7 January 2010'), '2010-01-07'], + ['en', new \DateTime('2016-06-23'), '2016-06-23'], + ['de', new \DateTime('1980-12-14'), '14.12.1980'], + ['ru', new \DateTime('1980-12-14'), '14.12.1980'], + ]; + } + + /** + * @param \DateTime $date + * @param string $result + * @dataProvider getMonthData + */ + public function testMonthName(\DateTime $date, $result) + { + $sut = $this->getSut('en', []); + $this->assertEquals($result, $sut->monthName($date)); + } + + public function getMonthData() + { + return [ + [new \DateTime('January 2016'), 'month.1'], + [new \DateTime('2016-06-23'), 'month.6'], + [new \DateTime('2016-12-23'), 'month.12'], + ]; + } +} diff --git a/tests/Twig/ExtensionsTest.php b/tests/Twig/ExtensionsTest.php index 0cc3decb..5d02725b 100644 --- a/tests/Twig/ExtensionsTest.php +++ b/tests/Twig/ExtensionsTest.php @@ -12,6 +12,8 @@ namespace App\Tests\Twig; use App\Entity\Timesheet; use App\Twig\Extensions; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Twig\TwigFilter; /** @@ -19,10 +21,25 @@ use Twig\TwigFilter; */ class ExtensionsTest extends TestCase { + /** + * @param string $locales + * @param string $locale + * @return Extensions + */ + protected function getSut($locales, $locale = 'en') + { + $request = new Request(); + $request->setLocale($locale); + $requestStack = new RequestStack(); + $requestStack->push($request); + + return new Extensions($requestStack, $locales); + } + public function testGetFilters() { $filters = ['duration', 'money', 'currency', 'country']; - $sut = new Extensions('de'); + $sut = $this->getSut('de'); $twigFilters = $sut->getFilters(); $this->assertCount(count($filters), $twigFilters); $i = 0; @@ -35,7 +52,7 @@ class ExtensionsTest extends TestCase public function testGetFunctions() { $functions = ['locales']; - $sut = new Extensions('de'); + $sut = $this->getSut('de'); $twigFunctions = $sut->getFunctions(); $this->assertCount(count($functions), $twigFunctions); $i = 0; @@ -53,7 +70,7 @@ class ExtensionsTest extends TestCase ['code' => 'ru', 'name' => 'русский'], ]; - $sut = new Extensions('en|de|ru'); + $sut = $this->getSut('en|de|ru'); $this->assertEquals($locales, $sut->getLocales()); } @@ -65,7 +82,7 @@ class ExtensionsTest extends TestCase 'RUB' => 'RUB', ]; - $sut = new Extensions('en'); + $sut = $this->getSut('en'); foreach ($symbols as $name => $symbol) { $this->assertEquals($symbol, $sut->currency($name)); } @@ -79,33 +96,48 @@ class ExtensionsTest extends TestCase 'ES' => 'Spain', ]; - $sut = new Extensions('en'); + $sut = $this->getSut('en'); foreach ($countries as $locale => $name) { $this->assertEquals($name, $sut->country($locale)); } } - public function testMoney() + /** + * @param string $result + * @param int $amount + * @param string $currency + * @param string $locale + * @dataProvider getMoneyData + */ + public function testMoney($result, $amount, $currency, $locale) { - $money = [ - [2222, 'EUR', '2,222.00 €'], - [13.75, 'USD', '13.75 $'], - ]; + $sut = $this->getSut('en', $locale); + $this->assertEquals($result, $sut->money($amount, $currency)); + } - $sut = new Extensions('en'); - foreach ($money as $entry) { - $amount = $entry[0]; - $currency = $entry[1]; - $expected = $entry[2]; - $this->assertEquals($expected, $sut->money($amount, $currency)); - } + public function getMoneyData() + { + return [ + ['2,345 €', 2345, 'EUR', 'en'], + ['2,345 €', 2345, 'EUR', 'en'], + ['2.345,01 €', 2345.009, 'EUR', 'de'], + ['2.345,01 €', 2345.009, 'EUR', 'de'], + ['13.75 $', 13.75, 'USD', 'en'], + ['13,75 $', 13.75, 'USD', 'de'], + ['13,75 RUB', 13.75, 'RUB', 'de'], + ['13,5 RUB', 13.50, 'RUB', 'de'], + ['13,75 ₽', 13.75, 'RUB', 'ru'], + ['14 ¥', 13.75, 'JPY', 'de'], + ['13 933 ¥', 13933.49, 'JPY', 'ru'], + ['1.234.567,89 $', 1234567.891234567890000, 'USD', 'de'], + ]; } public function testDuration() { $record = $this->getTimesheet(9437); - $sut = new Extensions('en'); + $sut = $this->getSut('en'); $this->assertEquals('02:37 h', $sut->duration($record->getDuration())); $this->assertEquals('02:37:17 h', $sut->duration($record->getDuration(), true)); diff --git a/var/docs/configurations.md b/var/docs/configurations.md index a5a25015..5fa9fffc 100644 --- a/var/docs/configurations.md +++ b/var/docs/configurations.md @@ -111,10 +111,5 @@ kimai: default: begin: 15 end: 15 - -twig: - globals: - kimai_context: - date_1: "Y-m-d" ``` After changing the file you have to clear the cache with `bin/console cache:clear` or `bin/console cache:clear --env=prod`.
{{ 'label.date'|trans }}{{ model.invoiceDate|date('F Y') }}{{ model.invoiceDate|month_name|trans }} {{ model.invoiceDate|date('Y') }}
{{ 'label.customer'|trans }}
{{ entry.begin|date(kimai_context.date_1) }}{{ entry.begin|date_short }} {{ entry.activity.name }} / {{ entry.activity.project.name }} {{ entry.duration|duration }}
{{ entry.begin|date(kimai_context.date_1) }}{{ entry.begin|date_short }}{{ entry.begin|date("H:i") }}