ProducciónSe aplica aLite y Pro
Idiomas, fechas civiles y zonas horarias
Asigna a `locale` cualquier id de locale de Intl, como `fr-fr`: los nombres de meses y días, el reloj de 12 o 24 horas y el primer día de la semana lo siguen, y `timeFormat` y `weekStarts` sobrescriben los dos últimos. Las fechas son valores civiles de reloj de pared: el scheduler nunca convierte zonas horarias y nunca expande eventos recurrentes. Convierte los instantes a la zona horaria del negocio antes de pasar los eventos, vuelve a convertirlos al guardar y expande las series en ocurrencias dentro de tu aplicación.
Un scheduler muestra fechas a personas, así que aquí se juntan dos cuestiones distintas. La localización decide cómo se escribe una fecha: nombres, orden, reloj y primer día de la semana. La semántica temporal decide qué fecha es: la librería trabaja con valores civiles de reloj de pared y deja las zonas horarias y la recurrencia a tu aplicación. Esta guía cubre las dos, para Pro y, donde se indica, para Lite.
Definir el locale
locale acepta cualquier id de locale que entienda Intl, escrito al estilo de SuperScheduler, en minúsculas: en-us (el valor por defecto), en-gb, fr-fr, de-de, es-es, pt-br, nl-nl, ja-jp, etc. No hay ninguna lista que registrar. en_US se normaliza a en-us, y un id que Intl no sabe resolver recurre a en-us.
En Pro, el locale determina:
- los nombres de meses y días en las cabeceras de tiempo por defecto y en cualquier patrón
formatde cabecera; - el reloj por defecto cuando
timeFormates'Auto'(12 horas paraen-us, 24 horas para la mayoría de los locales europeos); - el primer día de la semana cuando
weekStartses'Auto'(domingo paraen-usypt-br, lunes para la mayoría de los locales europeos); - los patrones de fecha por defecto de las cabeceras de día y las fechas de la tarjeta de arrastre;
- el idioma de los anuncios de teclado y de algunas etiquetas integradas (consulta Textos integrados).
Lite acepta los mismos ids de locale para sus cabeceras de día.
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}
/>
</>
)
}Deberías ver cabeceras como «lundi 5 octobre» sobre columnas de horas etiquetadas de 0 a 23, una semana que empieza en lunes y un resumen en francés al hacer clic en un evento.
Formato de hora y primer día de la semana
timeFormat acepta 'Auto', 'Clock12Hours' o 'Clock24Hours'. Solo cambia las etiquetas de hora por defecto; un format explícito en timeHeaders siempre prevalece ('HH:mm' para 24 horas, 'h:mm tt' para 12 horas). Es un ajuste de presentación, no una conversión horaria: cambiarlo nunca mueve un evento.
weekStarts acepta 'Auto' o un número de día de 0 (domingo) a 6 (sábado). Afecta a las celdas Week y a los grupos de cabecera, a las líneas de semana que se dibujan con el zoom alejado y a los números de semana por defecto: numeración ISO cuando las semanas empiezan en lunes, numeración estadounidense en los demás casos. En tu propio código, date.firstDayOfWeek() usa el domingo por defecto, así que pásale el mismo valor (firstDayOfWeek(1)) o el id de locale (firstDayOfWeek('fr-fr')).
A qué se resuelve 'Auto' para algunos ids:
| Locale | ddd d MMM | Reloj | La semana empieza en |
|---|---|---|---|
en-us | Mo 5 Oct | 12 horas | domingo |
en-gb | Mo 5 Oct | 24 horas | lunes |
es-es | L 5 oct | 24 horas | lunes |
de-de | Mo 5 Okt | 24 horas | lunes |
fr-fr | lu 5 oct. | 24 horas | lunes |
pt-br | se 5 out. | 24 horas | domingo |
Formatear fechas en tu propia interfaz
SuperScheduler.Date formatea con patrones de SuperScheduler: yyyy, yy, MMMM, MMM, MM, M, dddd, ddd, dd, d, HH, H, hh, h, mm, m, ss, s y tt (AM/PM). Todo lo demás es texto literal.
ddd es el nombre corto del día de la semana que da Intl, que en varios locales tiene una o dos letras: «Mo» en inglés, «L» en español, «lu» en francés, «dl» en catalán. Usa dddd para el nombre completo, registra tus propios nombres cortos (más abajo) o formatea directamente con Intl. date.toDate() devuelve un Date nativo con los mismos ticks, pensado para leerse en UTC, así que formatéalo con timeZone: 'UTC' para mostrar exactamente el valor civil en cualquier dispositivo:
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())
}Nombres y patrones propios
SuperScheduler.Locale.register() sustituye los nombres y patrones de un id para todos los schedulers y todas las llamadas a toString que lo usen. Un SuperScheduler.Locale pasado directamente como opción locale se registra automáticamente.
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."Textos integrados y sus idiomas
La librería escribe algunos textos propios. El idioma es la primera parte del id de locale (ca-es da ca).
| Textos | Idiomas | Cómo sustituirlos |
|---|---|---|
| Anuncios de teclado y etiquetas de foco | inglés, español, catalán, euskera, gallego, alemán, francés, italiano, portugués | No configurable |
| Duraciones y rechazos de la tarjeta de arrastre («2 nights», «Overlaps», «Not allowed») | Los mismos nueve idiomas | dragCard={{ labels: { ... } }} |
| Nombre de la cuadrícula y textos de carga, vacío y error | inglés, o español para locales es | emptyState, errorState y loadingLabelText; el nombre accesible de la cuadrícula es fijo |
| Etiquetas de las entradas del historial («Move», «Resize») | inglés, o español para locales es | createHistory({ labels }) |
| Etiquetas del minimapa | inglés, o español para locales es | createMinimap(control, element, { labels }) o la prop labels de SchedulerMinimap |
| Distintivo de nivel de detalle | inglés, o español para locales es | tercer argumento de createLodBadge |
| Lite: etiqueta de la cuadrícula y texto de vacío | inglés | ariaLabel, emptyState |
Cualquier otro idioma se muestra en inglés, así que una aplicación en francés o en alemán debería pasar sus propios textos para el historial, el minimapa, el distintivo y los textos de estado. El texto de los eventos, los nombres de los recursos y cualquier HTML que renderices son tuyos para traducir.
Fechas civiles
Todas las fechas de SuperScheduler son valores civiles de reloj de pared sin zona horaria. '2026-10-01T10:00:00' significa las diez en el tablero de planificación, se abra la página donde se abra. Las consecuencias:
- Las cadenas necesitan segundos.
'2026-10-01'y'2026-10-01T10:00:00'son válidas;'2026-10-01T10:00'lanza «is not an ISO 8601 date». Los objetosDatenativos no pasan la comprobación de tipos comostartoend. - Las zonas se convierten a UTC. Una cadena con
Zo con un desfase se convierte al reloj de pared UTC:'2026-10-01T10:00:00+02:00'pasa a ser08:00:00. Quita las zonas solo después de haber convertido tú a la zona horaria del negocio. - Sin sorpresas con el horario de verano.
2026-03-29T02:30:00existe, y sumarle una hora da03:30, sea cual sea la zona del navegador. Las duraciones son simples diferencias de reloj de pared. - Los intervalos son semiabiertos. Un evento de
14:00a16:00termina antes de una reserva que empieza a las16:00. - Fechas nativas.
new SuperScheduler.Date(date)lee los campos UTC de unDatenativo;new SuperScheduler.Date(date, true)lee sus campos locales.toDate()devuelve unDateque hay que leer en UTC;toDateLocal()devuelve uno cuyos campos locales muestran el reloj de pared. - «Hoy» es el del dispositivo de quien mira.
SuperScheduler.Date.today(), elstartDatepor defecto, el resaltado de hoy y la línea de la hora actual usan el reloj del dispositivo. Alguien que planifica desde Nueva York un hotel de Madrid ve el hoy de Nueva York. Si eso importa, calcula tú el «hoy» del negocio y pásalo comostartDateo ascrollTo.
Las zonas horarias son trabajo de tu aplicación
Si tu backend guarda instantes (marcas de tiempo UTC), elige la zona horaria que representa cada scheduler, normalmente la de la sede o la del responsable del recurso, y convierte en los extremos. Intl.DateTimeFormat con un timeZone te da el reloj de pared de cualquier instante, sin dependencias adicionales:
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()Convierte a la entrada (toWallClock al transformar las filas de la API en eventos) y a la salida (fromWallClock al guardar start y end, que después de un arrastre son objetos SuperScheduler.Date). En la carga por rangos, convierte de la misma forma el start y el end civiles del bloque antes de consultar una API en UTC.
Las horas que se saltan o se repiten con el cambio de hora son una regla de negocio, no un detalle de formato. Una reserva a las 02:30 la noche en que se adelantan los relojes no existe en Madrid; decide si la rechazas, la mueves o la guardas de otra manera.
Eventos recurrentes
El scheduler no tiene motor de recurrencia. Los campos recurrent y recurrentMasterId tienen tipos, pero control.events.findRecurrent() está reservado y devuelve null. Guarda las series en tu aplicación y expándelas en eventos normales para las fechas que hay en pantalla. Dale a cada ocurrencia un id estable entre peticiones, como el id de la serie más su fecha, para que la carga por rangos pueda combinarla:
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 edición también es cosa tuya. «Solo esta ocurrencia» suele significar añadir la fecha a las excepciones de la serie y crear un evento independiente; «esta y las siguientes» divide la serie; «todas las ocurrencias» cambia la serie y la vuelve a expandir. Traduce los cambios de onEventsChange a estas operaciones mediante el id de la ocurrencia. Expandir dentro de la función load de un cargador por rangos mantiene baratas las series largas: solo se expanden los bloques visibles.
Lista de comprobación
- Una constante
LOCALE, pasada al componente y a cada llamada atoString. weekStartsyfirstDayOfWeek()coinciden.- Tus propios textos para el historial, el minimapa, el distintivo y los textos de estado en idiomas distintos del inglés y el español.
- Fechas de evento como cadenas civiles con segundos, convertidas a la zona horaria del negocio antes de llegar al scheduler.
- Al guardar, se vuelve a convertir a instantes si tu backend los guarda.
- Series expandidas por rango visible con ids de ocurrencia estables.
Planificación de producción audiovisualUn rodaje se alarga. Mueve la edición que dependía de él, entiende por qué y vuelve atrás. Guías relacionadas: Recursos, eventos e intervalos, Escalas de tiempo y zoom, Teclado, accesibilidad y táctil y Carga de datos por rangos de fechas.