fix: use IntlDateFormatter instead of strftime

This CL introduces breaking changes, since it uses non-constant static
variable initializers, which are only available since PHP 8.3. Thus, the
codebase now only supports PHP 8.3 (and the docs have been modified to
reflect this and the fact that the Intl extension is necessary).

Fixed: hores:2
GitOrigin-RevId: 9a91312c96b90fa4c9ae5fc5f6e972fd95c9cd57
diff --git a/src/inc/calendarsView.php b/src/inc/calendarsView.php
index 46a3761..03d1486 100644
--- a/src/inc/calendarsView.php
+++ b/src/inc/calendarsView.php
@@ -33,7 +33,7 @@
 
       if ($dow == 1) echo "</tr>";
       if ($dom == 1) echo "</table>";
-      if ($dom == 1 || $start) echo "<div class='month'>".security::htmlsafe(ucfirst(strftime("%B %G", $current->getTimestamp())))."</div><table class='calendar'>";
+      if ($dom == 1 || $start) echo "<div class='month'>".security::htmlsafe(ucfirst(date::getMonthYear($current->getTimestamp())))."</div><table class='calendar'>";
       if ($dow == 1 || $start) echo "<tr>";
       if ($dom == 1 || $start) {
         for ($i = 1; $i < $dow; $i++) {
diff --git a/src/inc/date.php b/src/inc/date.php
new file mode 100644
index 0000000..ba89c09
--- /dev/null
+++ b/src/inc/date.php
@@ -0,0 +1,35 @@
+<?php
+class date {
+  const LOCALE = 'es';
+
+  private static function createFormatter(string $pattern) {
+    return new IntlDateFormatter(
+        locale: self::LOCALE,
+        dateType: IntlDateFormatter::FULL,
+        timeType: IntlDateFormatter::FULL,
+        timezone: null,
+        calendar: IntlDateFormatter::GREGORIAN,
+        pattern: $pattern
+    );
+  }
+
+  public static function getMonthYear(IntlCalendar|DateTimeInterface|array|string|int|float $timestamp) {
+    static $formatter = self::createFormatter("MMMM yyyy");
+    return $formatter->format($timestamp);
+  }
+
+  public static function getShortDate(IntlCalendar|DateTimeInterface|array|string|int|float $timestamp) {
+    static $formatter = self::createFormatter("dd MMM yyyy");
+    return $formatter->format($timestamp);
+  }
+
+  public static function getShortDateWithTime(IntlCalendar|DateTimeInterface|array|string|int|float $timestamp) {
+    static $formatter = self::createFormatter("dd MMM yyyy HH:mm:ss");
+    return $formatter->format($timestamp);
+  }
+
+  public static function getLongDate(IntlCalendar|DateTimeInterface|array|string|int|float $timestamp) {
+    static $formatter = self::createFormatter("dd 'de' MMMM 'de' yyyy");
+    return $formatter->format($timestamp);
+  }
+}
diff --git a/src/inc/incidents.php b/src/inc/incidents.php
index b558ed9..7bda42d 100644
--- a/src/inc/incidents.php
+++ b/src/inc/incidents.php
@@ -486,13 +486,13 @@
 
       if (!count($to)) return 0;
 
-      $subject = "Incidencia del tipo \"".security::htmlsafe($incidenttype["name"])."\" creada para ".security::htmlsafe($workerName)." el ".strftime("%d %b %Y", $sday);
+      $subject = "Incidencia del tipo \"".security::htmlsafe($incidenttype["name"])."\" creada para ".security::htmlsafe($workerName)." el ".date::getShortDate($sday);
       $body = mail::bodyTemplate("<p>Hola,</p>
       <p>Este es un mensaje automático para avisarte de que ".security::htmlsafe(people::userData("name"))." ha introducido la siguiente incidencia en el sistema de registro horario:</p>
       <ul>
         <li><b>Trabajador:</b> ".security::htmlsafe($workerName)."</li>
         <li><b>Motivo:</b> ".security::htmlsafe($incidenttype["name"])."</li>
-        <li><b>Fecha:</b> ".strftime("%d %b %Y", $sday)." ".(($sbegins == 0 && $sends == self::ENDOFDAY) ? "(todo el día)": schedules::sec2time($sbegins)."-".schedules::sec2time($sends))."</li>".
+        <li><b>Fecha:</b> ".date::getShortDate($sday)." ".(($sbegins == 0 && $sends == self::ENDOFDAY) ? "(todo el día)": schedules::sec2time($sbegins)."-".schedules::sec2time($sends))."</li>".
         (!empty($details) ? "<li><b>Observaciones:</b> <span style='white-space: pre-wrap;'>".security::htmlsafe($details)."</span></li>" : "").
       "</ul>
       <p style='font-size: 11px;'>Has recibido este mensaje porque estás configurado como persona responsable de la categoría a la que pertenece este trabajador o eres el administrador del sistema.</p>");
@@ -568,9 +568,9 @@
           "email" => $workerEmail,
           "name" => $workerName
         )];
-        $subject = "Incidencia del ".strftime("%d %b %Y", $incident["day"])." ".($value == 1 ? "verificada" : "rechazada");
+        $subject = "Incidencia del ".date::getShortDate($incident["day"])." ".($value == 1 ? "verificada" : "rechazada");
         $body = mail::bodyTemplate("<p>Bienvenido ".security::htmlsafe($workerName).",</p>
-        <p>Este es un mensaje automático para avisarte de que la incidencia que introduciste para el día ".strftime("%d de %B de %Y", $incident["day"])." ha sido ".($value == 1 ? "aceptada" : "rechazada").".</p><p>Puedes ver el estado de todas tus incidencias en el <a href='".security::htmlsafe($conf["fullPath"])."'>aplicativo web</a>.</p>");
+        <p>Este es un mensaje automático para avisarte de que la incidencia que introduciste para el día ".date::getLongDate($incident["day"])." ha sido ".($value == 1 ? "aceptada" : "rechazada").".</p><p>Puedes ver el estado de todas tus incidencias en el <a href='".security::htmlsafe($conf["fullPath"])."'>aplicativo web</a>.</p>");
 
         mail::send($to, [], $subject, $body);
       }
diff --git a/src/inc/incidentsView.php b/src/inc/incidentsView.php
index 1f90bc8..5bd0bda 100644
--- a/src/inc/incidentsView.php
+++ b/src/inc/incidentsView.php
@@ -119,7 +119,7 @@
               <?php
             }
             ?>
-            <td class="can-strike"><?=strftime("%d %b %Y", $incident["day"])." ".security::htmlsafe($incident["allday"] ? "(todo el día)" : schedules::sec2time($incident["begins"])."-".schedules::sec2time($incident["ends"]))?></td>
+            <td class="can-strike"><?=date::getShortDate($incident["day"])." ".security::htmlsafe($incident["allday"] ? "(todo el día)" : schedules::sec2time($incident["begins"])."-".schedules::sec2time($incident["ends"]))?></td>
             <td>
               <a href="dynamic/editincidentcomment.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" data-dyndialog-href="dynamic/editincidentcomment.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" title="Ver/editar las observaciones"><i class="material-icons icon"><?=(empty($incident["details"]) && empty($incident["workerdetails"]) ? "mode_" : "")?>comment</i></a>
               <span<?=($attachments > 0 ? ' class="mdl-badge mdl-badge--overlap" data-badge="'.$attachments.'"' : '')?>><a href="dynamic/incidentattachments.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" data-dyndialog-href="dynamic/incidentattachments.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" title="Ver/gestionar los archivos adjuntos"><i class="material-icons icon">attach_file</i></a></span>
diff --git a/src/inc/registryView.php b/src/inc/registryView.php
index f068558..c0c84f6 100644
--- a/src/inc/registryView.php
+++ b/src/inc/registryView.php
@@ -84,7 +84,7 @@
               <?php
             }
             ?>
-            <td class="can-strike"><?=strftime("%d %b %Y", $record["day"])?></td>
+            <td class="can-strike"><?=date::getShortDate($record["day"])?></td>
             <td class="centered can-strike"><?=schedules::sec2time($record["beginswork"])." - ".schedules::sec2time($record["endswork"])?></td>
             <td class="centered can-strike"><?=(intervals::measure($breakfastInt) == 0 ? "-" : ($isForWorker ? export::sec2hours(intervals::measure($breakfastInt)) : schedules::sec2time($record["beginsbreakfast"])." - ".schedules::sec2time($record["endsbreakfast"])))?></td>
             <td class="centered can-strike"><?=(intervals::measure($lunchInt) == 0 ? "-" : ($isForWorker ? export::sec2hours(intervals::measure($lunchInt)) : schedules::sec2time($record["beginslunch"])." - ".schedules::sec2time($record["endslunch"])))?></td>