# Idiomas, fechas civiles y zonas horarias

> Localiza cabeceras y etiquetas con cualquier locale de Intl, formatea valores SuperScheduler.Date y deja las zonas horarias y la recurrencia en tu aplicación.

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

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 `format` de cabecera;
- el reloj por defecto cuando `timeFormat` es `'Auto'` (12 horas para `en-us`, 24 horas para la mayoría de los locales europeos);
- el primer día de la semana cuando `weekStarts` es `'Auto'` (domingo para `en-us` y `pt-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](#built-in-strings)).

Lite acepta los mismos ids de `locale` para sus cabeceras de día.

```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}
      />
    </>
  )
}
```
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.

> **Behavior:**
> `date.toString(pattern, locale)` no lee el `locale` del scheduler. Sin el segundo argumento, formatea en `en-us`. Guarda el id de locale en una constante y pásala al componente y a cada llamada a `toString`, como en el ejemplo anterior.

`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:

```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())
}
```
## 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.

```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:**
> Los campos que omites en `new SuperScheduler.Locale(id, fields)` toman los valores por defecto del inglés de EE. UU. (nombres en inglés, `M/d/yyyy`, reloj de 12 horas, domingo), no los datos de `Intl` para ese id. Parte de `SuperScheduler.Locale.find(id)`, como se muestra, y sobrescribe a partir de ahí.

## 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 objetos `Date` nativos no pasan la comprobación de tipos como `start` o `end`.
- **Las zonas se convierten a UTC.** Una cadena con `Z` o con un desfase se convierte al reloj de pared UTC: `'2026-10-01T10:00:00+02:00'` pasa a ser `08: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:00` existe, y sumarle una hora da `03: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:00` a `16:00` termina antes de una reserva que empieza a las `16:00`.
- **Fechas nativas.** `new SuperScheduler.Date(date)` lee los campos UTC de un `Date` nativo; `new SuperScheduler.Date(date, true)` lee sus campos locales. `toDate()` devuelve un `Date` que 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()`, el `startDate` por 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 como `startDate` o a `scrollTo`.

## 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:

```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()
```
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.

> **Limitation:**
> Un scheduler tiene un solo eje de tiempo. Las filas que viven en zonas horarias distintas pueden compartirlo, pero entonces decides tú qué significa el eje: convertir todas las filas a una sola zona de visualización, o mostrar cada fila en su hora local y aceptar que la misma columna represente instantes distintos.

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:

```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 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 a `toString`.
- `weekStarts` y `firstDayOfWeek()` 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.

→ https://superscheduler.org/es/examples/video-production/
Guías relacionadas: [Recursos, eventos e intervalos](https://superscheduler.org/es/docs/resources-events-intervals/), [Escalas de tiempo y zoom](https://superscheduler.org/es/docs/time-scales-zoom/), [Teclado, accesibilidad y táctil](https://superscheduler.org/es/docs/keyboard-accessibility-touch/) y [Carga de datos por rangos de fechas](https://superscheduler.org/es/docs/range-loading/).
