Создание собственных локалей

В библиотеке date-fns локализация реализована через набор JavaScript-объектов, описывающих правила форматирования, парсинга и языковых преобразований для конкретного языка. В отличие от монолитных систем интернационализации, локали здесь представляют собой модульные структуры, подключаемые точечно и не влияющие на глобальное состояние.

Локаль в date-fns определяет:

  • правила форматирования длинных и коротких дат;
  • преобразование числовых значений в текстовые представления;
  • правила относительного времени;
  • шаблоны сопоставления строк с датами;
  • языковые особенности (порядковые числительные, названия месяцев, периодов суток).

Ключевая особенность подхода — отсутствие скрытой магии: вся логика локализации явно выражена через функции.


Интерфейс объекта локали

Типичная структура локали в date-fns выглядит как набор функций и словарей:

const locale = {
  code: 'xx',
  formatDistance,
  formatLong,
  formatRelative,
  localize,
  match,
  options
}

Каждое поле отвечает за отдельный слой локализации:

  • formatDistance — формирование строк относительного времени
  • formatLong — правила длинных и коротких форматов дат
  • formatRelative — форматирование дат относительно текущего момента
  • localize — преобразование числовых значений в текст
  • match — обратное сопоставление строк с датами

Форматирование длинных и коротких дат через formatLong

Модуль formatLong отвечает за шаблоны представления дат в разных контекстах: полная дата, время, комбинированные форматы.

Структура обычно включает функции:

  • date
  • time
  • dateTime

Пример реализации:

const formatLong = {
  date: (options) => {
    return options?.width === 'short'
      ? 'dd.MM.yyyy'
      : 'd MMMM yyyy'
  },

  time: (options) => {
    return options?.width === 'short'
      ? 'HH:mm'
      : 'HH:mm:ss'
  },

  dateTime: () => {
    return 'd MMMM yyyy, HH:mm'
  }
}

Функции принимают объект options, позволяющий изменять формат в зависимости от контекста (короткий, средний, длинный).


Форматирование относительного времени

Функция formatDistance определяет, как будут выглядеть выражения вроде:

  • «5 минут назад»
  • «через 2 часа»
  • «около 1 месяца»

Сигнатура обычно выглядит так:

function formatDistance(token, count, options) {
  return string
}

token описывает тип интервала, например:

  • lessThanXMinutes
  • xMinutes
  • xHours
  • xDays
  • aboutXMonths

Пример реализации:

const formatDistance = (token, count, options) => {
  const result = {
    lessThanXMinutes: 'менее минуты',
    xMinutes: `${count} минут`,
    xHours: `${count} часов`,
    xDays: `${count} дней`,
    aboutXMonths: `около ${count} месяцев`
  }

  return result[token]
}

Более сложные реализации учитывают:

  • склонение числительных;
  • грамматический род;
  • падежи;
  • исключения для единицы.

Форматирование относительных дат

formatRelative отвечает за выражения вида:

  • «вчера в 14:00»
  • «сегодня в 09:00»
  • «завтра в 18:00»

Функция получает:

  • токен периода (lastWeek, yesterday, today, tomorrow, nextWeek)
  • дату
  • опции форматирования

Пример:

const formatRelative = (token, date, baseDate, options) => {
  const formats = {
    lastWeek: "'в прошлую неделю в' HH:mm",
    yesterday: "'вчера в' HH:mm",
    today: "'сегодня в' HH:mm",
    tomorrow: "'завтра в' HH:mm",
    nextWeek: "'на следующей неделе в' HH:mm"
  }

  return formats[token]
}

В реальных локалях часто учитываются:

  • различие между буднями и выходными;
  • контекст времени суток;
  • формальная и разговорная стилистика.

Локализация отдельных значений (localize)

localize — один из самых объёмных модулей локали. Он отвечает за преобразование чисел и кодов в текстовые представления.

Обычно включает:

  • месяцы
  • дни недели
  • части суток
  • порядковые числительные
  • кварталы
  • эры

Структура:

const localize = {
  month: (n, options) => string,
  day: (n, options) => string,
  ordinalNumber: (n, options) => string,
  quarter: (n, options) => string,
  dayPeriod: (period, options) => string
}

Пример:

const localize = {
  month: (n) => {
    const months = [
      'январь', 'февраль', 'март', 'апрель',
      'май', 'июнь', 'июль', 'август',
      'сентябрь', 'октябрь', 'ноябрь', 'декабрь'
    ]
    return months[n]
  },

  day: (n) => {
    const days = [
      'воскресенье', 'понедельник', 'вторник',
      'среда', 'четверг', 'пятница', 'суббота'
    ]
    return days[n]
  },

  ordinalNumber: (n) => `${n}-й`
}

Особое внимание уделяется порядковым числительным, поскольку в разных языках они формируются по-разному:

  • суффиксы;
  • изменения корня;
  • исключения.

Сопоставление текстовых значений (match)

Модуль match используется при парсинге строковых дат. Он определяет, как текстовые значения преобразуются обратно в числа.

Структура обычно зеркальна localize:

const match = {
  month: (string) => number,
  day: (string) => number,
  ordinalNumber: (string) => number,
  dayPeriod: (string) => value
}

Пример:

const match = {
  month: (str) => {
    const map = {
      'январь': 0,
      'февраль': 1,
      'март': 2
    }

    return map[str.toLowerCase()]
  }
}

Часто используется сопоставление через:

  • регулярные выражения;
  • словари;
  • нормализацию регистра;
  • стемминг (в сложных локалях).

Создание локали с нуля

Полная кастомная локаль формируется путём реализации всех ключевых модулей.

Пример минимальной локали:

export const customLocale = {
  code: 'custom',

  formatLong: {
    date: () => 'yyyy-MM-dd',
    time: () => 'HH:mm',
    dateTime: () => 'yyyy-MM-dd HH:mm'
  },

  formatDistance: (token, count) => {
    const map = {
      xMinutes: `${count}m`,
      xHours: `${count}h`,
      xDays: `${count}d`
    }

    return map[token]
  },

  formatRelative: (token) => {
    const map = {
      today: 'сегодня',
      yesterday: 'вчера',
      tomorrow: 'завтра'
    }

    return map[token]
  },

  localize: {
    month: (n) => String(n + 1),
    day: (n) => String(n),
    ordinalNumber: (n) => `${n}`
  },

  match: {
    month: (str) => Number(str) - 1,
    day: (str) => Number(str)
  }
}

Такая реализация минимальна и подходит только для контролируемых систем, где входные данные ограничены.


Использование внутренних фабрик date-fns

Внутри date-fns существует набор вспомогательных функций для построения локалей:

  • buildFormatLong
  • buildLocalize
  • buildMatchPattern
  • buildMatch

Они позволяют уменьшить дублирование кода и стандартизировать структуру.

Пример использования:

import buildLocalize from 'date-fns/locale/_lib/buildLocalize'

const localize = buildLocalize({
  values: {
    month: [...],
    day: [...]
  },
  defaultWidth: 'wide'
})

Такой подход используется в официальных локалях, чтобы избежать ручной реализации повторяющейся логики.


Наследование и переиспользование базовой локали

Часто кастомная локаль строится поверх существующей, например поверх enUS или ru.

Подход заключается в:

  • переопределении только нужных частей;
  • использовании spread-оператора;
  • точечной замене функций.

Пример:

import { enUS } from 'date-fns/locale'

export const extendedLocale = {
  ...enUS,

  localize: {
    ...enUS.localize,

    month: (n) => `Month ${n + 1}`
  }
}

Такой способ позволяет сохранять совместимость с внутренними механизмами date-fns.


Типичные ошибки при создании локалей

Одной из частых проблем становится неполное покрытие токенов. При отсутствии нужного ключа поведение может становиться непредсказуемым.

Другие распространённые ошибки:

  • возврат строк вместо функций в formatDistance;
  • несоответствие индексации месяцев (0-based vs 1-based);
  • игнорирование падежей и контекста в языках с развитой морфологией;
  • отсутствие синхронизации между localize и match;
  • несовместимость форматов между formatLong и реальными форматами парсинга.

Особенно критично расхождение между localize.month и match.month, так как оно приводит к невозможности обратного преобразования даты из строки.