intl-messageformat and messageformat are both JavaScript libraries that implement the ICU MessageFormat standard for internationalizing user-facing text. They enable developers to handle complex translation scenarios involving variables, plurals, gender selection, and number/date formatting in a locale-aware way. intl-messageformat relies on the native Intl API available in modern JavaScript environments and parses message patterns at runtime, while messageformat compiles ICU message strings into optimized JavaScript functions during build time, offering greater runtime performance and support for custom formatters.
When building multilingual web applications, handling complex translation strings with variables, plurals, and gender agreement is a common challenge. Both intl-messageformat and messageformat aim to solve this using the ICU MessageFormat standard, but they differ significantly in architecture, runtime requirements, and developer experience. Let’s compare them in depth.
intl-messageformat parses ICU messages at runtime using JavaScript. It relies on the built-in Intl APIs (like Intl.NumberFormat and Intl.DateTimeFormat) for formatting values, making it lightweight in bundle size but requiring parsing overhead on each render.
// intl-messageformat: Parse and format at runtime
import IntlMessageFormat from 'intl-messageformat';
const message = new IntlMessageFormat('Hello, {name}! You have {unreadCount, plural, one {# message} other {# messages}}.');
const output = message.format({ name: 'Alice', unreadCount: 5 });
// Output: "Hello, Alice! You have 5 messages."
messageformat compiles ICU messages into pure JavaScript functions ahead of time (typically during build). This eliminates runtime parsing, resulting in faster execution but larger bundle sizes due to generated code.
// messageformat: Precompile messages into functions
import MessageFormat from 'messageformat';
const mf = new MessageFormat('en');
const compileFn = mf.compile('Hello, {name}! You have {unreadCount, plural, one {# message} other {# messages}}.');
const output = compileFn({ name: 'Alice', unreadCount: 5 });
// Output: "Hello, Alice! You have 5 messages."
intl-messageformat depends directly on the browser's or Node.js environment's Intl implementation. This means:
Intl implementations differ.Intl provides.// intl-messageformat uses native Intl under the hood
const msg = new IntlMessageFormat('{date, date, long}', 'en-US');
msg.format({ date: new Date() }); // Uses Intl.DateTimeFormat
messageformat includes its own locale data and formatting logic, independent of the host environment's Intl. This gives you consistent behavior everywhere but increases bundle size.
// messageformat bundles its own locale data
import MessageFormat from 'messageformat';
// Locale data must be explicitly loaded if not using default
MessageFormat.locale.en = require('messageformat-formatters');
const mf = new MessageFormat('en');
intl-messageformat is typically used as a runtime-only library. There’s no official build tooling to extract or precompile messages, so all parsing happens in the browser or server.
messageformat offers strong build-time tooling support. The messageformat-cli package can extract messages from source files and compile them into optimized JavaScript modules, which integrates well with bundlers like Webpack or Rollup.
# Example: Precompile messages with messageformat-cli
npx messageformat --locale=en --output-dir ./i18n ./src/messages.json
This results in importable modules:
// Generated by messageformat-cli
import messages from './i18n/en/messages.js';
const greeting = messages.welcome({ name: 'Bob' });
Both libraries support the core ICU MessageFormat syntax, including:
{name}){count, plural, one {...} other {...}}){gender, select, male {...} female {...} other {...}})However, messageformat supports nested message structures and custom formatters more flexibly:
// messageformat: Custom formatter example
const mf = new MessageFormat('en');
mf.addFormatters({
upcase: (v) => String(v).toUpperCase()
});
const fn = mf.compile('Hello, {name, upcase}!');
fn({ name: 'alice' }); // "Hello, ALICE!"
intl-messageformat does not support custom formatters. All formatting must go through standard Intl types (number, date, time) or be handled externally before passing values to .format().
Because intl-messageformat parses messages on every use (unless you cache the IntlMessageFormat instance), it incurs CPU cost during rendering. However, the library itself is small since it delegates to native Intl.
messageformat shifts that cost to build time. The compiled functions execute instantly, but each message becomes a small JavaScript function in your bundle. For apps with hundreds of messages, this can noticeably increase bundle size.
💡 Best practice with
intl-messageformat: Cache parsed message instances.
// Cache to avoid reparsing
const messageCache = new Map();
function formatMessage(pattern, locale) {
const key = `${locale}:${pattern}`;
if (!messageCache.has(key)) {
messageCache.set(key, new IntlMessageFormat(pattern, locale));
}
return messageCache.get(key);
}
intl-messageformat throws clear errors for malformed ICU syntax at parse time, which helps catch mistakes early during development.
messageformat also validates syntax during compilation, but if you use runtime compilation (not recommended in production), errors may surface later.
Both provide decent error messages, but messageformat’s build-time compilation means syntax errors are caught before deployment.
messageformatintl-messageformatIntl reduce overhead, especially if message count is low.messageformatmessageformat allows registering custom formatting functions within the ICU string.intl-messageformat if you trust Node.js’s Intl support (v13+ has full ICU).messageformat if you need guaranteed consistency across Node versions or want to avoid Intl polyfills.| Feature | intl-messageformat | messageformat |
|---|---|---|
| Parsing | Runtime | Build-time (precompiled functions) |
| Intl Dependency | Yes (uses native Intl) | No (bundles its own locale data) |
| Custom Formatters | ❌ Not supported | ✅ Supported |
| Bundle Impact | Small library, CPU cost per render | Larger bundles, zero runtime parse |
| Build Tooling | Minimal | Rich (CLI, bundler plugins) |
| Error Detection | Runtime (unless cached) | Build-time (if precompiled) |
Choose intl-messageformat if you:
Intl supportChoose messageformat if you:
Both are mature, well-maintained solutions — the right pick depends on whether you prioritize bundle size (intl-messageformat) or runtime performance and flexibility (messageformat).
Choose intl-messageformat if you're building an application that targets modern browsers or Node.js environments with full Intl support, and you prefer a smaller bundle footprint over runtime parsing overhead. It’s ideal when you don’t need custom formatters inside your translation strings and are comfortable caching parsed message instances to avoid repeated parsing costs.
Choose messageformat if you need maximum runtime performance through precompiled message functions, require custom formatting logic (like uppercase or markdown) directly within ICU strings, or want to guarantee consistent internationalization behavior across all JavaScript environments—including older ones—by bundling locale data yourself.
We've migrated the docs to https://formatjs.github.io/docs/intl-messageformat.