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

    Для чего он предназначался

    Главной целью было серверное и настольное программное обеспечение: корпоративные приложения на 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 ту же самую структуру можно представить обычными данными: объектом, ключи которого соответствуют категориям множественного числа, проверяемым компилятором TypeScript и редактором кода без промежуточных парсеров.

    Полнота возможностей формирует накладные расходы

    Спецификация ICU включает обширный перечень механизмов:

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

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

    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({
          ru: 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. Единственное, что требуется от современного формата интернационализации, это логика ветвления условий, и ее гораздо эффективнее представлять в виде структурированных типизированных данных.

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

    Комментарии

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

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

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