diff --git a/UPGRADING.md b/UPGRADING.md index 2dcb6dec..85b19995 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -10,13 +10,15 @@ Perform EACH version specific task between your version and the new one, otherwi ## [1.10](https://github.com/kevinpapst/kimai2/releases/tag/1.10) +**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).** + - Invoice renderer `CSV` was removed - Sessions are now stored in the database (all users have to re-login after upgrade) ### Developer - **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf` changed -- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more +- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more ## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9) diff --git a/src/EventSubscriber/TimezoneSubscriber.php b/src/EventSubscriber/UserEnvironmentSubscriber.php similarity index 79% rename from src/EventSubscriber/TimezoneSubscriber.php rename to src/EventSubscriber/UserEnvironmentSubscriber.php index 6bb01e7d..687079c9 100644 --- a/src/EventSubscriber/TimezoneSubscriber.php +++ b/src/EventSubscriber/UserEnvironmentSubscriber.php @@ -15,12 +15,12 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -class TimezoneSubscriber implements EventSubscriberInterface +class UserEnvironmentSubscriber implements EventSubscriberInterface { /** * @var TokenStorageInterface */ - protected $storage; + private $storage; public function __construct(TokenStorageInterface $tokenStorage) { @@ -30,11 +30,11 @@ class TimezoneSubscriber implements EventSubscriberInterface public static function getSubscribedEvents(): array { return [ - KernelEvents::REQUEST => ['setTimezone', 100], + KernelEvents::REQUEST => ['prepareEnvironment', 100], ]; } - public function setTimezone(RequestEvent $event) + public function prepareEnvironment(RequestEvent $event) { if (null === $this->storage->getToken()) { return; @@ -44,6 +44,7 @@ class TimezoneSubscriber implements EventSubscriberInterface if ($user instanceof User) { date_default_timezone_set($user->getTimezone()); + \Locale::setDefault($user->getLocale()); } } } diff --git a/src/Invoice/DefaultInvoiceFormatter.php b/src/Invoice/DefaultInvoiceFormatter.php index fc22456b..3a19ebfb 100644 --- a/src/Invoice/DefaultInvoiceFormatter.php +++ b/src/Invoice/DefaultInvoiceFormatter.php @@ -10,8 +10,7 @@ namespace App\Invoice; use App\Twig\DateExtensions; -use App\Twig\Extensions; -use Symfony\Contracts\Translation\TranslatorInterface; +use App\Twig\LocaleExtensions; final class DefaultInvoiceFormatter implements InvoiceFormatter { @@ -19,25 +18,13 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter * @var DateExtensions */ private $dateExtension; - /** - * @var Extensions + * @var LocaleExtensions */ private $extension; - /** - * @var TranslatorInterface - */ - private $translator; - - /** - * @param TranslatorInterface $translator - * @param DateExtensions $dateExtension - * @param Extensions $extensions - */ - public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions) + public function __construct(DateExtensions $dateExtension, LocaleExtensions $extensions) { - $this->translator = $translator; $this->dateExtension = $dateExtension; $this->extension = $extensions; } @@ -60,23 +47,24 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter return $this->dateExtension->time($date); } - /** - * @param int $amount - * @param string $currency - * @return string - */ - public function getFormattedMoney($amount, $currency) - { - return $this->extension->money($amount, $currency); - } - /** * @param \DateTime $date * @return mixed */ public function getFormattedMonthName(\DateTime $date) { - return $this->translator->trans($this->dateExtension->monthName($date)); + return $this->dateExtension->monthName($date); + } + + /** + * @param float|int $amount + * @param string|null $currency + * @param bool $withCurrency + * @return string + */ + public function getFormattedMoney($amount, ?string $currency, bool $withCurrency = true) + { + return $this->extension->money($amount, $currency, $withCurrency); } /** diff --git a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php index 19c95d06..56368a23 100644 --- a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php @@ -64,13 +64,13 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator 'entry.type' => $item->getType(), 'entry.category' => $item->getCategory(), 'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency), - 'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, null), + 'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, $currency, false), 'entry.rate_plain' => $appliedRate, 'entry.rate_internal' => $formatter->getFormattedMoney($internalRate, $currency), - 'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, null), + 'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, $currency, false), 'entry.rate_internal_plain' => $internalRate, 'entry.total' => $formatter->getFormattedMoney($rate, $currency), - 'entry.total_nc' => $formatter->getFormattedMoney($rate, null), + 'entry.total_nc' => $formatter->getFormattedMoney($rate, $currency, false), 'entry.total_plain' => $rate, 'entry.currency' => $currency, 'entry.duration' => $item->getDuration(), diff --git a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php index 245024ab..545720b1 100644 --- a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php @@ -31,15 +31,15 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency), 'invoice.vat' => $model->getCalculator()->getVat(), 'invoice.tax' => $formatter->getFormattedMoney($tax, $currency), - 'invoice.tax_nc' => $formatter->getFormattedMoney($tax, null), + 'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false), 'invoice.tax_plain' => $tax, 'invoice.total_time' => $formatter->getFormattedDuration($model->getCalculator()->getTimeWorked()), 'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()), 'invoice.total' => $formatter->getFormattedMoney($total, $currency), - 'invoice.total_nc' => $formatter->getFormattedMoney($total, null), + 'invoice.total_nc' => $formatter->getFormattedMoney($total, $currency, false), 'invoice.total_plain' => $total, 'invoice.subtotal' => $formatter->getFormattedMoney($subtotal, $currency), - 'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, null), + 'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, $currency, false), 'invoice.subtotal_plain' => $subtotal, 'template.name' => $model->getTemplate()->getName(), diff --git a/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php b/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php index f3e22ba4..c5b2a3ab 100644 --- a/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelProjectHydrator.php @@ -53,7 +53,7 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator $prefix . 'end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '', $prefix . 'order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '', $prefix . 'budget_money' => $formatter->getFormattedMoney($project->getBudget(), $currency), - $prefix . 'budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), null), + $prefix . 'budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), $currency, false), $prefix . 'budget_money_plain' => $project->getBudget(), $prefix . 'budget_time' => $project->getTimeBudget(), $prefix . 'budget_time_decimal' => $formatter->getFormattedDecimalDuration($project->getTimeBudget()), diff --git a/src/Invoice/InvoiceFormatter.php b/src/Invoice/InvoiceFormatter.php index a8ae7b90..de2479fc 100644 --- a/src/Invoice/InvoiceFormatter.php +++ b/src/Invoice/InvoiceFormatter.php @@ -29,9 +29,10 @@ interface InvoiceFormatter /** * @param int|float $amount * @param string|null $currency - * @return mixed + * @param bool $withCurrency + * @return string */ - public function getFormattedMoney($amount, $currency); + public function getFormattedMoney($amount, ?string $currency, bool $withCurrency = true); /** * @param \DateTime $date diff --git a/src/Invoice/Renderer/AbstractTwigRenderer.php b/src/Invoice/Renderer/AbstractTwigRenderer.php new file mode 100644 index 00000000..89bd2d4a --- /dev/null +++ b/src/Invoice/Renderer/AbstractTwigRenderer.php @@ -0,0 +1,74 @@ +twig = $twig; + } + + protected function renderTwigTemplate(InvoiceDocument $document, InvoiceModel $model): string + { + $previousLocale = $this->changeTwigLocale($this->twig, $model->getTemplate()->getLanguage()); + + $content = $this->twig->render('@invoice/' . basename($document->getFilename()), [ + 'model' => $model + ]); + + $this->changeTwigLocale($this->twig, $previousLocale); + + return $content; + } + + private function changeTwigLocale(Environment $twig, ?string $locale = null): ?string + { + // for invoices that don't have a language configured (using request locale) + if (null === $locale) { + return null; + } + + /** @var TranslationExtension $extension */ + $extension = $twig->getExtension(TranslationExtension::class); + /** @var LocaleAwareInterface $translator */ + $translator = $extension->getTranslator(); + $previousLocale = $translator->getLocale(); + + $translator->setLocale($locale); + + /** @var LocaleExtensions $extension */ + $extension = $twig->getExtension(LocaleExtensions::class); + $extension->setLocale($locale); + + /** @var DateExtensions $extension */ + $extension = $twig->getExtension(DateExtensions::class); + $extension->setLocale($locale); + + return $previousLocale; + } +} diff --git a/src/Invoice/Renderer/JsonRenderer.php b/src/Invoice/Renderer/JsonRenderer.php index 01181abf..1bdd585e 100644 --- a/src/Invoice/Renderer/JsonRenderer.php +++ b/src/Invoice/Renderer/JsonRenderer.php @@ -12,23 +12,11 @@ namespace App\Invoice\Renderer; use App\Entity\InvoiceDocument; use App\Invoice\InvoiceFilename; use App\Invoice\InvoiceModel; -use App\Invoice\RendererInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Twig\Environment; -final class JsonRenderer implements RendererInterface +final class JsonRenderer extends AbstractTwigRenderer { - /** - * @var Environment - */ - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; - } - public function supports(InvoiceDocument $document): bool { return stripos($document->getFilename(), '.json.twig') !== false; @@ -36,9 +24,7 @@ final class JsonRenderer implements RendererInterface public function render(InvoiceDocument $document, InvoiceModel $model): Response { - $content = $this->twig->render('@invoice/' . basename($document->getFilename()), [ - 'model' => $model - ]); + $content = $this->renderTwigTemplate($document, $model); $filename = (string) new InvoiceFilename($model); $response = new Response($content); diff --git a/src/Invoice/Renderer/PdfRenderer.php b/src/Invoice/Renderer/PdfRenderer.php index 60f2c772..8fbdfac1 100644 --- a/src/Invoice/Renderer/PdfRenderer.php +++ b/src/Invoice/Renderer/PdfRenderer.php @@ -12,18 +12,13 @@ namespace App\Invoice\Renderer; use App\Entity\InvoiceDocument; use App\Invoice\InvoiceFilename; use App\Invoice\InvoiceModel; -use App\Invoice\RendererInterface; use App\Utils\HtmlToPdfConverter; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Twig\Environment; -final class PdfRenderer implements RendererInterface +final class PdfRenderer extends AbstractTwigRenderer { - /** - * @var Environment - */ - private $twig; /** * @var HtmlToPdfConverter */ @@ -31,7 +26,7 @@ final class PdfRenderer implements RendererInterface public function __construct(Environment $twig, HtmlToPdfConverter $converter) { - $this->twig = $twig; + parent::__construct($twig); $this->converter = $converter; } @@ -42,9 +37,7 @@ final class PdfRenderer implements RendererInterface public function render(InvoiceDocument $document, InvoiceModel $model): Response { - $content = $this->twig->render('@invoice/' . basename($document->getFilename()), [ - 'model' => $model - ]); + $content = $this->renderTwigTemplate($document, $model); $content = $this->converter->convertToPdf($content, [ 'setAutoTopMargin' => 'pad', diff --git a/src/Invoice/Renderer/TextRenderer.php b/src/Invoice/Renderer/TextRenderer.php index e148f086..dfcb3674 100644 --- a/src/Invoice/Renderer/TextRenderer.php +++ b/src/Invoice/Renderer/TextRenderer.php @@ -12,23 +12,11 @@ namespace App\Invoice\Renderer; use App\Entity\InvoiceDocument; use App\Invoice\InvoiceFilename; use App\Invoice\InvoiceModel; -use App\Invoice\RendererInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Twig\Environment; -final class TextRenderer implements RendererInterface +final class TextRenderer extends AbstractTwigRenderer { - /** - * @var Environment - */ - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; - } - public function supports(InvoiceDocument $document): bool { return stripos($document->getFilename(), '.txt.twig') !== false; @@ -36,9 +24,7 @@ final class TextRenderer implements RendererInterface public function render(InvoiceDocument $document, InvoiceModel $model): Response { - $content = $this->twig->render('@invoice/' . basename($document->getFilename()), [ - 'model' => $model - ]); + $content = $this->renderTwigTemplate($document, $model); $filename = (string) new InvoiceFilename($model); $response = new Response($content); diff --git a/src/Invoice/Renderer/TwigRenderer.php b/src/Invoice/Renderer/TwigRenderer.php index ea77be29..7b008cd2 100644 --- a/src/Invoice/Renderer/TwigRenderer.php +++ b/src/Invoice/Renderer/TwigRenderer.php @@ -11,22 +11,10 @@ namespace App\Invoice\Renderer; use App\Entity\InvoiceDocument; use App\Invoice\InvoiceModel; -use App\Invoice\RendererInterface; use Symfony\Component\HttpFoundation\Response; -use Twig\Environment; -final class TwigRenderer implements RendererInterface +final class TwigRenderer extends AbstractTwigRenderer { - /** - * @var Environment - */ - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; - } - public function supports(InvoiceDocument $document): bool { return stripos($document->getFilename(), '.html.twig') !== false; @@ -34,9 +22,7 @@ final class TwigRenderer implements RendererInterface public function render(InvoiceDocument $document, InvoiceModel $model): Response { - $content = $this->twig->render('@invoice/' . basename($document->getFilename()), [ - 'model' => $model - ]); + $content = $this->renderTwigTemplate($document, $model); $response = new Response(); $response->setContent($content); diff --git a/src/Invoice/Renderer/XmlRenderer.php b/src/Invoice/Renderer/XmlRenderer.php index 6c1fb935..e666ee50 100644 --- a/src/Invoice/Renderer/XmlRenderer.php +++ b/src/Invoice/Renderer/XmlRenderer.php @@ -12,23 +12,11 @@ namespace App\Invoice\Renderer; use App\Entity\InvoiceDocument; use App\Invoice\InvoiceFilename; use App\Invoice\InvoiceModel; -use App\Invoice\RendererInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Twig\Environment; -final class XmlRenderer implements RendererInterface +final class XmlRenderer extends AbstractTwigRenderer { - /** - * @var Environment - */ - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; - } - public function supports(InvoiceDocument $document): bool { return stripos($document->getFilename(), '.xml.twig') !== false; @@ -36,9 +24,7 @@ final class XmlRenderer implements RendererInterface public function render(InvoiceDocument $document, InvoiceModel $model): Response { - $content = $this->twig->render('@invoice/' . basename($document->getFilename()), [ - 'model' => $model - ]); + $content = $this->renderTwigTemplate($document, $model); $filename = (string) new InvoiceFilename($model); $response = new Response($content); diff --git a/src/Twig/DateExtensions.php b/src/Twig/DateExtensions.php index f401b34c..cd100b29 100644 --- a/src/Twig/DateExtensions.php +++ b/src/Twig/DateExtensions.php @@ -9,8 +9,11 @@ namespace App\Twig; -use App\Utils\LocaleSettings; +use App\Configuration\LanguageFormattings; +use App\Constants; +use App\Utils\LocaleFormats; use DateTime; +use Symfony\Component\HttpFoundation\RequestStack; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -21,9 +24,9 @@ use Twig\TwigFunction; class DateExtensions extends AbstractExtension { /** - * @var LocaleSettings|null + * @var LocaleFormats|null */ - protected $localeSettings = null; + protected $localeFormats = null; /** * @var string */ @@ -44,13 +47,26 @@ class DateExtensions extends AbstractExtension * @var bool */ protected $isTwentyFourHour = null; - /** - * @param LocaleSettings $localeSettings + * @var string */ - public function __construct(LocaleSettings $localeSettings) + private $locale; + /** + * @var LanguageFormattings + */ + private $formats; + + public function __construct(RequestStack $requestStack, LanguageFormattings $formats) { - $this->localeSettings = $localeSettings; + $locale = Constants::DEFAULT_LOCALE; + + // request is null in a console command + if (null !== $requestStack->getMasterRequest()) { + $locale = $requestStack->getMasterRequest()->getLocale(); + } + + $this->formats = $formats; + $this->setLocale($locale); } /** @@ -60,6 +76,7 @@ class DateExtensions extends AbstractExtension { return [ new TwigFilter('month_name', [$this, 'monthName']), + new TwigFilter('day_name', [$this, 'dayName']), new TwigFilter('date_short', [$this, 'dateShort']), new TwigFilter('date_time', [$this, 'dateTime']), new TwigFilter('date_full', [$this, 'dateTimeFull']), @@ -79,15 +96,25 @@ class DateExtensions extends AbstractExtension ]; } + /** + * Allows to switch the locale used for all twig filter and functions. + * + * @param string $locale + */ + public function setLocale(string $locale) + { + $this->locale = $locale; + $this->localeFormats = new LocaleFormats($this->formats, $locale); + } + /** * @param DateTime|string $date * @return string - * @throws \Exception */ public function dateShort($date) { if (null === $this->dateFormat) { - $this->dateFormat = $this->localeSettings->getDateFormat(); + $this->dateFormat = $this->localeFormats->getDateFormat(); } if (!$date instanceof DateTime) { @@ -98,18 +125,17 @@ class DateExtensions extends AbstractExtension } } - return date_format($date, $this->dateFormat); + return $date->format($this->dateFormat); } /** * @param DateTime|string $date * @return string - * @throws \Exception */ public function dateTime($date) { if (null === $this->dateTimeFormat) { - $this->dateTimeFormat = $this->localeSettings->getDateTimeFormat(); + $this->dateTimeFormat = $this->localeFormats->getDateTimeFormat(); } if (!$date instanceof DateTime) { @@ -125,14 +151,12 @@ class DateExtensions extends AbstractExtension /** * @param DateTime|string $date - * @param bool $userTimezone * @return bool|false|string - * @throws \Exception */ - public function dateTimeFull($date, bool $userTimezone = true) + public function dateTimeFull($date) { if (null === $this->dateTimeTypeFormat) { - $this->dateTimeTypeFormat = $this->localeSettings->getDateTimeTypeFormat(); + $this->dateTimeTypeFormat = $this->localeFormats->getDateTimeTypeFormat(); } if (!$date instanceof DateTime) { @@ -143,17 +167,11 @@ class DateExtensions extends AbstractExtension } } - $timezone = date_default_timezone_get(); - - if (!$userTimezone) { - $timezone = $date->getTimezone()->getName(); - } - $formatter = new \IntlDateFormatter( - $this->localeSettings->getLocale(), + $this->locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM, - $timezone, + date_default_timezone_get(), \IntlDateFormatter::GREGORIAN, $this->dateTimeTypeFormat ); @@ -177,34 +195,52 @@ class DateExtensions extends AbstractExtension } } - return date_format($date, $format); + return $date->format($format); } /** * @param DateTime|string $date * @return string - * @throws \Exception */ public function time($date) { if (null === $this->timeFormat) { - $this->timeFormat = $this->localeSettings->getTimeFormat(); + $this->timeFormat = $this->localeFormats->getTimeFormat(); } if (!$date instanceof DateTime) { $date = new DateTime($date); } - return date_format($date, $this->timeFormat); + return $date->format($this->timeFormat); } - /** - * @param \DateTime $date - * @return string - */ - public function monthName(\DateTime $date) + public function monthName(\DateTime $dateTime): string { - return 'month.' . $date->format('n'); + $formatter = new \IntlDateFormatter( + $this->locale, + \IntlDateFormatter::FULL, + \IntlDateFormatter::FULL, + $dateTime->getTimezone()->getName(), + \IntlDateFormatter::GREGORIAN, + 'LLLL' + ); + + return $formatter->format($dateTime); + } + + public function dayName(\DateTime $dateTime): string + { + $formatter = new \IntlDateFormatter( + $this->locale, + \IntlDateFormatter::FULL, + \IntlDateFormatter::FULL, + $dateTime->getTimezone()->getName(), + \IntlDateFormatter::GREGORIAN, + 'EEEE' + ); + + return $formatter->format($dateTime); } /** @@ -215,7 +251,7 @@ class DateExtensions extends AbstractExtension public function hour24($twentyFour, $twelveHour) { if (null === $this->isTwentyFourHour) { - $this->isTwentyFourHour = $this->localeSettings->isTwentyFourHours(); + $this->isTwentyFourHour = $this->localeFormats->isTwentyFourHours(); } if (true === $this->isTwentyFourHour) { @@ -230,6 +266,6 @@ class DateExtensions extends AbstractExtension */ public function getDurationFormat() { - return $this->localeSettings->getDurationFormat(); + return $this->localeFormats->getDurationFormat(); } } diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php index c05d4c04..99cddd05 100644 --- a/src/Twig/Extensions.php +++ b/src/Twig/Extensions.php @@ -10,14 +10,6 @@ namespace App\Twig; use App\Constants; -use App\Entity\Timesheet; -use App\Utils\Duration; -use App\Utils\LocaleSettings; -use NumberFormatter; -use Symfony\Component\Intl\Countries; -use Symfony\Component\Intl\Currencies; -use Symfony\Component\Intl\Languages; -use Symfony\Component\Intl\Locales; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -27,49 +19,12 @@ use Twig\TwigFunction; */ class Extensions extends AbstractExtension { - /** - * @var LocaleSettings - */ - protected $localeSettings; - /** - * @var string - */ - protected $locale; - /** - * @var Duration - */ - protected $durationFormatter; - /** - * @var NumberFormatter - */ - protected $numberFormatter; - /** - * @var NumberFormatter - */ - protected $moneyFormatter; - - /** - * @param LocaleSettings $localeSettings - */ - public function __construct(LocaleSettings $localeSettings) - { - $this->localeSettings = $localeSettings; - $this->durationFormatter = new Duration(); - } - /** * {@inheritdoc} */ public function getFilters() { return [ - new TwigFilter('duration', [$this, 'duration']), - new TwigFilter('duration_decimal', [$this, 'durationDecimal']), - new TwigFilter('money', [$this, 'money']), - new TwigFilter('currency', [$this, 'currency']), - new TwigFilter('country', [$this, 'country']), - new TwigFilter('language', [$this, 'language']), - new TwigFilter('amount', [$this, 'amount']), new TwigFilter('docu_link', [$this, 'documentationLink']), new TwigFilter('multiline_indent', [$this, 'multilineIndent']), ]; @@ -81,7 +36,6 @@ class Extensions extends AbstractExtension public function getFunctions() { return [ - new TwigFunction('locales', [$this, 'getLocales']), new TwigFunction('class_name', [$this, 'getClassName']), ]; } @@ -120,115 +74,6 @@ class Extensions extends AbstractExtension return implode(PHP_EOL, $parts); } - /** - * Transforms seconds into a duration string. - * - * @param int|Timesheet $duration - * @param bool $decimal - * @return string - */ - public function duration($duration, $decimal = false) - { - if ($decimal) { - return $this->durationDecimal($duration); - } - - $duration = $this->getSecondsForDuration($duration); - $format = $this->localeSettings->getDurationFormat(); - - return $this->formatDuration($duration, $format); - } - - /** - * Transforms seconds into a decimal formatted duration string. - * - * @param int|Timesheet $duration - * @return string - */ - public function durationDecimal($duration) - { - $duration = $this->getSecondsForDuration($duration); - - return $this->getNumberFormatter()->format(number_format($duration / 3600, 2)); - } - - /** - * @param string|float $amount - * @return bool|false|string - */ - public function amount($amount) - { - return $this->getNumberFormatter()->format($amount); - } - - private function getSecondsForDuration($duration): int - { - if (null === $duration) { - $duration = 0; - } - - if ($duration instanceof Timesheet) { - if (null === $duration->getEnd()) { - $duration = time() - $duration->getBegin()->getTimestamp(); - } else { - $duration = $duration->getDuration(); - } - } - - return (int) $duration; - } - - protected function formatDuration(int $seconds, string $format): string - { - if ($seconds < 0) { - return '?'; - } - - return $this->durationFormatter->format($seconds, $format); - } - - /** - * @param string $currency - * @return string - */ - public function currency($currency) - { - try { - return Currencies::getSymbol(strtoupper($currency)); - } catch (\Exception $ex) { - } - - return $currency; - } - - /** - * @param string $language - * @return string - */ - public function language($language) - { - try { - return Languages::getName(strtolower($language), $this->locale); - } catch (\Exception $ex) { - } - - return $language; - } - - /** - * @param string $country - * @return string - */ - public function country($country) - { - try { - return Countries::getName(strtoupper($country)); - } catch (\Exception $ex) { - } - - return $country; - } - /** * @param string $url * @return string @@ -237,62 +82,4 @@ class Extensions extends AbstractExtension { return Constants::HOMEPAGE . '/documentation/' . $url; } - - private function initLocale() - { - $locale = $this->localeSettings->getLocale(); - - if ($this->locale === $locale) { - return; - } - - $this->locale = $locale; - $this->numberFormatter = new NumberFormatter($locale, NumberFormatter::DECIMAL); - $this->moneyFormatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); - } - - private function getNumberFormatter(): NumberFormatter - { - $this->initLocale(); - - return $this->numberFormatter; - } - - private function getMoneyFormatter(): NumberFormatter - { - $this->initLocale(); - - return $this->moneyFormatter; - } - - /** - * @param float $amount - * @param string $currency - * @return string - */ - public function money($amount, $currency = null) - { - if (null !== $currency) { - return $this->getMoneyFormatter()->formatCurrency($amount, $currency); - } - - return $this->getNumberFormatter()->format($amount); - } - - /** - * Takes the list of codes of the locales (languages) enabled in the - * application and returns an array with the name of each locale written - * in its own language (e.g. English, Français, Español, etc.) - * - * @return array - */ - public function getLocales() - { - $locales = []; - foreach ($this->localeSettings->getAvailableLanguages() as $locale) { - $locales[] = ['code' => $locale, 'name' => Locales::getName($locale, $locale)]; - } - - return $locales; - } } diff --git a/src/Twig/LocaleExtensions.php b/src/Twig/LocaleExtensions.php new file mode 100644 index 00000000..2c01f7c7 --- /dev/null +++ b/src/Twig/LocaleExtensions.php @@ -0,0 +1,219 @@ +getMasterRequest()) { + $locale = $requestStack->getMasterRequest()->getLocale(); + } + + $this->durationFormatter = new Duration(); + $this->formats = $formats; + $this->setLocale($locale); + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return [ + new TwigFilter('duration', [$this, 'duration']), + new TwigFilter('duration_decimal', [$this, 'durationDecimal']), + new TwigFilter('money', [$this, 'money']), + new TwigFilter('currency', [$this, 'currency']), + new TwigFilter('country', [$this, 'country']), + new TwigFilter('language', [$this, 'language']), + new TwigFilter('amount', [$this, 'amount']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return [ + new TwigFunction('locales', [$this, 'getLocales']), + ]; + } + + /** + * Allows to switch the locale used for all twig filter and functions. + * + * @param string $locale + */ + public function setLocale(string $locale) + { + $this->helper = new LocaleHelper($locale); + $this->localeFormats = new LocaleFormats($this->formats, $locale); + } + + /** + * Transforms seconds into a duration string. + * + * @param int|Timesheet $duration + * @param bool $decimal + * @return string + */ + public function duration($duration, $decimal = false) + { + if ($decimal) { + return $this->durationDecimal($duration); + } + + $seconds = $this->getSecondsForDuration($duration); + $format = $this->localeFormats->getDurationFormat(); + + return $this->formatDuration($seconds, $format); + } + + /** + * Transforms seconds into a decimal formatted duration string. + * + * @param int|Timesheet $duration + * @return string + */ + public function durationDecimal($duration) + { + $seconds = $this->getSecondsForDuration($duration); + + return $this->helper->durationDecimal($seconds); + } + + private function getSecondsForDuration($duration): int + { + if (null === $duration) { + $duration = 0; + } + + if ($duration instanceof Timesheet) { + if (null === $duration->getEnd()) { + $duration = time() - $duration->getBegin()->getTimestamp(); + } else { + $duration = $duration->getDuration(); + } + } + + return (int) $duration; + } + + private function formatDuration(int $seconds, string $format): string + { + if ($seconds < 0) { + return '?'; + } + + return $this->durationFormatter->format($seconds, $format); + } + + /** + * @param string|float $amount + * @return bool|false|string + */ + public function amount($amount) + { + return $this->helper->amount($amount); + } + + /** + * @param string $currency + * @return string + */ + public function currency($currency) + { + return $this->helper->currency($currency); + } + + /** + * @param string $language + * @return string + */ + public function language($language) + { + return $this->helper->language($language); + } + + /** + * @param string $country + * @return string + */ + public function country($country) + { + return $this->helper->country($country); + } + + /** + * @param float $amount + * @param string|null $currency + * @param bool $withCurrency + * @return string + */ + public function money($amount, ?string $currency = null, bool $withCurrency = true) + { + return $this->helper->money($amount, $currency, $withCurrency); + } + + /** + * Takes the list of codes of the locales (languages) enabled in the + * application and returns an array with the name of each locale written + * in its own language (e.g. English, Français, Español, etc.) + * + * @return array + */ + public function getLocales() + { + $locales = []; + foreach ($this->localeFormats->getAvailableLanguages() as $locale) { + $locales[] = ['code' => $locale, 'name' => Locales::getName($locale, $locale)]; + } + + return $locales; + } +} diff --git a/src/Utils/LocaleFormats.php b/src/Utils/LocaleFormats.php new file mode 100644 index 00000000..cb963789 --- /dev/null +++ b/src/Utils/LocaleFormats.php @@ -0,0 +1,144 @@ +formats = $formats; + $this->locale = $locale; + } + + /** + * Returns an array with all available locale/language codes. + * + * @return string[] + */ + public function getAvailableLanguages(): array + { + return $this->formats->getAvailableLanguages(); + } + + /** + * Returns the current locale used by the user in this request. + * + * @return string + */ + public function getLocale(): string + { + return $this->locale; + } + + /** + * Returns the format which is used by the form component to handle date values. + * + * @return string + */ + public function getDateTypeFormat(): string + { + return $this->formats->getDateTypeFormat($this->getLocale()); + } + + /** + * Returns the format which is used by the Javascript component to handle date values. + * + * @return string + */ + public function getDatePickerFormat(): string + { + return $this->formats->getDatePickerFormat($this->getLocale()); + } + + /** + * Returns the format which is used by the form component to handle datetime values. + * + * @return string + */ + public function getDateTimeTypeFormat(): string + { + return $this->formats->getDateTimeTypeFormat($this->getLocale()); + } + + /** + * Returns the format which is used by the Javascript component to handle datetime values. + * + * @return string + */ + public function getDateTimePickerFormat(): string + { + return $this->formats->getDateTimePickerFormat($this->getLocale()); + } + + /** + * Returns the locale specific date format, which should be used in combination with the twig filter "|date". + * + * @return string + */ + public function getDateFormat(): string + { + return $this->formats->getDateFormat($this->getLocale()); + } + + /** + * Returns the locale specific time format, which should be used in combination with the twig filter "|time". + * + * @return string + */ + public function getTimeFormat(): string + { + return $this->formats->getTimeFormat($this->getLocale()); + } + + /** + * Returns the locale specific datetime format, which should be used in combination with the twig filter "|date". + * + * @return string + */ + public function getDateTimeFormat(): string + { + return $this->formats->getDateTimeFormat($this->getLocale()); + } + + /** + * Returns the format used in the "|duration" twig filter to display a Timesheet duration. + * + * @return string + */ + public function getDurationFormat(): string + { + return $this->formats->getDurationFormat($this->getLocale()); + } + + /** + * Returns whether this locale uses the 24 hour format. + * + * @return bool + */ + public function isTwentyFourHours(): bool + { + return $this->formats->isTwentyFourHours($this->getLocale()); + } +} diff --git a/src/Utils/LocaleHelper.php b/src/Utils/LocaleHelper.php new file mode 100644 index 00000000..dc4bbb1a --- /dev/null +++ b/src/Utils/LocaleHelper.php @@ -0,0 +1,151 @@ +locale = $locale; + } + + /** + * Transforms seconds into a decimal formatted duration string. + * + * @param int $seconds + * @return string + */ + public function durationDecimal(int $seconds) + { + return $this->getNumberFormatter()->format(number_format($seconds / 3600, 2)); + } + + /** + * @param string|float $amount + * @return bool|false|string + */ + public function amount($amount) + { + return $this->getNumberFormatter()->format($amount); + } + + /** + * @param string $currency + * @return string + */ + public function currency($currency) + { + try { + return Currencies::getSymbol(strtoupper($currency), $this->locale); + } catch (\Exception $ex) { + } + + return $currency; + } + + /** + * @param string $language + * @return string + */ + public function language(string $language) + { + try { + return Languages::getName(strtolower($language), $this->locale); + } catch (\Exception $ex) { + } + + return $language; + } + + /** + * @param string $country + * @return string + */ + public function country(string $country) + { + try { + return Countries::getName(strtoupper($country), $this->locale); + } catch (\Exception $ex) { + } + + return $country; + } + + /** + * @param int|float $amount + * @param string|null $currency + * @param bool $withCurrency + * @return string + */ + public function money($amount, ?string $currency = null, bool $withCurrency = true) + { + if (null === $currency) { + $withCurrency = false; + } + + return $this->getMoneyFormatter($withCurrency)->formatCurrency($amount, $currency); + } + + private function getNumberFormatter(): NumberFormatter + { + if (null === $this->numberFormatter) { + $this->numberFormatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL); + } + + return $this->numberFormatter; + } + + private function getMoneyFormatter(bool $withCurrency = true): NumberFormatter + { + if (null === $this->moneyFormatter) { + $this->moneyFormatter = new NumberFormatter($this->locale, NumberFormatter::CURRENCY); + } + + if ($withCurrency) { + return $this->moneyFormatter; + } + + if (null === $this->moneyFormatterNoCurrency) { + // if anyone knows a better way of achieving this, please let me know! + $this->moneyFormatterNoCurrency = new NumberFormatter($this->locale, NumberFormatter::CURRENCY); + $pattern = $this->moneyFormatterNoCurrency->getPattern(); + $pattern = str_replace('¤ ', '¤', $pattern); + $pattern = str_replace(' ¤', '¤', $pattern); + $this->moneyFormatterNoCurrency->setPattern($pattern); + $this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::CURRENCY_SYMBOL, ''); + $this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::CURRENCY_CODE, ''); + $this->moneyFormatterNoCurrency->setSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL, ''); + } + + return $this->moneyFormatterNoCurrency; + } +} diff --git a/src/Utils/LocaleSettings.php b/src/Utils/LocaleSettings.php index 5a66808e..c68b2c78 100644 --- a/src/Utils/LocaleSettings.php +++ b/src/Utils/LocaleSettings.php @@ -16,133 +16,15 @@ use Symfony\Component\HttpFoundation\RequestStack; /** * Use this class, when you want information about formats for the "current request locale". */ -final class LocaleSettings +final class LocaleSettings extends LocaleFormats { - /** - * @var LanguageFormattings - */ - private $formats; - /** - * @var string - */ - private $locale = Constants::DEFAULT_LOCALE; - public function __construct(RequestStack $requestStack, LanguageFormattings $formats) { + $locale = Constants::DEFAULT_LOCALE; // request is null in a console command if (null !== $requestStack->getMasterRequest()) { - $this->locale = $requestStack->getMasterRequest()->getLocale(); + $locale = $requestStack->getMasterRequest()->getLocale(); } - $this->formats = $formats; - } - - /** - * Returns an array with all available locale/language codes. - * - * @return string[] - */ - public function getAvailableLanguages(): array - { - return $this->formats->getAvailableLanguages(); - } - - /** - * Returns the current locale used by the user in this request. - * - * @return string - */ - public function getLocale(): string - { - return $this->locale; - } - - /** - * Returns the format which is used by the form component to handle date values. - * - * @return string - */ - public function getDateTypeFormat(): string - { - return $this->formats->getDateTypeFormat($this->getLocale()); - } - - /** - * Returns the format which is used by the Javascript component to handle date values. - * - * @return string - */ - public function getDatePickerFormat(): string - { - return $this->formats->getDatePickerFormat($this->getLocale()); - } - - /** - * Returns the format which is used by the form component to handle datetime values. - * - * @return string - */ - public function getDateTimeTypeFormat(): string - { - return $this->formats->getDateTimeTypeFormat($this->getLocale()); - } - - /** - * Returns the format which is used by the Javascript component to handle datetime values. - * - * @return string - */ - public function getDateTimePickerFormat(): string - { - return $this->formats->getDateTimePickerFormat($this->getLocale()); - } - - /** - * Returns the locale specific date format, which should be used in combination with the twig filter "|date". - * - * @return string - */ - public function getDateFormat(): string - { - return $this->formats->getDateFormat($this->getLocale()); - } - - /** - * Returns the locale specific time format, which should be used in combination with the twig filter "|time". - * - * @return string - */ - public function getTimeFormat(): string - { - return $this->formats->getTimeFormat($this->getLocale()); - } - - /** - * Returns the locale specific datetime format, which should be used in combination with the twig filter "|date". - * - * @return string - */ - public function getDateTimeFormat(): string - { - return $this->formats->getDateTimeFormat($this->getLocale()); - } - - /** - * Returns the format used in the "|duration" twig filter to display a Timesheet duration. - * - * @return string - */ - public function getDurationFormat(): string - { - return $this->formats->getDurationFormat($this->getLocale()); - } - - /** - * Returns whether this locale uses the 24 hour format. - * - * @return bool - */ - public function isTwentyFourHours(): bool - { - return $this->formats->isTwentyFourHours($this->getLocale()); + parent::__construct($formats, $locale); } } diff --git a/templates/customer/details.html.twig b/templates/customer/details.html.twig index 510f0df1..cbdb225f 100644 --- a/templates/customer/details.html.twig +++ b/templates/customer/details.html.twig @@ -85,7 +85,7 @@ {% if customer.timezone is not empty %} {{ 'label.timezone'|trans }} - {{ now|date_full(false) }} + {{ now|date_time }} {% endif %} {% if customer.currency is not empty %} diff --git a/templates/customer/embed_budget.html.twig b/templates/customer/embed_budget.html.twig index c30511f6..af5fdf89 100644 --- a/templates/customer/embed_budget.html.twig +++ b/templates/customer/embed_budget.html.twig @@ -3,17 +3,18 @@ {% block box_title %}{{ 'label.budget'|trans }}{% endblock %} {% block box_attributes %}id="budget_box"{% endblock %} {% block box_body %} + {% set currency = customer.currency %} + {% set params = { '%activity%': '' ~ stats.activityAmount ~ '', '%project%': '' ~ stats.projectAmount ~ '', '%customer%': '' ~ customer.name ~ '', '%records%': '' ~ stats.recordAmount ~ '', '%duration%': '' ~ stats.recordDuration|duration ~ '', - '%rate%': '' ~ stats.recordRate|money ~ '' + '%rate%': '' ~ stats.recordRate|money(currency) ~ '', + '%internal_rate%': '' ~ stats.recordInternalRate|money(currency) ~ '' } %} - {% set currency = customer.currency %} -

{{ 'admin_customer.short_stats'|trans(params)|raw }} {{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}. diff --git a/templates/invoice/renderer/default-pdf.pdf.twig b/templates/invoice/renderer/default-pdf.pdf.twig index 2c155c32..c65c8a19 100644 --- a/templates/invoice/renderer/default-pdf.pdf.twig +++ b/templates/invoice/renderer/default-pdf.pdf.twig @@ -22,7 +22,7 @@ {{ model.template.title }} - {{ 'label.date'|trans({}, 'messages', language) }}: {{ model.invoiceDate|date_short }} + {{ 'label.date'|trans }}: {{ model.invoiceDate|date_short }} @@ -32,10 +32,10 @@