Graceful degradation

Международный API в JavaScript проектировался как слой стандартизированной локализации поверх различий платформ. Несмотря на спецификацию ECMAScript Internationalization API, реальная поддержка функций зависит от окружения: браузера, версии движка, Node.js и наличия ICU-данных. Поэтому ключевым архитектурным принципом становится graceful degradation — плавное снижение функциональности без поломки поведения приложения.

Основная идея заключается в том, что форматирование и локализация не должны быть бинарной функцией «работает / не работает». Вместо этого система должна деградировать по уровням:

  • полная поддержка локали и расширенных опций;
  • частичная поддержка (например, только базовые форматы);
  • fallback на дефолтную локаль en или системную;
  • ручное форматирование без Intl.

Проверка доступности возможностей Intl

Перед использованием любой части Intl API критично учитывать, что не все классы и опции доступны в каждом окружении.

Проверка существования конструктора

if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
  // безопасное использование DateTimeFormat
}

Такой подход предотвращает падение кода в средах с урезанным ICU.


Проверка поддержки локалей

Разные движки могут поддерживать разные локали. Даже при наличии Intl результат может отличаться.

Intl.DateTimeFormat.supportedLocalesOf(["ru-RU", "en-US"]);

Возвращаемый массив показывает фактически доступные локали. При отсутствии поддержки конкретной локали необходимо переходить к fallback.


Базовый уровень деградации: fallback локали

Наиболее частый сценарий graceful degradation — замена неподдерживаемой локали на дефолтную.

function getLocale(requested) {
  const supported = Intl.DateTimeFormat.supportedLocalesOf(requested);
  return supported.length ? supported[0] : "en-US";
}

Если ru-RU недоступен, система автоматически переключается на en-US, не ломая форматирование.


DateTimeFormat и деградация форматов даты

Проблема различий ICU

Даже при наличии Intl.DateTimeFormat поведение может различаться:

  • формат короткой даты;
  • поддержка timeZone;
  • отображение месяцев и дней;
  • порядок компонентов.

Базовая деградация через опции

const fmt = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "full",
  timeStyle: "short"
});

Если dateStyle/timeStyle не поддерживаются, необходимо вручную перейти к классическим полям:

const fallback = new Intl.DateTimeFormat("ru-RU", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit"
});

Деградация через formatToParts

Метод formatToParts() обеспечивает контроль над структурой результата, позволяя строить собственный форматтер при неполной поддержке ICU.

const dtf = new Intl.DateTimeFormat("ru-RU");
const parts = dtf.formatToParts(new Date());

Если требуется fallback, можно использовать части как строительные блоки и игнорировать отсутствующие токены.


NumberFormat и уровни совместимости

Потенциальные точки деградации

  • notation: "compact"
  • unitDisplay
  • currencyDisplay
  • signDisplay

Безопасное форматирование чисел

function formatNumber(value, locale) {
  try {
    return new Intl.NumberFormat(locale, {
      notation: "compact"
    }).format(value);
  } catch {
    return String(value);
  }
}

Если compact notation не поддерживается, происходит возврат к строковому представлению.


Fallback на базовый формат

new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 0,
  maximumFractionDigits: 2
});

Такой вариант обеспечивает стабильность вне зависимости от ICU возможностей.


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

Сортировка строк зависит от Unicode Collation Algorithm, но реализация может отличаться.

const collator = new Intl.Collator("ru-RU", {
  sensitivity: "base"
});

Fallback сортировки

Если Intl.Collator отсутствует или ограничен:

array.sort((a, b) => a.localeCompare(b));

При отсутствии полноценного ICU возможны расхождения в порядке сортировки, поэтому важна единая точка принятия решения.


RelativeTimeFormat и деградация относительных выражений

Поддержка Intl.RelativeTimeFormat появилась позднее других компонентов.

const rtf = new Intl.RelativeTimeFormat("ru", {
  numeric: "auto"
});

Ручной fallback

function relativeDay(diff) {
  if (diff === -1) return "вчера";
  if (diff === 0) return "сегодня";
  if (diff === 1) return "завтра";
  return `${diff} дн.`;
}

Деградация здесь чаще семантическая, а не техническая: поведение сохраняется, но теряется локализация.


ListFormat и деградация списков

const lf = new Intl.ListFormat("ru", {
  style: "long",
  type: "conjunction"
});

Fallback без ListFormat

function formatList(arr) {
  return arr.length <= 1
    ? arr.join("")
    : arr.slice(0, -1).join(", ") + " и " + arr[arr.length - 1];
}

Такой подход сохраняет читаемость даже без Intl.


Деградация через feature detection

Стратегия уровней поддержки

Типичная схема:

  1. Проверка конструктора
  2. Проверка опций
  3. Проверка локали
  4. fallback форматтер
function createFormatter(locale) {
  if (typeof Intl === "undefined") {
    return (x) => String(x);
  }

  if (Intl.NumberFormat) {
    return new Intl.NumberFormat(locale).format;
  }

  return (x) => String(x);
}

Поведение при урезанном ICU

В некоторых сборках Node.js ICU может быть минимальным. Это влияет на:

  • количество доступных локалей;
  • поддержку региональных правил;
  • корректность plural rules;
  • форматирование дат.

Проверка ICU уровня

if (typeof process !== "undefined" && process.versions?.icu) {
  // ICU доступен
}

PluralRules и деградация множественных форм

const pr = new Intl.PluralRules("ru");

Ручной fallback для plural logic

function pluralRu(n) {
  const mod10 = n % 10;
  const mod100 = n % 100;

  if (mod10 === 1 && mod100 !== 11) return "one";
  if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return "few";
  return "many";
}

Intl снимает необходимость ручной реализации, но fallback остаётся необходимым при отсутствии поддержки.


Деградация через нормализацию локалей

Некоторые локали могут быть нераспознаны движком.

const locale = Intl.NumberFormat.supportedLocalesOf("ru-KZ")[0] || "ru";

Если региональная локаль отсутствует, используется базовая языковая метка.


Стабильность через унифицированный слой форматирования

В архитектуре приложений Intl часто инкапсулируется в слой форматирования:

const Format = {
  number: (v) => new Intl.NumberFormat("en-US").format(v),
  date: (d) => new Intl.DateTimeFormat("en-US").format(d),
  list: (arr) => arr.join(", ")
};

Даже при деградации интерфейс остаётся стабильным, изменяется только реализация.


Деградация опций и игнорируемые параметры

Intl может игнорировать неподдерживаемые опции без ошибок.

new Intl.NumberFormat("en-US", {
  compactDisplay: "short",
  signDisplay: "exceptZero"
});

В разных окружениях результат может отличаться, поэтому логика не должна зависеть от точного присутствия всех параметров.


Слоистая модель graceful degradation

Поведение Intl API можно представить как уровни:

Уровень 1: Полная поддержка

  • все локали
  • все опции
  • ICU full

Уровень 2: Частичная поддержка

  • ограниченные локали
  • игнор некоторых опций

Уровень 3: Базовый Intl

  • только core форматирование

Уровень 4: Fallback JS

  • localeCompare
  • string concatenation
  • ручные функции

Практика устойчивого форматирования

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

Любая функция форматирования должна:

  • работать без Intl;
  • улучшаться при наличии Intl;
  • не менять семантику данных;
  • сохранять предсказуемость при частичной поддержке.