# Dates and calendar systems

@vega-ui/i18n provides the framework-agnostic locale and calendar-system layer used by Calendar, DayPicker, MonthPicker, YearPicker, DateField, DateTimeField, and DateRangeField.

## Date Values

Every date that crosses a component boundary is an ISO `YYYY-MM-DD` string: `value`, `defaultValue`, `min`, `max`, `month`, `year`, `disabledDates`, and the cell keys of the pickers. Date-time values use `YYYY-MM-DDTHH:mm[:ss]`.

An ISO day is calendar-neutral. The calendar system only decides how days are grouped into months and years and how they are labeled, so the stored value never changes when the calendar does.

- `DayPicker` cells are keyed by the day itself.
- `MonthPicker` cells are keyed by the first day of each month.
- `YearPicker` cells are keyed by the first day of each year.
- `month` and `year` props accept any day inside that month or year.

## Install

`@vega-ui/react` depends on `@vega-ui/i18n`. Install it directly when the app imports the calendar adapters or helpers, keeping the same version as the one `@vega-ui/react` uses:

```bash
pnpm add @vega-ui/i18n
```

`@vega-ui/i18n` has no dependencies and knows nothing about the components: it holds the `CalendarSystem` type, the Gregorian calendar, ISO day and calendar-aware date helpers, locale and `Intl` formatting helpers. Typed entry of the date fields builds on it in `@vega-ui/utils`.

## Locale And Calendar Props

Locale and calendar system are root props, like the options of `Intl.DateTimeFormat`. `Calendar`, `DateField`, `DateTimeField` and `DateRangeField`, and the low-level `DayPicker`, `MonthPicker` and `YearPicker`, take:

- `locale?: string`: BCP 47 locale of month and day names and of the first day of the week. Defaults to the locale of the runtime, `getDefaultLocale()`.
- `calendar?: CalendarSystem`: calendar system months and years are shown in. Defaults to `gregorianCalendar`.

Every part inside a root (month and year labels, the day grid and its first day of the week, the month and year grids) reads them from the root. The low-level picker layouts also accept their own `locale` to override it for that layout. `Calendar` sets `lang` on its root only when `locale` is passed.

```tsx
import { Calendar, CalendarMonthLabel } from '@vega-ui/react';

<Calendar locale='de-DE' defaultValue='2026-03-15'>
  <CalendarMonthLabel />
</Calendar>
```

`getDefaultLocale()` returns the runtime locale, `new Intl.DateTimeFormat().resolvedOptions().locale`. Pass `locale` explicitly when rendering on the server, so server and client output match.

Other locale helpers take the locales argument of `Intl` (a tag, an `Intl.Locale` or a list) and, like `Intl`, use the runtime locale when it is omitted:

| Helper | Result |
| --- | --- |
| `getFirstDayOfWeek(locale)` | First day of the week of a locale, `0` = Sunday. |
| `getWeekDayNames(locale, style, firstDayOfWeek?)` | Weekday names starting from the first day of the week. |
| `getDateFormat(calendar, locale)` / `getDateSeparator(calendar, locale)` | Order of the numeric date fields (`DD.MM.YYYY`) and their separator in a locale and calendar system. Eras and bidi marks are left out; a calendar that names its months (Hebrew, Chinese) uses the Gregorian order of the locale. |
| `toLatinDigits(value)` | Replaces the digits of other numbering systems with Latin digits: `۱۴۰۵/۰۱/۰۱` → `1405/01/01`. |

## Text Direction

Direction is not derived from `locale`. Like native elements, `Calendar`, the pickers, and the calendars of the date fields follow the text direction they are rendered in: `dir` on `<html>` or an ancestor, a `dir` attribute on the component root, or the CSS `direction` property.

```tsx
import { Calendar, CalendarDays, CalendarWeekdays, CalendarWeekday, CalendarWeeks, CalendarDay } from '@vega-ui/react';

<div dir='rtl'>
  <Calendar locale='ar-EG'>
    <CalendarDays>
      <CalendarWeekdays>
        <CalendarWeekday />
      </CalendarWeekdays>
      <CalendarWeeks>
        <CalendarDay />
      </CalendarWeeks>
    </CalendarDays>
  </Calendar>
</div>
```

In a right-to-left context:

- day, month, and year grids run from right to left, so the first day of the week is the rightmost column
- `ArrowRight` moves to the previous cell and `ArrowLeft` to the next one; `ArrowUp`, `ArrowDown`, `Home`, and `End` are unchanged
- the next month or page of years is on the left: swiping to the left and `CalendarNextButton` show it
- the default chevrons of `CalendarPrevButton` and `CalendarNextButton` are mirrored

A calendar rendered in a portal, such as a popover, inherits the direction of the portal container, so set `dir` on `<html>` or on the calendar itself.

## App-Wide Settings

The library has no global providers. To use the same locale or calendar system everywhere, wrap the component once in the app and use the wrapper instead:

```tsx
import { Calendar, CalendarProps, CalendarSelection } from '@vega-ui/react';

const appLocale = 'de-DE';

export const AppCalendar = <S extends CalendarSelection = 'single'>(props: CalendarProps<S>) => (
  <Calendar locale={appLocale} {...props} />
);
```

Props given to the wrapper still win, so a single screen can override the app setting.

## Calendar System Adapter

A calendar system is a small adapter object. Two kinds ship with the package, neither with dependencies:

- `gregorianCalendar`, the shared instance of the `GregorianCalendar` class, used when no `calendar` is given. Its methods are bound to the instance, so they can be passed as callbacks, and the class can be extended (see [Extending GregorianCalendar](#extending-gregoriancalendar));
- `createCalendarSystem(identifier)`, any other calendar built into `Intl`: `'persian'`, `'hebrew'`, `'japanese'`, `'buddhist'`, `'roc'`, `'indian'`, `'coptic'`, `'ethiopic'`, `'islamic-umalqura'`, `'islamic-civil'`, `'chinese'`, `'dangi'`… An identifier the runtime does not support throws a `RangeError`.

```ts
import { createCalendarSystem } from '@vega-ui/i18n';

export const persianCalendar = createCalendarSystem('persian');
```

`createCalendarSystem` reads the calendar from `Intl.DateTimeFormat`, so the result is the same as the dates `Intl` formats:

- months are numbered from the start of the year: 1 to 13 in a Hebrew leap year (Adar I and Adar II included), and a leap month of the Chinese calendar gets its own number;
- eras are the short English era names of `Intl`, such as `'Reiwa'` and `'Heisei'`; fields without an era use the current one, so a typed year is a year of the current era;
- a year that ends with an era (Japanese Heisei 31, January 1 to April 30, 2019) has only its own months; fields past its end are clamped to it;
- results are cached per instance, so create each calendar once.

Any other object implementing the `CalendarSystem` interface works as well:

| Member | Purpose |
| --- | --- |
| `identifier` | Calendar id understood by `Intl.DateTimeFormat`, such as `gregory`, `persian`, `hebrew`. |
| `toFields(date)` | Converts an ISO day into `{ era?, year, month, day }` of this calendar; `month` and `day` are 1-based. |
| `fromFields({ era?, year, month, day })` | Converts fields of this calendar back into an ISO day. |
| `getMonthsInYear({ era?, year })` | Number of months in a year, for example 13 in a Hebrew leap year. |
| `getDaysInMonth({ era?, year, month })` | Number of days in a month. |

Month, year, and era names always come from `Intl.DateTimeFormat` with `identifier`, so the adapter does not deal with labels.

Create each calendar once, outside of render, and pass it to the root of a component:

```tsx
const persianCalendar = createCalendarSystem('persian');

<DateField format='yyyy/MM/dd' separator='/' locale='fa-IR' calendar={persianCalendar}>
  <DateFieldInput />
</DateField>
```

Typing `1405/01/01` is validated against Persian month lengths and resolves to the ISO day `2026-03-21`.

### Extending GregorianCalendar

A calendar that shares Gregorian months and days can extend `GregorianCalendar` instead of writing a full adapter. Methods are regular class methods bound in the constructor, so overrides can call `super` and still be passed as callbacks.

The Thai solar Buddhist calendar (`buddhist` in `Intl`) is the Gregorian calendar with years counted from 543 BC:

```ts
import { GregorianCalendar, type CalendarDateFields } from '@vega-ui/i18n';

class BuddhistCalendar extends GregorianCalendar {
  readonly identifier = 'buddhist';

  toFields(date: string): CalendarDateFields {
    const fields = super.toFields(date);
    return { ...fields, year: fields.year + 543 };
  }

  fromFields(fields: CalendarDateFields) {
    return super.fromFields({ ...fields, year: fields.year - 543 });
  }

  getDaysInMonth(fields: Pick<CalendarDateFields, 'era' | 'year' | 'month'>) {
    return super.getDaysInMonth({ ...fields, year: fields.year - 543 });
  }
}

export const buddhistCalendar = new BuddhistCalendar();
```

`buddhistCalendar.toFields('2026-02-15')` gives `{ year: 2569, month: 2, day: 15 }`, and labels come from `Intl` with the `buddhist` identifier (`2569 BE`). Calendars with their own month lengths, such as Persian or Hebrew, come from `createCalendarSystem`.

## Date Helpers

All helpers take the calendar first and take and return ISO `YYYY-MM-DD` strings.

| Helper | Result |
| --- | --- |
| `getCalendarMonthStart(calendar, date)` / `getCalendarMonthEnd(calendar, date)` | First / last day of the month containing `date`. |
| `getCalendarYearStart(calendar, date)` / `getCalendarYearEnd(calendar, date)` | First / last day of the year containing `date`. |
| `addCalendarMonths(calendar, date, amount)` | First day of the month `amount` months away. |
| `addCalendarYears(calendar, date, amount)` | First day of the year `amount` years away. |
| `getCalendarMonthsDistance(calendar, from, to)` / `getCalendarYearsDistance(calendar, from, to)` | Signed number of months / years between two days. |
| `getCalendarMonthsOfYear(calendar, date)` | First days of all months of the year. |
| `getSameDayInCalendarMonth(calendar, date, month)` | Same day number in another month, clamped to its length. |
| `getSameMonthInCalendarYear(calendar, month, year)` | Same month number in another year, clamped to its month count. |
| `isSameCalendarMonth(calendar, a, b)` | Whether two days share a month. |
| `addISODays(date, days)` / `getISOWeekday(date)` | Day arithmetic and weekday (`0` = Sunday), the same in every calendar. |
| `DAYS_IN_WEEK` | `7`: every calendar of `Intl` uses the ISO week, which runs on across months, years and eras, so day grids always have seven columns. |

## Formatting And Parsing

`formatCalendarDate(calendar, date, locale, options)` formats an ISO day with `Intl.DateTimeFormat` in a calendar system:

```tsx
import { formatCalendarDate, gregorianCalendar } from '@vega-ui/i18n';

formatCalendarDate(gregorianCalendar, '2026-03-01', 'en-US', { month: 'short' });
```

`formatCalendarDay(calendar, date, locale, format)` formats only the day number, without the suffix some locales add to a standalone day: `15` rather than `15日` in `ja-JP`.

Creating an `Intl.DateTimeFormat` is the costly part, so to format many days create one formatter and reuse it. `createCalendarDateFormatter(calendar, locale, options)` and `createCalendarDayFormatter(calendar, locale, format)` return `(date) => string`; the day, month and year grids use them:

```ts
import { createCalendarDayFormatter, gregorianCalendar } from '@vega-ui/i18n';

const formatDay = createCalendarDayFormatter(gregorianCalendar, 'ja-JP', 'numeric');
formatDay('2026-03-15'); // '15'
```

Template helpers implement typed entry in the date fields, so they live in `@vega-ui/utils` and take the calendar system as their first argument. They read the digits of any numbering system (`٠٥.٠١.٢٠٢٦`), and the date fields accept them while typing. In a template, `dd`, `mm`, and `yyyy` are calendar day, month, and year, and `HH`, `MM`, `SS` are hours, minutes, and seconds:

- `formatTemplateDate(calendar, date, template)`: ISO value to a typed string
- `parseTemplateDate(calendar, value, template)`: typed string to ISO value, or `undefined` when incomplete or non-existent
- `parseTemplateRange(calendar, value, template, separator)`: typed range to a `[start, end]` tuple, `''` for an incomplete bound
- `normalizeTemplateDate(calendar, value, template, { min, max })`: pads a single digit that cannot start a two-digit segment, clamps complete segments to the calendar, and clamps a complete value to `min` / `max`
- `stepTimeSegment(value, template, position, direction, step)`: steps the time segment under the caret; time does not depend on the calendar, so it takes none

## Common Mistakes

- Passing `Date` objects, timestamps, numeric years, or zero-based months instead of ISO strings.
- Converting values to the display calendar before storing them; store the ISO day and let the calendar format it.
- Creating a new adapter on every render.
- Giving a picker and its surrounding field different `calendar` values.
