Регистрация расширений

Регистрация расширений — это механизм, при котором расширение объявляет себя доступным и получает доступ к API. В контексте timeago.js это касается как локалей, так и кастомных модулей поведения.


Регистрация локали через register

import { register } from 'timeago.js';

// Простейшая регистрация
register('my_locale', (number, index) => {
  const forms: [string, string][] = [
    ['только что', 'сейчас'],
    ['%s с назад', 'через %s с'],
    // ... индексы 2-14
  ];
  return forms[index] ?? ['давно', 'потом'];
});

Зарегистрировать локаль нужно один раз — до первого вызова format с этой локалью.


Реестр расширений

import { register, format as baseFormat, render as baseRender } from 'timeago.js';
import type { LocaleFunc } from 'timeago.js';

interface Extension {
  name:    string;
  version: string;
  install: (registry: ExtensionRegistry) => void;
}

class ExtensionRegistry {
  private installed = new Map<string, string>(); // name → version
  private locales   = new Set<string>();

  use(extension: Extension): this {
    if (this.installed.has(extension.name)) {
      console.warn(`Extension "${extension.name}" already installed`);
      return this;
    }
    extension.install(this);
    this.installed.set(extension.name, extension.version);
    return this;
  }

  registerLocale(code: string, fn: LocaleFunc): void {
    if (this.locales.has(code)) {
      console.warn(`Locale "${code}" already registered`);
      return;
    }
    register(code, fn);
    this.locales.add(code);
  }

  hasLocale(code: string): boolean {
    return this.locales.has(code);
  }

  getInstalled(): Map<string, string> {
    return new Map(this.installed);
  }
}

export const registry = new ExtensionRegistry();

Создание расширения-локали

// extensions/ru-formal.ts
import type { LocaleFunc } from 'timeago.js';

const fn: LocaleFunc = (n, i) => {
  const forms: [string, string][] = [
    ['только что',         'в ближайшее время'],
    ['%s секунду назад',   'через %s секунду'],
    ['%s секунды назад',   'через %s секунды'],
    ['%s секунд назад',    'через %s секунд'],
    ['минуту назад',       'через минуту'],
    ['%s минуты назад',    'через %s минуты'],
    ['%s минут назад',     'через %s минут'],
    ['час назад',          'через час'],
    ['%s часа назад',      'через %s часа'],
    ['%s часов назад',     'через %s часов'],
    ['день назад',         'через день'],
    ['%s дня назад',       'через %s дня'],
    ['%s дней назад',      'через %s дней'],
    ['год назад',          'через год'],
    ['%s лет назад',       'через %s лет'],
  ];
  return forms[i] ?? ['давно', 'скоро'];
};

export const RuFormalExtension = {
  name:    'ru-formal',
  version: '1.0.0',
  install: (reg: ExtensionRegistry) => {
    reg.registerLocale('ru_formal', fn);
  },
};

Расширение с конфигурацией

interface CacheExtensionOptions {
  ttl?:      number;
  maxSize?:  number;
}

function createCacheExtension(opts: CacheExtensionOptions = {}) {
  const { ttl = 30_000, maxSize = 500 } = opts;

  return {
    name:    'cache',
    version: '1.0.0',

    install(reg: ExtensionRegistry) {
      // Регистрация поведения через monkey-patch
      const cache = new Map<string, { value: string; ts: number }>();

      reg.wrapFormat((original) => (date, locale, options) => {
        if (cache.size >= maxSize) {
          const firstKey = cache.keys().next().value;
          cache.delete(firstKey);
        }

        const key    = `${date}:${locale}`;
        const cached = cache.get(key);
        const now    = Date.now();

        if (cached && now - cached.ts < ttl) {
          return cached.value;
        }

        const value = original(date, locale, options);
        cache.set(key, { value, ts: now });
        return value;
      });
    },
  };
}

Порядок регистрации

Порядок имеет значение, если расширения оборачивают один и тот же метод:

registry
  .use(ValidationExtension)  // Первый — проверяет входные данные
  .use(CacheExtension)        // Второй — кеширует результат
  .use(LogExtension);         // Третий — логирует каждый вызов

Стек выполнения (снаружи внутрь):

LogExtension → CacheExtension → ValidationExtension → baseFormat

Автоматическая регистрация через import

// Расширение регистрирует себя при импорте
// extensions/auto-register-ru.ts
import { register } from 'timeago.js';
import ruLocale from 'timeago.js/esm/lang/ru';

// Выполняется при импорте файла
register('ru', ruLocale);

export {}; // Пустой экспорт для ESM совместимости
// Использование
import './extensions/auto-register-ru';
import { format } from 'timeago.js';

format(new Date(), 'ru'); // ru уже зарегистрирована

Проверка наличия регистрации

const REGISTERED_LOCALES = new Set<string>();

const originalRegister = register;
const trackedRegister = (locale: string, fn: LocaleFunc): void => {
  REGISTERED_LOCALES.add(locale);
  originalRegister(locale, fn);
};

function isLocaleRegistered(code: string): boolean {
  return REGISTERED_LOCALES.has(code);
}

function requireLocale(code: string): void {
  if (!isLocaleRegistered(code)) {
    throw new Error(`Locale "${code}" is not registered. Call register("${code}", fn) first.`);
  }
}

Точка входа для всех расширений

// src/lib/timeago-setup.ts
import { registry } from './registry';
import { RuFormalExtension }    from '../extensions/ru-formal';
import { createCacheExtension } from '../extensions/cache';
import { LogExtension }         from '../extensions/log';

export function setupTimeago(): void {
  registry
    .use(RuFormalExtension)
    .use(createCacheExtension({ ttl: 60_000 }))
    .use(LogExtension);
}

// В точке входа приложения
setupTimeago();