Универсальные паттерны интеграции

Независимо от используемого фреймворка, интеграция timeago.js следует нескольким устоявшимся паттернам. Знание этих паттернов позволяет быстро применять библиотеку в любом контексте.


Паттерн: обёртка над format

Создать единую точку форматирования для приложения:

// src/lib/time.js
import { format } from 'timeago.js';

const DEFAULT_LOCALE = 'ru';

export function ago(date, locale = DEFAULT_LOCALE) {
  if (!date) return '';
  const d = new Date(date);
  if (isNaN(d.getTime())) return '';
  return format(d, locale);
}

Все компоненты используют ago() вместо прямых вызовов format.


Паттерн: централизованная инициализация

// src/lib/timeago-setup.js
import { register } from 'timeago.js';

const LOCALES = import.meta.glob('./locales/*.js');

export async function setupTimeago(localeKey = 'ru') {
  const path = `./locales/${localeKey}.js`;
  if (LOCALES[path]) {
    const mod = await LOCALES[path]();
    register(localeKey, mod.default);
  }
}

Паттерн: адаптер для источников данных

// src/lib/time-adapter.js
import { format } from 'timeago.js';

export function fromAPI(apiDate, locale = 'ru') {
  // API возвращает строку без Z
  const normalized = apiDate.includes('Z') ? apiDate : apiDate + 'Z';
  return format(normalized, locale);
}

export function fromDB(dbDate, locale = 'ru') {
  // MySQL: "2025-05-26 10:00:00"
  const isoDate = dbDate.replace(' ', 'T') + 'Z';
  return format(isoDate, locale);
}

export function fromUnix(seconds, locale = 'ru') {
  return format(seconds * 1000, locale);
}

Паттерн: smart format с fallback

import { format } from 'timeago.js';

export function smartAgo(date, { locale = 'ru', maxDays = 30 } = {}) {
  const d   = new Date(date);
  const age = (Date.now() - d.getTime()) / 86400000;

  if (age > maxDays) {
    return new Intl.DateTimeFormat(locale, {
      day: 'numeric', month: 'long', year: 'numeric',
    }).format(d);
  }

  return format(d, locale);
}

Паттерн: менеджер жизненного цикла

Для ванильного JS — класс, управляющий монтированием/размонтированием:

import { render, cancel } from 'timeago.js';

export class TimeagoManager {
  #locale;
  #elements = new WeakSet();

  constructor(locale = 'ru') {
    this.#locale = locale;
  }

  mount(container, selector = '[datetime]') {
    const nodes = container.querySelectorAll(selector);
    render(nodes, this.#locale);
  }

  unmount(container, selector = '[datetime]') {
    const nodes = container.querySelectorAll(selector);
    cancel(nodes);
  }

  refresh(container, selector = '[datetime]') {
    this.unmount(container, selector);
    this.mount(container, selector);
  }

  setLocale(locale) {
    this.#locale = locale;
  }
}

Паттерн: ленивая инициализация

import { format, register } from 'timeago.js';

const loadedLocales = new Set(['en_US']);

export async function formatLazy(date, locale = 'ru') {
  if (!loadedLocales.has(locale)) {
    try {
      const mod = await import(`timeago.js/esm/lang/${locale}.js`);
      register(locale, mod.default);
      loadedLocales.add(locale);
    } catch {
      return format(date, 'en_US');
    }
  }
  return format(date, locale);
}

Паттерн: enrichment при получении данных

Добавлять timeAgo при маппинге данных из API:

import { format } from 'timeago.js';

function enrichPosts(posts, locale = 'ru') {
  return posts.map(post => ({
    ...post,
    timeAgo: format(post.createdAt, locale),
  }));
}

const posts = enrichPosts(await fetchPosts(), 'ru');

Паттерн: SSR + hydration

На сервере генерируется абсолютная дата (стабильная), на клиенте заменяется относительной:

// Сервер
const html = `<time data-timeago datetime="${post.createdAt}">
  ${new Intl.DateTimeFormat('ru').format(new Date(post.createdAt))}
</time>`;

// Клиент
import { render } from 'timeago.js';

document.addEventListener('DOMContentLoaded', () => {
  const nodes = document.querySelectorAll('[data-timeago]');
  render(nodes, 'ru');
});

Паттерн: обновление при возврате на вкладку

import { cancel, render } from 'timeago.js';

const SELECTOR = '[data-timeago]';

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    const nodes = document.querySelectorAll(SELECTOR);
    cancel(nodes);
    render(nodes, 'ru');
  }
});

Чеклист интеграции

  • register вызывается один раз при старте приложения.
  • cancel вызывается при каждом размонтировании.
  • cancel вызывается перед повторным render.
  • Строки без явного часового пояса нормализуются до UTC.
  • Для SSR используется format, не render.
  • Для автообновления в браузере используется render.