FormatJS

Parses ICU Message strings into an AST via JavaScript.

npm Version size

Overview

This package implements a parser in JavaScript that parses the industry standard ICU Message strings — used for internationalization — into an AST. The produced AST can then be used by a compiler, like intl-messageformat, to produce localized formatted strings for display to users.

The parser detects Unicode property escape support at startup and falls back to generated character ranges when unavailable. No regex polyfill is required for identifier parsing.

Rich-text tags and ICU arguments work on ASP.NET ScriptManager pages where MicrosoftAjax overrides String.prototype.startsWith.

Usage

const ast = parse(`this is {count, plural,
  one{# dog}
  other{# dogs}
}`)

Preserving escapes

Use captureEscapes: true when parsing messages that will be printed or transformed. Literal nodes keep their decoded value and add a rawValue string containing their original ICU spelling:

import {parse} from '@formatjs/icu-messageformat-parser'
import {printAST} from '@formatjs/icu-messageformat-parser/printer.js'

const message = "a''''{name}"
const ast = parse(message, {captureEscapes: true})
printAST(ast) // "a''''{name}"

rawValue survives JSON serialization. Formatting still uses the decoded value. The printer validates rawValue against the current value and context before reusing it. Stale or unsuitable spelling is escaped again. Moving literals into another context can require different quoting; selector ordering and argument formatting remain normalized. Without rawValue, the printer emits equivalent canonical escapes rather than preserving source spelling.

Capture is disabled by default to keep runtime ASTs compact. The Babel plugin, TypeScript transformer, unplugin, and Rust CLI enable it when flattening messages. Rust callers use ParserOptions { capture_escapes: true, ..Default::default() }.

Selector hoisting preserves plural # scope and offsets. When moving an offset-adjusted # would change its meaning, flattening keeps that part of the message in its original plural scope.

Example

<pre>
  {JSON.stringify(
    IcuMessageFormatParser.parse(
      `On {takenDate, date, short} <bold>{name}</bold> took {numPhotos, plural,
          =0 {no photos.}
          =1 {one photo.}
          other {# photos.}
        }`
    ),
    undefined,
    2
  )}
</pre>

Supported DateTime Skeleton

ICU provides a wide array of pattern to customize date time format. However, not all of them are available via ECMA402's Intl API. Therefore, our parser only support the following patterns

SymbolMeaningNotes
GEra designator
yyear
Mmonth in year
Lstand-alone month in year
dday in month
Eday of week
elocal day of weeke..eee is not supported
cstand-alone local day of weekc..ccc is not supported
aAM/PM marker
hHour [1-12]
HHour [0-23]
KHour [0-11]
kHour [1-24]
mMinute
sSecond
zTime Zone

With a locale parse option, j uses the locale’s preferred hour cycle. Explicit hourCycle options and u-hc extensions take precedence. The parser supports both Intl.Locale.getHourCycles() and the legacy hourCycles property.

Structural comparison

isStructurallySame from @formatjs/icu-messageformat-parser/manipulator.js accepts shared date/time arguments such as Resets {resetDate, date, ::EEE} at {resetDate, time, short}. Translations may reorder those formats but must retain both roles.

Benchmarks

complex_msg AST length 10861
normal_msg AST length 1665
simple_msg AST length 364
string_msg AST length 131

complex_msg x 29,940 ops/sec ±1.23% (89 runs sampled)
normal_msg x 253,612 ops/sec ±1.01% (92 runs sampled)
simple_msg x 1,805,642 ops/sec ±0.70% (94 runs sampled)
string_msg x 2,694,133 ops/sec ±1.05% (95 runs sampled)