@messageformat/parser

An AST parser for ICU MessageFormat strings, turning plural, select, and argument syntax into a typed token tree.

Library
npm
v5.1.1
1,769stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
56/100Fair
Development Activity24
Maintenance32
Community68
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
74/100Good
Architecture78
Code Quality82
Innovation55
Learning Curve80

@messageformat/parser is the parsing layer behind the messageformat monorepo’s ICU MessageFormat 1 (MF1) tooling. It takes a raw ICU MessageFormat string — the kind of syntax used for pluralization and gender-aware translations across the Unicode CLDR locale set — and turns it into an array of typed tokens: content, argument, function, plural/select/selectordinal, and octothorpe nodes, each carrying position context (offset, line, column, raw text) for precise error reporting.

Internally the parser is a hand-written recursive-descent parser built on top of a moo-based lexer with three lexer states (body, arg, select) that push and pop as nested plural/select blocks are entered and exited. This replaced an earlier PEG.js-based implementation, trading a grammar file for direct control over error messages and edge cases like apostrophe-escaped literals and octothorpe (#) handling inside nested selects.

It underpins @messageformat/core (the string-to-JS-function compiler), the CLI, the Rollup/Webpack loaders, and the React bindings, but is also usable standalone by any project that needs to validate or transform ICU MessageFormat source without pulling in a full formatting runtime — for example, linting translation files or building custom message-format tooling.

The package predates its current name: it was originally published as messageformat-parser before the monorepo’s 2020 rename to the @messageformat npm scope, and has been stable at major version 5 since 2021, with the whole monorepo maintained under the OpenJS Foundation.

What You Get

  • A single parse(src, options) function returning a fully typed array of Content, PlainArg, FunctionArg, Select, and Octothorpe tokens
  • Locale-aware validation of plural/selectordinal case keys against configurable cardinal/ordinal CLDR category arrays
  • A strict mode that enforces the ICU spec more literally (restricted argType set, spec-accurate argStyle quoting rules)
  • Per-token ctx position data (offset, line, col, raw text, line breaks) for building precise syntax-error messages and source maps
  • A ParseError class with formatted, pointer-style error output showing exactly where parsing failed
  • Dual CJS/ESM builds (lib/ and esm/) with generated .d.ts declarations via api-extractor

Common Use Cases

  • Parsing translator-authored ICU MessageFormat strings before compiling them into runtime formatting functions (as @messageformat/core does)
  • Building custom lint or CI checks that validate translation files use only the plural/select keys valid for a given locale
  • Writing tooling that converts ICU MessageFormat into another localization format (e.g. Fluent or MF2), using the AST as an intermediate representation
  • Powering editor/IDE extensions that need to syntax-highlight or validate ICU MessageFormat strings interactively
  • Extracting the set of arguments (arg names) referenced by a message for automated translation-key extraction pipelines

Under The Hood

Architecture The package is a two-file implementation: lexer.ts defines a moo state machine with three states — body (plain content, #, {argument} starts), arg (immediately after {name, deciding between a plain argument, a func-simple/func-args formatter, or a plural|select|selectordinal block), and select (parsing offset:, key{, and the closing }) — using push/pop/next transitions to track nesting depth implicitly through the lexer’s own state stack. parser.ts wraps this lexer in a Parser class whose parseBody/parseArgToken/parseSelect methods recursively descend through the token stream, accumulating a typed Token[] array and merging adjacent literal text into single Content nodes. Nesting depth and the inPlural flag (which decides whether # is a special octothorpe token) are threaded explicitly through recursive calls rather than a separate context stack, keeping state minimal and traceable through parser.ts’s ~500 lines.

Tech Stack Written in TypeScript with moo (a fast, regex-table-based lexer) as its only runtime dependency. The build pipeline compiles twice via tsc — once to CommonJS (lib/) and once to ESM (esm/, using moduleResolution: bundler) — with @microsoft/api-extractor rolling up .d.ts declarations and generating the public API surface used for the project’s published API docs. No web framework or runtime beyond Node/browser JS is involved; this is a pure parsing library with zero DOM or filesystem dependencies at runtime.

Code Quality Tests live in parser.test.ts (over 650 lines) and exercise the full grammar: plain arguments, nested selects, plural offsets, strict-mode restrictions, apostrophe-escaping edge cases, and malformed-input error paths, run through the monorepo’s shared Vitest configuration. Source is fully typed with exported public interfaces (Content, PlainArg, FunctionArg, Select, SelectCase, Octothorpe, ParseOptions) annotated with TSDoc @public/@internal tags consumed by api-extractor, and the monorepo enforces ESLint + Prettier formatting across all packages via a shared root config. Error handling is explicit throughout — every unexpected lexer state throws a ParseError with formatted source-position context rather than failing silently or returning partial results.

API Design The public surface is intentionally minimal: one parse() function and one options object, which keeps the barrier to adoption low for a single-purpose parsing library. Naming is consistent with the wider messageformat ecosystem (cardinal/ordinal/strict mirror concepts used in @messageformat/core), and the returned AST shape is documented directly in the README with runnable REPL-style examples rather than requiring readers to infer it from source. The trade-off is that consumers needing anything beyond raw AST production (formatting, compilation) must reach for a sibling package, which the monorepo’s structure makes straightforward to discover.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search