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

    Формат повідомлень ICU: синтаксис і типові підводні камені

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

    Зміст

    Проблема на практиці

    Ось код, який майже кожен розробник пише на самому початку:

    ts
    const label = count + " " + (count === 1 ? t("item") : t("items"));
    

    Це працює в англійській мові, але призводить до помилок практично в усіх інших:

    • Українська та польська потребують трьох або чотирьох форм, а не двох.
    • Японська потребує лише однієї форми, а доданий пробіл виявляється недоречним.
    • Арабська вимагає шість форм, а саме число має відображатися за місцевою системою числення.
    • Французька вимагає нерозривний пробіл перед деякими розділовими знаками, що руйнується конкатенацією + " ".

    Глибша проблема полягає в тому, що речення було розбито на ізольовані частини. Перекладач бачить окремі слова item та items без контексту і позбавлений можливості змінити порядок слів у реченні. ICU MessageFormat вирішує це, зберігаючи все речення в одному рядку для перекладу та надаючи перекладачеві оператори розгалуження.

    Прості аргументи

    Найменшою одиницею є заповнювач (placeholder) у фігурних дужках:

    text
    Hello, {name}!
    

    Під час форматування передається { name: "Alice" }, що дає Hello, Alice!. Фігурні дужки є єдиними спеціальними символами. Щоб вивести звичайну фігурну дужку, її слід взяти в одинарні лапки: '{'.

    Це вся функціональність інтерполяції. Усі інші можливості ICU побудовані поверх неї.

    Множина (plural)

    Оператор plural обирає гілку на основі числового значення:

    text
    {count, plural,
      one {You have one unread message}
      other {You have # unread messages}
    }
    

    Три правила, які необхідно знати:

    • # замінюється відформатованим значенням count з урахуванням локалі. Наприклад, 1234 перетворюється на 1,234 в en-US та на 1 234 в uk-UA.
    • Гілка other є обов'язковою. Будь-яка реалізація ICU викличе помилку або не пройде валідацію за її відсутності. Вона є резервною, якщо жодна інша категорія не підійшла.
    • Значення =0, =1, … зіставляються з точними числами та перевіряються раніше за категорії CLDR. Використовуйте їх для особливих випадків ("Немає повідомлень"), а не як заміну one.
    text
    {count, plural,
      =0 {No unread messages}
      one {One unread message}
      other {# unread messages}
    }
    

    offset

    Параметр offset:n віднімає число n від значення перед вибором категорії та підстановкою #. Він використовується для конструкцій на зразок "Аліса та ще 3 користувачі вподобали це":

    text
    {count, plural, offset:1
      =0 {No one liked this}
      =1 {{name} liked this}
      one {{name} and one other liked this}
      other {{name} and # others liked this}
    }
    

    За значення count: 4 символ # відобразить 3. Опція offset дуже корисна, але рівень її підтримки в різних середовищах виконання різниться, тому перевіряйте сумісність перед використанням.

    Категорії множини залежать від мови

    Саме тут виникає найбільше непорозумінь. Назви категорій zero, one, two, few, many, other не є універсальними комірками, спільними для всіх мов. Кожна мова використовує підмножину, визначену правилами множини CLDR, і ці правила базуються на граматиці, а не на математичній інтуїції.

    МоваТегВикористовувані категоріїРазом
    Японськаjaother1
    Китайськаzhother1
    Англійськаenone, other2
    Німецькаdeone, other2
    Французькаfrone, many, other3
    Чеськаcsone, few, many, other4
    Польськаplone, few, many, other4
    Українськаukone, few, many, other4
    Російськаruone, few, many, other4
    Арабськаarzero, one, two, few, many, other6
    Валлійськаcyzero, one, two, few, many, other6

    Два наслідки, які часто дивують розробників:

    • one не обов'язково означає число 1. В українській мові one охоплює 1, 21, 31, 101: будь-яке число, що закінчується на 1, крім чисел на 11. У французькій мові число 0 також належить до категорії one.
    • Додавання категорій до англійського вихідного файлу нічого не змінює. Англійське повідомлення потребує лише one та other; український переклад вимагає чотирьох гілок, і ця структура має міститися саме в українському рядку. Будь-який формат, що змушує всі мови дотримуватися єдиної структури ключів, створюватиме тут конфлікти.

    Поведінку середовища виконання можна перевірити безпосередньо без встановлення сторонніх пакетів:

    ts
    new Intl.PluralRules("uk").select(2); // "few"
    new Intl.PluralRules("uk").select(5); // "many"
    new Intl.PluralRules("uk").select(21); // "one"
    new Intl.PluralRules("ar").select(0); // "zero"
    

    Інтерфейс Intl.PluralRules містить дані CLDR в усіх сучасних браузерах та в Node.js. Будь-яка бібліотека з підтримкою CLDR майже завжди звертається до цього вбудованого API.

    select та selectordinal

    Оператор select розгалужує текст на основі довільного рядка: статі, ролі користувача, статусу чи тарифного плану.

    text
    {gender, select,
      female {She updated her profile}
      male {He updated his profile}
      other {They updated their profile}
    }
    

    Ключі порівнюються дослівно, і гілка other тут також обов'язкова. select є ідеальним інструментом, коли структура речення залежить від значення enum, оскільки в різних мовах граматичні форми залежать від різних факторів.

    Оператор selectordinal має таку саму структуру, як і plural, але використовує правила для порядкових числівників (1-й, 2-й тощо), що відрізняються від кількісних:

    text
    {rank, selectordinal,
      one {#st place}
      two {#nd place}
      few {#rd place}
      other {#th place}
    }
    

    Англійська мова використовує чотири категорії для порядкових числівників (1st, 2nd, 3rd, 4th), хоча для кількісних застосовує лише дві. Ця асиметрія є причиною, чому ці два оператори існують окремо.

    Аргументи чисел, дат і часу

    ICU дозволяє форматувати інтерпольовані значення прямо в тексті:

    text
    Total: {price, number, currency}
    Published {publishedAt, date, long} at {publishedAt, time, short}
    Conversion: {rate, number, percent}
    

    Сучасним форматом є скелетон (skeleton), представлений в ICU 60 із префіксом ::. Скелетони надають значно більшу гнучкість, ніж застарілі назви стилів:

    text
    {price, number, ::currency/EUR}
    {value, number, ::percent scale/100}
    {amount, number, ::compact-short}
    {distance, number, ::unit/kilometer unit-width-narrow}
    

    Підтримка скелетонів в екосистемі різна. FormatJS підтримує їх у повному обсязі, тоді як деякі інші середовища приймають лише традиційні формати на кшталт number, currency або date, long. Обов'язково перевірте роботу :: у вашому середовищі перед релізом.

    Вкладеність і читабельність

    Синтаксис ICU є модульним і підтримує комбінування. Гілка plural може містити select, який своєю чергою може містити інший plural:

    text
    {hostGender, select,
      female {{guestCount, plural, offset:1
        =0 {{host} does not give a party}
        =1 {{host} invites {guest} to her party}
        other {{host} invites {guest} and # other people to her party}
      }}
      other {{guestCount, plural, offset:1
        =0 {{host} does not give a party}
        other {{host} invites {guest} and # other people to their party}
      }}
    }
    

    Це класичний приклад ICU і водночас головний аргумент проти надмірної вкладеності. Починаючи з двох рівнів, перекладачі легко припускаються помилок із фігурними дужками, а редактори TMS втрачають ефективність. Не використовуйте більше двох рівнів вкладеності. Якщо потрібен третій рівень, розділіть речення на два окремих повідомлення.

    Підтримка ICU в бібліотеках JavaScript

    БібліотекаПідтримка ICUЩо ви пишете на практиці
    react-intl (FormatJS)Нативна, повнаРядки ICU, включно зі скелетонами та тегами розмітки
    next-intlНативнаРядки ICU через пакет intl-messageformat від FormatJS
    i18nextПотрібен плагінСуфікси ключів key_one / key_other та {{name}}; ICU через i18next-icu
    vue-i18nЧасткова / власнаІнтерполяція {name} і гілки множини через вертикальну риску
    Angular ($localize)ПідмножинаICU plural / select усередині шаблонів, експорт до XLIFF

    Кілька важливих уточнень:

    • Синтаксис за замовчуванням в i18next не є ICU, і це не є недоліком. Суфікси ключів (item_one, item_few) відповідають категоріям Intl.PluralRules і часто зручніші для редагування у звичайному JSON. Проте select і складні вкладені гілки відсутні, тому потрібно додавати i18next-icu або писати логіку в коді.
    • Гілки через вертикальну риску в vue-i18n за замовчуванням спираються на функцію правила для кожної мови замість категорій CLDR. Це працює, але правило множини зберігається в конфігурації застосунку, а не в самих даних.
    • FormatJS є еталонною реалізацією у світі JS. Коли в контексті JavaScript згадують "ICU MessageFormat", зазвичай мають на увазі стандарт, що підтримується FormatJS.
    • Повна підтримка ICU збільшує розмір бандла. Парсер та обробка скелетонів додають близько 10 КБ стисненого JavaScript. Дивіться чому ICU не підходить для JavaScript.

    Підхід Intlayer

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

    **/*.content.ts
    import { plural, t, type Dictionary } from "intlayer";
    
    const openingsContent = {
      key: "total_openings",
      content: {
        totalOpenings: t({
          en: plural({
            one: "{{count}} opening",
            other: "{{count}} openings",
          }),
          uk: plural({
            one: "{{count}} вакансія",
            few: "{{count}} вакансії",
            many: "{{count}} вакансій",
            other: "{{count}} вакансій",
          }),
          pl: plural({
            one: "{{count}} oferta",
            few: "{{count}} oferty",
            many: "{{count}} ofert",
            other: "{{count}} ofert",
          }),
        }),
      },
    } satisfies Dictionary;
    
    export default openingsContent;
    
    **/*.tsx
    const { totalOpenings } = useIntlayer("total_openings");
    
    totalOpenings(5); // Українська локаль → "5 вакансій"
    

    Зв'язок із концепціями ICU є прямим і прозорим:

    Конструкція ICUВідповідник в Intlayer
    {name}insert("Hello {{name}}") або автовиявлення
    {count, plural, …}plural({ one, few, many, other })
    {value, select, …}select({ draft, published, fallback })
    гілка роду в selectgender({ male, female, fallback })
    логічна гілка в selectcond({ true, false })
    діапазони чисел (не CLDR)enu({ "0": …, ">5": …, fallback: … })
    {n, number, ::currency/EUR}useCurrency()(1234.5, { currency: "EUR" })

    Оператор plural делегує вибір категорії системному Intl.PluralRules, тому вищезазначена таблиця CLDR застосовується без змін. Форматування залишається розділеним: числа, дати, валюти та списки обробляються через хуки форматування, а не вбудовуються безпосередньо в текст.

    Особливості моделі:

    • Intlayer вимагає етапу збирання: компілятор витягує оголошення під час білду. Якщо вам потрібен звичайний JSON, що динамічно завантажується під час виконання, це інша архітектура.
    • Усередині гілок plural наразі не можна вкладати t(): слід огортати plural у t(), а не навпаки.
    • Екосистема новіша за i18next, кількість готових інтеграцій із зовнішніми системами TMS продовжує розширюватися.

    Якщо ви переходите з проєкту, що вже містить рядки ICU, адаптер сумісності react-intl розбирає їх безпосередньо: plural, select, selectordinal, # та традиційні аргументи number, date, time. Скелетони та offset: цим резолвером не підтримуються і потребують перевірки під час міграції. Адаптер i18next обробляє суфіксні форми (key_one, key_male) через Intl.PluralRules.

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

    • Жорстке кодування правил множини в JS. Конструкція count === 1 ? a : b дає хибний результат для 8 із 10 мов у наведеній вище таблиці. Щойно тернарний оператор з'являється в коді, перекладач не може на нього вплинути.
    • Об'єднання перекладених фрагментів. Порядок слів, граматичні узгодження та пробіли перед знаками пунктуації залежать від локалі. Завжди зберігайте речення як єдине ціле.
    • Пропуск гілки other. Це сувора вимога специфікації, а не рекомендація. Більшість парсерів видадуть помилку, а решта нічого не покаже.
    • Припущення, що всі мови мають однакові категорії. Якщо в англійському джерелі є лише one та other, це не означає, що польський чи український переклад матиме лише дві гілки. Кожна мова повинна мати можливість оголошувати власні гілки. Дивіться оголошення контенту за локалями.
    • Використання =1 замість one. Форма =1 відповідає виключно точному числу 1. В українській мові число 21 потребує категорії one, і правило =1 ніколи для нього не спрацює.
    • Розміщення символу # поза межами гілки plural. Він має спеціальне значення лише всередині блоків plural або selectordinal. В інших місцях він сприймається як звичайний символ решітки.
    • Забування про те, що # уже відформатовано. Якщо потрібне число без регіональних розділювачів розрядів, інтерполюйте аргумент за його назвою.

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

    Коментарі

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

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

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