new \IntlDateFormatter($locale, \IntlDateFormatter::FULL, \IntlDateFormatter::FULL), 'FULLDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::FULL, \IntlDateFormatter::NONE), 'FULLTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::FULL), 'LONG' => new \IntlDateFormatter($locale, \IntlDateFormatter::LONG, \IntlDateFormatter::LONG), 'LONGDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::LONG, \IntlDateFormatter::NONE), 'LONGTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::LONG), 'MEDIUM' => new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM), 'MEDIUMDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE), 'MEDIUMTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM), 'SHORT' => new \IntlDateFormatter($locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT), 'SHORTDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE), 'SHORTTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT), // Use a custom pattern default => new \IntlDateFormatter($locale, \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, null, null, $format), }; } return $dateFormatter->format($date) ?: ''; } /** * Locale-formatted strftime using IntlDateFormatter (PHP 8.1 compatible) * This provides a cross-platform alternative to strftime() for when it will be removed from PHP. * Note that output can be slightly different between libc sprintf and this function as it is using ICU. * * Original author BohwaZ * Adapted from https://github.com/alphp/strftime * MIT licensed */ public function strftime(string $format, int|string|\DateTimeInterface|null $timestamp, string|Locale|null $locale = null, $useUtcTimeZone = false): string { if (!$timestamp instanceof \DateTimeInterface) { $timestamp = MathUtility::canBeInterpretedAsInteger($timestamp) ? '@' . $timestamp : (string)$timestamp; try { $timestamp = new \DateTime($timestamp); } catch (\Exception $e) { throw new \InvalidArgumentException('$timestamp argument is neither a valid UNIX timestamp, a valid date-time string or a DateTime object.', 1679091446, $e); } $timestamp->setTimezone(new \DateTimeZone($useUtcTimeZone ? 'UTC' : date_default_timezone_get())); } if (empty($locale)) { // get current locale $locale = (string)setlocale(LC_TIME, '0'); } else { $locale = (string)$locale; } // Use fallback locale if 'C' is provided. if ($locale === 'C') { $locale = 'en-US'; } // remove trailing part not supported by ext-intl locale $locale = preg_replace('/[^\w-].*$/', '', $locale); $intl_formats = [ '%a' => 'EEE', // An abbreviated textual representation of the day Sun through Sat '%A' => 'EEEE', // A full textual representation of the day Sunday through Saturday '%b' => 'MMM', // Abbreviated month name, based on the locale Jan through Dec '%B' => 'MMMM', // Full month name, based on the locale January through December '%h' => 'MMM', // Abbreviated month name, based on the locale (an alias of %b) Jan through Dec ]; $intl_formatter = function (\DateTimeInterface $timestamp, string $format) use ($intl_formats, $locale): string { $tz = $timestamp->getTimezone(); $date_type = \IntlDateFormatter::FULL; $time_type = \IntlDateFormatter::FULL; $pattern = ''; switch ($format) { // %c = Preferred date and time stamp based on locale // Example: Tue Feb 5 00:45:10 2009 for February 5, 2009 at 12:45:10 AM case '%c': $date_type = \IntlDateFormatter::LONG; $time_type = \IntlDateFormatter::SHORT; break; // %x = Preferred date representation based on locale, without the time // Example: 02/05/09 for February 5, 2009 case '%x': $date_type = \IntlDateFormatter::SHORT; $time_type = \IntlDateFormatter::NONE; break; // Localized time format case '%X': $date_type = \IntlDateFormatter::NONE; $time_type = \IntlDateFormatter::MEDIUM; break; default: $pattern = $intl_formats[$format]; } // In October 1582, the Gregorian calendar replaced the Julian in much of Europe, and // the 4th October was followed by the 15th October. // ICU (including IntlDateFormattter) interprets and formats dates based on this cutover. // Posix (including strftime) and timelib (including DateTimeImmutable) instead use // a "proleptic Gregorian calendar" - they pretend the Gregorian calendar has existed forever. // This leads to the same instants in time, as expressed in Unix time, having different representations // in formatted strings. // To adjust for this, a custom calendar can be supplied with a cutover date arbitrarily far in the past. $calendar = new \IntlGregorianCalendar(); $calendar->setGregorianChange(PHP_INT_MIN); return (new \IntlDateFormatter($locale, $date_type, $time_type, $tz, $calendar, $pattern))->format($timestamp) ?: ''; }; // Same order as https://www.php.net/manual/en/function.strftime.php $translation_table = [ // Day '%a' => $intl_formatter, '%A' => $intl_formatter, '%d' => 'd', '%e' => function (\DateTimeInterface $timestamp, string $_): string { return sprintf('% 2u', $timestamp->format('j')); }, '%j' => function (\DateTimeInterface $timestamp, string $_): string { // Day number in year, 001 to 366 return sprintf('%03d', (int)($timestamp->format('z')) + 1); }, '%u' => 'N', '%w' => 'w', // Week '%U' => function (\DateTimeInterface $timestamp, string $_): string { // Number of weeks between date and first Sunday of year $day = new \DateTime(sprintf('%d-01 Sunday', $timestamp->format('Y'))); return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); }, '%V' => 'W', '%W' => function (\DateTimeInterface $timestamp, string $_): string { // Number of weeks between date and first Monday of year $day = new \DateTime(sprintf('%d-01 Monday', $timestamp->format('Y'))); return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); }, // Month '%b' => $intl_formatter, '%B' => $intl_formatter, '%h' => $intl_formatter, '%m' => 'm', // Year '%C' => function (\DateTimeInterface $timestamp, string $_): string { // Century (-1): 19 for 20th century return (string)floor($timestamp->format('Y') / 100); }, '%g' => function (\DateTimeInterface $timestamp, string $_): string { return substr($timestamp->format('o'), -2); }, '%G' => 'o', '%y' => 'y', '%Y' => 'Y', // Time '%H' => 'H', '%k' => function (\DateTimeInterface $timestamp, string $_): string { return sprintf('% 2u', $timestamp->format('G')); }, '%I' => 'h', '%l' => function (\DateTimeInterface $timestamp, string $_): string { return sprintf('% 2u', $timestamp->format('g')); }, '%M' => 'i', '%p' => 'A', // AM PM (this is reversed on purpose!) '%P' => 'a', // am pm '%r' => 'h:i:s A', // %I:%M:%S %p '%R' => 'H:i', // %H:%M '%S' => 's', '%T' => 'H:i:s', // %H:%M:%S '%X' => $intl_formatter, // Preferred time representation based on locale, without the date // Timezone '%z' => 'O', '%Z' => 'T', // Time and Date Stamps '%c' => $intl_formatter, '%D' => 'm/d/Y', '%F' => 'Y-m-d', '%s' => 'U', '%x' => $intl_formatter, ]; $out = preg_replace_callback('/(?format($replace); } else { $result = $replace($timestamp, $pattern); } return match ($prefix) { '_' => preg_replace('/\G0(?=.)/', ' ', $result), '#', '-' => preg_replace('/^0+(?=.)/', '', $result), default => $result, }; }, $format); return str_replace('%%', '%', $out); } /** * @internal */ public function convertPhpFormatToLuxon(string $phpFormat): string { $phpToLuxonMapping = [ // Day 'd' => 'dd', 'D' => 'EEE', 'j' => 'd', 'l' => 'EEEE', 'N' => 'E', 'S' => 'o', // Note: Luxon doesn't directly support ordinal suffixes 'w' => 'E', // Luxon uses 1-7 for day of the week (1 = Monday, 7 = Sunday) 'z' => 'o', // Week 'W' => 'WW', // Month 'F' => 'MMMM', 'm' => 'MM', 'M' => 'MMM', 'n' => 'M', 't' => '', // Luxon doesn't have a token for the number of days in the month // Year 'L' => '', // Luxon doesn't have a token for leap year 'o' => 'GGGG', 'Y' => 'yyyy', 'y' => 'yy', // Time 'a' => 'a', 'A' => 'a', 'B' => '', // Luxon doesn't support Swatch Internet time 'g' => 'h', 'G' => 'H', 'h' => 'hh', 'H' => 'HH', 'i' => 'mm', 's' => 'ss', 'u' => 'SSS', // Note: Luxon uses SSS for milliseconds // Timezone 'e' => 'ZZZZ', 'I' => '', // Luxon doesn't have a token for DST 'O' => 'ZZ', 'P' => 'ZZ', 'p' => 'ZZ', 'T' => 'ZZZ', 'Z' => 'ZZ', // Full Date/Time 'c' => 'yyyy-MM-dd\'T\'HH:mm:ssZZ', 'r' => 'EEE, dd MMM yyyy HH:mm:ss ZZ', 'U' => 'X', ]; $luxonFormat = ''; $length = strlen($phpFormat); for ($i = 0; $i < $length; $i++) { $char = $phpFormat[$i]; $luxonFormat .= $phpToLuxonMapping[$char] ?? ("'" . $char . "'"); } return $luxonFormat; } /** * @param \DateInterval $interval Date interval as generated by \DateTimeInterface::diff() * @param string $labels Translated label string, e.g. `$GLOBALS['LANG']->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')` */ public static function formatDateInterval( \DateInterval $interval, ?string $labels = null, ): string { $labels = GeneralUtility::trimExplode('|', $labels, true); if (count($labels) === 4) { $labels = array_merge($labels, $labels); } elseif (count($labels) !== 8) { $labels = ['min', 'hrs', 'days', 'yrs', 'min', 'hour', 'day', 'year']; } $sign = $interval->invert ? '-' : ''; if ($interval->y > 0) { $years = $interval->y; $singular = $years === 1; return $sign . $years . ' ' . ($singular ? $labels[7] : $labels[3]); } $days = $interval->days !== false && $interval->days > 0 ? $interval->days : $interval->d; if ($days > 0) { $singular = $days === 1; return $sign . $days . ' ' . ($singular ? $labels[6] : $labels[2]); } if ($interval->h > 0) { $hours = $interval->h; $singular = $hours === 1; return $sign . $hours . ' ' . ($singular ? $labels[5] : $labels[1]); } $minutes = $interval->i; $singular = $minutes === 1; if ($minutes === 0) { $sign = ''; } return $sign . $minutes . ' ' . ($singular ? $labels[4] : $labels[0]); } }