Temporary localization on Symfony 4 + Twig

The need for temporary localization of the product arises when the product grows to such a scale that requires work in different time zones (evidence). I would like to describe a variant of a simple idea for solving this case.

The background is this: they developed a niche CRM / ERP system, and then we were told that tomorrow they would work with this system on a franchise from Vladivostok to Kaliningrad. Unfortunately, such a scenario was not originally thought out, and we began to learn how to do this with minimal cost and maximum convenience.

In total, three tasks turned out to be enlarged: how we output the data and how we enter, and between them the task how we store it all. Since time, as you know, is relatively literally and figuratively, it was decided to store time as before in Moscow UTC + 3, but process it at the input and output (and keep in mind that the reference point is UTC + 3). Of course, we understood that there are other solutions in this and other directions. You can convert all existing entries to UTC + -0, as well as use specialized types in the DBMS that store the time zone, you can write this custom type yourself, if suddenly the database does not fully support such features. But guided by the principle of simplicity, we went along the proposed path, all the more, at first glance, he did not lose much to the rest,and the logic for determining the desired time zone was quite simple.

After Moscow became a reference point, we added a custom time zone parameter to each user, as well as a number of related entities (organization, city, applications, transactions, etc.). Then it was possible to unambiguously establish in which time zone the user or entity with which he works. The logic there is standard and precisely often specific to projects. We wrapped this logic in the service and got a time zone where you need to

$localizationService->getTimezone();

The decision to localize dates in the templates was as follows: when initializing Twig extensions, the time zone was changed to the desired one:

function __construct(Environment $twig, LocalizationService $localizationService) {
    $twig->getExtension('Twig_Extension_Core')->setTimezone($localizationService->getTimezone());
}

Our situation was further complicated by the fact that after any date-time is displayed, it is necessary to make a subscript β€œ01.01.2020 12:30 (Moscow)”, so that, for example, in a conditional order / task / transaction that is tied to the time zone, information about the time belt. For practical reasons, this is necessary so that a single call center can comfortably work with different time zones within the framework of a task / application / transaction.

All the logic for determining the priority of time zones was wired into the aforementioned getTimezone.

Further, we were faced with the fact that if you make your twig filter or function, you will need to change a bunch of templates, but you would like to avoid this. Therefore, after a little asking, we decided to redefine the standard twig-filter date

...
new TwigFilter('date', [$this, 'date'], ['needs_environment' => true]),
...

function date(Twig_Environment $env, $date, $format = null, $timezone = null)
{
    $appendix = '';
    if (format && strpos($format, 'H:i') !== false)
        $appendix = ' ('.DateTimeFunctions::getRussianAbbrev($this->localizationService->getTimezone()).')';   
...
    //    date   $result
...
   return $result.$appendix;
}

Also, since we took the standard filter, the old version was redefined:

...
new TwigFilter('native_date', [$this, 'nativeDate'], [ 'needs_environment' => true]),
...
public function nativeDate(Twig_Environment $env, $date, $format = null, $timezone = null)
{
    //    date 
}

The standard date filter code can be found in / twig / twig / lib / Twig / Extension / Core :: twig_date_format_filter. Although in fact in most cases a simple, not much different option will do:

$date->setTimeZone($timezone)
$result = $date->format($format);

Of course, you can also fork or redefine the more substantial part of Twig, but if the functionality of the standard filter suits you, you can simply take it out separately and lose nothing.

It remains to solve the problem of entering the date-time. One solution:

private function getOffsetHours()
{
    if (!$this->isInit)
        $this->init();

    $local = new \DateTime('now', new \DateTimeZone($this->getTimezone()));
    $user = new \DateTime('now');

    $localOffset = $local->getOffset() / 3600;
    $globalOffset = $user->getOffset() / 3600;

    $diff = $globalOffset - $localOffset;
    return $diff;
}

public function toGlobalTime(\DateTimeInterface $dateTime): \DateTimeInterface 
{
    if (!$this->isInit)
        $this->init();

    $offsetHours = $this->getOffsetHours();

    if ($offsetHours > 0) {
        return $dateTime->modify('+ '.$offsetHours.' hours');
    } else  if($offsetHours < 0){
        return $dateTime->modify($offsetHours.' hours');
    }

    return $dateTime;
}

Then call before saving the date-time, for example, in listeners. This option came up to us, since basically the time and date in the project is fixed for certain events, and not entered manually. For another extreme case, where time is constantly introduced through forms, the solution may not be optimal.

As a bonus. The project uses Omines datatables-bundle to output tables. There the solution was even easier. Instead of DateTimeColumn for localization the following was used:

class CustomDateTimeColumn extends DateTimeColumn
{
    
    private $localizationService;
    private $timeZone;
    
    public function __construct(LocalizationService $localizationService)
    {
        $this->localizationService = $localizationService;
        $this->timeZone = $localizationService->getTimezoneObject();
    }
    
   
    public function normalize($value)
    {
        $value->setTimeZone($this->timeZone);
        return parent::normalize($value);
    }
}

Thank you for your time. If someone helps to improve the basic things of the solution, I will be very grateful. We are talking about basic ones, since it is clear that the code is vacuum and in reality has a much larger DI and all sorts of goodies for internal use in the project.

In summary. The idea of ​​a simple solution for quick temporary localization of the project is presented. It does not depend on versions, or if it does, it is weak. This solution successfully migrated from Symfony 4.2 to 5.

All Articles