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

    Чому ICU MessageFormat не підходить для JavaScript

    ICU MessageFormat є надійним стандартом. Він функціональний, перекладачі добре його знають, а більшість систем керування перекладами (TMS) вміють з ним працювати. Проблема полягає у середовищі виконання, для якого його було створено. ICU походить із C++ та Java, де повноцінний парсер і форматер повідомлень створюють мізерне навантаження на фоні всієї програми. У бандлі для браузера за цей парсер доводиться платити під час кожного завантаження сторінки.

    У цій статті розглядається походження ICU, причини громіздкості його синтаксису для форм множини, а також те, чому повна сумісність збільшує розмір будь-якої бібліотеки інтернаціоналізації для JavaScript. Якщо вам потрібен безпосередньо синтаксис, спершу ознайомтеся з довідником з ICU Message Format.

    Від IBM до консорціуму Unicode

    Абревіатура ICU розшифровується як International Components for Unicode. Синтаксис його повідомлень виник у Java: Taligent, спільне підприємство Apple та IBM, розробило класи інтернаціоналізації для JDK 1.1 (1997 рік), включно з java.text.MessageFormat. IBM продовжила їх розвиток під назвою ICU4J, портувала на C/C++ як ICU4C та відкрила початковий код проєкту у 1999 році. У 2016 році ICU перейшов під керівництво консорціуму Unicode, який також опікується CLDR, сховищем мовних даних, на які спирається формат.

    Для чого він використовувався спочатку

    Основним призначенням було серверне та десктопне програмне забезпечення: корпоративні додатки Java, продукти IBM, а згодом і операційні системи. Повідомлення зберігалися у файлах .properties Java, які завантажувалися через ResourceBundle, або у власному форматі ресурсів ICU для C/C++:

    messages_fr.properties
    inbox.unread={count, plural, one {# message non lu} other {# messages non lus}}
    
    java
    String pattern = bundle.getString("inbox.unread");
    String text = new MessageFormat(pattern, Locale.FRENCH)
        .format(Map.of("count", 5)); // "5 messages non lus"
    

    Початкова версія JDK не містила ключового слова plural. Вона використовувала choice з числовими інтервалами ({0,choice,0#no files|1#one file|1<{0} files}), що підходило лише для мов із граматикою множини за зразком англійської. ICU додав конструкцію plural на основі правил CLDR у 2008 році (ICU 4.0), а директиву select запровадив у 2010 році (ICU 4.4).

    Відмінність від .po

    ICU часто плутають із gettext, проте це дві абсолютно різні традиції. Файли .po походять із GNU gettext (C, Linux, пізніше PHP і Python). Запис .po містить прості пари msgid / msgstr, а форми множини обираються за допомогою виразу на C у заголовку файлу (Plural-Forms: nplurals=2; plural=(n > 1);). Усередині самого повідомлення розгалуження відсутні. ICU натомість розміщує логіку розгалуження безпосередньо всередині рядка, завдяки чому одне повідомлення може комбінувати plural, select і форматування чисел.

    Де ICU працює сьогодні

    Бібліотека ICU4C вбудована в Android, iOS, macOS, Windows, Node.js та рушії JavaScript браузерів Chrome і Firefox. Стандартні API Intl у браузері великою мірою побудовані саме на ній. Таким чином, браузер уже містить правила плюралізації, форматування чисел і дат з ICU. Чого він не має, так це вбудованого парсера повідомлень: специфікація Intl.MessageFormat все ще перебуває на початковій стадії пропозицій TC39, базується на новому синтаксисі MessageFormat 2 і не сумісна з ICU MessageFormat 1.

    Ця історія пояснює прийняті архітектурні рішення:

    • Орієнтація на серверні та десктопні середовища виконання. Парсинг рядка під час виконання там коштує дешево, а сама бібліотека встановлюється один раз на рівні системи і не завантажується кожним користувачем мережею.
    • Це DSL усередині рядка. Розгалуження, форматування чисел, дат і вкладеність описані єдиним синтаксисом, доступним для редагування перекладачам без потреби втручання в код.
    • Прагнення до абсолютної повноти. Для будь-якого граматичного випадку, з яким може стикнутися перекладач, передбачено окремий оператор.

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

    Форми множини є занадто громіздкими

    Найпоширеніша конструкція ICU водночас виявляється найбільш перевантаженою. Опис підрахунку з нульовим варіантом виглядає так:

    text
    {count, plural,
      =0 {No unread messages}
      one {# unread message}
      other {# unread messages}
    }
    

    Для цього потрібні назва аргументу, ключове слово plural, мітка для кожної гілки, вкладені фігурні дужки та символ #, що діє як спеціальний токен виключно всередині секцій множини. Якщо додати граматичний рід, рівень вкладеності суттєво зростає:

    text
    {gender, select,
      female {{count, plural,
        one {She has # unread message}
        other {She has # unread messages}
      }}
      male {{count, plural,
        one {He has # unread message}
        other {He has # unread messages}
      }}
      other {{count, plural,
        one {They have # unread message}
        other {They have # unread messages}
      }}
    }
    

    Дев'ять із п'ятнадцяти рядків займає виключно синтаксичний каркас. В українській чи польській мовах для кожного з цих трьох варіантів роду потрібно по три-чотири форми множини. У результаті перекладений рядок перетворюється на блок із фігурних дужок, де одна пропущена дужка } ламає все повідомлення, нерідко непомітно аж до самого рантайму.

    У JavaScript ту саму структуру можна виразити простими даними: об'єктом, ключі якого відповідають категоріям множини, перевіреним системою типів і редактором, без потреби у проміжному парсері між файлом і кінцевим значенням.

    Повнота можливостей формує ціну

    ICU охоплює величезну кількість випадків:

    • plural із точним збігом (=0) та зміщеннями (offset:)
    • selectordinal із власною таблицею порядкових числівників CLDR
    • select із довільною глибиною вкладеності
    • Аргументи number, date та time як у класичному форматі (number, currency), так і у формі скелетонів (::currency/EUR compact-short)
    • Правила екранування та лапок ('{', '')
    • Теги форматування rich-text у низці реалізацій (<b>…</b>)

    Бібліотека, яка заявляє про сумісність 1:1 з ICU, змушена містити всі ці компоненти, оскільки під час збирання неможливо передбачити, які саме можливості використовуються у ваших текстах. На практиці це вимагає:

    1. Парсер, який перетворює рядок на AST з обробкою синтаксичних помилок у дужках.
    2. Парсер скелетонів для синтаксису :: у числах і датах, що є окремою компактною мовою.
    3. Форматер, який обходить AST і зіставляє кожен вузол з Intl.PluralRules, Intl.NumberFormat та Intl.DateTimeFormat.

    Третя частина є компактною, оскільки сучасний JavaScript уже має логіку CLDR всередині Intl. Натомість перші дві існують лише для розбору текстового синтаксису. У бібліотеці intl-messageformat від FormatJS, яка слугує основою для react-intl та next-intl, це становить близько 10 КБ стисненого JavaScript-коду, що завантажується кожним користувачем ще до отримання самих перекладів.

    Більшість проєктів використовує лише незначну частину: підстановку {name} та кілька блоків plural. Проте вони все одно завантажують повний парсер для скелетонів, порядкових числівників та зміщень, адже розбір рядків під час виконання не дозволяє бандлеру видалити зайвий код.

    next-intl зіткнувся з тією ж проблемою

    Це не лише теоретичні міркування. next-intl, одна з найпопулярніших бібліотек на базі ICU, дійшла аналогічного висновку. У версії 4.8 (січень 2026 року) було впроваджено експериментальну опцію precompile. Вона аналізує повідомлення ICU під час збирання, створює компактний AST і замінює парсер у рантаймі легким інтерпретатором. За даними розробників, це дозволило зменшити розмір бандла приблизно на 9 КБ стисненого JavaScript.

    Такий компроміс демонструє межі цього підходу: функція t.raw перестає працювати за умови передкомпіляції, оскільки початковий рядок ICU більше не існує під час виконання. Щойно браузер припиняє парсити рядок, ви фактично більше не поширюєте чистий ICU. Ви передаєте скомпільоване представлення, а синтаксис рядків залишається лише форматом написання.

    У цей момент виникає закономірне запитання: якщо браузер ніколи не читає цей рядок, навіщо розробникам і перекладачам його писати?

    Як виглядає нативний підхід у JavaScript

    JavaScript уже містить найскладніші механізми. Intl.PluralRules знає правила для мов із кількома формами множини та правила для порядкових числівників. Intl.NumberFormat та Intl.DateTimeFormat відповідають за валюти, одиниці вимірювання, компактні нотації та календарі. Залишається лише вибрати гілку та підставити значення, що потребує лічених рядків коду, якщо структура оформлена як дані, а не рядок.

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

    **/*.content.ts
    import { gender, plural, t, type Dictionary } from "intlayer";
    
    const inboxContent = {
      key: "inbox",
      content: {
        unread: t({
          uk: plural({
            one: "{{count}} непрочитане повідомлення",
            few: "{{count}} непрочитаних повідомлення",
            many: "{{count}} непрочитаних повідомлень",
            other: "{{count}} непрочитаного повідомлення",
          }),
          en: plural({
            one: "{{count}} unread message",
            other: "{{count}} unread messages",
          }),
          pl: plural({
            one: "{{count}} nieprzeczytana wiadomość",
            few: "{{count}} nieprzeczytane wiadomości",
            many: "{{count}} nieprzeczytanych wiadomości",
            other: "{{count}} nieprzeczytanej wiadomości",
          }),
        }),
      },
    } satisfies Dictionary;
    
    export default inboxContent;
    
    **/*.tsx
    const { unread } = useIntlayer("inbox");
    
    unread(5); // Польська локаль → "5 nieprzeczytanych wiadomości"
    

    Головні переваги порівняно з ICU:

    • Жодного парсера в бандлі. Структура є готовим об'єктом у момент потрапляння до браузера. Функція plural обирає ключ через стандартний Intl.PluralRules, наданий середовищем.
    • Помилки виявляються під час збирання. Пропущена гілка або помилка в ключі викличе помилку типів TypeScript замість збою в продакшені.
    • Форматування залишається за межами повідомлення. Числа, дати та валюти форматуються через хуки форматерів, які працюють з Intl безпосередньо, без парсингу скелетонів.
    • Невикористовувані можливості не впливають на розмір. Якщо в проєкті не використовується gender, бандлер видалить його за допомогою tree-shaking.

    • Хуки форматерів

    Звісно, цей підхід має власні особливості: потрібен етап збирання, файли контенту є програмним кодом, а деякі інструменти TMS, заточені виключно під ICU, не можуть зчитувати файли TypeScript безпосередньо.

    Коли ICU залишається доречним вибором

    ICU залишається кращим рішенням, якщо:

    • Ваш робочий процес перекладу побудований навколо нього. Багато інструментів TMS імпортують та експортують рядки ICU, а перекладачі звикли працювати з цим синтаксисом.
    • Повідомлення використовуються на багатьох платформах. Спільний каталог для додатків на iOS, Android та веб є вагомим аргументом на користь єдиного формату.
    • У вас уже є великий масив текстів у форматі ICU. Переписування тисяч повідомлень рідко виправдовує себе самостійно.

    В останньому випадку не потрібно обирати між повною переробкою та збереженням важкого парсера. Адаптер сумісності react-intl від Intlayer вміє читати наявні рядки ICU (plural, select, selectordinal, #, класичні формати number / date / time), завдяки чому можна мігрувати поступово і нести витрати на ICU лише там, де старі повідомлення все ще цього потребують.

    Висновок

    ICU MessageFormat вирішив важливу проблему: граматика належить перекладачам, а не умовам if (count === 1) у коді програми. Він успішно вирішив її для середовищ, де розбір текстового DSL не створює навантаження. Проте у веб-браузері повна підтримка вимагає завантаження парсера для можливостей, які більшість додатків не використовує, і навіть бібліотеки на базі ICU тепер оптимізують свої повідомлення заздалегідь.

    JavaScript уже має правила CLDR у складі Intl. Усе, що потрібно від формату i18n, це логіка умовних розгалужень, і її набагато зручніше виражати у вигляді структурованих типізованих даних.

    Дізнатися більше

    Коментарі

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

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

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