Кастомизация логики форматирования

Кастомизация форматирования выходит за рамки изменения строк локали. Иногда требуется изменить саму логику — как число вычисляется, как выбирается интервал, что происходит в граничных случаях. Библиотека предоставляет несколько точек расширения.


Уровни кастомизации

Уровень Механизм Сложность
Строки локали register(key, fn) Низкая
Логика выбора формы Логика внутри функции локали Средняя
Опорная дата relativeDate в опциях Низкая
Пороги интервалов Обёртка над format Средняя
Полная замена Собственная функция вместо format Высокая

Кастомизация внутри функции локали

Функция локали может содержать произвольную логику. Это позволяет полностью переопределить поведение для конкретного индекса:

import { register, format } fr om 'timeago.js';

register('ru_smart', (number, index) => {
  // Специальное поведение для "только что"
  if (index === 0) return ['только что', 'прямо сейчас'];

  // Специальное поведение для 1 дня
  if (index === 7) return ['вчера', 'завтра'];

  // Специальное поведение для 2 дней
  if (index === 8 && number === 2) return ['позавчера', 'послезавтра'];

  // Стандартное поведение для остального
  return standardRu(number, index);
});

Кастомизация через wrapper function

Вместо изменения локали создаётся обёртка, меняющая вход или выход:

import { format } fr om 'timeago.js';

function smartFormat(date, locale = 'ru') {
  const d    = new Date(date);
  const diff = Math.abs(Date.now() - d.getTime());
  const DAY  = 86400000;
  const YEAR = 365 * DAY;

  // Для очень старых дат — абсолютный формат
  if (diff > YEAR * 2) {
    return new Intl.DateTimeFormat('ru', {
      year: 'numeric', month: 'long',
    }).format(d);
  }

  return format(date, locale);
}

Кастомизация через подмену %s

По умолчанию %s заменяется числом. Можно перехватить это поведение:

register('ru_ordinal', (number, index) => {
  const ordinal = number + '-й';
  return [
    [`${ordinal} день назад`, `через ${ordinal} день`],
  ][0];
});

Динамическая логика по контексту

Функция локали получает два аргумента: number и index. Дополнительный контекст может передаваться через замыкание:

function createContextLocale(context = 'default') {
  return function(number, index) {
    if (context === 'formal') {
      return formalStrings(number, index);
    }
    return informalStrings(number, index);
  };
}

import { register } fr om 'timeago.js';

register('ru_formal',   createContextLocale('formal'));
register('ru_informal', createContextLocale('informal'));

Кастомизация числа перед выводом

register('ru_rounded', (number, index) => {
  // Округлять до ближайшего удобного числа
  let displayNumber = number;

  if (index === 4 && number > 10) {
    displayNumber = Math.round(number / 5) * 5; // 13 → 15, 27 → 25
  }

  return [
    `${displayNumber} мин. назад`,
    `через ${displayNumber} мин.`,
  ];
});

Подмена вычисления разницы (полная замена format)

Если нужна принципиально иная логика расчёта:

function customRelativeTime(date, locale = 'ru') {
  const now  = Date.now();
  const then = new Date(date).getTime();
  const diff = Math.abs(now - then);
  const isPast = then < now;

  const steps = [
    { lim it: 30000,                label: 'только что' },
    { lim it: 60000,                label: `${Math.round(diff / 1000)} сек.` },
    { lim it: 3600000,              label: `${Math.round(diff / 60000)} мин.` },
    { limit: 86400000,             label: `${Math.round(diff / 3600000)} ч.` },
    { limit: 604800000,            label: `${Math.round(diff / 86400000)} дн.` },
    { limit: Infinity,             label: `${Math.round(diff / 604800000)} нед.` },
  ];

  const step = steps.find(s => diff < s.limit);
  const label = step ? step.label : 'давно';

  if (label === 'только что') return label;
  return isPast ? `${label} назад` : `через ${label}`;
}

Кастомизация на уровне рендера

Собственный рендер, который не только обновляет текст, но и меняет классы:

import { format } from 'timeago.js';

function semanticRender(elements, locale = 'ru') {
  elements.forEach(el => {
    const date = el.getAttribute('datetime');

    function update() {
      const diff = Date.now() - new Date(date).getTime();
      el.textContent = format(date, locale);

      el.classList.remove('fresh', 'recent', 'old');

      if (diff < 300000)    el.classList.add('fresh');   // < 5 мин
      else if (diff < 86400000) el.classList.add('recent'); // < 1 дня
      else                  el.classList.add('old');
    }

    update();
    setInterval(update, 60000);
  });
}

Кастомизация через middleware-паттерн

const formatMiddlewares = [];

function use(fn) {
  formatMiddlewares.push(fn);
}

function applyFormat(date, locale) {
  let result = format(date, locale);

  for (const middleware of formatMiddlewares) {
    result = middleware(result, date, locale);
  }

  return result;
}

// Добавить middleware
use((result) => result.toUpperCase());
use((result, date) => {
  if (new Date(date).getTime() < Date.now()) return result;
  return `⏳ ${result}`;
});