Gérer les fuseaux horaires dans un système de réservation
Comment afficher les créneaux dans le fuseau du visiteur sans casser la cohérence côté admin — retour sur l'architecture du booking de Nocly.fr.
Le problème
Quand une visiteuse de Montréal réserve un créneau qui apparaît à 14h00 et qu'elle reçoit un courriel confirmant un rendez-vous à 20h00, elle décroche. Multipliez par quatre locales — fr-FR, fr-CA, en-US, en-CA — et le moindre créneau devient un casse-tête. La règle est simple : le serveur stocke en UTC, le client affiche dans le fuseau de la visiteuse, et chaque courriel rappelle explicitement le fuseau utilisé.
Côté client : Intl.DateTimeFormat
Plutôt que d'embarquer une lib comme moment-timezone (180 ko gzip), j'utilise l'API native Intl.DateTimeFormat. Elle détecte le fuseau de l'utilisateur et formate la date en conséquence, sans dépendance.
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Ex: "Europe/Paris" ou "America/Toronto"
const formatted = new Intl.DateTimeFormat(locale, {
dateStyle: "full",
timeStyle: "short",
timeZone: tz,
}).format(new Date(slot.starts_at));Côté serveur : stocker le fuseau du visiteur
Au moment de la réservation, le client envoie son fuseau IANA dans le payload. Je le stocke dans la colonne timezone de la table bookings. Ça permet de regénérer les courriels et le fichier ICS dans le bon fuseau, même si la personne revient depuis un autre appareil.
Pièges à éviter
Premier piège : ne jamais comparer des heures locales — toujours convertir en UTC avant. Deuxième piège : un fichier ICS doit indiquer le fuseau dans la propriété DTSTART;TZID=…, sinon Outlook et Google Agenda interprètent différemment. Troisième piège : le passage à l'heure d'été peut casser un créneau récurrent — privilégier des slots ponctuels stockés en UTC plutôt qu'une règle locale.
Cet article fait partie d'une série sur les choix techniques de Nocly.fr. Une question ? Le formulaire de contact est ouvert.