Автор:
    Создание: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 решает эту задачу, сохраняя все предложение в виде единой строки и предоставляя переводчику операторы ветвления.

    Простые аргументы

    Базовой единицей является заполнитель в одинарных фигурных скобках:

    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 для ru-RU.
    • Ветка 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
    Русский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("pl").select(2); // "few"
    new Intl.PluralRules("pl").select(5); // "many"
    new Intl.PluralRules("ru").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 незаменим, когда построение предложения зависит от значения перечисления, поскольку в разных языках такие параметры влияют на структуру по-разному.

    Оператор selectordinal имеет такую же структуру, как и plural, но использует правила порядковых числительных, которые отличаются от количественных:

    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 в библиотеках JS

    БиблиотекаПоддержка 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. Когда говорят об "ICU MessageFormat" в контексте JavaScript, чаще всего подразумевают именно реализацию 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",
          }),
          ru: 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 является прямым:

    Конструкция ICUIntlayer
    {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.

    Распространенные ошибки

    • Жесткое кодирование плюрализации в коде. Конструкция count === 1 ? a : b дает некорректный результат для 8 из 10 языков в таблице выше. Если тернарный оператор зашит в код, переводчик не сможет ничего исправить.
    • Склеивание переведенных фрагментов. Порядок слов, грамматические согласования и пробелы перед знаками препинания зависят от языка. Храните предложение целиком.
    • Пропуск ветки other. Это строгое требование спецификации, а не рекомендация. Большинство парсеров выбросят ошибку, а остальные ничего не отобразят.
    • Предположение об одинаковых категориях. Если в английском источнике указаны one и other, это не означает, что в польском или русском файле будет две ветки. Каждая локаль должна объявлять собственные категории. См. объявление контента по локалям.
    • Использование =1 вместо one. Форма =1 совпадает только с точным числом 1. В русском языке для 21 требуется категория one, и правило =1 никогда не сработает для таких чисел.
    • Размещение символа # вне ветки plural. Он имеет специальное значение только внутри блоков plural или selectordinal. В остальных местах это обычный символ решетки.
    • Забывание о том, что # уже отформатирован. Если требуется сырое число без региональных разделителей разрядов, интерполируйте аргумент по имени.

    Дополнительные материалы

    Комментарии

    Пока нет комментариев. Будьте первым, кто поделится своими мыслями.

    Похожие сообщения

    Последние сообщения