Автор:
    Дата створення:2026-09-02Останнє оновлення:2026-09-02

    Форматування дат і чисел за локалями за допомогою Intl

    Переклад текстових рядків — це лише видима половина інтернаціоналізації (i18n). Інша половина, яка регулярно генерує звіти про помилки, це форматування: німецький користувач бачить 1,234.56 замість 1.234,56, японський користувач бачить 08/02/2026 і сприймає це як серпень, або дата по-різному рендериться на сервері та клієнті, спричиняючи помилку гідратації в React.

    Жодна з цих ситуацій не вимагає сторонніх бібліотек. Стандартний Intl присутній у кожному сучасному середовищі виконання JavaScript.

    Зміст

    Почніть із видалення самописних хелперів для дат

    Майже в кожній кодовій базі є функція formatDate, написана ще до того, як хтось задумався про підтримку локалей. Вона жорстко фіксує порядок відображення, роздільник і переважно англійські назви місяців.

    ts
    // Код, який слід видалити:
    const formatDate = (d: Date) =>
      `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
    

    Intl.DateTimeFormat повністю замінює її та забезпечує коректний результат для будь-якої локалі:

    ts
    new Intl.DateTimeFormat("de-DE", { dateStyle: "long" }).format(date);
    // "2. August 2026"
    new Intl.DateTimeFormat("ja-JP", { dateStyle: "long" }).format(date);
    // "2026年8月2日"
    

    Те саме стосується чисел. Виклик toFixed(2) повсюдно повертає 1234.56, що є неправильним для більшості країн Європи.

    Що охоплює Intl

    API Сфера застосування
    Intl.DateTimeFormat Дати та час із готовими пресетами dateStyle / timeStyle
    Intl.NumberFormat Десяткові дроби, валюти, відсотки, одиниці, компактний запис
    Intl.RelativeTimeFormat "3 дні тому", "через 2 години"
    Intl.ListFormat "а, б і в" згідно з граматикою мови
    Intl.PluralRules Визначення граматичної категорії множини для чисел
    Intl.Collator Коректне алфавітне сортування рядків з урахуванням мови

    Intl.Collator часто несправедливо забувають. Звичайний array.sort() на рядках сортує за кодовими точками Unicode, через що літери з діакритикою опиняються після літери z, а шведська ö потрапляє не на своє місце. Якщо ви сортуєте видимі користувачам списки, завжди використовуйте collator.

    ts
    ["zebra", "édouard", "apple"].sort(new Intl.Collator("uk").compare);
    // ["apple", "édouard", "zebra"]
    

    Віддавайте перевагу пресетам замість ручного збирання параметрів

    Параметри dateStyle та timeStyle дозволяють самій локалі визначити прийнятний порядок полів та роздільники. Ручне вказування year, month та day надає контроль, якого краще уникати, оскільки правильний порядок змінюється залежно від регіону, і ви ризикуєте замінити дані CLDR власними хибними припущеннями.

    ts
    // Локаль сама обирає правильну структуру:
    new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(d);
    
    // Власноруч нав'язана структура, некоректна в інших країнах:
    new Intl.DateTimeFormat(locale, {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
    }).format(d);
    

    Задавайте складові явно лише тоді, коли дизайн безапеляційно вимагає фіксованої ширини, наприклад у вузькій колонці таблиці.

    Створення форматерів є ресурсомістким

    Це ключовий нюанс продуктивності. Ініціалізація Intl.NumberFormat супроводжується завантаженням значного обсягу даних локалі, і цей процес набагато важчий за наступний виклик .format(). Виконання цієї операції всередині циклу рендерингу на тисячу рядків спричиняє відчутні затримки.

    ts
    // Перестворює форматер на кожному рядку (повільно):
    rows.map((r) => new Intl.NumberFormat(locale).format(r.total));
    
    // Створюється один раз і перевикористовується (швидко):
    const nf = new Intl.NumberFormat(locale);
    rows.map((r) => nf.format(r.total));
    

    Функції toLocaleDateString() та toLocaleString() містять ту саму приховану ваду: кожен виклик створює новий форматер. Вони підходять для поодиноких випадків, але непридатні для списків.

    Кешуйте форматери за комбінацією локалі та набору опцій:

    ts
    const cache = new Map<string, Intl.NumberFormat>();
    
    const getNumberFormat = (
      locale: string,
      options: Intl.NumberFormatOptions = {}
    ) => {
      const key = `${locale}:${JSON.stringify(options)}`;
      let formatter = cache.get(key);
      if (!formatter) {
        formatter = new Intl.NumberFormat(locale, options);
        cache.set(key, formatter);
      }
      return formatter;
    };
    

    Баг із часовим поясом, що проявляється лише в продакшені

    Ця проблема забрала чимало годин у багатьох команд. Сервер формує дату під час SSR, браузер виконує гідратацію на клієнті, і React аварійно завершує роботу через помилку hydration mismatch, оскільки два середовища згенерували різний текст.

    Причина: Intl.DateTimeFormat використовує системний часовий пояс хоста, якщо ви не передали його явно. Сервер продакшену налаштовано на UTC, а комп'ютер розробника — на локальний пояс. Тому локально помилка не відтворюється і проявляється лише після викатки.

    ts
    // Сервер в UTC і браузер в UTC+2 розходяться в результатах. Помилка гідратації:
    new Intl.DateTimeFormat(locale, { dateStyle: "short" }).format(d);
    
    // Обидві сторони повністю узгоджені:
    new Intl.DateTimeFormat(locale, { dateStyle: "short", timeZone: "UTC" }).format(
      d
    );
    

    Три робочі підходи:

    • Зафіксувати часовий пояс на сервері та передавати його явно. Надійно та передбачувано, але всі бачать час у форматі UTC.
    • Рендерити виключно на клієнті, показуючи нейтральний плейсхолдер під час SSR. Точно для кожного користувача, але викликає легке мерехтіння.
    • Зберігати часовий пояс користувача і передавати його на сервер та клієнт. Найкращий досвід, що вимагає дещо більше інфраструктурного коду.

    Хоч би який шлях ви обрали, завжди вказуйте timeZone явно для будь-якої дати, яка рендериться і на сервері, і на клієнті. Дата без зазначеного поясу — це дата з двома різними значеннями.

    Валюта потребує коду валюти, а не локалі

    Локаль і валюта — незалежні речі. fr-FR не означає автоматично розрахунки в євро: французький користувач може переглядати інвойс у доларах США.

    ts
    new Intl.NumberFormat("fr-FR", { style: "currency", currency: "USD" }).format(
      1234.5
    );
    // "1 234,50 $US"
    

    Локаль відповідає за роздільники, групування цифр та розміщення символу. Валюта береться з бізнес-даних операції. Спроба вивести одне з іншого призводить до бухгалтерських помилок.

    Зверніть увагу на параметр currencyDisplay. В інтерфейсах, де сусідують різні валюти зі знаком долара ($), значення "code" знімає будь-яку неоднозначність між американськими, канадськими та австралійськими доларами.

    Відносний час сприймається легше за точну дату

    Для недавніх дій формулювання "2 години тому" набагато зручніше за сухий часовий штамп, і Intl.RelativeTimeFormat якісно це локалізує.

    ts
    new Intl.RelativeTimeFormat("uk", { numeric: "auto" }).format(-1, "day");
    // "учора"
    

    Параметр numeric: "auto" забезпечує виведення "учора" замість неприродного "1 день тому".

    Що додає Intlayer

    Intlayer загортає ці можливості у легкі хелпери з автоматичним кешуванням, звільняючи від ручного контролю над Map, та автоматично застосовує активну локаль без потреби передавати її на кожному кроці.

    ts
    import {
      number,
      currency,
      date,
      relativeTime,
      units,
      compact,
      list,
    } from "intlayer";
    
    number(1234.5); // "1 234,5"
    currency(1234.5, { currency: "EUR" }); // "1 234,50 €"
    date(new Date(), "short");
    relativeTime(now, twoHoursAgo, { unit: "hour", numeric: "auto" }); // "2 години тому"
    units(5, { unit: "kilometer", unitDisplay: "long" }); // "5 кілометрів"
    compact(1200); // "1,2 тис."
    list(["яблуко", "банан", "апельсин"]); // "яблуко, банан і апельсин"
    

    Функція date() також підтримує пресети ("short", "long", "dateOnly", "timeOnly", "full"). Для React та Vue доступні хуки й composables, які дістають активну локаль безпосередньо з контексту.

    Це зручний прошарок кешування та підтримки локалі за замовчуванням над стандартним API платформи. Сама поведінка форматування повністю спирається на Intl. Докладні сигнатури доступні в документації форматерів.

    Поширені помилки

    • Виклик toLocaleDateString() без вказування локалі. Використовує локаль середовища хоста, яка на сервері залежить від контейнера.
    • Форматування в циклі без кешування. Створення форматера споживає більшість процесорного часу.
    • Відсутність timeZone на ізоморфних датах. Провокує помилки гідратації, які неможливо виявити локально.
    • Виведення валюти з коду мови. fr-FR не гарантує використання євро.
    • Звичайний sort() для видимого тексту. Завжди використовуйте Intl.Collator.
    • Ручний хардкод назв місяців чи днів. Усі вони вже зібрані в CLDR для кожної мови.
    • Залишення numeric: "always" для відносного часу. Призводить до "1 день тому" там, де є природне слово учора.

    Корисні матеріали

    Коментарі

    Поки що немає коментарів. Будьте першим, хто поділиться своїми думками.

    Схожі публікації

    Останні публікації