JS protip : Formatter une date/heure selon les usages locaux
Par Christophe Porteneuve • Publié le 26 octobre 2022 • 5 min

This page is also available in English.

Ah, les joies du formatage de dates (et d’heures)… Bien sûr, on peut faire un format unique, numérique, et dire qu’on a fini. Ça peut passer (surtout pour des formats techniques), mais ce n’est pas idéal, et même franchement pas propre lorsqu’on s’adresse à des humains, avec leurs cultures et les usages de formatage de celles-ci.

Après tout, si un site américain t’affiche 08/12/2022, s’agit-il du 12 août ou du 8 décembre ? Sans savoir quelle approche il retient pour la prise en compte de ta locale, on n’a aucune certitude !

Voyons ensemble quelles options on a pour faire ça bien sans surcharger le JS de notre appli.

Les méthodes historiques de Date

Au commencement était la méthode toString() de Date. Celle-ci nous sort un format américain abrégé de la date, de l’heure et du fuseau horaire, pour le fuseau local par défaut. La forme n’est pas sans rappeler celle des champs d’horodatage des protocoles Internet, tels que définis en dernier lieu par la RFC 5322 :

// 🤨 Meh.  Format fixe, pas adapté au locale

new Date(2022, 9, 26, 9).toString()
// => 'Wed Oct 26 2022 09:00:00 GMT+0200 (heure d’été d’Europe centrale)'

Remarque comme le nom du fuseau horaire (timezone) est, lui, localisé selon tes préférences navigateur, alors que le reste utilise en dur les abréviations anglaises. #CaptainConsistency

Ce format est en fait la concaténation, entre autres, des méthodes toDateString() et toTimeString(), qui sont là pratiquement depuis le début aussi, au même titre que toGMTString(), qui a été dépréciée au profit de toUTCString() (l’UTC ayant remplacé le GMT depuis bien longtemps). ES5 a aussi rajouté le très utile toISOString(), qui suit le standard ISO8601, utilisé par exemple dans JSON et tout plein de standards comme mécanisme de sérialisation par défaut des dates/heures :

// 🤨 Meh.  Formats fixes, pas adapté au locale

const when = new Date(2022, 9, 26, 9)
when.toDateString() // => 'Wed Oct 26 2022'
when.toTimeString() // => '09:00:00 GMT+0200 (heure d’été d’Europe centrale)'
when.toUTCString() // => 'Wed, 26 Oct 2022 07:00:00 GMT'
when.toISOString() // => '2022-10-26T07:00:00.000Z'
when.toJSON() // => '2022-10-26T07:00:00.000Z'

Et oui, toUTCString() utilise un code de fuseau… GMT 🤦🏻 soupir Bref, tout ça ne vaut pas grand chose quand on affiche des horodatages à destination des humains.

On utilise des bibliothèques alors ?

Historiquement, pour pouvoir formater intelligemment des dates et heures, on avait recours à des bibliothèques tierces.

La plus célèbre dans ce domaine, encore très utilisée même si ça fait des années que son équipe nous encourage à utiliser autre chose, est Moment.js. C’est une super manière d’embarquer 300Ko minifiés de JS dans nos bundles pour rien (et même si tu configures ton bundler pour n’inclure que les locales qui t’intéressent, tu en as pour 70Ko minimum).

Depuis quelques années, Luxon (successeur officiel de Moment) et date-fns ont pris le relais, même si leur cœur de métier n’est pas tant le formatage que les manipulations (distances, avancement / recul, etc.). Ces deux dernières sont d’ailleurs entièrement basées sur l’API Intl (standard ECMA-402), qui fait partie de la bibliothèque standard de JavaScript.

Du coup, la valeur ajoutée de ces bibliothèques, pour les cas qui nous occupent, est à peu près nulle.

Intl.DateTimeFormat

Voilà des années que nous disposons de l’API Intl (standardisée par le même comité qui s’occupe de JavaScript et JSON), dont la raison d’être consiste à nous donner un accès JavaScript le plus complet possible aux formatages usuels de données, pour tous les locales standardisés.

Dates et heures, montants numériques, listes, pluralisation, collation / tri… Chaque locale a ses conventions, habitudes, usages… Et tout ça est activement maintenu au sein du Common Locale Data Repository, ou CLDR, qui fait lui-même partie d’un projet plus vaste appelé les International Components for Unicode (ICU).

Tu t’en doutes, tout ça fait un gros paquet de données, présent dans chaque OS au travers d’une ou plusieurs bibliothèques système. Sur Ubuntu par exemple, la libicu66 totalise plus de 30Mo de données. C’est à peu près le même volume sur les autres OS, mais on n’a évidemment pas envie de trimballer tout ça dans notre JavaScript.

L’API Intl nous offre un accès direct à la plupart des infos de la CLDR de notre OS d’exécution, ce qui est trop cool 😀

Pour ce qui est des dates et heures, on a deux classes, la plus connue étant Intl.DateTimeFormat. Le constructeur a la signature suivante :

new Intl.DateTimeFormat([locales[, options]])

On peut lui préciser un ou plusieurs locales souhaités, par ordre décroissant de priorité. Il s’agit de chaînes BCP47 plus ou moins raffinées (allant du simple code de langue générique, comme 'fr', pour le français en général au code détaillé de variante type de-DE-u-co-phonebk, qui est la variante annuaire de l’ordre de tri allemand). Je te conseille de toujours préciser la déclinaison pays (ex. fr-FR pour le français de France), mais au-delà c’est rarement pertinent.

Le système utilisera le premier locale qu’il est à même de prendre en charge vu sa base CLDR sous-jacente.

On peut aussi ajouter à l’info de locales des options, et là y’en a beaucoup. On ne va pas toutes les couvrir, en particulier nous allons ignorer celles qui permettent de choisir explicitement les segments d’information (jour de la semaine, jour du mois, mois, année, heures, minutes, secondes, etc.) ou d’altérer les comportements par défaut du locale (cycles horaires, etc.).

Cependant, nous allons nous intéresser ici à des options très utiles lorsqu’on suit les usages en vigueur d’un locale :

  • dateStyle pour le style de date
  • timeStyle pour le style d’heure
  • timeZone pour représenter l’information dans un fuseau horaire spécifique

dateStyle et timeStyle

Les options dateStyle et timeStyle autorisent chacune 4 valeurs possibles : 'short', 'medium', 'long' et 'full', par ordre croissant de niveau de détail. Tous les locales ne proposent pas autant de variations, mais l’API ajustera automatiquement le tir.

Voici quelques exemples sur 3 langues bien distinctes :

// 🤯 Intl en force !

const when = new Date(2022, 9, 26, 9)

new Intl.DateTimeFormat('fr-FR', { dateStyle: 'short' }).format(when)
// => '26/10/2022'

new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(when)
// => '26 oct. 2022, 09:00'

new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'long',
timeStyle: 'short',
}).format(when)
// => '26 octobre 2022 à 09:00'

new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'full',
timeStyle: 'medium',
}).format(when)
// => 'mercredi 26 octobre 2022 à 09:00:00'

new Intl.DateTimeFormat('de-DE', { dateStyle: 'full' }).format(when)
// => 'Mittwoch, 26. Oktober 2022'

new Intl.DateTimeFormat('ja-JP', {
dateStyle: 'full',
timeStyle: 'short',
}).format(when)
// => '2022年10月26日水曜日 9:00'

Pas mal hein ?! 😉

Fuseaux horaires

On peut bien sûr demander à afficher l’heure dans un fuseau donné. L’objet Date qu’on lui passe représente un moment absolu (même si, quand on l’a créé, on a précisé ou supposé un fuseau de référence). Exemples :

// Comme tu n'exécutes pas forcément ça dans une TZ FR, je cale la TZ d'office…
const videoFR = new Date('2022-10-26 09:00:00 GMT+02:00')
const meetingFR = new Date('2022-11-04 09:00:00 GMT+01:00')

const valleyFormatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'America/Los_Angeles',
})

valleyFormatter.format(videoFR) // => Oct 26, 2022, 12:00 AM (9h de décalage, classique)
valleyFormatter.format(meetingFR) // => Nov 4, 2022, 1:00 AM (8h ! Les US sont encore en DST…)

Les raccourcis sur Date : toLocale…String()

Si tu as plein d’horodatages à formater, tu auras tout intérêt à construire ton ou tes formateur(s) une seule fois, et les réutiliser pour chaque donnée à formater. En particulier si tu les enrobes dans une fonction utilitaire, assure-toi de ne pas les ré-instancier à chaque fois, fais-en des singletons de ton module :

// ❌ Argh, on re-crée un formateur fixe à chaque appel !

export function formatTimelineStamp(stamp) {
return new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(stamp)
}

// ✅ Yes, on réutilise le formateur d'une fois sur l'autre

const timelineFormatter = new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'medium',
timeStyle: 'short',
})

export function formatTimelineStamp(stamp) {
return timelineFormatter.format(stamp)
}

En revanche, si tu as un besoin “one-shot” de formatage, tu peux raccourcir un peu ton code en utilisant directement les méthodes toLocaleString(), toLocaleDateString() et toLocaleTimeString() de Date. Elles prennent toutes les mêmes arguments que le constructeur de Intl.DateTimeFormat (mais restreints aux options de date ou d’heure pour les variantes spécifiques) :

// ⚠️ Uniquement pour du one-shot !

const when = new Date(2022, 9, 26, 9)

when.toLocaleString('fr-FR')
// => '26/10/2022 09:00:00'

when.toLocaleDateString('fr-FR')
// => ''26/10/2022'

when.toLocaleTimeString('fr-FR')
// => '09:00:00'

when.toLocaleString('fr-FR', { dateStyle: 'long', timeStyle: 'short' })
// => '26 octobre 2022 à 09:00 '

when.toLocaleDateString('fr-FR', { dateStyle: 'full' })
// => 'mercredi 26 octobre 2022'

when.toLocaleTimeString('fr-FR', { timeStyle: 'medium' })
// => '09:00:00'

Envie d’aller plus loin ?

Tu as de la veine, on a 2 protips complémentaires autour des intervalles de date et des distances temporelles qui sortent demain et après-demain !

C’est dispo où ?

Bin écoute, partout. Si on prend tout ce que je t’ai montré ici, tu es tranquille sur tout navigateur qui a, disons, moins de 3 ans, ainsi que depuis Node 13 et Deno 1.8. Donc vas-y !

Des astuces en veux-tu en voilà !

On a tout plein d’articles et de vidéos existants et encore beaucoup à venir. Pour ne rien manquer, tu devrais penser à t’abonner à notre newsletter, à notre chaîne YouTube, nous suivre sur Twitter ou encore mieux, à suivre une de nos formations du feu de dieu 🔥 !