Стратегии поддержки старых браузеров

API интернационализации в JavaScript построен поверх ICU (International Components for Unicode) и тесно зависит от реализации движка. Это означает, что поведение Intl в браузерах и Node.js не всегда одинаково и не всегда полнофункционально. В старых браузерах встречаются три основных типа проблем:

  • отсутствие Intl полностью;
  • частичная реализация (например, отсутствуют RelativeTimeFormat, ListFormat, PluralRules);
  • некорректная или устаревшая локализация (ошибки в форматировании дат, чисел, валют).

Ключевая сложность заключается в том, что Intl невозможно “дополнить” стандартным полифиллом без учёта объёма ICU-данных и архитектуры окружения.


Проверка поддержки и feature detection

Базовая стратегия начинается не с подключения полифиллов, а с определения доступного функционала.

Проверка наличия API

if (typeof Intl === "undefined") {
  // Полное отсутствие Intl
}

Это редкий случай для современных браузеров, но он важен для устаревших встроенных WebView или сильно ограниченных окружений.

Проверка конкретных форматтеров

if (!Intl.NumberFormat) {
  // нет поддержки числового форматирования
}

if (!Intl.RelativeTimeFormat) {
  // нет относительного времени
}

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

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

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


Стратегия graceful degradation

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

Fallback на базовое форматирование

function formatNumber(value, locale) {
  if (typeof Intl !== "undefined" && Intl.NumberFormat) {
    return new Intl.NumberFormat(locale).format(value);
  }
  return String(value);
}

Упрощённые локали

Вместо полного набора локалей можно ограничиться:

  • "en" вместо "en-US", "en-GB"
  • "ru" вместо "ru-RU"

Это снижает зависимость от конкретной реализации ICU.


Использование полифиллов Intl

Полный polyfill пакет

Наиболее распространённый подход — подключение пакета intl:

npm install intl
import "intl";
import "intl/locale-data/jsonp/ru";

Особенности:

  • требует явного подключения локалей;
  • увеличивает размер бандла;
  • полезен для сред без ICU.

Полифиллы от FormatJS

Экосистема @formatjs предоставляет модульные полифиллы:

  • @formatjs/intl-datetimeformat
  • @formatjs/intl-numberformat
  • @formatjs/intl-relativetimeformat
  • @formatjs/intl-pluralrules

Пример подключения:

import "@formatjs/intl-numberformat/polyfill";
import "@formatjs/intl-numberformat/locale-data/ru";

Преимущество подхода — гранулярность: подключаются только нужные части API.


Динамическая загрузка полифиллов

Стратегия lazy-loading позволяет не включать Intl-полифиллы в основной бандл.

async function ensureIntl() {
  if (!Intl.NumberFormat) {
    await import("@formatjs/intl-numberformat/polyfill");
    await import("@formatjs/intl-numberformat/locale-data/ru");
  }
}

Использование:

  • ускоряет первичную загрузку;
  • позволяет загружать полифиллы только для старых клиентов;
  • особенно эффективно в SSR + hydration сценариях.

Разделение по окружениям

Браузеры

Для браузеров основная стратегия:

  • проверка feature detection;
  • подключение модульных полифиллов;
  • использование core-js не решает Intl напрямую, требуется отдельный слой.

Node.js

Node.js зависит от сборки ICU:

  • full-icu — полная локализация;
  • small-icu — ограниченный набор локалей;
  • intl полифиллы почти не нужны в современных версиях.

Проверка:

console.log(Intl.DateTimeFormat.supportedLocalesOf(["ru-RU"]));

Работа с Babel и транспиляцией

Babel сам по себе не полифиллит Intl, но может помочь с инфраструктурой загрузки.

preset-env и targets

{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "ie": "11"
      }
    }]
  ]
}

IE11 требует явного подключения Intl polyfill.


Fallback-архитектура форматирования

В крупных приложениях используется слой абстракции:

const formatters = {
  number: (value, locale) => {
    try {
      return new Intl.NumberFormat(locale).format(value);
    } catch {
      return String(value);
    }
  },

  date: (value, locale) => {
    try {
      return new Intl.DateTimeFormat(locale).format(value);
    } catch {
      return new Date(value).toISOString();
    }
  }
};

Этот слой:

  • изолирует Intl от бизнес-логики;
  • позволяет централизованно управлять fallback;
  • упрощает тестирование.

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

Старые реализации часто:

  • игнорируют options;
  • не поддерживают timeZone;
  • неправильно обрабатывают арабские и индийские локали.

Стратегия компенсации:

  • минимизация опций;
  • отказ от сложных конфигураций;
  • использование заранее протестированных локалей.

Селективная деградация функций

Не всегда требуется отключать весь Intl. Можно деградировать только отдельные возможности.

Пример: RelativeTimeFormat

function formatRelativeTime(value, unit, locale) {
  if (Intl.RelativeTimeFormat) {
    const rtf = new Intl.RelativeTimeFormat(locale);
    return rtf.format(value, unit);
  }

  return `${value} ${unit}`;
}

Кэширование форматтеров

В старых браузерах важно снижать стоимость повторных вызовов:

const cache = new Map();

function getFormatter(locale) {
  if (!cache.has(locale)) {
    cache.set(locale, new Intl.NumberFormat(locale));
  }
  return cache.get(locale);
}

При отсутствии Intl:

const fallback = {
  format: (v) => String(v)
};

Polyfill по требованию (feature gating)

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

if (!Intl.RelativeTimeFormat) {
  import("@formatjs/intl-relativetimeformat");
}

Это особенно важно для:

  • медленных сетей;
  • старых Android WebView;
  • embedded браузеров.

Проблемы bundle size и ICU данных

Intl-полифиллы часто содержат:

  • данные локалей;
  • правила множественного числа;
  • календарные системы.

Это может увеличивать размер бандла в несколько мегабайт.

Стратегии оптимизации:

  • подключение только нужных локалей;
  • исключение неиспользуемых форматов;
  • разделение бандла по регионам (locale-based splitting).

Progressive enhancement подход

Модель построения приложения:

  1. Базовая функциональность без Intl.
  2. Улучшение при наличии Intl.NumberFormat.
  3. Расширение до Intl.DateTimeFormat.
  4. Полный набор Intl API при наличии polyfill или modern engine.

Каждый уровень не ломает предыдущий, а только расширяет возможности.


Контроль качества локализации

Тестирование в старых браузерах включает:

  • проверку fallback строк;
  • сравнение форматов дат;
  • валидацию валютных отображений;
  • тесты на отсутствие исключений при отсутствии Intl.

Особое внимание уделяется:

  • Safari 9–10;
  • Android WebView < 60;
  • Internet Explorer 11;
  • старым embedded WebKit.

Изоляция Intl через адаптеры

Архитектурно Intl лучше не использовать напрямую:

class LocaleService {
  constructor(locale) {
    this.locale = locale;
  }

  formatNumber(value) {
    return Intl.NumberFormat
      ? new Intl.NumberFormat(this.locale).format(value)
      : String(value);
  }
}

Это упрощает замену реализации в будущем без рефакторинга всей кодовой базы.


Стратегия отказа от сложных API

Некоторые Intl возможности не имеют стабильной поддержки в старых окружениях:

  • Intl.ListFormat;
  • Intl.Segmenter;
  • Intl.DisplayNames.

Подход:

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

Серверная интернационализация как резерв

Когда клиентская поддержка ограничена:

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

Это особенно эффективно в SSR приложениях:

  • Node.js с полным ICU;
  • единый источник истины для локалей;
  • отсутствие зависимости от браузерных ограничений.