Паттерн адаптера

Паттерн адаптера оборачивает существующий API в новый интерфейс, совместимый с требованиями системы. В контексте timeago.js он позволяет подключить библиотеку туда, где ожидается другой интерфейс форматирования, или сменить реализацию без изменения потребителей.


Проблема несовместимых интерфейсов

// Существующий компонент ожидает функцию такого типа
type FormatFn = (date: string, options: { locale: string; relative: boolean }) => string;

// timeago.js имеет другую сигнатуру
// format(date: DateInput, locale?: string): string

// Прямое использование невозможно без адаптера

Адаптер для timeago.js

import { format } from 'timeago.js';

type AppFormatFn = (date: string, options: { locale: string; relative: boolean }) => string;

function timeagoAdapter(): AppFormatFn {
  return (date, options) => {
    if (!options.relative) {
      return new Date(date).toLocaleDateString(options.locale.replace('_', '-'));
    }
    return format(date, options.locale);
  };
}

const appFormat = timeagoAdapter();
appFormat('2025-05-31', { locale: 'ru', relative: true }); // "день назад"

Двусторонний адаптер

// Система A ожидает один интерфейс, система B предоставляет другой

interface SystemAFormatter {
  getRelativeTime(timestamp: number, lang: string): string;
}

// timeago.js → SystemA интерфейс
class TimeagoToSystemAAdapter implements SystemAFormatter {
  getRelativeTime(timestamp: number, lang: string): string {
    return format(new Date(timestamp), lang);
  }
}

// date-fns → SystemA интерфейс
class DateFnsToSystemAAdapter implements SystemAFormatter {
  getRelativeTime(timestamp: number, lang: string): string {
    const locale = { ru, de, fr }[lang];
    return formatDistanceToNow(new Date(timestamp), {
      locale,
      addSuffix: true,
    });
  }
}

Адаптер для i18n систем

// i18next ожидает функцию форматирования такого вида
type I18nextFormatter = (
  value: unknown,
  format: string,
  lng: string
) => string;

function createTimeagoI18nextAdapter(): I18nextFormatter {
  return (value, formatType, lng) => {
    if (formatType !== 'relative') {
      return String(value);
    }

    if (
      typeof value !== 'string' &&
      typeof value !== 'number' &&
      !(value instanceof Date)
    ) {
      return '';
    }

    return format(value, lng.replace('-', '_'));
  };
}

// Регистрация в i18next
i18n.services.formatter.add('relative', (value, lng) => {
  return format(value, lng.replace('-', '_'));
});

Адаптер для Vue I18n

// Vue I18n форматирование дат
const datetimeFormats = {
  ru: {
    short: { year: 'numeric', month: 'short', day: 'numeric' },
  },
};

// Кастомный форматтер — адаптер timeago.js под Vue I18n
const vueI18nTimeagoPlugin = {
  install(app) {
    app.config.globalProperties.$timeago = (date: string, locale = 'ru') => {
      return format(date, locale);
    };
  },
};

Адаптер для Angular pipes

import { Pipe, PipeTransform } from '@angular/core';
import { format } from 'timeago.js';

@Pipe({ name: 'timeago' })
export class TimeagoPipe implements PipeTransform {
  transform(value: string | Date | number, locale = 'ru'): string {
    if (!value) return '';
    try {
      return format(value, locale);
    } catch {
      return '';
    }
  }
}

Адаптер для Handlebars/шаблонизаторов

const Handlebars = require('handlebars');
const { format } = require('timeago.js');

Handlebars.registerHelper('timeago', function(date, options) {
  const locale = options?.hash?.locale ?? 'ru';
  return format(new Date(date), locale);
});
{{timeago post.createdAt locale="ru"}}

Адаптер для серверного рендеринга (Express)

const { format } = require('timeago.js');

// Middleware, добавляющее helper в locals
app.use((req, res, next) => {
  res.locals.timeago = (date, locale = 'ru') => format(date, locale);
  next();
});
<time datetime="<%= post.createdAt %>">
  <%= timeago(post.createdAt, 'ru') %>
</time>

Адаптер для замены библиотеки

При смене timeago.js на другую библиотеку адаптер позволяет изменить реализацию в одном месте:

// src/adapters/time-format.ts

// Версия 1: timeago.js
export function relativeFormat(date: Date | string, locale = 'ru'): string {
  return format(date, locale);
}

// Версия 2: Intl.RTF (замена без изменения потребителей)
export function relativeFormat(date: Date | string, locale = 'ru'): string {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
  const diffSec = (new Date(date as any).getTime() - Date.now()) / 1000;
  // ...
}

Все компоненты импортируют relativeFormat из адаптера — при смене реализации менять ничего не нужно.


Таблица сценариев применения адаптера

Сценарий Описание
Несовместимый API Обернуть timeago.js под ожидаемый интерфейс
i18n интеграция Подключить как форматтер дат в i18next/Vue I18n
Шаблонизатор Handlebars helper, EJS locals
Смена библиотеки Изолировать зависимость для безболезненной замены
Серверный рендеринг Express middleware с helpers