fix: ISO countdown date, IntlDateFormatter and UTF-8 truncation
This commit is contained in:
+12
-12
@@ -20,7 +20,7 @@ setSecurityHeaders();
|
|||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" href="css/countdown.css?v=<?php echo filemtime('css/countdown.css'); ?>">
|
<link rel="stylesheet" href="css/countdown.css?v=<?php echo filemtime('css/countdown.css'); ?>">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous">
|
||||||
|
|
||||||
<!-- Favicons -->
|
<!-- Favicons -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
|
||||||
@@ -40,9 +40,7 @@ setSecurityHeaders();
|
|||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="<?php echo SITE_NAME; ?> - Ouverture prochaine">
|
<meta property="og:title" content="<?php echo SITE_NAME; ?> - Ouverture prochaine">
|
||||||
<meta property="og:description" content="La plateforme multimédia <?php echo SITE_NAME; ?> ouvrira ses portes le <?php
|
<meta property="og:description" content="La plateforme multimédia <?php echo SITE_NAME; ?> ouvrira ses portes le <?php
|
||||||
$targetDate = new DateTime(COUNTDOWN_TARGET_DATE);
|
echo formatDateFr(new DateTime(COUNTDOWN_TARGET_DATE));
|
||||||
setlocale(LC_TIME, 'fr_FR.UTF-8');
|
|
||||||
echo strftime('%e %B %Y', $targetDate->getTimestamp());
|
|
||||||
?>. Restez connectés !">
|
?>. Restez connectés !">
|
||||||
<meta property="og:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
<meta property="og:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
||||||
<meta property="og:url" content="<?php echo htmlspecialchars(getCurrentUrl()); ?>">
|
<meta property="og:url" content="<?php echo htmlspecialchars(getCurrentUrl()); ?>">
|
||||||
@@ -54,15 +52,19 @@ setSecurityHeaders();
|
|||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="<?php echo SITE_NAME; ?> - Ouverture prochaine">
|
<meta name="twitter:title" content="<?php echo SITE_NAME; ?> - Ouverture prochaine">
|
||||||
<meta name="twitter:description" content="La plateforme multimédia <?php echo SITE_NAME; ?> ouvrira ses portes le <?php
|
<meta name="twitter:description" content="La plateforme multimédia <?php echo SITE_NAME; ?> ouvrira ses portes le <?php
|
||||||
$targetDate = new DateTime(COUNTDOWN_TARGET_DATE);
|
echo formatDateFr(new DateTime(COUNTDOWN_TARGET_DATE));
|
||||||
setlocale(LC_TIME, 'fr_FR.UTF-8');
|
|
||||||
echo strftime('%e %B %Y', $targetDate->getTimestamp());
|
|
||||||
?>. Restez connectés !">
|
?>. Restez connectés !">
|
||||||
<meta name="twitter:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
<meta name="twitter:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
||||||
|
|
||||||
<!-- Configuration JavaScript -->
|
<!-- Configuration JavaScript : date cible en ISO 8601 avec fuseau horaire -->
|
||||||
<script nonce="<?php echo getCspNonce(); ?>">
|
<script nonce="<?php echo getCspNonce(); ?>">
|
||||||
const COUNTDOWN_TARGET_DATE = '<?php echo defined('COUNTDOWN_TARGET_DATE') ? COUNTDOWN_TARGET_DATE : '2025-10-11 00:00:00'; ?>';
|
const COUNTDOWN_TARGET_DATE = '<?php
|
||||||
|
$countdownTargetDate = new DateTime(
|
||||||
|
defined('COUNTDOWN_TARGET_DATE') ? COUNTDOWN_TARGET_DATE : '2025-10-11 00:00:00',
|
||||||
|
new DateTimeZone(DEFAULT_TIMEZONE)
|
||||||
|
);
|
||||||
|
echo $countdownTargetDate->format(DateTime::ATOM);
|
||||||
|
?>';
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -100,9 +102,7 @@ setSecurityHeaders();
|
|||||||
<!-- Message -->
|
<!-- Message -->
|
||||||
<div class="countdown-message">
|
<div class="countdown-message">
|
||||||
<p>La plateforme ouvrira ses portes le <strong><?php
|
<p>La plateforme ouvrira ses portes le <strong><?php
|
||||||
$targetDate = new DateTime(COUNTDOWN_TARGET_DATE);
|
echo formatDateFr(new DateTime(COUNTDOWN_TARGET_DATE), true);
|
||||||
setlocale(LC_TIME, 'fr_FR.UTF-8');
|
|
||||||
echo strftime('%e %B %Y à %H:%M', $targetDate->getTimestamp());
|
|
||||||
?></strong> (heure de La Réunion).</p>
|
?></strong> (heure de La Réunion).</p>
|
||||||
|
|
||||||
<!-- Heures des autres territoires -->
|
<!-- Heures des autres territoires -->
|
||||||
|
|||||||
@@ -430,22 +430,62 @@ function formatDateISO8601($dateString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tronque un texte à une longueur donnée
|
* Formate une date en français (remplace strftime, déprécié depuis PHP 8.1)
|
||||||
|
*
|
||||||
|
* Utilise IntlDateFormatter quand l'extension intl est disponible,
|
||||||
|
* sinon replie sur un formatage manuel (mois français en toutes lettres).
|
||||||
|
*
|
||||||
|
* @param DateTimeInterface $date Date à formater
|
||||||
|
* @param bool $withTime true pour ajouter « à HH:mm »
|
||||||
|
* @return string Date formatée, ex. « 11 octobre 2025 » ou « 11 octobre 2025 à 00:00 »
|
||||||
|
*/
|
||||||
|
function formatDateFr($date, $withTime = false) {
|
||||||
|
if (class_exists('IntlDateFormatter')) {
|
||||||
|
$formatter = new IntlDateFormatter(
|
||||||
|
'fr_FR',
|
||||||
|
IntlDateFormatter::FULL,
|
||||||
|
IntlDateFormatter::FULL,
|
||||||
|
$date->getTimezone(), // Le formateur ignore le fuseau du DateTime sans ça
|
||||||
|
IntlDateFormatter::GREGORIAN,
|
||||||
|
$withTime ? "d MMMM yyyy 'à' HH:mm" : 'd MMMM yyyy'
|
||||||
|
);
|
||||||
|
$formatted = $formatter->format($date);
|
||||||
|
if ($formatted !== false) {
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repli sans extension intl : mois français en toutes lettres
|
||||||
|
$months = [
|
||||||
|
1 => 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
|
||||||
|
'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'
|
||||||
|
];
|
||||||
|
$formatted = $date->format('j') . ' ' . $months[(int) $date->format('n')] . ' ' . $date->format('Y');
|
||||||
|
|
||||||
|
if ($withTime) {
|
||||||
|
$formatted .= ' à ' . $date->format('H:i');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tronque un texte à une longueur donnée (en caractères UTF-8)
|
||||||
*
|
*
|
||||||
* @param string $text Texte à tronquer
|
* @param string $text Texte à tronquer
|
||||||
* @param int $length Longueur maximale
|
* @param int $length Longueur maximale
|
||||||
* @return string Texte tronqué
|
* @return string Texte tronqué
|
||||||
*/
|
*/
|
||||||
function truncateText($text, $length = 200) {
|
function truncateText($text, $length = 200) {
|
||||||
if (strlen($text) <= $length) {
|
if (mb_strlen($text) <= $length) {
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
$truncated = substr($text, 0, $length);
|
$truncated = mb_substr($text, 0, $length);
|
||||||
$lastSpace = strrpos($truncated, ' ');
|
$lastSpace = mb_strrpos($truncated, ' ');
|
||||||
|
|
||||||
if ($lastSpace !== false) {
|
if ($lastSpace !== false) {
|
||||||
$truncated = substr($truncated, 0, $lastSpace);
|
$truncated = mb_substr($truncated, 0, $lastSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $truncated . '...';
|
return $truncated . '...';
|
||||||
|
|||||||
+50
-1
@@ -2,9 +2,58 @@
|
|||||||
* Script de compte à rebours pour la page de maintenance
|
* Script de compte à rebours pour la page de maintenance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une date ISO 8601 en timestamp, compatible Safari.
|
||||||
|
*
|
||||||
|
* Safari renvoie Invalid Date pour les formats non strictement ISO
|
||||||
|
* (ex. « 2025-10-11 00:00:00 » émis historiquement par PHP) : la chaîne
|
||||||
|
* est donc analysée manuellement. Sans fuseau explicite, la date est
|
||||||
|
* interprétée en UTC — PHP émet de toute façon toujours un décalage.
|
||||||
|
*
|
||||||
|
* @param {string|number} value Date ISO 8601 (ou timestamp déjà numérique)
|
||||||
|
* @returns {number} Timestamp en millisecondes
|
||||||
|
*/
|
||||||
|
function parseTargetDate(value) {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return new Date(value).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = String(value).match(
|
||||||
|
/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
// Repli sur l'analyse native pour les formats inattendus
|
||||||
|
return new Date(value).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, year, month, day, hours = '0', minutes = '0', seconds = '0', offset] = match;
|
||||||
|
|
||||||
|
let timestamp = Date.UTC(
|
||||||
|
parseInt(year, 10),
|
||||||
|
parseInt(month, 10) - 1,
|
||||||
|
parseInt(day, 10),
|
||||||
|
parseInt(hours, 10),
|
||||||
|
parseInt(minutes, 10),
|
||||||
|
parseInt(seconds, 10)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (offset && offset !== 'Z') {
|
||||||
|
// Un décalage « +02:00 » signifie « 2 h d'avance sur UTC » : on le soustrait
|
||||||
|
const sign = offset[0] === '+' ? 1 : -1;
|
||||||
|
const offsetDigits = offset.slice(1).replace(':', '');
|
||||||
|
const offsetMinutes = sign * (
|
||||||
|
parseInt(offsetDigits.slice(0, 2), 10) * 60 + parseInt(offsetDigits.slice(2, 4), 10)
|
||||||
|
);
|
||||||
|
timestamp -= offsetMinutes * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
class CountdownTimer {
|
class CountdownTimer {
|
||||||
constructor(targetDate) {
|
constructor(targetDate) {
|
||||||
this.targetDate = new Date(targetDate).getTime();
|
this.targetDate = parseTargetDate(targetDate);
|
||||||
this.elements = {
|
this.elements = {
|
||||||
days: document.getElementById('countdown-days'),
|
days: document.getElementById('countdown-days'),
|
||||||
hours: document.getElementById('countdown-hours'),
|
hours: document.getElementById('countdown-hours'),
|
||||||
|
|||||||
@@ -80,11 +80,17 @@ function loadCountdown({ elements = null, now = FIXED_NOW } = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vm.createContext(sandbox);
|
vm.createContext(sandbox);
|
||||||
const CountdownTimer = vm.runInContext(COUNTDOWN_SOURCE + '\nCountdownTimer;', sandbox);
|
const { CountdownTimer, parseTargetDate } = vm.runInContext(
|
||||||
|
COUNTDOWN_SOURCE + '\n({ CountdownTimer, parseTargetDate });',
|
||||||
|
sandbox
|
||||||
|
);
|
||||||
|
|
||||||
return { CountdownTimer, window: windowMock, timers };
|
return { CountdownTimer, parseTargetDate, window: windowMock, timers };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convertit un timestamp en chaîne ISO 8601, comme le fait PHP via DateTime::ATOM
|
||||||
|
const toISO = (timestamp) => new Date(timestamp).toISOString();
|
||||||
|
|
||||||
describe('CountdownTimer', () => {
|
describe('CountdownTimer', () => {
|
||||||
test('formatNumber complète les nombres sur deux chiffres', () => {
|
test('formatNumber complète les nombres sur deux chiffres', () => {
|
||||||
const { CountdownTimer } = loadCountdown();
|
const { CountdownTimer } = loadCountdown();
|
||||||
@@ -104,7 +110,7 @@ describe('CountdownTimer', () => {
|
|||||||
|
|
||||||
const elements = makeElements();
|
const elements = makeElements();
|
||||||
const { CountdownTimer } = loadCountdown({ elements });
|
const { CountdownTimer } = loadCountdown({ elements });
|
||||||
const timer = new CountdownTimer(FIXED_NOW + distance);
|
const timer = new CountdownTimer(toISO(FIXED_NOW + distance));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(elements['countdown-days'].textContent, '02');
|
assert.strictEqual(elements['countdown-days'].textContent, '02');
|
||||||
@@ -121,7 +127,7 @@ describe('CountdownTimer', () => {
|
|||||||
|
|
||||||
const elements = makeElements();
|
const elements = makeElements();
|
||||||
const { CountdownTimer } = loadCountdown({ elements });
|
const { CountdownTimer } = loadCountdown({ elements });
|
||||||
const timer = new CountdownTimer(FIXED_NOW + distance);
|
const timer = new CountdownTimer(toISO(FIXED_NOW + distance));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(elements['countdown-days'].textContent, '01');
|
assert.strictEqual(elements['countdown-days'].textContent, '01');
|
||||||
@@ -136,7 +142,7 @@ describe('CountdownTimer', () => {
|
|||||||
test('une date passée arrête le compte à rebours et redirige vers /', () => {
|
test('une date passée arrête le compte à rebours et redirige vers /', () => {
|
||||||
const elements = makeElements();
|
const elements = makeElements();
|
||||||
const { CountdownTimer, window } = loadCountdown({ elements });
|
const { CountdownTimer, window } = loadCountdown({ elements });
|
||||||
const timer = new CountdownTimer(FIXED_NOW - 1000);
|
const timer = new CountdownTimer(toISO(FIXED_NOW - 1000));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// onComplete() redirige vers la page principale
|
// onComplete() redirige vers la page principale
|
||||||
@@ -155,7 +161,7 @@ describe('CountdownTimer', () => {
|
|||||||
test('une distance nulle affiche zéro partout sans rediriger', () => {
|
test('une distance nulle affiche zéro partout sans rediriger', () => {
|
||||||
const elements = makeElements();
|
const elements = makeElements();
|
||||||
const { CountdownTimer, window } = loadCountdown({ elements });
|
const { CountdownTimer, window } = loadCountdown({ elements });
|
||||||
const timer = new CountdownTimer(FIXED_NOW);
|
const timer = new CountdownTimer(toISO(FIXED_NOW));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(elements['countdown-days'].textContent, '00');
|
assert.strictEqual(elements['countdown-days'].textContent, '00');
|
||||||
@@ -170,7 +176,7 @@ describe('CountdownTimer', () => {
|
|||||||
|
|
||||||
test('fonctionne sans éléments DOM présents', () => {
|
test('fonctionne sans éléments DOM présents', () => {
|
||||||
const { CountdownTimer } = loadCountdown({ elements: null });
|
const { CountdownTimer } = loadCountdown({ elements: null });
|
||||||
const timer = new CountdownTimer(FIXED_NOW + 60 * 1000);
|
const timer = new CountdownTimer(toISO(FIXED_NOW + 60 * 1000));
|
||||||
|
|
||||||
// Aucune erreur attendue malgré l'absence des éléments
|
// Aucune erreur attendue malgré l'absence des éléments
|
||||||
timer.stop();
|
timer.stop();
|
||||||
@@ -178,7 +184,7 @@ describe('CountdownTimer', () => {
|
|||||||
|
|
||||||
test('démarre un intervalle d\'une seconde et stop() le nettoie', () => {
|
test('démarre un intervalle d\'une seconde et stop() le nettoie', () => {
|
||||||
const { CountdownTimer, timers } = loadCountdown();
|
const { CountdownTimer, timers } = loadCountdown();
|
||||||
const timer = new CountdownTimer(FIXED_NOW + 60 * 1000);
|
const timer = new CountdownTimer(toISO(FIXED_NOW + 60 * 1000));
|
||||||
|
|
||||||
assert.strictEqual(timers.intervals.length, 1);
|
assert.strictEqual(timers.intervals.length, 1);
|
||||||
assert.strictEqual(timers.intervals[0].delay, 1000);
|
assert.strictEqual(timers.intervals[0].delay, 1000);
|
||||||
@@ -187,4 +193,78 @@ describe('CountdownTimer', () => {
|
|||||||
timer.stop();
|
timer.stop();
|
||||||
assert.deepStrictEqual(timers.cleared, [timer.interval]);
|
assert.deepStrictEqual(timers.cleared, [timer.interval]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('accepte une date ISO 8601 avec fuseau horaire (contrat PHP)', () => {
|
||||||
|
// FIXED_NOW = 2030-01-01T00:00:00Z
|
||||||
|
// Cible : 2030-01-02T05:00:00+04:00, soit 2030-01-02T01:00:00Z → 1 jour + 1 heure
|
||||||
|
const elements = makeElements();
|
||||||
|
const { CountdownTimer } = loadCountdown({ elements });
|
||||||
|
const timer = new CountdownTimer('2030-01-02T05:00:00+04:00');
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.strictEqual(elements['countdown-days'].textContent, '01');
|
||||||
|
assert.strictEqual(elements['countdown-hours'].textContent, '01');
|
||||||
|
assert.strictEqual(elements['countdown-minutes'].textContent, '00');
|
||||||
|
assert.strictEqual(elements['countdown-seconds'].textContent, '00');
|
||||||
|
} finally {
|
||||||
|
timer.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseTargetDate', () => {
|
||||||
|
test('analyse une date ISO 8601 avec décalage horaire', () => {
|
||||||
|
const { parseTargetDate } = loadCountdown();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
parseTargetDate('2025-10-11T00:00:00-04:00'),
|
||||||
|
Date.UTC(2025, 9, 11, 4, 0, 0)
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
parseTargetDate('2025-10-11T00:00:00+02:00'),
|
||||||
|
Date.UTC(2025, 9, 10, 22, 0, 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('analyse une date ISO 8601 suffixée Z', () => {
|
||||||
|
const { parseTargetDate } = loadCountdown();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
parseTargetDate('2025-10-11T00:00:00Z'),
|
||||||
|
Date.UTC(2025, 9, 11, 0, 0, 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepte le format historique avec espace (cas Safari)', () => {
|
||||||
|
const { parseTargetDate } = loadCountdown();
|
||||||
|
|
||||||
|
// Sans fuseau explicite, la date est interprétée en UTC
|
||||||
|
assert.strictEqual(
|
||||||
|
parseTargetDate('2025-10-11 00:00:00'),
|
||||||
|
Date.UTC(2025, 9, 11, 0, 0, 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepte les millisecondes et les décalages sans deux-points', () => {
|
||||||
|
const { parseTargetDate } = loadCountdown();
|
||||||
|
|
||||||
|
assert.strictEqual(parseTargetDate('2030-01-01T00:00:00.000Z'), FIXED_NOW);
|
||||||
|
assert.strictEqual(
|
||||||
|
parseTargetDate('2025-10-11T00:00:00-0400'),
|
||||||
|
Date.UTC(2025, 9, 11, 4, 0, 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepte un timestamp numérique tel quel', () => {
|
||||||
|
const { parseTargetDate } = loadCountdown();
|
||||||
|
|
||||||
|
assert.strictEqual(parseTargetDate(FIXED_NOW), FIXED_NOW);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retourne le même instant que new Date pour une date ISO valide', () => {
|
||||||
|
const { parseTargetDate } = loadCountdown();
|
||||||
|
const iso = '2030-06-15T12:30:45+00:00';
|
||||||
|
|
||||||
|
assert.strictEqual(parseTargetDate(iso), new Date(iso).getTime());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tests unitaires pour les fonctions de formatage de includes/config.php
|
* Tests unitaires pour les fonctions de formatage de includes/lib/format.php
|
||||||
* (formatDuration, formatViewCount, formatDate, formatVideosData)
|
* (formatDuration, formatViewCount, formatDate, formatVideosData)
|
||||||
* et truncateText de includes/structured-data.php
|
* et truncateText de includes/structured-data.php
|
||||||
*/
|
*/
|
||||||
@@ -210,3 +210,39 @@ assertEquals(
|
|||||||
truncateText($textNoSpace, 200),
|
truncateText($textNoSpace, 200),
|
||||||
'truncateText coupe à la limite quand il n\'y a pas d\'espace'
|
'truncateText coupe à la limite quand il n\'y a pas d\'espace'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- truncateText : UTF-8 multioctet (mb_substr) -----------------------------
|
||||||
|
|
||||||
|
$utf8WithSpace = str_repeat('é', 150) . ' ' . str_repeat('b', 100); // 251 caractères
|
||||||
|
assertEquals(
|
||||||
|
str_repeat('é', 150) . '...',
|
||||||
|
truncateText($utf8WithSpace, 200),
|
||||||
|
'truncateText compte les caractères UTF-8 et non les octets'
|
||||||
|
);
|
||||||
|
|
||||||
|
$utf8NoSpace = str_repeat('€', 250); // 3 octets par caractère
|
||||||
|
$truncatedUtf8 = truncateText($utf8NoSpace, 200);
|
||||||
|
assertEquals(
|
||||||
|
str_repeat('€', 200) . '...',
|
||||||
|
$truncatedUtf8,
|
||||||
|
'truncateText ne coupe pas un caractère multioctet en deux'
|
||||||
|
);
|
||||||
|
assertEquals(1, preg_match('//u', $truncatedUtf8), 'truncateText retourne de l\'UTF-8 valide');
|
||||||
|
|
||||||
|
// --- formatDateFr (includes/structured-data.php) -----------------------------
|
||||||
|
|
||||||
|
$dateFr = new DateTime('2025-10-11 00:00:00', new DateTimeZone('Indian/Reunion'));
|
||||||
|
assertEquals('11 octobre 2025', formatDateFr($dateFr), 'formatDateFr formate la date longue en français');
|
||||||
|
assertEquals(
|
||||||
|
'11 octobre 2025 à 00:00',
|
||||||
|
formatDateFr($dateFr, true),
|
||||||
|
'formatDateFr ajoute l\'heure quand demandé'
|
||||||
|
);
|
||||||
|
|
||||||
|
$dateFrSummer = new DateTime('2026-07-04 12:30:00', new DateTimeZone('UTC'));
|
||||||
|
assertEquals('4 juillet 2026', formatDateFr($dateFrSummer), 'formatDateFr sans zéro initial sur le jour');
|
||||||
|
assertEquals(
|
||||||
|
'4 juillet 2026 à 12:30',
|
||||||
|
formatDateFr($dateFrSummer, true),
|
||||||
|
'formatDateFr utilise le fuseau de la date fournie'
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user