# Langues, dates civiles et fuseaux horaires

> Localisez en-têtes et libellés avec toute locale Intl, formatez les SuperScheduler.Date, et gérez fuseaux horaires et récurrence dans votre application.

Source: https://superscheduler.org/fr/docs/locales-dates-timezones/
Reviewed: 2026-10-07

Définissez `locale` avec n’importe quel identifiant de locale Intl, comme `fr-fr` : les noms des mois et des jours, l’horloge sur 12 ou 24 heures et le premier jour de la semaine en découlent, et `timeFormat` et `weekStarts` remplacent ces deux derniers. Les dates sont des valeurs civiles d’horloge murale : le planificateur ne convertit jamais de fuseau horaire et ne développe jamais les événements récurrents. Convertissez les instants dans le fuseau horaire de l’activité avant de passer les événements, reconvertissez-les à l’enregistrement, et développez les séries en occurrences dans votre application.

Un planificateur montre des dates à des personnes : deux préoccupations distinctes se rencontrent donc ici. La localisation décide de la façon d’écrire une date : noms, ordre, horloge et premier jour de la semaine. La sémantique du temps décide de quelle date il s’agit : la bibliothèque travaille avec des valeurs civiles d’horloge murale et laisse les fuseaux horaires et la récurrence à votre application. Ce guide couvre les deux, pour Pro et, quand c’est indiqué, pour Lite.

## Définir la locale
`locale` accepte n’importe quel identifiant de locale que `Intl` comprend, écrit à la manière de SuperScheduler, en minuscules : `en-us` (par défaut), `en-gb`, `fr-fr`, `de-de`, `es-es`, `pt-br`, `nl-nl`, `ja-jp`, etc. Il n’y a aucune liste à enregistrer. `en_US` est normalisé en `en-us`, et un identifiant que `Intl` ne sait pas résoudre se rabat sur `en-us`.

Dans Pro, la locale détermine :

- les noms des mois et des jours dans les en-têtes de temps par défaut et dans chaque motif `format` d’en-tête ;
- l’horloge par défaut quand `timeFormat` vaut `'Auto'` (12 heures pour `en-us`, 24 heures pour la plupart des locales européennes) ;
- le premier jour de la semaine quand `weekStarts` vaut `'Auto'` (dimanche pour `en-us` et `pt-br`, lundi pour la plupart des locales européennes) ;
- les motifs de date par défaut des en-têtes de jour et les dates de la carte de glissement ;
- la langue des annonces clavier et de certains libellés intégrés (voir [Chaînes intégrées](#built-in-strings)).

Lite accepte les mêmes identifiants `locale` pour ses en-têtes de jour.

```tsx
// src/FrenchPlanning.tsx
import { useCallback, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventClickArgs } from 'super-scheduler'

// One constant for the scheduler and for every date you format yourself.
const LOCALE = 'fr-fr'
const WEEK_START = SuperScheduler.Date.today().firstDayOfWeek(1)

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Day', format: 'dddd d MMMM' }, // "lundi 5 octobre"
  { groupBy: 'Hour' }, // default labels follow timeFormat: "0" to "23" here, "2 PM" in 12-hour mode
]

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly events: SuperScheduler.EventData[]
}

export function FrenchPlanning({ rooms, events }: Props) {
  const [summary, setSummary] = useState('')

  // SuperScheduler.Date#toString does not read the scheduler's locale: pass it explicitly.
  const onEventClick = useCallback((args: SchedulerEventClickArgs) => {
    const start = args.e.start().toString('dddd d MMMM, HH:mm', LOCALE)
    const end = args.e.end().toString('HH:mm', LOCALE)
    setSummary(`${args.e.text()} : ${start} – ${end}`)
  }, [])

  return (
    <>
      <p aria-live="polite">{summary}</p>
      <SuperSchedulerComponent
        locale={LOCALE}
        // 'Auto' already gives Monday and a 24-hour clock for fr-fr; explicit values ignore the locale.
        weekStarts={1}
        timeFormat="Clock24Hours"
        startDate={WEEK_START}
        days={7}
        scale="Hour"
        cellWidth={48}
        timeHeaders={TIME_HEADERS}
        resources={rooms}
        events={events}
        onEventClick={onEventClick}
      />
    </>
  )
}
```
Vous devriez voir des en-têtes comme « lundi 5 octobre » au-dessus de colonnes d’heures numérotées de `0` à `23`, une semaine qui commence le lundi, et un résumé en français quand vous cliquez sur un événement.

## Format d’horloge et premier jour de la semaine
`timeFormat` accepte `'Auto'`, `'Clock12Hours'` ou `'Clock24Hours'`. Il ne modifie que les libellés d’heures par défaut ; un `format` explicite dans `timeHeaders` l’emporte toujours (`'HH:mm'` pour 24 heures, `'h:mm tt'` pour 12 heures). C’est un réglage d’affichage, pas une conversion horaire : le changer ne déplace jamais un événement.

`weekStarts` accepte `'Auto'` ou un numéro de jour de `0` (dimanche) à `6` (samedi). Il agit sur les cellules et groupes d’en-tête `Week`, sur les lignes de semaine tracées en vue dézoomée et sur les numéros de semaine par défaut : numéros ISO quand les semaines commencent le lundi, numéros américains sinon. Dans votre propre code, `date.firstDayOfWeek()` prend le dimanche par défaut : passez donc la même valeur (`firstDayOfWeek(1)`) ou l’identifiant de locale (`firstDayOfWeek('fr-fr')`).

Ce que donne `'Auto'` pour quelques identifiants :

| Locale | `ddd d MMM` | Horloge | Début de semaine |
|---|---|---|---|
| `en-us` | Mo 5 Oct | 12 heures | dimanche |
| `en-gb` | Mo 5 Oct | 24 heures | lundi |
| `es-es` | L 5 oct | 24 heures | lundi |
| `de-de` | Mo 5 Okt | 24 heures | lundi |
| `fr-fr` | lu 5 oct. | 24 heures | lundi |
| `pt-br` | se 5 out. | 24 heures | dimanche |

## Formater les dates dans votre propre interface
`SuperScheduler.Date` formate avec les motifs de SuperScheduler : `yyyy`, `yy`, `MMMM`, `MMM`, `MM`, `M`, `dddd`, `ddd`, `dd`, `d`, `HH`, `H`, `hh`, `h`, `mm`, `m`, `ss`, `s` et `tt` (AM/PM). Tout le reste est du texte littéral.

> **Behavior:**
> `date.toString(pattern, locale)` ne lit pas la `locale` du planificateur. Sans le second argument, il formate en `en-us`. Gardez l’identifiant de locale dans une seule constante et passez-la au composant et à chaque appel de `toString`, comme dans l’exemple ci-dessus.

`ddd` est le nom court du jour fourni par `Intl`, qui fait une ou deux lettres dans plusieurs locales : « Mo » en anglais, « L » en espagnol, « lu » en français, « dl » en catalan. Utilisez `dddd` pour le nom complet, enregistrez vos propres noms courts (voir plus bas), ou formatez directement avec `Intl`. `date.toDate()` renvoie une `Date` native avec les mêmes ticks, à lire en UTC : formatez-la donc avec `timeZone: 'UTC'` pour afficher exactement la valeur civile sur n’importe quel appareil :

```ts
// src/format.ts
import type { SuperScheduler } from 'super-scheduler'

// toDate() returns a native Date with the same ticks, meant to be read in UTC.
// Formatting it with timeZone 'UTC' shows exactly the civil value, on any device.
const dayFormat = new Intl.DateTimeFormat('fr-FR', {
  weekday: 'short',
  day: 'numeric',
  month: 'short',
  timeZone: 'UTC',
})
const stayFormat = new Intl.DateTimeFormat('en-GB', {
  day: 'numeric',
  month: 'short',
  hour: '2-digit',
  minute: '2-digit',
  timeZone: 'UTC',
})

export function formatDay(date: SuperScheduler.Date): string {
  return dayFormat.format(date.toDate()) // "lun. 5 oct."
}

export function formatStay(start: SuperScheduler.Date, end: SuperScheduler.Date): string {
  return stayFormat.formatRange(start.toDate(), end.toDate())
}
```
## Noms et motifs personnalisés
`SuperScheduler.Locale.register()` remplace les noms et les motifs d’un identifiant pour tous les planificateurs et tous les appels de `toString` qui l’utilisent. Un `SuperScheduler.Locale` passé directement dans l’option `locale` est enregistré automatiquement.

```ts
// src/locale.ts
import { SuperScheduler } from 'super-scheduler'

// Start from the Intl data for the id: fields you leave out fall back to US English, not to French.
const base = SuperScheduler.Locale.find('fr-fr')

// Run once at startup, before the first scheduler mounts.
SuperScheduler.Locale.register(
  new SuperScheduler.Locale('fr-fr', {
    ...base,
    // `ddd` gives two-letter Intl abbreviations ("lu"); these read "lun.".
    dayNamesShort: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
  }),
)

// Every scheduler with locale="fr-fr" and every toString(pattern, 'fr-fr') now uses these names.
export const sample = new SuperScheduler.Date('2026-10-05').toString('ddd d MMM', 'fr-fr') // "lun. 5 oct."
```
> **Behavior:**
> Les champs que vous omettez dans `new SuperScheduler.Locale(id, fields)` prennent les valeurs par défaut de l’anglais américain (noms anglais, `M/d/yyyy`, horloge sur 12 heures, dimanche), et non les données `Intl` de cet identifiant. Partez de `SuperScheduler.Locale.find(id)` comme dans l’exemple, puis remplacez ce qu’il faut.

## Chaînes intégrées et leurs langues
La bibliothèque écrit elle-même quelques chaînes. La langue est la première partie de l’identifiant de locale (`ca-es` donne `ca`).

| Chaînes | Langues | Personnalisation |
|---|---|---|
| Annonces clavier et libellés de focus | anglais, espagnol, catalan, basque, galicien, allemand, français, italien, portugais | Non configurable |
| Durées et refus de la carte de glissement (« 2 nights », « Overlaps », « Not allowed ») | Les neuf mêmes langues | `dragCard={{ labels: { ... } }}` |
| Nom de la grille, textes de chargement, d’état vide et d’erreur | anglais, ou espagnol pour les locales `es` | `emptyState`, `errorState` et `loadingLabelText` ; le nom accessible de la grille est fixe |
| Libellés des entrées d’historique (« Move », « Resize ») | anglais, ou espagnol pour les locales `es` | `createHistory({ labels })` |
| Libellés de la minimap | anglais, ou espagnol pour les locales `es` | `createMinimap(control, element, { labels })` ou la prop `labels` de `SchedulerMinimap` |
| Badge du niveau de détail | anglais, ou espagnol pour les locales `es` | troisième argument de `createLodBadge` |
| Lite : libellé de la grille et texte d’état vide | anglais | `ariaLabel`, `emptyState` |

Toute autre langue affiche l’anglais : une application en français ou en allemand doit donc passer ses propres chaînes pour l’historique, la minimap, le badge et les textes d’état. Le texte des événements, les noms des ressources et tout HTML que vous affichez sont à traduire par vous.

## Dates civiles
Dans SuperScheduler, chaque date est une valeur civile d’horloge murale, sans fuseau horaire. `'2026-10-01T10:00:00'` signifie dix heures sur le planning, où que la page soit ouverte. Les conséquences :

- **Les chaînes exigent les secondes.** `'2026-10-01'` et `'2026-10-01T10:00:00'` sont valides ; `'2026-10-01T10:00'` lève l’erreur « is not an ISO 8601 date ». Les objets `Date` natifs ne passent pas la vérification de types pour `start` ou `end`.
- **Les fuseaux sont convertis en UTC.** Une chaîne avec `Z` ou un décalage est convertie en heure murale UTC : `'2026-10-01T10:00:00+02:00'` devient `08:00:00`. Ne retirez les fuseaux qu’après avoir vous-même converti dans le fuseau horaire de l’activité.
- **Pas de surprise au changement d’heure.** `2026-03-29T02:30:00` existe, et lui ajouter une heure donne `03:30`, quel que soit le fuseau du navigateur. Les durées sont de simples différences d’horloge murale.
- **Les intervalles sont semi-ouverts.** Un événement de `14:00` à `16:00` se termine avant une réservation qui commence à `16:00`.
- **Dates natives.** `new SuperScheduler.Date(date)` lit les champs UTC d’une `Date` native ; `new SuperScheduler.Date(date, true)` lit ses champs locaux. `toDate()` renvoie une `Date` à lire en UTC ; `toDateLocal()` en renvoie une dont les champs locaux affichent l’heure murale.
- **« Aujourd’hui » est celui de l’appareil du visiteur.** `SuperScheduler.Date.today()`, le `startDate` par défaut, la mise en évidence du jour et la ligne de l’heure actuelle utilisent l’horloge de l’appareil. Une personne à New York qui consulte le planning d’un hôtel à Madrid voit la date du jour à New York. Si cela compte, calculez vous-même la date du jour de l’activité et passez-la dans `startDate` ou à `scrollTo`.

## Les fuseaux horaires relèvent de votre application
Si votre backend stocke des instants (horodatages UTC), choisissez le fuseau horaire que représente chaque planificateur, en général celui du site ou du responsable de la ressource, et convertissez aux frontières. `Intl.DateTimeFormat` avec un `timeZone` vous donne l’heure murale de n’importe quel instant, sans dépendance supplémentaire :

```ts
// src/time-zones.ts
import { SuperScheduler } from 'super-scheduler'

const formatters = new Map<string, Intl.DateTimeFormat>()

function partsFormatter(timeZone: string): Intl.DateTimeFormat {
  let formatter = formatters.get(timeZone)
  if (formatter === undefined) {
    formatter = new Intl.DateTimeFormat('en-US', {
      timeZone,
      hourCycle: 'h23',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
    })
    formatters.set(timeZone, formatter)
  }
  return formatter
}

/** What a wall clock in `timeZone` shows at `instant`, as a civil ISO string: "2026-10-01T10:00:00". */
export function toWallClock(instant: Date, timeZone: string): string {
  const part: Record<string, string> = {}
  for (const { type, value } of partsFormatter(timeZone).formatToParts(instant)) part[type] = value
  return `${part.year}-${part.month}-${part.day}T${part.hour}:${part.minute}:${part.second}`
}

function offsetAt(ms: number, timeZone: string): number {
  return Date.parse(`${toWallClock(new Date(ms), timeZone)}Z`) - ms
}

/**
 * The instant at which a wall clock in `timeZone` shows `wall`. Times skipped or repeated by a
 * daylight-saving change have no single answer: this picks a neighbouring instant, so validate
 * them in your application if they matter.
 */
export function fromWallClock(wall: SuperScheduler.DateInput, timeZone: string): Date {
  const asUtc = Date.parse(`${new SuperScheduler.Date(wall).value}Z`)
  const guess = asUtc - offsetAt(asUtc, timeZone)
  return new Date(asUtc - offsetAt(guess, timeZone))
}

/** API instants to scheduler events on the property's wall clock. */
export async function loadBookings(fromUtc: string, toUtc: string, timeZone: string) {
  const rows = await fetchBookingInstants(fromUtc, toUtc)
  return rows.map((row): SuperScheduler.EventData => ({
    id: row.id,
    resource: row.roomId,
    text: row.guest,
    start: toWallClock(new Date(row.startUtc), timeZone),
    end: toWallClock(new Date(row.endUtc), timeZone),
  }))
}

// Saving goes the other way: fromWallClock(event.start, 'Europe/Madrid').toISOString()
```
Convertissez à l’entrée (`toWallClock` quand vous transformez les lignes de l’API en événements) et à la sortie (`fromWallClock` quand vous enregistrez `start` et `end`, qui sont des objets `SuperScheduler.Date` après un glissement). Pour le chargement par plages, convertissez de la même façon le `start` et le `end` civils de la tranche avant d’interroger une API en UTC.

> **Limitation:**
> Un planificateur a un seul axe du temps. Des lignes situées dans des fuseaux horaires différents peuvent quand même le partager, mais c’est alors à vous de décider ce que signifie l’axe : convertir chaque ligne dans un fuseau d’affichage unique, ou afficher chaque ligne dans son heure locale en acceptant qu’une même colonne corresponde à des instants différents.

Les heures sautées ou répétées lors des changements d’heure sont une règle métier, pas un détail de formatage. Une réservation à 02:30 la nuit du passage à l’heure d’été n’existe pas à Madrid ; décidez s’il faut la refuser, la déplacer ou la stocker autrement.

## Événements récurrents
Le planificateur n’a pas de moteur de récurrence. Les champs `recurrent` et `recurrentMasterId` sont typés, mais `control.events.findRecurrent()` est réservé et renvoie `null`. Stockez les séries dans votre application et développez-les en événements ordinaires pour les dates affichées. Donnez à chaque occurrence un id stable d’une requête à l’autre, par exemple l’id de la série suivi de sa date, pour que le chargement par plages puisse la fusionner :

```ts
// src/recurrence.ts
import { SuperScheduler } from 'super-scheduler'

/** A weekly series as your application stores it. */
export interface WeeklySeries {
  readonly id: string
  readonly resource: string
  readonly text: string
  /** First occurrence, civil date-time with seconds. */
  readonly start: string
  readonly durationMinutes: number
  /** 0 = Sunday ... 6 = Saturday. */
  readonly weekdays: readonly number[]
  /** Last day of the series, inclusive, as yyyy-MM-dd. */
  readonly until: string
  /** Days removed from the series, as yyyy-MM-dd. */
  readonly exceptions: readonly string[]
}

/** The occurrences that overlap [from, to), each with an id that is stable across requests. */
export function expandWeekly(
  series: WeeklySeries,
  from: SuperScheduler.Date,
  to: SuperScheduler.Date,
) {
  const first = new SuperScheduler.Date(series.start)
  const firstDay = first.getDatePart().ticks
  const afterLastDay = new SuperScheduler.Date(series.until).addDays(1).ticks
  // Look back far enough to catch an occurrence that started earlier and is still running.
  const lookBack = Math.ceil(series.durationMinutes / 1440)
  const events: SuperScheduler.EventData[] = []
  for (
    let day = from.getDatePart().addDays(-lookBack);
    day.ticks < to.ticks;
    day = day.addDays(1)
  ) {
    if (day.ticks < firstDay || day.ticks >= afterLastDay) continue
    if (!series.weekdays.includes(day.getDayOfWeek())) continue
    const key = day.toString('yyyy-MM-dd')
    if (series.exceptions.includes(key)) continue
    const start = day.addTime(first.getTimePart())
    const end = start.addMinutes(series.durationMinutes)
    if (end.ticks <= from.ticks) continue
    events.push({
      id: `${series.id}:${key}`,
      resource: series.resource,
      text: series.text,
      start,
      end,
    })
  }
  return events
}
```
La modification vous revient aussi. « Cette occurrence uniquement » signifie en général ajouter la date aux exceptions de la série et créer un événement indépendant ; « Cette occurrence et les suivantes » scinde la série ; « Toutes les occurrences » modifie la série et la développe à nouveau. Faites correspondre les modifications de `onEventsChange` à ces opérations grâce à l’id de l’occurrence. Développer les séries dans la fonction `load` d’un chargeur de plages garde les longues séries peu coûteuses : seules les tranches visibles sont développées.

## Liste de vérification
- Une seule constante `LOCALE`, passée au composant et à chaque appel de `toString`.
- `weekStarts` et `firstDayOfWeek()` concordent.
- Vos propres chaînes pour l’historique, la minimap, le badge et les textes d’état dans toute langue autre que l’anglais et l’espagnol.
- Des dates d’événements sous forme de chaînes civiles avec secondes, converties dans le fuseau horaire de l’activité avant d’arriver au planificateur.
- L’enregistrement reconvertit en instants si votre backend les stocke ainsi.
- Des séries développées par plage visible, avec des id d’occurrence stables.

→ https://superscheduler.org/fr/examples/video-production/
Guides associés : [Ressources, événements et intervalles](https://superscheduler.org/fr/docs/resources-events-intervals/), [Échelles de temps et zoom](https://superscheduler.org/fr/docs/time-scales-zoom/), [Clavier, accessibilité et tactile](https://superscheduler.org/fr/docs/keyboard-accessibility-touch/) et [Chargement des données par plage de dates](https://superscheduler.org/fr/docs/range-loading/).
