# Даты и календарные системы

@vega-ui/i18n — независимый от фреймворка слой локалей и календарных систем, на котором работают Calendar, DayPicker, MonthPicker, YearPicker, DateField, DateTimeField и DateRangeField.

## Значения дат

Каждая дата, пересекающая границу компонента, — это ISO-строка `YYYY-MM-DD`: `value`, `defaultValue`, `min`, `max`, `month`, `year`, `disabledDates` и ключи ячеек пикеров. Значения даты и времени имеют вид `YYYY-MM-DDTHH:mm[:ss]`.

ISO-день не зависит от календаря. Календарная система решает только, как дни группируются в месяцы и годы и как они подписываются, поэтому хранимое значение не меняется при смене календаря.

- Ячейки `DayPicker` имеют ключом сам день.
- Ячейки `MonthPicker` имеют ключом первый день каждого месяца.
- Ячейки `YearPicker` имеют ключом первый день каждого года.
- Пропсы `month` и `year` принимают любой день внутри этого месяца или года.

## Установка

`@vega-ui/react` зависит от `@vega-ui/i18n`. Установите его напрямую, если приложение импортирует календарные адаптеры или помощники, — той же версии, что использует `@vega-ui/react`:

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

У `@vega-ui/i18n` нет зависимостей, и он ничего не знает о компонентах: в нём тип `CalendarSystem`, григорианский календарь, помощники для ISO-дней и календарно-зависимых дат, помощники для локалей и форматирования через `Intl`. Ввод дат с клавиатуры в полях дат построен поверх него в `@vega-ui/utils`.

## Пропсы локали и календаря

Локаль и календарная система — корневые пропсы, как опции `Intl.DateTimeFormat`. `Calendar`, `DateField`, `DateTimeField` и `DateRangeField`, а также низкоуровневые `DayPicker`, `MonthPicker` и `YearPicker` принимают:

- `locale?: string`: локаль BCP 47 для названий месяцев и дней и для первого дня недели. По умолчанию — локаль среды выполнения, `getDefaultLocale()`.
- `calendar?: CalendarSystem`: календарная система, в которой показываются месяцы и годы. По умолчанию — `gregorianCalendar`.

Каждая часть внутри корня (подписи месяца и года, сетка дней и её первый день недели, сетки месяцев и лет) берёт их из корня. Раскладки низкоуровневых пикеров также принимают собственный `locale`, чтобы переопределить его для этой раскладки. `Calendar` задаёт `lang` на своём корне, только если передан `locale`.

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

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

`getDefaultLocale()` возвращает локаль среды выполнения, `new Intl.DateTimeFormat().resolvedOptions().locale`. При рендеринге на сервере передавайте `locale` явно, чтобы вывод сервера и клиента совпадал.

Остальные помощники для локалей принимают аргумент locales из `Intl` (тег, `Intl.Locale` или список) и, как `Intl`, используют локаль среды выполнения, если он не передан:

| Помощник | Результат |
| --- | --- |
| `getFirstDayOfWeek(locale)` | Первый день недели в локали, `0` — воскресенье. |
| `getWeekDayNames(locale, style, firstDayOfWeek?)` | Названия дней недели, начиная с первого дня недели. |
| `getDateFormat(calendar, locale)` / `getDateSeparator(calendar, locale)` | Порядок числовых полей даты (`DD.MM.YYYY`) и их разделитель в локали и календарной системе. Эры и метки направления текста (bidi) опускаются; календарь с названиями месяцев (еврейский, китайский) использует григорианский порядок локали. |
| `toLatinDigits(value)` | Заменяет цифры других систем счисления на латинские: `۱۴۰۵/۰۱/۰۱` → `1405/01/01`. |

## Направление текста

Направление не выводится из `locale`. Как и нативные элементы, `Calendar`, пикеры и календари полей дат следуют направлению текста, в котором они отрендерены: `dir` на `<html>` или предке, атрибут `dir` на корне компонента или CSS-свойство `direction`.

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

В контексте справа налево:

- сетки дней, месяцев и лет идут справа налево, поэтому первый день недели — крайний правый столбец
- `ArrowRight` переходит к предыдущей ячейке, а `ArrowLeft` — к следующей; `ArrowUp`, `ArrowDown`, `Home` и `End` не меняются
- следующий месяц или страница лет находится слева: её показывают свайп влево и `CalendarNextButton`
- стандартные шевроны `CalendarPrevButton` и `CalendarNextButton` отражены зеркально

Календарь, отрендеренный в портале, например в поповере, наследует направление контейнера портала, поэтому задавайте `dir` на `<html>` или на самом календаре.

## Настройки на всё приложение

У библиотеки нет глобальных провайдеров. Чтобы везде использовать одну локаль или календарную систему, один раз оберните компонент в приложении и используйте обёртку:

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

Пропсы, переданные обёртке, по-прежнему имеют приоритет, поэтому отдельный экран может переопределить настройку приложения.

## Адаптер календарной системы

Календарная система — небольшой объект-адаптер. В пакете есть два вида, оба без зависимостей:

- `gregorianCalendar` — общий экземпляр класса `GregorianCalendar`, используется, когда `calendar` не передан. Его методы привязаны к экземпляру, поэтому их можно передавать как колбэки, а класс можно расширять (см. [Расширение GregorianCalendar](#расширение-gregoriancalendar));
- `createCalendarSystem(identifier)` — любой другой календарь, встроенный в `Intl`: `'persian'`, `'hebrew'`, `'japanese'`, `'buddhist'`, `'roc'`, `'indian'`, `'coptic'`, `'ethiopic'`, `'islamic-umalqura'`, `'islamic-civil'`, `'chinese'`, `'dangi'`… Идентификатор, который среда выполнения не поддерживает, выбрасывает `RangeError`.

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

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

`createCalendarSystem` читает календарь из `Intl.DateTimeFormat`, поэтому результат совпадает с датами, которые форматирует `Intl`:

- месяцы нумеруются от начала года: от 1 до 13 в високосный год еврейского календаря (включая Адар I и Адар II), а високосный месяц китайского календаря получает собственный номер;
- эры — короткие английские названия эр из `Intl`, например `'Reiwa'` и `'Heisei'`; поля без эры используют текущую, поэтому введённый год — это год текущей эры;
- год, который заканчивается вместе с эрой (японский Хэйсэй 31, с 1 января по 30 апреля 2019 года), содержит только свои месяцы; поля за его концом ограничиваются им;
- результаты кешируются для каждого экземпляра, поэтому создавайте каждый календарь один раз.

Подойдёт и любой другой объект, реализующий интерфейс `CalendarSystem`:

| Член | Назначение |
| --- | --- |
| `identifier` | Идентификатор календаря, понятный `Intl.DateTimeFormat`, например `gregory`, `persian`, `hebrew`. |
| `toFields(date)` | Преобразует ISO-день в `{ era?, year, month, day }` этого календаря; `month` и `day` начинаются с 1. |
| `fromFields({ era?, year, month, day })` | Преобразует поля этого календаря обратно в ISO-день. |
| `getMonthsInYear({ era?, year })` | Число месяцев в году, например 13 в високосный год еврейского календаря. |
| `getDaysInMonth({ era?, year, month })` | Число дней в месяце. |

Названия месяцев, лет и эр всегда берутся из `Intl.DateTimeFormat` с `identifier`, поэтому адаптер не занимается подписями.

Создавайте каждый календарь один раз, вне рендера, и передавайте его в корень компонента:

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

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

Ввод `1405/01/01` проверяется по длинам месяцев персидского календаря и превращается в ISO-день `2026-03-21`.

### Расширение GregorianCalendar

Календарь с теми же месяцами и днями, что у григорианского, может расширить `GregorianCalendar` вместо написания полного адаптера. Методы — обычные методы класса, привязанные в конструкторе, поэтому переопределения могут вызывать `super` и всё равно передаваться как колбэки.

Тайский солнечный буддийский календарь (`buddhist` в `Intl`) — это григорианский календарь, в котором годы отсчитываются от 543 года до н. э.:

```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')` даёт `{ year: 2569, month: 2, day: 15 }`, а подписи берутся из `Intl` с идентификатором `buddhist` (`2569 BE`). Календари с собственными длинами месяцев, например персидский или еврейский, создаются через `createCalendarSystem`.

## Помощники для дат

Все помощники принимают календарь первым аргументом, а принимают и возвращают ISO-строки `YYYY-MM-DD`.

| Помощник | Результат |
| --- | --- |
| `getCalendarMonthStart(calendar, date)` / `getCalendarMonthEnd(calendar, date)` | Первый / последний день месяца, содержащего `date`. |
| `getCalendarYearStart(calendar, date)` / `getCalendarYearEnd(calendar, date)` | Первый / последний день года, содержащего `date`. |
| `addCalendarMonths(calendar, date, amount)` | Первый день месяца, отстоящего на `amount` месяцев. |
| `addCalendarYears(calendar, date, amount)` | Первый день года, отстоящего на `amount` лет. |
| `getCalendarMonthsDistance(calendar, from, to)` / `getCalendarYearsDistance(calendar, from, to)` | Число месяцев / лет между двумя днями со знаком. |
| `getCalendarMonthsOfYear(calendar, date)` | Первые дни всех месяцев года. |
| `getSameDayInCalendarMonth(calendar, date, month)` | Тот же номер дня в другом месяце, ограниченный его длиной. |
| `getSameMonthInCalendarYear(calendar, month, year)` | Тот же номер месяца в другом году, ограниченный числом месяцев. |
| `isSameCalendarMonth(calendar, a, b)` | Относятся ли два дня к одному месяцу. |
| `addISODays(date, days)` / `getISOWeekday(date)` | Арифметика дней и день недели (`0` — воскресенье), одинаковые в любом календаре. |
| `DAYS_IN_WEEK` | `7`: все календари `Intl` используют ISO-неделю, которая продолжается через границы месяцев, лет и эр, поэтому в сетках дней всегда семь столбцов. |

## Форматирование и разбор

`formatCalendarDate(calendar, date, locale, options)` форматирует ISO-день через `Intl.DateTimeFormat` в календарной системе:

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

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

`formatCalendarDay(calendar, date, locale, format)` форматирует только номер дня, без суффикса, который некоторые локали добавляют к отдельному дню: `15`, а не `15日` в `ja-JP`.

Самое затратное — создание `Intl.DateTimeFormat`, поэтому для форматирования многих дней создайте один форматтер и переиспользуйте его. `createCalendarDateFormatter(calendar, locale, options)` и `createCalendarDayFormatter(calendar, locale, format)` возвращают `(date) => string`; их используют сетки дней, месяцев и лет:

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

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

Помощники для шаблонов реализуют ввод с клавиатуры в полях дат, поэтому лежат в `@vega-ui/utils` и принимают календарную систему первым аргументом. Они читают цифры любой системы счисления (`٠٥.٠١.٢٠٢٦`), и поля дат принимают их при вводе. В шаблоне `dd`, `mm` и `yyyy` — день, месяц и год календаря, а `HH`, `MM`, `SS` — часы, минуты и секунды:

- `formatTemplateDate(calendar, date, template)`: ISO-значение в введённую строку
- `parseTemplateDate(calendar, value, template)`: введённая строка в ISO-значение или `undefined`, если она неполная или такой даты нет
- `parseTemplateRange(calendar, value, template, separator)`: введённый диапазон в кортеж `[start, end]`, `''` для неполной границы
- `normalizeTemplateDate(calendar, value, template, { min, max })`: дополняет одиночную цифру, с которой не может начинаться двузначный сегмент, ограничивает заполненные сегменты рамками календаря и ограничивает полное значение пределами `min` / `max`
- `stepTimeSegment(value, template, position, direction, step)`: меняет сегмент времени под кареткой на шаг; время не зависит от календаря, поэтому календарь не передаётся

## Частые ошибки

- Передача объектов `Date`, временных меток, числовых годов или месяцев с нуля вместо ISO-строк.
- Преобразование значений в отображаемый календарь перед сохранением; храните ISO-день, а форматирование оставьте календарю.
- Создание нового адаптера на каждом рендере.
- Разные значения `calendar` у пикера и окружающего его поля.
