messageformat-plus
v0.5.1
Published
Messageformat with flexible parser and minimal spec deviation
Readme
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
Quick Start
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"Built-in Formatters
| 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.)
Usage Notes
Compiling & Reuse
compile() returns a pure function. Reuse it for multiple param sets:
const line = mf.compile("Hi {name, select, alice{Alice} other{User}}");
line({ name: "alice" }); // "Hi Alice"
line({ name: "bob" }); // "Hi User"Variable Paths
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).
How This Deviates From MessageFormat v1
This project intentionally does not aim for byte‑for‑byte compatibility with
the original messageformat v1 runtime. Key differences:
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.Formatter argument token flexibility Arguments like
currency:EURmay arrive from the parser as:["currency:EUR"]["currency", ":", "EUR"]["currency", ":", "EUR", " "]Thenumberformatter normalizes these automatically.
Select value coercion
selectcoerces the incoming value toString(value)so numeric option keys work:{count, select, 1{One} other{Many}}.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.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.Error handling Unknown formatter names throw early at runtime. Malformed argument groupings default to a sane fallback rather than fail the whole message.
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.
Custom Formatters
const mf = new MessageFormat("en", {
customFormatters: {
upper: (v) => String(v).toUpperCase(),
},
});
mf.compile("HELLO {x, upper}")({ x: "world" }); // "HELLO WORLD"A formatter signature:
((value: unknown, locale: string | string[], ...args: unknown[]) => string);Testing
Run all tests:
deno test -APerformance
The parser builds a compact JS expression and uses new Function bound with a
lightweight context (d = dispatcher, p = path resolver, l = locale). This
keeps runtime evaluation fast after the initial compile.
Nested Variables in Formatter Arguments
Variables can be nested within formatter arguments by wrapping them in braces:
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"Caveats
- Output can vary across environments due to
Intldifferences (especially currency & time zone strings).
Contributing
Issues & PRs welcome. Keep additions minimal & well‑tested. Preference is for incremental, opt‑in complexity rather than broad speculative feature builds.
License
MIT
