Messageformat with flexible parser and minimal spec deviation
npm install messageformat-plus



A lightweight, modern take on ICU MessageFormat-inspired message compilation
with a focus on:
- Simplicity of authoring
- Flexible path (dot & bracket) access
- Extensible, pluggable formatters
- Small, dependency‑lean parsing core
``ts
import { MessageFormat } from "messageformat-plus"; // adjust path for your environment
const mf = new MessageFormat("en", {
customFormatters: {
upcase: (v: unknown) => String(v).toUpperCase(),
},
});
const greet = mf.compile("Hello {user.name, upcase}! Today is {today, date}.");
console.log(greet({
user: { name: "Ada" },
today: Date.now(),
}));
// → e.g. "Hello ADA! Today is Feb 21, 2016"
`
| Name | Syntax Examples |
| ---------- | ------------------------------------------------------------------------------------------------------------------- |
| date | {d, date}, {d, date, short}, {d, date, full} |time
| | {t, time}, {t, time, short}, {t, time, long} |duration
| | {s, duration} (seconds -> H:MM:SS(.fff)) |number
| | {n, number}, {n, number, integer}, {p, number, percent}, {v, number, currency}, {v, number, currency:EUR} |select
| | {g, select, male{He} female{She} other{They}}, {name, select, other{Hello {name}!}} |
(You can register your own via customFormatters.)
compile() returns a pure function. Reuse it for multiple param sets:
`ts`
const line = mf.compile("Hi {name, select, alice{Alice} other{User}}");
line({ name: "alice" }); // "Hi Alice"
line({ name: "bob" }); // "Hi User"
Supports mixed dot & bracket style:
- {user.name}{user.profile[0].email}
- {data.items[order.index].price}
- (nested bracketed expression tokens are
parsed if they form a valid path sequence)
Missing segments yield undefined (no crash).
This project intentionally does not aim for byte‑for‑byte compatibility with
the original messageformat v1 runtime. Key differences:
1. More permissive path resolution Dot & bracket nesting (a.b[c[d].e].f) is
parsed into a uniform path array and resolved dynamically. Some syntactic
forms that classic MF might reject are accepted here.
2. Formatter argument token flexibility Arguments like currency:EUR may arrive["currency:EUR"]
from the parser as:
- ["currency", ":", "EUR"]
- ["currency", ":", "EUR", " "]
- The number formatter normalizes these
automatically.
3. Select value coercion select coerces the incoming value to String(value){count, select, 1{One} other{Many}}
so numeric option keys work: .
4. Partial feature surface (lean core) Not (yet) implementing: plural,selectordinal
, rich nested message interpolation inside select arms, or
skeleton-based date/number formatting. Only a focused subset is provided.
5. Enhanced argument parsing model Transformer (formatter) arguments are passed
as a structured array. Nested {} inside formatter args containing variables
are parsed recursively, allowing for dynamic content within select options
and other formatter arguments.
6. Error handling Unknown formatter names throw early at runtime. Malformed
argument groupings default to a sane fallback rather than fail the whole
message.
7. Escaping semantics Backslash escaping is pragmatic: \{ and \} produce
literal braces. Behavior for some edge escape sequences may differ from
canonical ICU rules.
If you need strict ICU / MessageFormat v1 behavior, use the original library.
This project optimizes for ergonomic flexibility and a smaller mental model.
`ts
const mf = new MessageFormat("en", {
customFormatters: {
upper: (v) => String(v).toUpperCase(),
},
});
mf.compile("HELLO {x, upper}")({ x: "world" }); // "HELLO WORLD"
`
A formatter signature:
`ts`
((value: unknown, locale: string | string[], ...args: unknown[]) => string);
Run all tests:
``
deno test -A
The parser builds a compact JS expression and uses new Function bound with ad
lightweight context ( = dispatcher, p = path resolver, l = locale). This
keeps runtime evaluation fast after the initial compile.
Variables can be nested within formatter arguments by wrapping them in braces:
`ts
const mf = new MessageFormat("en");
// Simple select with nested variable
const greet = mf.compile("{name, select, other{Hello {name}!}}");
greet({ name: "Alice" }); // "Hello Alice!"
// More complex example with multiple nested variables
const status = mf.compile({user.role, select,
admin{Welcome {user.name}, you have {permissions.count} permissions}
user{Hi {user.name}!}
other{Hello guest}
});
status({
user: { name: "Bob", role: "admin" },
permissions: { count: 15 },
}); // "Welcome Bob, you have 15 permissions"
`
- Output can vary across environments due to Intl` differences (especially
currency & time zone strings).
Issues & PRs welcome. Keep additions minimal & well‑tested. Preference is for
incremental, opt‑in complexity rather than broad speculative feature builds.
MIT