Pipe для темплейтов

В системе интернационализации i18next шаблоны сообщений строятся вокруг интерполяции значений, где динамические данные подставляются в строки перевода. Базовый механизм опирается на конструкцию вида {{value}}, однако для практических сценариев локализации этого недостаточно: требуется форматирование чисел, дат, падежей, единиц измерения и комбинированных преобразований. Одним из удобных архитектурных подходов становится pipe-подобная модель обработки значений внутри темплейтов.

Механизм интерполяции в i18next позволяет вставлять переменные прямо в строку перевода:

{
  "price": "Цена: {{value}}"
}
t('price', { value: 150 })

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

Ограничения базовой интерполяции

При усложнении интерфейсов возникают задачи:

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

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

Pipe-модель как расширение интерполяции

Pipe-подход вводит концепцию последовательного преобразования значения внутри шаблона:

{{value | format1 | format2 | format3}}

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

В контексте i18next такая модель реализуется не нативно, а через кастомную интерполяцию или форматтеры.

Реализация pipe через interpolation.format

i18next предоставляет механизм форматирования через функцию interpolation.format, которая может быть переопределена:

import i18next from 'i18next';

i18next.init({
  interpolation: {
    format: (value, format, lng) => {
      if (format === 'upper') return String(value).toUpperCase();
      if (format === 'lower') return String(value).toLowerCase();
      return value;
    }
  }
});

Шаблон:

{{name, upper}}

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

Эмуляция pipe через цепочку форматов

Чтобы приблизиться к pipe-синтаксису, формат может интерпретироваться как последовательность операций:

const formatters = {
  upper: v => String(v).toUpperCase(),
  trim: v => String(v).trim(),
  exclaim: v => `${v}!`
};

i18next.init({
  interpolation: {
    format: (value, format) => {
      if (!format) return value;

      const steps = format.split('|');

      return steps.reduce((acc, step) => {
        const fn = formatters[step.trim()];
        return fn ? fn(acc) : acc;
      }, value);
    }
  }
});

Шаблон:

{{name, upper|trim|exclaim}}

Значение проходит последовательно через цепочку функций, что повторяет поведение pipe-оператора.

Поведение цепочки преобразований

Pipe-модель строится на принципе композиции функций:

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

Такой подход делает локализацию более декларативной.

Работа с числами и форматированием локали

Pipe особенно полезен при форматировании числовых значений:

const formatters = {
  currency: (v, lng) =>
    new Intl.NumberFormat(lng, {
      style: 'currency',
      currency: 'USD'
    }).format(v),

  percent: (v) => `${v * 100}%`
};

Шаблон:

{{price, currency}}

Или цепочка:

{{value, percent|exclaim}}

Интеграция с языковыми параметрами

Функция форматирования получает текущую локаль, что позволяет адаптировать pipe-операции под язык:

interpolation: {
  format: (value, format, lng) => {
    const steps = format.split('|');

    return steps.reduce((acc, step) => {
      if (step === 'currency') {
        return new Intl.NumberFormat(lng, {
          style: 'currency',
          currency: lng === 'ru' ? 'RUB' : 'USD'
        }).format(acc);
      }

      return acc;
    }, value);
  }
}

Таким образом, pipe становится контекстно-зависимым.

Композиция сложных преобразований

Pipe-цепочки позволяют строить сложные трансформации без изменения шаблонов перевода:

{{username, trim|lower|exclaim}}

или

{{date, formatDate|relativeTime}}

Каждый шаг инкапсулирует отдельную ответственность:

  • нормализация данных
  • форматирование
  • локализация
  • постобработка строки

Ограничения pipe-подхода в i18next

Несмотря на выразительность, pipe-модель сталкивается с рядом ограничений:

  • отсутствует нативная поддержка синтаксиса pipe
  • усложнение отладки при длинных цепочках
  • риск смешивания бизнес-логики и слоя представления
  • необходимость ручной реализации парсинга

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

Безопасность и предсказуемость форматтеров

При проектировании pipe-цепочек важным становится ограничение набора доступных функций:

const formatters = Object.freeze({
  upper: v => String(v).toUpperCase(),
  lower: v => String(v).toLowerCase()
});

Запрещается динамическое выполнение произвольного кода из строки формата, так как шаблон перевода может поступать из внешних источников.

Расширение pipe через плагинную архитектуру

Форматтеры могут быть вынесены в отдельные модули:

export const dateFormatters = {
  shortDate: (v, lng) =>
    new Intl.DateTimeFormat(lng).format(new Date(v))
};

Далее они объединяются в общий реестр:

const formatters = {
  ...dateFormatters,
  ...stringFormatters,
  ...numberFormatters
};

Это превращает pipe-модель в расширяемую систему преобразований.

Сравнение с альтернативным форматом интерполяции

В i18next также используется формат:

{{value, formatName}}

Он более строгий и безопасный, но менее выразительный, чем pipe-цепочки. Pipe-модель выигрывает в гибкости композиции, но проигрывает в стандартизации и читаемости при длинных выражениях.

Встраивание pipe в шаблонный слой

Pipe-логика остаётся частью слоя представления и не должна проникать в бизнес-логику. В шаблонах переводов сохраняется декларативность:

{
  "welcome": "Привет, {{name, trim|capitalize}}"
}

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