Skip to content
SuperScheduler

ProductionApplies toLite and Pro

Languages, civil dates and time zones

Set `locale` to any Intl locale id such as `fr-fr`: month and day names, the 12- or 24-hour clock and the first day of the week follow it, and `timeFormat` and `weekStarts` override the last two. Dates are civil wall-clock values: the scheduler never converts time zones and never expands recurring events. Convert instants to the business time zone before you pass events, convert back when you save, and expand series into occurrences in your application.

Verified against v0.1.0 · reviewed October 7, 2026.md

A scheduler shows dates to people, so two separate concerns meet here. Localization decides how a date is written: names, order, clock and first day of the week. Time semantics decide which date it is: the library works with civil wall-clock values and leaves time zones and recurrence to your application. This guide covers both, for Pro and, where noted, Lite.

Set the locale

locale accepts any locale id that Intl understands, written SuperScheduler-style in lower case: en-us (the default), en-gb, fr-fr, de-de, es-es, pt-br, nl-nl, ja-jp and so on. There is no list to register. en_US is normalized to en-us, and an id that Intl cannot resolve falls back to en-us.

In Pro, the locale drives:

  • month and day names in the default time headers and in every header format pattern;
  • the default clock when timeFormat is 'Auto' (12-hour for en-us, 24-hour for most European locales);
  • the first day of the week when weekStarts is 'Auto' (Sunday for en-us and pt-br, Monday for most European locales);
  • the default date patterns of day headers and the dates on the drag card;
  • the language of keyboard announcements and of some built-in labels (see Built-in strings).

Lite accepts the same locale ids for its day headers.

src/FrenchPlanning.tsxtsx
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}
      />
    </>
  )
}

You should see headers such as "lundi 5 octobre" over hour columns labeled 0 to 23, a week that starts on Monday, and a French summary when you click an event.

Clock format and first day of the week

timeFormat takes 'Auto', 'Clock12Hours' or 'Clock24Hours'. It changes the default hour labels only; an explicit format in timeHeaders always wins ('HH:mm' for 24 hours, 'h:mm tt' for 12 hours). It is a display setting, not a time conversion: switching it never moves an event.

weekStarts takes 'Auto' or a day number from 0 (Sunday) to 6 (Saturday). It affects Week cells and header groups, the week lines drawn when zoomed out, and the default week numbers: ISO numbers when weeks start on Monday, US numbers otherwise. In your own code, date.firstDayOfWeek() defaults to Sunday, so pass the same value (firstDayOfWeek(1)) or the locale id (firstDayOfWeek('fr-fr')).

What 'Auto' resolves to for a few ids:

Localeddd d MMMClockWeek starts
en-usMo 5 Oct12-hourSunday
en-gbMo 5 Oct24-hourMonday
es-esL 5 oct24-hourMonday
de-deMo 5 Okt24-hourMonday
fr-frlu 5 oct.24-hourMonday
pt-brse 5 out.24-hourSunday

Format dates in your own UI

SuperScheduler.Date formats with SuperScheduler patterns: yyyy, yy, MMMM, MMM, MM, M, dddd, ddd, dd, d, HH, H, hh, h, mm, m, ss, s and tt (AM/PM). Everything else is literal text.

ddd is the short weekday name from Intl, which is one or two letters in several locales: "Mo" in English, "L" in Spanish, "lu" in French, "dl" in Catalan. Use dddd for the full name, register your own short names (below), or format with Intl directly. date.toDate() returns a native Date with the same ticks, meant to be read in UTC, so format it with timeZone: 'UTC' to show exactly the civil value on any device:

src/format.tsts
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())
}

Custom names and patterns

SuperScheduler.Locale.register() replaces the names and patterns of an id for every scheduler and every toString call that uses it. A SuperScheduler.Locale passed directly as the locale option is registered automatically.

src/locale.tsts
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."

Built-in strings and their languages

The library writes a few strings of its own. The language is the first part of the locale id (ca-es gives ca).

StringsLanguagesOverride
Keyboard announcements and focus labelsEnglish, Spanish, Catalan, Basque, Galician, German, French, Italian, PortugueseNot configurable
Drag card durations and refusals ("2 nights", "Overlaps", "Not allowed")Same nine languagesdragCard={{ labels: { ... } }}
Grid name, loading, empty and error textsEnglish, or Spanish for es localesemptyState, errorState and loadingLabelText; the grid's accessible name is fixed
History entry labels ("Move", "Resize")English, or Spanish for es localescreateHistory({ labels })
Minimap labelsEnglish, or Spanish for es localescreateMinimap(control, element, { labels }) or the labels prop of SchedulerMinimap
Level-of-detail badgeEnglish, or Spanish for es localesthird argument of createLodBadge
Lite: grid label and empty textEnglishariaLabel, emptyState

Any other language reads English, so a French or German application should pass its own strings for the history, minimap, badge and status texts. Event text, resource names and any HTML you render are yours to translate.

Civil dates

Every date in SuperScheduler is a civil wall-clock value without a time zone. '2026-10-01T10:00:00' means ten o'clock on the planning board, wherever the page is opened. The consequences:

  • Strings need seconds. '2026-10-01' and '2026-10-01T10:00:00' are valid; '2026-10-01T10:00' throws "is not an ISO 8601 date". Native Date objects do not type-check as start or end.
  • Zones are converted to UTC. A string with Z or an offset is converted to the UTC wall clock: '2026-10-01T10:00:00+02:00' becomes 08:00:00. Strip zones only after converting to the business time zone yourself.
  • No daylight-saving surprises. 2026-03-29T02:30:00 exists, and adding an hour to it gives 03:30, regardless of the browser's zone. Durations are plain wall-clock differences.
  • Intervals are half-open. An event from 14:00 to 16:00 ends before a booking that starts at 16:00.
  • Native dates. new SuperScheduler.Date(date) reads a native Date's UTC fields; new SuperScheduler.Date(date, true) reads its local fields. toDate() returns a Date to read in UTC; toDateLocal() returns one whose local fields show the wall clock.
  • "Today" is the viewer's device. SuperScheduler.Date.today(), the default startDate, the today highlight and the now line use the device's clock. A planner in New York looking at a hotel in Madrid sees New York's today. If that matters, compute the business "today" yourself and pass it as startDate or to scrollTo.

Time zones are your application's job

If your backend stores instants (UTC timestamps), choose the time zone each scheduler represents, usually the site's or the resource owner's, and convert at the edges. Intl.DateTimeFormat with a timeZone gives you the wall clock of any instant, with no extra dependency:

src/time-zones.tsts
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()

Convert on the way in (toWallClock when you map API rows to events) and on the way out (fromWallClock when you save start and end, which are SuperScheduler.Date objects after a drag). For range loading, convert the chunk's civil start and end the same way before querying a UTC API.

Times skipped or repeated when clocks change are a business rule, not a formatting detail. A booking at 02:30 on the night the clocks go forward does not exist in Madrid; decide whether to reject it, move it, or store it differently.

Recurring events

The scheduler has no recurrence engine. The recurrent and recurrentMasterId fields are typed, but control.events.findRecurrent() is reserved and returns null. Store series in your application and expand them into ordinary events for the dates on screen. Give each occurrence an id that is stable across requests, such as the series id plus its date, so range loading can merge it:

src/recurrence.tsts
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
}

Editing is also yours. "Only this occurrence" usually means adding the date to the series' exceptions and creating a standalone event; "this and following" splits the series; "all occurrences" changes the series and expands it again. Map onEventsChange changes back to these operations by the occurrence id. Expanding in a range loader's load function keeps long series cheap: only the visible chunks are expanded.

Checklist

  • One LOCALE constant, passed to the component and to every toString call.
  • weekStarts and firstDayOfWeek() agree.
  • Your own strings for history, minimap, badge and status texts outside English and Spanish.
  • Event dates as civil strings with seconds, converted to the business time zone before they reach the scheduler.
  • Saving converts back to instants if your backend stores them.
  • Series expanded per visible range with stable occurrence ids.

Video production planningA shoot runs long. Move the edit that depended on it, see why, and take it back. Related guides: Resources, events and intervals, Time scales and zoom, Keyboard, accessibility and touch and Loading data by date range.