invoices: unified money, number and date formats and fully respect configured language (#1814)

This commit is contained in:
Kevin Papst
2020-07-10 15:09:52 +02:00
committed by GitHub
parent 19e4ebf88c
commit 32c1e3258e
67 changed files with 1593 additions and 2335 deletions

View File

@@ -10,6 +10,8 @@ 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)

View File

@@ -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());
}
}
}

View File

@@ -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);
}
/**

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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()),

View File

@@ -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

View File

@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceModel;
use App\Invoice\RendererInterface;
use App\Twig\DateExtensions;
use App\Twig\LocaleExtensions;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Twig\Environment;
/**
* @internal
*/
abstract class AbstractTwigRenderer implements RendererInterface
{
/**
* @var Environment
*/
private $twig;
public function __construct(Environment $twig)
{
$this->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;
}
}

View File

@@ -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);

View File

@@ -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',

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,219 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Configuration\LanguageFormattings;
use App\Constants;
use App\Entity\Timesheet;
use App\Utils\Duration;
use App\Utils\LocaleFormats;
use App\Utils\LocaleHelper;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Languages;
use Symfony\Component\Intl\Locales;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* Locale specific Twig extensions
*/
final class LocaleExtensions extends AbstractExtension
{
/**
* @var LocaleFormats
*/
private $localeFormats;
/**
* @var Duration
*/
private $durationFormatter;
/**
* @var LocaleHelper
*/
private $helper;
/**
* @var LanguageFormattings
*/
private $formats;
public function __construct(RequestStack $requestStack, LanguageFormattings $formats)
{
$locale = Constants::DEFAULT_LOCALE;
// request is null in a console command
if (null !== $requestStack->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;
}
}

144
src/Utils/LocaleFormats.php Normal file
View File

@@ -0,0 +1,144 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Utils;
use App\Configuration\LanguageFormattings;
use App\Constants;
/**
* Use this class, when you want information about formats for a locale.
*/
class LocaleFormats
{
/**
* @var LanguageFormattings
*/
private $formats;
/**
* @var string
*/
private $locale = Constants::DEFAULT_LOCALE;
public function __construct(LanguageFormattings $formats, string $locale)
{
$this->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());
}
}

151
src/Utils/LocaleHelper.php Normal file
View File

@@ -0,0 +1,151 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Utils;
use NumberFormatter;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Intl\Currencies;
use Symfony\Component\Intl\Languages;
final class LocaleHelper
{
/**
* @var string
*/
private $locale;
/**
* @var NumberFormatter
*/
private $numberFormatter;
/**
* @var NumberFormatter
*/
private $moneyFormatter;
/**
* @var NumberFormatter
*/
private $moneyFormatterNoCurrency;
public function __construct(string $locale)
{
$this->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;
}
}

View File

@@ -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);
}
}

View File

@@ -85,7 +85,7 @@
{% if customer.timezone is not empty %}
<tr>
<th>{{ 'label.timezone'|trans }}</th>
<td><span data-toggle="tooltip" data-placement="top" title="{{ customer.timezone }}">{{ now|date_full(false) }}</span></td>
<td><span data-toggle="tooltip" data-placement="top" title="{{ customer.timezone }}">{{ now|date_time }}</span></td>
</tr>
{% endif %}
{% if customer.currency is not empty %}

View File

@@ -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%': '<strong>' ~ stats.activityAmount ~ '</strong>',
'%project%': '<strong>' ~ stats.projectAmount ~ '</strong>',
'%customer%': '<strong>' ~ customer.name ~ '</strong>',
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>',
'%rate%': '<strong>' ~ stats.recordRate|money ~ '</strong>'
'%rate%': '<strong>' ~ stats.recordRate|money(currency) ~ '</strong>',
'%internal_rate%': '<strong>' ~ stats.recordInternalRate|money(currency) ~ '</strong>'
} %}
{% set currency = customer.currency %}
<p>
{{ 'admin_customer.short_stats'|trans(params)|raw }}
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.

View File

@@ -22,7 +22,7 @@
{{ model.template.title }}
</td>
<td class="date">
{{ 'label.date'|trans({}, 'messages', language) }}: {{ model.invoiceDate|date_short }}
{{ 'label.date'|trans }}: {{ model.invoiceDate|date_short }}
</td>
</tr>
</table>
@@ -32,10 +32,10 @@
<table class="footer">
<tr>
<td>
<strong>{{ 'label.contact'|trans({}, 'messages', language) }}</strong>:
<strong>{{ 'label.contact'|trans }}</strong>:
{{ model.template.contact|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
<br>
<strong>{{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}</strong>:
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>:
{{ model.template.paymentDetails|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
</td>
<td align="right">
@@ -50,33 +50,33 @@ mpdf-->
<table class="addresses">
<tr>
<td width="60%">
{{ 'invoice.to'|trans({}, 'messages', language) }}
{{ 'invoice.to'|trans }}
<br>
<strong>{{ model.customer.company|default(model.customer.name) }}</strong>
<br>
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
{% endif %}
{% if model.customer.number is not empty %}
<br>
{{ 'label.number'|trans({}, 'messages', language) }}: {{ model.customer.number }}
{{ 'label.number'|trans }}: {{ model.customer.number }}
{% endif %}
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
<br>
{{ 'label.orderNumber'|trans({}, 'messages', language) }}: {{ model.query.project.orderNumber }}
{{ 'label.orderNumber'|trans }}: {{ model.query.project.orderNumber }}
{% endif %}
</td>
<td>
{{ 'invoice.from'|trans({}, 'messages', language) }}
{{ 'invoice.from'|trans }}
<br>
<strong>{{ model.template.company }}</strong>
<br>
{{ model.template.address|trim|nl2br }}
{% if model.template.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans({}, 'messages', language) }}:
{{ 'label.vat_id'|trans }}:
{{ model.template.vatId }}
{% endif %}
</td>
@@ -84,22 +84,22 @@ mpdf-->
</table>
<p>
<strong>{{ 'invoice.number'|trans({}, 'messages', language) }}:</strong>
<strong>{{ 'invoice.number'|trans }}:</strong>
{{ model.invoiceNumber }}
<br>
<strong>{{ 'invoice.due_days'|trans({}, 'messages', language) }}:</strong>
<strong>{{ 'invoice.due_days'|trans }}:</strong>
{{ model.dueDate|date_short }}
</p>
<table class="items">
<thead>
<tr>
<th class="first">{{ 'label.date'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.description'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.unit_price'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.amount'|trans({}, 'messages', language) }}</th>
<th class="last text-right">{{ 'label.total_rate'|trans({}, 'messages', language) }}</th>
<th class="first">{{ 'label.date'|trans }}</th>
<th>{{ 'label.description'|trans }}</th>
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
<th class="text-right">{{ 'label.amount'|trans }}</th>
<th class="last text-right">{{ 'label.total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -129,19 +129,19 @@ mpdf-->
<tfoot>
<tr>
<td colspan="4" class="text-right">
{{ 'invoice.subtotal'|trans({}, 'messages', language) }}
{{ 'invoice.subtotal'|trans }}
</td>
<td class="last text-right">{{ model.calculator.subtotal|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-right">
{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
</td>
<td class="last text-right">{{ model.calculator.tax|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-right text-nowrap">
<strong>{{ 'invoice.total'|trans({}, 'messages', language) }}</strong>
<strong>{{ 'invoice.total'|trans }}</strong>
</td>
<td class="last text-right">
<strong>{{ model.calculator.total|money(currency) }}</strong>

View File

@@ -1,48 +1,46 @@
{% extends 'invoice/layout.html.twig' %}
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
{% block invoice %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
<span contenteditable="true">{{ model.template.title }}</span>
<small class="pull-right">{{ 'label.date'|trans({}, 'messages', language) }}: {{ model.invoiceDate|date_short }}</small>
<small class="pull-right">{{ 'label.date'|trans }}: {{ model.invoiceDate|date_short }}</small>
</h2>
</div>
</div>
<div class="row">
<div class="col-sm-5">
{{ 'invoice.to'|trans({}, 'messages', language) }}
{{ 'invoice.to'|trans }}
<address contenteditable="true">
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
{% endif %}
{% if model.customer.number is not empty %}
<br>
{{ 'label.number'|trans({}, 'messages', language) }}: {{ model.customer.number }}
{{ 'label.number'|trans }}: {{ model.customer.number }}
{% endif %}
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
<br>
{{ 'label.orderNumber'|trans({}, 'messages', language) }}: {{ model.query.project.orderNumber }}
{{ 'label.orderNumber'|trans }}: {{ model.query.project.orderNumber }}
{% endif %}
</address>
</div>
<div class="col-sm-2"></div>
<div class="col-sm-5">
{{ 'invoice.from'|trans({}, 'messages', language) }}
{{ 'invoice.from'|trans }}
<address contenteditable="true">
<strong>{{ model.template.company }}</strong><br>
{{ model.template.address|trim|nl2br }}
{% if model.template.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans({}, 'messages', language) }}:
{{ 'label.vat_id'|trans }}:
{{ model.template.vatId }}
{% endif %}
</address>
@@ -52,11 +50,11 @@
<div class="row">
<div class="col-sm-5">
<p contenteditable="true">
<strong>{{ 'invoice.number'|trans({}, 'messages', language) }}:</strong>
<strong>{{ 'invoice.number'|trans }}:</strong>
{{ model.invoiceNumber }}
<br>
<strong>{{ 'invoice.due_days'|trans({}, 'messages', language) }}:</strong>
<strong>{{ 'invoice.due_days'|trans }}:</strong>
{{ model.dueDate|date_short }}
</p>
</div>
@@ -68,11 +66,11 @@
<table class="table">
<thead>
<tr>
<th>{{ 'label.date'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.description'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.unit_price'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.amount'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.total_rate'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'label.description'|trans }}</th>
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
<th class="text-right">{{ 'label.amount'|trans }}</th>
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -102,19 +100,19 @@
<tfoot>
<tr>
<td colspan="4" class="text-right">
{{ 'invoice.subtotal'|trans({}, 'messages', language) }}
{{ 'invoice.subtotal'|trans }}
</td>
<td class="text-right">{{ model.calculator.subtotal|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-right">
{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
</td>
<td class="text-right">{{ model.calculator.tax|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-right text-nowrap">
<strong>{{ 'invoice.total'|trans({}, 'messages', language) }}</strong>
<strong>{{ 'invoice.total'|trans }}</strong>
</td>
<td class="text-right">
<strong>{{ model.calculator.total|money(currency) }}</strong>
@@ -137,12 +135,11 @@
<footer class="footer">
<p>
<strong>{{ 'label.address'|trans({}, 'messages', language) }}</strong>: {{ model.template.company }} &ndash; {{ model.template.address|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.company }} &ndash; {{ model.template.address|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
<br>
<strong>{{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
<br>
<strong>{{ 'label.contact'|trans({}, 'messages', language) }}</strong>: {{ model.template.contact|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
<strong>{{ 'label.contact'|trans }}</strong>: {{ model.template.contact|replace({"\n": ' &ndash; ', "\r\n": ' &ndash; ', "\r": ' &ndash; '})|raw }}
</p>
</footer>
{% endblock %}

View File

@@ -1,11 +1,9 @@
{% import "macros/widgets.html.twig" as widgets %}
{% extends 'invoice/layout.html.twig' %}
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
{% block invoice %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
<div class="row" id="freelancer-invoice">
<div class="col-xs-12">
<header>
@@ -14,33 +12,33 @@
</address>
</header>
<article class="address">
<h1>{{ 'invoice.to'|trans({}, 'messages', language) }}</h1>
<h1>{{ 'invoice.to'|trans }}</h1>
<address>
<p>
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
{% endif %}
</p>
</address>
<table class="meta">
<tr>
<th>{{ 'label.date'|trans({}, 'messages', language) }}:</th>
<th>{{ 'label.date'|trans }}:</th>
<td contenteditable="true">{{ model.invoiceDate|date_short }}</td>
</tr>
<tr>
<th>{{ 'invoice.service_date'|trans({}, 'messages', language) }}:</th>
<td><span contenteditable="true">{{ model.query.end|month_name|trans({}, 'messages', language) }} {{ model.query.end|date('Y') }}</span></td>
<th>{{ 'invoice.service_date'|trans }}:</th>
<td><span contenteditable="true">{{ model.query.end|month_name }} {{ model.query.end|date('Y') }}</span></td>
</tr>
<tr>
<th>{{ 'invoice.number'|trans({}, 'messages', language) }}:</th>
<th>{{ 'invoice.number'|trans }}:</th>
<td>{{ model.invoiceNumber }}</td>
</tr>
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
<tr>
<th>{{ 'label.orderNumber'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.orderNumber'|trans }}</th>
<td contenteditable="true">
{{ model.query.project.orderNumber }}
</td>
@@ -48,7 +46,7 @@
{% endif %}
{% if model.template.vatId is not empty %}
<tr>
<th>{{ 'label.vat_id'|trans({}, 'messages', language) }}:</th>
<th>{{ 'label.vat_id'|trans }}:</th>
<td>
{{ model.template.vatId }}
</td>
@@ -61,10 +59,10 @@
<table class="inventory">
<thead>
<tr>
<th>{{ 'label.description'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.unit_price'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.amount'|trans({}, 'messages', language) }}</th>
<th class="text-right">{{ 'label.total_rate'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.description'|trans }}</th>
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
<th class="text-right">{{ 'label.amount'|trans }}</th>
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -94,15 +92,15 @@
</table>
<table class="balance">
<tr>
<th>{{ 'invoice.subtotal'|trans({}, 'messages', language) }}</th>
<th>{{ 'invoice.subtotal'|trans }}</th>
<td>{{ model.calculator.subtotal|money(currency) }}</td>
</tr>
<tr>
<th>{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)</th>
<th>{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)</th>
<td>{{ model.calculator.tax|money(currency) }}</td>
</tr>
<tr>
<th class="total">{{ 'invoice.total'|trans({}, 'messages', language) }}</th>
<th class="total">{{ 'invoice.total'|trans }}</th>
<td class="total">{{ model.calculator.total|money(currency) }}</td>
</tr>
</table>
@@ -118,7 +116,7 @@
<div class="row">
<div class="col-sm-4" contenteditable="true">
<p>
<strong>{{ 'label.address'|trans({}, 'messages', language) }}</strong>
<strong>{{ 'label.address'|trans }}</strong>
</p>
<p>
{{ model.template.company }}<br>
@@ -127,7 +125,7 @@
</div>
<div class="col-sm-4 text-center" contenteditable="true">
<p>
<strong>{{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}</strong>
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>
</p>
<p>
{{ model.template.paymentDetails|nl2br }}
@@ -135,7 +133,7 @@
</div>
<div class="col-sm-4 text-right" contenteditable="true">
<p>
<strong>{{ 'label.contact'|trans({}, 'messages', language) }}</strong>
<strong>{{ 'label.contact'|trans }}</strong>
</p>
<p>
{{ model.template.contact|nl2br }}

View File

@@ -1,10 +1,8 @@
{% import "macros/widgets.html.twig" as widgets %}
{% extends 'invoice/layout.html.twig' %}
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% block invoice %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
@@ -17,7 +15,7 @@
<div class="col-xs-12">
<table class="table no-border table-condensed">
<tr>
<th>{{ 'invoice.from'|trans({}, 'messages', language) }}</th>
<th>{{ 'invoice.from'|trans }}</th>
<td contenteditable="true">
{% if model.query.user is not empty %}
{{ widgets.username(model.query.user) }}
@@ -27,17 +25,17 @@
</td>
</tr>
<tr>
<th>{{ 'label.date'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.date'|trans }}</th>
<td contenteditable="true">
{% if model.query.begin|date('m') != model.query.end|date('m') or model.query.begin|date('Y') != model.query.end|date('Y') %}
{{ model.query.begin|date_short }} - {{ model.query.end|date_short }}
{% else %}
{{ model.query.end|month_name|trans({}, 'messages', language) }} {{ model.query.end|date('Y') }}
{{ model.query.end|month_name }} {{ model.query.end|date('Y') }}
{% endif %}
</td>
</tr>
<tr>
<th>{{ 'label.customer'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.customer'|trans }}</th>
<td contenteditable="true">
{% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %}
{{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %}
@@ -45,7 +43,7 @@
</tr>
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
<tr>
<th>{{ 'label.orderNumber'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.orderNumber'|trans }}</th>
<td contenteditable="true">
{{ model.query.project.orderNumber }}
</td>
@@ -60,12 +58,12 @@
<table class="table table-striped">
<thead>
<tr>
<th>{{ 'label.date'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.date'|trans }}</th>
{% if model.query.user is empty %}
<th>{{ 'label.user'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.user'|trans }}</th>
{% endif %}
<th>{{ 'label.activity'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.hours'|trans({}, 'messages', language) }}</th>
<th>{{ 'label.activity'|trans }}</th>
<th>{{ 'label.hours'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -92,7 +90,7 @@
{% if model.query.user is empty %}
<th></th>
{% endif %}
<th>{{ 'invoice.total_working_time'|trans({}, 'messages', language) }}</th>
<th>{{ 'invoice.total_working_time'|trans }}</th>
<th class="text-nowrap">{{ model.calculator.timeWorked|duration(isDecimal) }}</th>
</tr>
</tfoot>
@@ -103,7 +101,7 @@
<div class="row">
<div class="col-xs-12">
{% if model.template.paymentTerms is not empty %}
<p class="lead">{{ 'label.payment_terms'|trans({}, 'messages', language) }}</p>
<p class="lead">{{ 'label.payment_terms'|trans }}</p>
<p class="text-muted well well-sm no-shadow" contenteditable="true" style="margin-bottom: 100px">
{{ model.template.paymentTerms|trim|nl2br }}
@@ -114,10 +112,10 @@
<table class="table">
<tbody>
<tr>
<th style="padding-bottom: 60px">{{ 'invoice.signature_user'|trans({}, 'messages', language) }}</th>
<th style="padding-bottom: 60px">{{ 'invoice.signature_user'|trans }}</th>
</tr>
<tr>
<th>{{ 'invoice.signature_customer'|trans({}, 'messages', language) }}</th>
<th>{{ 'invoice.signature_customer'|trans }}</th>
</tr>
</tbody>
</table>

View File

@@ -32,6 +32,7 @@
{% if seeOwnRate %}
<tr>
<th>{{ 'stats.amountMonth'|trans }}</th>
{# TODO which currency shall we use here? #}
<td class="text-nowrap pull-right">{{ stats.amountThisMonth|money }}</td>
</tr>
{% endif %}
@@ -42,6 +43,7 @@
{% if seeOwnRate %}
<tr>
<th>{{ 'stats.amountTotal'|trans }}</th>
{# TODO which currency shall we use here? #}
<td class="text-nowrap pull-right">{{ stats.amountTotal|money }}</td>
</tr>
{% endif %}
@@ -103,73 +105,3 @@
});
</script>
{% endblock %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_list_unused(user, items) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user-2">
<!-- Add the bg color to the header using any of the bg-* classes -->
<div class="widget-user-header bg-green">
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<!-- /.widget-user-image -->
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="box-footer no-padding">
<ul class="nav nav-stacked">
{% for entry in items %}
<li><a href="{{ entry.url }}">{{ entry.title|trans }} <span class="pull-right badge bg-{{ entry.color }}">{{ entry.value }}</span></a></li>
{% endfor %}
</ul>
</div>
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_box_unused(user, stats) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user">
<div class="widget-user-header bg-green">
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<div class="box-footer">
<div class="row">
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationTotal|duration }}</h5>
<span class="description-text">{{ 'stats.durationTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountTotal|money }}</h5>
<span class="description-text">{{ 'stats.amountTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationThisMonth|duration }}</h5>
<span class="description-text">{{ 'stats.durationMonth'|trans }}</span>
</div>
</div>
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountThisMonth|money }}</h5>
<span class="description-text">{{ 'stats.amountMonth'|trans }}</span>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -53,7 +53,7 @@
<div class="col-sm-3 col-xs-6">
<div class="description-block border-right">
<h5 class="description-header">{{ data.month|duration }}</h5>
<span class="description-text">{{ 'stats.workingTimeMonth'|trans({'%month%': data.begin|month_name|trans, '%year%': data.begin|date_format('Y')}) }}</span>
<span class="description-text">{{ 'stats.workingTimeMonth'|trans({'%month%': data.begin|month_name, '%year%': data.begin|date_format('Y')}) }}</span>
</div>
</div>
<div class="col-sm-3 col-xs-6">

View File

@@ -28,6 +28,8 @@ class TimesheetTest extends TestCase
public function testDefaultValues()
{
$sut = new Timesheet();
self::assertEquals('timesheet', $sut->getType());
self::assertEquals('work', $sut->getCategory());
self::assertNull($sut->getId());
self::assertNull($sut->getBegin());
self::assertNull($sut->getEnd());

View File

@@ -29,7 +29,6 @@ use App\Export\ExportRendererInterface;
use App\Export\RendererInterface;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use App\Utils\LocaleSettings;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -60,10 +59,8 @@ abstract class AbstractRendererTest extends KernelTestCase
$request->setLocale('en');
$requestStack->push($request);
$localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($languages));
$translator = $this->createMock(TranslatorInterface::class);
$dateExtension = new DateExtensions($localeSettings);
$dateExtension = new DateExtensions($requestStack, new LanguageFormattings($languages));
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());

View File

@@ -28,7 +28,6 @@ use App\Event\TimesheetMetaDisplayEvent;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use App\Utils\LocaleSettings;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -59,10 +58,8 @@ abstract class AbstractRendererTest extends KernelTestCase
$request->setLocale('en');
$requestStack->push($request);
$localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($languages));
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$dateExtension = new DateExtensions($localeSettings);
$dateExtension = new DateExtensions($requestStack, new LanguageFormattings($languages));
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());

View File

@@ -32,17 +32,22 @@ class DebugFormatter implements InvoiceFormatter
}
/**
* @param mixed $amount
* @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)
{
if (null !== $currency) {
if (null === $currency) {
$withCurrency = false;
}
if ($withCurrency) {
return $amount . ' ' . $currency;
}
return $amount;
return (string) $amount;
}
/**

View File

@@ -16,6 +16,7 @@ use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* @covers \App\Invoice\Renderer\AbstractTwigRenderer
* @covers \App\Invoice\Renderer\JsonRenderer
* @group integration
*/

View File

@@ -17,6 +17,7 @@ use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* @covers \App\Invoice\Renderer\AbstractTwigRenderer
* @covers \App\Invoice\Renderer\PdfRenderer
* @group integration
*/

View File

@@ -30,11 +30,9 @@ use App\Invoice\NumberGenerator\DateNumberGenerator;
use App\Invoice\Renderer\AbstractRenderer;
use App\Repository\Query\InvoiceQuery;
use App\Twig\DateExtensions;
use App\Twig\Extensions;
use App\Utils\LocaleSettings;
use App\Twig\LocaleExtensions;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\Translation\TranslatorInterface;
trait RendererTestTrait
{
@@ -87,13 +85,12 @@ trait RendererTestTrait
$request->setLocale('en');
$requestStack->push($request);
$localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($languages));
$formattings = new LanguageFormattings($languages);
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$dateExtension = new DateExtensions($localeSettings);
$extensions = new Extensions($localeSettings);
$dateExtension = new DateExtensions($requestStack, $formattings);
$extensions = new LocaleExtensions($requestStack, $formattings);
return new DefaultInvoiceFormatter($translator, $dateExtension, $extensions);
return new DefaultInvoiceFormatter($dateExtension, $extensions);
}
protected function getInvoiceModel(): InvoiceModel

View File

@@ -16,6 +16,7 @@ use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* @covers \App\Invoice\Renderer\AbstractTwigRenderer
* @covers \App\Invoice\Renderer\TextRenderer
* @group integration
*/

View File

@@ -16,6 +16,7 @@ use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* @covers \App\Invoice\Renderer\AbstractTwigRenderer
* @covers \App\Invoice\Renderer\TwigRenderer
* @group integration
*/
@@ -56,6 +57,7 @@ class TwigRendererTest extends KernelTestCase
$sut = new TwigRenderer($twig);
$model = $this->getInvoiceModel();
$model->getTemplate()->setLanguage('de');
$document = $this->getInvoiceDocument('timesheet.html.twig');
$response = $sut->render($document, $model);

View File

@@ -16,6 +16,7 @@ use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* @covers \App\Invoice\Renderer\AbstractTwigRenderer
* @covers \App\Invoice\Renderer\XmlRenderer
* @group integration
*/

View File

@@ -11,7 +11,6 @@ namespace App\Tests\Twig;
use App\Configuration\LanguageFormattings;
use App\Twig\DateExtensions;
use App\Utils\LocaleSettings;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -35,14 +34,12 @@ class DateExtensionsTest extends TestCase
$requestStack = new RequestStack();
$requestStack->push($request);
$localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($dateSettings));
return new DateExtensions($localeSettings);
return new DateExtensions($requestStack, new LanguageFormattings($dateSettings));
}
public function testGetFilters()
{
$filters = ['month_name', 'date_short', 'date_time', 'date_full', 'date_format', 'time', 'hour24'];
$filters = ['month_name', 'day_name', 'date_short', 'date_time', 'date_full', 'date_format', 'time', 'hour24'];
$sut = $this->getSut('de', []);
$twigFilters = $sut->getFilters();
$this->assertCount(\count($filters), $twigFilters);
@@ -121,22 +118,39 @@ class DateExtensionsTest extends TestCase
}
/**
* @param \DateTime $date
* @param string $result
* @dataProvider getMonthData
* @dataProvider getDayNameTestData
*/
public function testMonthName(\DateTime $date, $result)
public function testDayName(string $locale, string $date, string $expectedName)
{
$sut = $this->getSut('en', []);
$this->assertEquals($result, $sut->monthName($date));
$sut = $this->getSut($locale, []);
self::assertEquals($expectedName, $sut->dayName(new \DateTime($date)));
}
public function getMonthData()
public function getDayNameTestData()
{
return [
[new \DateTime('January 2016'), 'month.1'],
[new \DateTime('2016-06-23'), 'month.6'],
[new \DateTime('2016-12-23'), 'month.12'],
['de', '2020-07-09 12:00:00', 'Donnerstag'],
['en', '2020-07-09 12:00:00', 'Thursday']
];
}
/**
* @dataProvider getMonthNameTestData
*/
public function testMonthName(string $locale, string $date, string $expectedName)
{
$sut = $this->getSut($locale, []);
self::assertEquals($expectedName, $sut->monthName(new \DateTime($date)));
}
public function getMonthNameTestData()
{
return [
['de', '2020-07-09 23:59:59', 'Juli'],
['en', '2020-07-09 23:59:59', 'July'],
['de', 'January 2016', 'Januar'],
['en', 'January 2016', 'January'],
['en', '2016-12-23', 'December'],
];
}

View File

@@ -9,15 +9,9 @@
namespace App\Tests\Twig;
use App\Configuration\LanguageFormattings;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Twig\Extensions;
use App\Utils\LocaleSettings;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Twig\TwigFilter;
use Twig\TwigFunction;
@@ -26,32 +20,15 @@ use Twig\TwigFunction;
*/
class ExtensionsTest extends TestCase
{
private $localeEn = ['en' => ['date' => 'Y-m-d', 'duration' => '%h:%m h']];
private $localeDe = ['de' => ['date' => 'd.m.Y', 'duration' => '%h:%m h']];
private $localeRu = ['ru' => ['date' => 'd.m.Y', 'duration' => '%h:%m h']];
private $localeFake = ['XX' => ['date' => 'd.m.Y', 'duration' => '%h - %m - %s Zeit']];
/**
* @param array $locales
* @param string $locale
* @return Extensions
*/
protected function getSut($locales, $locale = 'en')
protected function getSut(): Extensions
{
$request = new Request();
$request->setLocale($locale);
$requestStack = new RequestStack();
$requestStack->push($request);
$localeSettings = new LocaleSettings($requestStack, new LanguageFormattings($locales));
return new Extensions($localeSettings);
return new Extensions();
}
public function testGetFilters()
{
$filters = ['duration', 'duration_decimal', 'money', 'currency', 'country', 'language', 'amount', 'docu_link', 'multiline_indent'];
$sut = $this->getSut($this->localeDe);
$filters = ['docu_link', 'multiline_indent'];
$sut = $this->getSut();
$twigFilters = $sut->getFilters();
$this->assertCount(\count($filters), $twigFilters);
$i = 0;
@@ -64,8 +41,8 @@ class ExtensionsTest extends TestCase
public function testGetFunctions()
{
$functions = ['locales', 'class_name'];
$sut = $this->getSut($this->localeDe);
$functions = ['class_name'];
$sut = $this->getSut();
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($functions), $twigFunctions);
$i = 0;
@@ -76,216 +53,6 @@ class ExtensionsTest extends TestCase
}
}
public function testLocales()
{
$locales = [
['code' => 'en', 'name' => 'English'],
['code' => 'de', 'name' => 'Deutsch'],
['code' => 'ru', 'name' => 'русский'],
];
$appLocales = array_merge($this->localeEn, $this->localeDe, $this->localeRu);
$sut = $this->getSut($appLocales);
$this->assertEquals($locales, $sut->getLocales());
}
public function testCurrency()
{
$symbols = [
'EUR' => '€',
'USD' => '$',
'RUB' => 'RUB',
'rub' => 'RUB',
123 => 123,
];
$sut = $this->getSut($this->localeEn);
foreach ($symbols as $name => $symbol) {
$this->assertEquals($symbol, $sut->currency($name));
}
}
public function testCountry()
{
$countries = [
'DE' => 'Germany',
'RU' => 'Russia',
'ES' => 'Spain',
'es' => 'Spain',
'12' => '12',
];
$sut = $this->getSut($this->localeEn);
foreach ($countries as $locale => $name) {
$this->assertEquals($name, $sut->country($locale));
}
}
public function testLanguage()
{
$languages = [
'de' => 'German',
'ru' => 'Russian',
'es' => 'Spanish',
'ES' => 'Spanish',
'12' => '12',
];
$sut = $this->getSut($this->localeEn);
foreach ($languages as $locale => $name) {
$this->assertEquals($name, $sut->language($locale));
}
}
public function testMoneyNull()
{
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('123.75', $sut->money(123.75));
$sut = $this->getSut($this->localeEn, 'de');
$this->assertEquals('123.234,755', $sut->money(123234.7554));
}
/**
* @dataProvider getMoneyData
*/
public function testMoney($result, $amount, $currency, $locale)
{
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->money($amount, $currency));
}
public function getMoneyData()
{
return [
['0,00 €', null, 'EUR', 'de'],
['2.345,00 €', 2345, 'EUR', 'de'],
['€2,345.00', 2345, 'EUR', 'en'],
['€2,345.01', 2345.009, 'EUR', 'en'],
['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'],
['14 ¥', 13.75, 'JPY', 'de'],
['13 933 ¥', 13933.49, 'JPY', 'ru'],
['1.234.567,89 $', 1234567.891234567890000, 'USD', 'de'],
];
}
/**
* @dataProvider getAmountData
*/
public function testAmount($result, $amount, $locale)
{
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->amount($amount));
}
public function getAmountData()
{
return [
['0', null, 'de'],
['2.345,01', 2345.01, 'de'],
['2.345', 2345, 'de'],
['2,345', 2345, 'en'],
['2,345.009', 2345.009, 'en'],
['2.345,009', 2345.009, 'de'],
['13.75', 13.75, 'en'],
['13,75', 13.75, 'de'],
['13 933,49', 13933.49, 'ru'],
['1.234.567,891', 1234567.891234567890000, 'de'],
];
}
/**
* @dataProvider getMoneyData62_1
*/
public function testMoney62_1($result, $amount, $currency, $locale)
{
IntlTestHelper::requireFullIntl($this, '62.1');
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->money($amount, $currency));
}
public function getMoneyData62_1()
{
return [
['RUB 13.50', 13.50, 'RUB', 'en'],
['13,75 ₽', 13.75, 'RUB', 'ru'],
];
}
public function testDuration()
{
$record = $this->getTimesheet(9437);
$sut = $this->getSut($this->localeEn);
$this->assertEquals('02:37 h', $sut->duration($record->getDuration()));
$this->assertEquals('2.62', $sut->duration($record->getDuration(), true));
// test Timesheet object
$this->assertEquals('02:37 h', $sut->duration($record));
$this->assertEquals('2.62', $sut->duration($record, true));
// test extended format
$sut = $this->getSut($this->localeFake, 'XX');
$this->assertEquals('02 - 37 - 17 Zeit', $sut->duration($record->getDuration()));
// test negative duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('?', $sut->duration('-1'));
// test zero duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('00:00 h', $sut->duration('0'));
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('00:00 h', $sut->duration(null));
$this->assertEquals('0', $sut->duration(null, true));
}
public function testDurationDecimal()
{
$record = $this->getTimesheet(9437);
$sut = $this->getSut($this->localeEn);
$this->assertEquals('2.62', $sut->durationDecimal($record->getDuration()));
// test Timesheet object
$this->assertEquals('2.62', $sut->durationDecimal($record));
// test extended format
$sut = $this->getSut($this->localeDe, 'de');
$this->assertEquals('2,62', $sut->durationDecimal($record->getDuration()));
// test negative duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('0', $sut->durationDecimal('-1'));
// test zero duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('0', $sut->durationDecimal('0'));
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('0', $sut->durationDecimal(null));
}
protected function getTimesheet($seconds)
{
$begin = new \DateTime();
$end = clone $begin;
$end->setTimestamp($begin->getTimestamp() + $seconds);
$record = new Timesheet();
$record->setBegin($begin);
$record->setEnd($end);
$record->setDuration($seconds);
return $record;
}
public function testDocuLink()
{
$data = [
@@ -295,7 +62,7 @@ class ExtensionsTest extends TestCase
'' => 'https://www.kimai.org/documentation/',
];
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut();
foreach ($data as $input => $expected) {
$result = $sut->documentationLink($input);
$this->assertEquals($expected, $result);
@@ -304,7 +71,7 @@ class ExtensionsTest extends TestCase
public function testGetClassName()
{
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut();
$this->assertEquals('DateTime', $sut->getClassName(new \DateTime()));
$this->assertEquals('stdClass', $sut->getClassName(new \stdClass()));
$this->assertNull($sut->getClassName(''));
@@ -346,7 +113,7 @@ sdfsdf' . PHP_EOL . "\n" .
*/
public function testMultilineIndent($indent, $string, $expected)
{
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut();
self::assertEquals(implode("\n", $expected), $sut->multilineIndent($string, $indent));
}
}

View File

@@ -0,0 +1,321 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
use App\Configuration\LanguageFormattings;
use App\Entity\Timesheet;
use App\Twig\LocaleExtensions;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* @covers \App\Twig\LocaleExtensions
*/
class LocaleExtensionsTest extends TestCase
{
private $localeEn = ['en' => ['date' => 'Y-m-d', 'duration' => '%h:%m h']];
private $localeDe = ['de' => ['date' => 'd.m.Y', 'duration' => '%h:%m h']];
private $localeRu = ['ru' => ['date' => 'd.m.Y', 'duration' => '%h:%m h']];
private $localeFake = ['XX' => ['date' => 'd.m.Y', 'duration' => '%h - %m - %s Zeit']];
/**
* @param array $locales
* @param string $locale
* @return LocaleExtensions
*/
protected function getSut($locales, $locale = 'en')
{
$request = new Request();
$request->setLocale($locale);
$requestStack = new RequestStack();
$requestStack->push($request);
return new LocaleExtensions($requestStack, new LanguageFormattings($locales));
}
public function testGetFilters()
{
$filters = ['duration', 'duration_decimal', 'money', 'currency', 'country', 'language', 'amount'];
$sut = $this->getSut($this->localeDe);
$twigFilters = $sut->getFilters();
$this->assertCount(\count($filters), $twigFilters);
$i = 0;
/** @var TwigFilter $filter */
foreach ($twigFilters as $filter) {
$this->assertInstanceOf(TwigFilter::class, $filter);
$this->assertEquals($filters[$i++], $filter->getName());
}
}
public function testGetFunctions()
{
$functions = ['locales'];
$sut = $this->getSut($this->localeDe);
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($functions), $twigFunctions);
$i = 0;
/** @var TwigFunction $filter */
foreach ($twigFunctions as $filter) {
$this->assertInstanceOf(TwigFunction::class, $filter);
$this->assertEquals($functions[$i++], $filter->getName());
}
}
public function testLocales()
{
$locales = [
['code' => 'en', 'name' => 'English'],
['code' => 'de', 'name' => 'Deutsch'],
['code' => 'ru', 'name' => 'русский'],
];
$appLocales = array_merge($this->localeEn, $this->localeDe, $this->localeRu);
$sut = $this->getSut($appLocales);
$this->assertEquals($locales, $sut->getLocales());
}
public function testCurrency()
{
$symbols = [
'EUR' => '€',
'USD' => '$',
'RUB' => 'RUB',
'rub' => 'RUB',
123 => 123,
];
$sut = $this->getSut($this->localeEn);
foreach ($symbols as $name => $symbol) {
$this->assertEquals($symbol, $sut->currency($name));
}
}
public function testCountry()
{
$countries = [
'DE' => 'Germany',
'RU' => 'Russia',
'ES' => 'Spain',
'es' => 'Spain',
'12' => '12',
];
$sut = $this->getSut($this->localeEn);
foreach ($countries as $locale => $name) {
$this->assertEquals($name, $sut->country($locale));
}
}
public function testLanguage()
{
$languages = [
'de' => 'German',
'ru' => 'Russian',
'es' => 'Spanish',
'ES' => 'Spanish',
'12' => '12',
];
$sut = $this->getSut($this->localeEn);
foreach ($languages as $locale => $name) {
$this->assertEquals($name, $sut->language($locale));
}
}
public function testMoneyWithoutCurrency()
{
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('123.75', $sut->money(123.75));
$sut = $this->getSut($this->localeEn, 'de');
$this->assertEquals('123.234,76', $sut->money(123234.7554, null, true));
$this->assertEquals('123.234,76', $sut->money(123234.7554, null, false));
$this->assertEquals('123.234,76', $sut->money(123234.7554, 'EUR', false));
}
/**
* @dataProvider getMoneyNoCurrencyData
*/
public function testMoneyNoCurrency($result, $amount, $currency, $locale)
{
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->money($amount, $currency, false));
}
public function getMoneyNoCurrencyData()
{
return [
['0,00', null, 'EUR', 'de'],
['2.345,00', 2345, 'EUR', 'de'],
['2,345.00', 2345, 'EUR', 'en'],
['2,345.01', 2345.009, 'EUR', 'en'],
['2.345,01', 2345.009, 'EUR', 'de'],
['13.75', 13.75, 'USD', 'en'],
['13,75', 13.75, 'USD', 'de'],
['13,75', 13.75, 'RUB', 'de'],
['13,75', 13.75, 'JPY', 'de'],
['13 933,49', 13933.49, 'JPY', 'ru'],
['13,75', 13.75, 'CNY', 'de'],
['13.933,00', 13933, 'CNY', 'de'],
['13 933,00', 13933, 'CNY', 'ru'],
['13,933.00', 13933, 'CNY', 'en'],
['13,933.00', 13933, 'CNY', 'zh_CN'],
['1.234.567,89', 1234567.891234567890000, 'USD', 'de'],
];
}
/**
* @dataProvider getMoneyData
*/
public function testMoney($result, $amount, $currency, $locale)
{
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->money($amount, $currency));
}
public function getMoneyData()
{
return [
['0,00 €', null, 'EUR', 'de'],
['2.345,00 €', 2345, 'EUR', 'de'],
['€2,345.00', 2345, 'EUR', 'en'],
['€2,345.01', 2345.009, 'EUR', 'en'],
['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'],
['14 ¥', 13.75, 'JPY', 'de'],
['13 933 ¥', 13933.49, 'JPY', 'ru'],
['13,75 CN¥', 13.75, 'CNY', 'de'],
['13.933,00 CN¥', 13933, 'CNY', 'de'],
['13 933,00 CN¥', 13933, 'CNY', 'ru'],
['CN¥13,933.00', 13933, 'CNY', 'en'],
['1.234.567,89 $', 1234567.891234567890000, 'USD', 'de'],
];
}
/**
* @dataProvider getAmountData
*/
public function testAmount($result, $amount, $locale)
{
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->amount($amount));
}
public function getAmountData()
{
return [
['0', null, 'de'],
['2.345,01', 2345.01, 'de'],
['2.345', 2345, 'de'],
['2,345', 2345, 'en'],
['2,345.009', 2345.009, 'en'],
['2.345,009', 2345.009, 'de'],
['13.75', 13.75, 'en'],
['13,75', 13.75, 'de'],
['13 933,49', 13933.49, 'ru'],
['1.234.567,891', 1234567.891234567890000, 'de'],
];
}
/**
* @dataProvider getMoneyData62_1
*/
public function testMoney62_1($result, $amount, $currency, $locale)
{
IntlTestHelper::requireFullIntl($this, '62.1');
$sut = $this->getSut($this->localeEn, $locale);
$this->assertEquals($result, $sut->money($amount, $currency));
}
public function getMoneyData62_1()
{
return [
['RUB 13.50', 13.50, 'RUB', 'en'],
['13,75 ₽', 13.75, 'RUB', 'ru'],
];
}
public function testDuration()
{
$record = $this->getTimesheet(9437);
$sut = $this->getSut($this->localeEn);
$this->assertEquals('02:37 h', $sut->duration($record->getDuration()));
$this->assertEquals('2.62', $sut->duration($record->getDuration(), true));
// test Timesheet object
$this->assertEquals('02:37 h', $sut->duration($record));
$this->assertEquals('2.62', $sut->duration($record, true));
// test extended format
$sut = $this->getSut($this->localeFake, 'XX');
$this->assertEquals('02 - 37 - 17 Zeit', $sut->duration($record->getDuration()));
// test negative duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('?', $sut->duration('-1'));
// test zero duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('00:00 h', $sut->duration('0'));
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('00:00 h', $sut->duration(null));
$this->assertEquals('0', $sut->duration(null, true));
}
public function testDurationDecimal()
{
$record = $this->getTimesheet(9437);
$sut = $this->getSut($this->localeEn);
$this->assertEquals('2.62', $sut->durationDecimal($record->getDuration()));
// test Timesheet object
$this->assertEquals('2.62', $sut->durationDecimal($record));
// test extended format
$sut = $this->getSut($this->localeDe, 'de');
$this->assertEquals('2,62', $sut->durationDecimal($record->getDuration()));
// test negative duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('0', $sut->durationDecimal('-1'));
// test zero duration
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('0', $sut->durationDecimal('0'));
$sut = $this->getSut($this->localeEn, 'en');
$this->assertEquals('0', $sut->durationDecimal(null));
}
protected function getTimesheet($seconds)
{
$begin = new \DateTime();
$end = clone $begin;
$end->setTimestamp($begin->getTimestamp() + $seconds);
$record = new Timesheet();
$record->setBegin($begin);
$record->setEnd($end);
$record->setDuration($seconds);
return $record;
}
}

View File

@@ -0,0 +1,191 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Utils;
use App\Configuration\LanguageFormattings;
use App\Utils\LocaleFormats;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Utils\LocaleFormats
* @covers \App\Configuration\LanguageFormattings
*/
class LocaleFormatsTest extends TestCase
{
protected function getSut(string $locale, array $settings)
{
return new LocaleFormats(new LanguageFormattings($settings), $locale);
}
protected function getDefaultSettings()
{
return [
'de' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'date_time' => 'd.m. H:i',
'duration' => '%h:%m h',
'time' => 'H:i',
'24_hours' => true,
],
'en' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d',
'date_time' => 'm-d H:i',
'duration' => '%h:%m h',
'time' => 'H:i:s',
'24_hours' => false,
],
'pt_BR' => [
'date_time_type' => 'dd-MM-yyyy HH:mm',
'date_type' => 'dd-MM-yyyy',
'date' => 'd-m-Y',
'duration' => '%h:%m h',
],
'it' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'duration' => '%h:%m h',
],
'fr' => [
'date_time_type' => 'dd/MM/yyyy HH:mm',
'date_type' => 'dd/MM/yyyy',
'date' => 'd/m/Y',
'duration' => '%h h %m',
],
'es' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'duration' => '%h:%m h',
],
'ru' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'duration' => '%h:%m h',
],
'ar' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d',
'duration' => '%h:%m h',
],
'hu' => [
'date_time_type' => 'yyyy.MM.dd HH:mm',
'date_type' => 'yyyy.MM.dd',
'date' => 'Y.m.d.',
'duration' => '%h:%m h',
],
];
}
public function testGetLocale()
{
$sut = $this->getSut('en', []);
$this->assertEquals('en', $sut->getLocale());
$sut = $this->getSut('ar', []);
$this->assertEquals('ar', $sut->getLocale());
}
public function testGetAvailableLanguages()
{
$sut = $this->getSut('en', []);
$this->assertEquals([], $sut->getAvailableLanguages());
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals(['de', 'en', 'pt_BR', 'it', 'fr', 'es', 'ru', 'ar', 'hu'], $sut->getAvailableLanguages());
}
public function testInvalidLocaleWithDefaultLocale()
{
$this->expectException(\InvalidArgumentException::class);
$sut = $this->getSut('en', []);
$sut->getDateFormat();
}
public function testInvalidLocaleWithGivenLocale()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown locale given: xx');
$sut = $this->getSut('xx', $this->getDefaultSettings());
$sut->getDateFormat();
}
public function testGetDurationFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('%h:%m h', $sut->getDurationFormat());
}
public function testGetDateFormat()
{
$sut = $this->getSut('de', $this->getDefaultSettings());
$this->assertEquals('d.m.Y', $sut->getDateFormat());
}
public function testGetDateTimeFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('m-d H:i', $sut->getDateTimeFormat());
}
public function testGetDateTypeFormat()
{
$sut = $this->getSut('de', $this->getDefaultSettings());
$this->assertEquals('dd.MM.yyyy', $sut->getDateTypeFormat());
}
public function testGetDatePickerFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('YYYY-MM-DD', $sut->getDatePickerFormat());
}
public function testGetDateTimeTypeFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('yyyy-MM-dd HH:mm', $sut->getDateTimeTypeFormat());
}
public function testGetDateTimePickerFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('YYYY-MM-DD HH:mm', $sut->getDateTimePickerFormat());
}
public function testIs24Hours()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertFalse($sut->isTwentyFourHours());
}
public function testGetTimeFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('H:i:s', $sut->getTimeFormat());
}
public function testUnknownSetting()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown setting for locale en: date_time_type');
$sut = $this->getSut('en', ['en' => [
'xxx' => 'dd.MM.yyyy HH:mm',
]]);
$sut->getDateTimePickerFormat();
}
}

View File

@@ -0,0 +1,223 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Utils;
use App\Entity\Timesheet;
use App\Utils\LocaleHelper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Intl\Util\IntlTestHelper;
/**
* @covers \App\Utils\LocaleHelper
*/
class LocaleHelperTest extends TestCase
{
protected function getSut(string $locale): LocaleHelper
{
return new LocaleHelper($locale);
}
public function testCurrency()
{
$symbols = [
'EUR' => '€',
'USD' => '$',
'RUB' => 'RUB',
'rub' => 'RUB',
123 => 123,
];
$sut = $this->getSut('en');
foreach ($symbols as $name => $symbol) {
$this->assertEquals($symbol, $sut->currency($name));
}
}
public function testCountry()
{
$countries = [
'DE' => 'Germany',
'RU' => 'Russia',
'ES' => 'Spain',
'es' => 'Spain',
'12' => '12',
];
$sut = $this->getSut('en');
foreach ($countries as $locale => $name) {
$this->assertEquals($name, $sut->country($locale));
}
}
public function testLanguage()
{
$languages = [
'de' => 'German',
'ru' => 'Russian',
'es' => 'Spanish',
'ES' => 'Spanish',
'12' => '12',
];
$sut = $this->getSut('en');
foreach ($languages as $locale => $name) {
$this->assertEquals($name, $sut->language($locale));
}
}
public function testMoneyWithoutCurrency()
{
$sut = $this->getSut('en');
$this->assertEquals('123.75', $sut->money(123.75));
$sut = $this->getSut('de');
$this->assertEquals('123.234,76', $sut->money(123234.7554, null, true));
$this->assertEquals('123.234,76', $sut->money(123234.7554, null, false));
$this->assertEquals('123.234,76', $sut->money(123234.7554, 'EUR', false));
}
/**
* @dataProvider getMoneyNoCurrencyData
*/
public function testMoneyNoCurrency($result, $amount, $currency, $locale)
{
$sut = $this->getSut($locale);
$this->assertEquals($result, $sut->money($amount, $currency, false));
}
public function getMoneyNoCurrencyData()
{
return [
['0,00', null, 'EUR', 'de'],
['2.345,00', 2345, 'EUR', 'de'],
['2,345.00', 2345, 'EUR', 'en'],
['2,345.01', 2345.009, 'EUR', 'en'],
['2.345,01', 2345.009, 'EUR', 'de'],
['13.75', 13.75, 'USD', 'en'],
['13,75', 13.75, 'USD', 'de'],
['13,75', 13.75, 'RUB', 'de'],
['13,75', 13.75, 'JPY', 'de'],
['13 933,49', 13933.49, 'JPY', 'ru'],
['13,75', 13.75, 'CNY', 'de'],
['13.933,00', 13933, 'CNY', 'de'],
['13 933,00', 13933, 'CNY', 'ru'],
['13,933.00', 13933, 'CNY', 'en'],
['13,933.00', 13933, 'CNY', 'zh_CN'],
['1.234.567,89', 1234567.891234567890000, 'USD', 'de'],
];
}
/**
* @dataProvider getMoneyData
*/
public function testMoney($result, $amount, $currency, $locale)
{
$sut = $this->getSut($locale);
$this->assertEquals($result, $sut->money($amount, $currency));
}
public function getMoneyData()
{
return [
['0,00 €', null, 'EUR', 'de'],
['2.345,00 €', 2345, 'EUR', 'de'],
['€2,345.00', 2345, 'EUR', 'en'],
['€2,345.01', 2345.009, 'EUR', 'en'],
['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'],
['14 ¥', 13.75, 'JPY', 'de'],
['13 933 ¥', 13933.49, 'JPY', 'ru'],
['13,75 CN¥', 13.75, 'CNY', 'de'],
['13.933,00 CN¥', 13933, 'CNY', 'de'],
['13 933,00 CN¥', 13933, 'CNY', 'ru'],
['CN¥13,933.00', 13933, 'CNY', 'en'],
['1.234.567,89 $', 1234567.891234567890000, 'USD', 'de'],
];
}
/**
* @dataProvider getAmountData
*/
public function testAmount($result, $amount, $locale)
{
$sut = $this->getSut($locale);
$this->assertEquals($result, $sut->amount($amount));
}
public function getAmountData()
{
return [
['0', null, 'de'],
['2.345,01', 2345.01, 'de'],
['2.345', 2345, 'de'],
['2,345', 2345, 'en'],
['2,345.009', 2345.009, 'en'],
['2.345,009', 2345.009, 'de'],
['13.75', 13.75, 'en'],
['13,75', 13.75, 'de'],
['13 933,49', 13933.49, 'ru'],
['1.234.567,891', 1234567.891234567890000, 'de'],
];
}
/**
* @dataProvider getMoneyData62_1
*/
public function testMoney62_1($result, $amount, $currency, $locale)
{
IntlTestHelper::requireFullIntl($this, '62.1');
$sut = $this->getSut($locale);
$this->assertEquals($result, $sut->money($amount, $currency));
}
public function getMoneyData62_1()
{
return [
['RUB 13.50', 13.50, 'RUB', 'en'],
['13,75 ₽', 13.75, 'RUB', 'ru'],
];
}
public function testDurationDecimal()
{
$record = $this->getTimesheet(9437);
$sut = $this->getSut('en');
$this->assertEquals('2.62', $sut->durationDecimal($record->getDuration()));
// test extended format
$sut = $this->getSut('de');
$this->assertEquals('2,62', $sut->durationDecimal($record->getDuration()));
// test negative duration
$sut = $this->getSut('en');
$this->assertEquals('0', $sut->durationDecimal('-1'));
// test zero duration
$sut = $this->getSut('en');
$this->assertEquals('0', $sut->durationDecimal('0'));
}
protected function getTimesheet($seconds)
{
$begin = new \DateTime();
$end = clone $begin;
$end->setTimestamp($begin->getTimestamp() + $seconds);
$record = new Timesheet();
$record->setBegin($begin);
$record->setEnd($end);
$record->setDuration($seconds);
return $record;
}
}

View File

@@ -11,7 +11,6 @@ namespace App\Tests\Utils;
use App\Configuration\LanguageFormattings;
use App\Utils\LocaleSettings;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -19,185 +18,15 @@ use Symfony\Component\HttpFoundation\RequestStack;
* @covers \App\Utils\LocaleSettings
* @covers \App\Configuration\LanguageFormattings
*/
class LocaleSettingsTest extends TestCase
class LocaleSettingsTest extends LocaleFormatsTest
{
protected function getRequestStack(string $locale)
protected function getSut(string $locale, array $settings)
{
$request = new Request();
$request->setLocale($locale);
$requestStack = new RequestStack();
$requestStack->push($request);
return $requestStack;
}
protected function getSut(string $locale, array $settings)
{
return new LocaleSettings($this->getRequestStack($locale), new LanguageFormattings($settings));
}
protected function getDefaultSettings()
{
return [
'de' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'date_time' => 'd.m. H:i',
'duration' => '%h:%m h',
'time' => 'H:i',
'24_hours' => true,
],
'en' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d',
'date_time' => 'm-d H:i',
'duration' => '%h:%m h',
'time' => 'H:i:s',
'24_hours' => false,
],
'pt_BR' => [
'date_time_type' => 'dd-MM-yyyy HH:mm',
'date_type' => 'dd-MM-yyyy',
'date' => 'd-m-Y',
'duration' => '%h:%m h',
],
'it' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'duration' => '%h:%m h',
],
'fr' => [
'date_time_type' => 'dd/MM/yyyy HH:mm',
'date_type' => 'dd/MM/yyyy',
'date' => 'd/m/Y',
'duration' => '%h h %m',
],
'es' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'duration' => '%h:%m h',
],
'ru' => [
'date_time_type' => 'dd.MM.yyyy HH:mm',
'date_type' => 'dd.MM.yyyy',
'date' => 'd.m.Y',
'duration' => '%h:%m h',
],
'ar' => [
'date_time_type' => 'yyyy-MM-dd HH:mm',
'date_type' => 'yyyy-MM-dd',
'date' => 'Y-m-d',
'duration' => '%h:%m h',
],
'hu' => [
'date_time_type' => 'yyyy.MM.dd HH:mm',
'date_type' => 'yyyy.MM.dd',
'date' => 'Y.m.d.',
'duration' => '%h:%m h',
],
];
}
public function testGetLocale()
{
$sut = $this->getSut('en', []);
$this->assertEquals('en', $sut->getLocale());
$sut = $this->getSut('ar', []);
$this->assertEquals('ar', $sut->getLocale());
}
public function testGetAvailableLanguages()
{
$sut = $this->getSut('en', []);
$this->assertEquals([], $sut->getAvailableLanguages());
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals(['de', 'en', 'pt_BR', 'it', 'fr', 'es', 'ru', 'ar', 'hu'], $sut->getAvailableLanguages());
}
public function testInvalidLocaleWithDefaultLocale()
{
$this->expectException(\InvalidArgumentException::class);
$sut = $this->getSut('en', []);
$sut->getDateFormat();
}
public function testInvalidLocaleWithGivenLocale()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown locale given: xx');
$sut = $this->getSut('xx', $this->getDefaultSettings());
$sut->getDateFormat();
}
public function testGetDurationFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('%h:%m h', $sut->getDurationFormat());
}
public function testGetDateFormat()
{
$sut = $this->getSut('de', $this->getDefaultSettings());
$this->assertEquals('d.m.Y', $sut->getDateFormat());
}
public function testGetDateTimeFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('m-d H:i', $sut->getDateTimeFormat());
}
public function testGetDateTypeFormat()
{
$sut = $this->getSut('de', $this->getDefaultSettings());
$this->assertEquals('dd.MM.yyyy', $sut->getDateTypeFormat());
}
public function testGetDatePickerFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('YYYY-MM-DD', $sut->getDatePickerFormat());
}
public function testGetDateTimeTypeFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('yyyy-MM-dd HH:mm', $sut->getDateTimeTypeFormat());
}
public function testGetDateTimePickerFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('YYYY-MM-DD HH:mm', $sut->getDateTimePickerFormat());
}
public function testIs24Hours()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertFalse($sut->isTwentyFourHours());
}
public function testGetTimeFormat()
{
$sut = $this->getSut('en', $this->getDefaultSettings());
$this->assertEquals('H:i:s', $sut->getTimeFormat());
}
public function testUnknownSetting()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown setting for locale en: date_time_type');
$sut = $this->getSut('en', ['en' => [
'xxx' => 'dd.MM.yyyy HH:mm',
]]);
$sut->getDateTimePickerFormat();
return new LocaleSettings($requestStack, new LanguageFormattings($settings));
}
}

View File

@@ -650,58 +650,6 @@
<target>حساب البنك</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>يناير</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>فبراير</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>آذار</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>نيسان</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>مايو</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>يونيو</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>يوليو</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>أغسطس</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>سبتمبر</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>أكتوبر</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>نوفمبر</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>ديسمبر</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -897,58 +897,6 @@
<target>Vytvořeno %date% s %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Leden</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Únor</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Březen</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Doben</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Květen</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Červen</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Červenec</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Srpen</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Září</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Říjen</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Listopad</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Prosinec</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -945,58 +945,6 @@
<target>Oprettet %date% med %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Januar</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Februar</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Marts</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>April</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Maj</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Juni</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Juli</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>August</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Oktober</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>December</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -1045,58 +1045,6 @@
<target>Erstellt %date% mit %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Januar</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Februar</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>März</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>April</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Mai</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Juni</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Juli</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>August</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Oktober</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Dezember</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -1045,58 +1045,6 @@
<target>Created %date% with %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>January</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>February</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>March</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>April</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>May</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>June</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>July</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>August</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>October</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>December</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -1009,58 +1009,6 @@
<target>Kreita je la %date% per %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Januaro</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Februaro</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Marto</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Aprilo</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Majo</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Junio</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Julio</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Aŭgusto</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Septembro</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Octobro</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Novembro</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Decembro</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -913,58 +913,6 @@
<target>Creado el %date% con %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Enero</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Febrero</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Marzo</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Abril</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Mayo</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Junio</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Julio</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Agosto</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Septiembre</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Octubre</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Noviembre</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Diciembre</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -953,58 +953,6 @@
<target>%date%-n sortua %kimai%-ren bitartez</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Urtarrila</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Otsaila</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Martxoa</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Apirila</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Maiatza</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Ekaina</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Uztaila</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Abuztua</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Iraila</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Urria</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Azaroa</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Abendua</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -837,58 +837,6 @@
<target>Généré le %date% avec %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Janvier</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Février</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Mars</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Avril</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Mai</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Juin</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Juillet</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Août</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Septembre</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Octobre</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Novembre</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Decembre</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -943,54 +943,6 @@
<source>export.date_copyright</source>
<target>נוצר ב %date%</target>
</trans-unit>
<trans-unit id="TzrTkzA" resname="month.1">
<source>month.1</source>
<target>ינואר</target>
</trans-unit>
<trans-unit id="Ybbwj3q" resname="month.2">
<source>month.2</source>
<target>פברואר</target>
</trans-unit>
<trans-unit id="ZUWOMta" resname="month.3">
<source>month.3</source>
<target>מרץ</target>
</trans-unit>
<trans-unit id="r7dXujQ" resname="month.4">
<source>month.4</source>
<target>אפריל</target>
</trans-unit>
<trans-unit id="5U8dGjy" resname="month.5">
<source>month.5</source>
<target>מאי</target>
</trans-unit>
<trans-unit id="S__KfXn" resname="month.6">
<source>month.6</source>
<target>יוני</target>
</trans-unit>
<trans-unit id="c6TKQ2p" resname="month.7">
<source>month.7</source>
<target>יולי</target>
</trans-unit>
<trans-unit id="js3tNpC" resname="month.8">
<source>month.8</source>
<target>אוגוסט</target>
</trans-unit>
<trans-unit id="2thDj5n" resname="month.9">
<source>month.9</source>
<target>ספטמבר</target>
</trans-unit>
<trans-unit id="HshzwEV" resname="month.10">
<source>month.10</source>
<target>אוקטובר</target>
</trans-unit>
<trans-unit id="Qi4.VFn" resname="month.11">
<source>month.11</source>
<target>נובמבר</target>
</trans-unit>
<trans-unit id="LXtp3N0" resname="month.12">
<source>month.12</source>
<target>דצמבר</target>
</trans-unit>
<trans-unit id="OxdYMR3" resname="active.entries">
<source>active.entries</source>
<target>מדדי זמני הפעילות שלך</target>

View File

@@ -837,58 +837,6 @@
<target>Készült: %date% A %kimai% segítségével</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Január</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Február</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Március</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Április</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Május</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Június</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Július</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Augusztus</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Szeptember</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Október</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>December</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -985,58 +985,6 @@
<target>Creato %date% con %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Gennaio</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Febbraio</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Marzo</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Aprile</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Maggio</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Giugno</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Luglio</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Agosto</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Settembre</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Ottobre</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Novembre</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Dicembre</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -837,58 +837,6 @@
<target>%kimai% で %date% に作成されました</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>1 月</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>2 月</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>3 月</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>4 月</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>5 月</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>6 月</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>7 月</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>8 月</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>9 月</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>10 月</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>11 月</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>12 月</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -857,58 +857,6 @@
<target state="translated">생성 %date% 의 %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target state="translated">1월</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target state="translated">2월</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target state="translated">3월</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target state="translated">4월</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target state="translated">5월</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target state="translated">6월</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target state="translated">7월</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target state="translated">8월</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target state="translated">9월</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target state="translated">10월</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target state="translated">11월</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target state="translated">12월</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -913,58 +913,6 @@
<target>Gemaakt op %date% met %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Januari</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Februari</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Maart</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>April</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Mei</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Juni</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Juli</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Augustus</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Oktober</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>December</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -973,58 +973,6 @@
<target>Utworzono %date% za pomocą %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>styczeń</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Luty</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Marzec</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Kwiecień</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Maj</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Czerwiec</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Lipiec</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Sierpień</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Wrzesień</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Pażdziernik</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Listopad</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Grudzień</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -837,58 +837,6 @@
<target>Criado %date% com %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Janeiro</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Fevereiro</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Março</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Abril</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Maio</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Junho</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Julho</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Agosto</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Setembro</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Outubro</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Novembro</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Dezembro</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -907,54 +907,7 @@
<source>export.date_copyright</source>
<target>Creat în %date% cu %kimai%</target>
</trans-unit>
<trans-unit id="TzrTkzA" resname="month.1">
<source>month.1</source>
<target>Ianuarie</target>
</trans-unit>
<trans-unit id="Ybbwj3q" resname="month.2">
<source>month.2</source>
<target>Februarie</target>
</trans-unit>
<trans-unit id="ZUWOMta" resname="month.3">
<source>month.3</source>
<target>Martie</target>
</trans-unit>
<trans-unit id="r7dXujQ" resname="month.4">
<source>month.4</source>
<target>Aprilie</target>
</trans-unit>
<trans-unit id="5U8dGjy" resname="month.5">
<source>month.5</source>
<target>Mai</target>
</trans-unit>
<trans-unit id="S__KfXn" resname="month.6">
<source>month.6</source>
<target>Iunie</target>
</trans-unit>
<trans-unit id="c6TKQ2p" resname="month.7">
<source>month.7</source>
<target>Iulie</target>
</trans-unit>
<trans-unit id="js3tNpC" resname="month.8">
<source>month.8</source>
<target>August</target>
</trans-unit>
<trans-unit id="2thDj5n" resname="month.9">
<source>month.9</source>
<target>Septembrie</target>
</trans-unit>
<trans-unit id="HshzwEV" resname="month.10">
<source>month.10</source>
<target>Octombrie</target>
</trans-unit>
<trans-unit id="Qi4.VFn" resname="month.11">
<source>month.11</source>
<target>Noiembrie</target>
</trans-unit>
<trans-unit id="LXtp3N0" resname="month.12">
<source>month.12</source>
<target>Decembrie</target>
</trans-unit>
<trans-unit id="OxdYMR3" resname="active.entries">
<source>active.entries</source>
<target>Înregistrările tale active</target>

View File

@@ -657,58 +657,6 @@
<target>Номер заказа</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Январь</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Февраль</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Март</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Апрель</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Май</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Июнь</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Июль</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Август</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Сентябрь</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Октябрь</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Ноябрь</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Декабрь</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -990,58 +990,6 @@
<target>Vytvorené %date%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Január</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Február</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Marec</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Apríl</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Máj</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Jún</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Júl</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>August</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Október</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>December</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -832,58 +832,6 @@
<target state="translated">Skapad %date% med %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target state="translated">Januari</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target state="translated">Februari</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target state="translated">Mars</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target state="translated">April</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target state="translated">Maj</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target state="translated">Juni</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target state="translated">Juli</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target state="translated">Augusti</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>September</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target state="translated">Oktober</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>November</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>December</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -913,58 +913,6 @@
<target state="translated">%date% günü %kimai% ile oluşturuldu.</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target state="translated">Ocak</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target state="translated">Şubat</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target state="translated">Mart</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target state="translated">Nisan</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target state="translated">Mayıs</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target state="translated">Haziran</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target state="translated">Temmuz</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target state="translated">Ağustos</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target state="translated">Eylül</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target state="translated">Ekim</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target state="translated">Kasım</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target state="translated">Aralık</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -1025,58 +1025,6 @@
<target>Tạo %date% với %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>Tháng 1</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>Tháng 2</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>Tháng 3</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>Tháng 4</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>Tháng 5</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>Tháng 6</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>Tháng 7</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>Tháng 8</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>Tháng 9</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>Tháng 10</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>Tháng 11</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>Tháng 12</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->

View File

@@ -944,58 +944,6 @@
<target>创建了 %date% 与 %kimai%</target>
</trans-unit>
<!--
Month names
-->
<trans-unit id="month.1">
<source>month.1</source>
<target>一月</target>
</trans-unit>
<trans-unit id="month.2">
<source>month.2</source>
<target>二月</target>
</trans-unit>
<trans-unit id="month.3">
<source>month.3</source>
<target>三月</target>
</trans-unit>
<trans-unit id="month.4">
<source>month.4</source>
<target>四月</target>
</trans-unit>
<trans-unit id="month.5">
<source>month.5</source>
<target>五月</target>
</trans-unit>
<trans-unit id="month.6">
<source>month.6</source>
<target>六月</target>
</trans-unit>
<trans-unit id="month.7">
<source>month.7</source>
<target>七月</target>
</trans-unit>
<trans-unit id="month.8">
<source>month.8</source>
<target>八月</target>
</trans-unit>
<trans-unit id="month.9">
<source>month.9</source>
<target>九月</target>
</trans-unit>
<trans-unit id="month.10">
<source>month.10</source>
<target>十月</target>
</trans-unit>
<trans-unit id="month.11">
<source>month.11</source>
<target>十一月</target>
</trans-unit>
<trans-unit id="month.12">
<source>month.12</source>
<target>十二月</target>
</trans-unit>
<!--
Navbar - recent entries and activities
-->