intl-messageformat vs messageformat
ICU MessageFormat Libraries for JavaScript Internationalization
intl-messageformatmessageformatSimilar Packages:

ICU MessageFormat Libraries for JavaScript Internationalization

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.

Npm Package Weekly Downloads Trend

3 Years

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
intl-messageformat0-112 kB-11 days agoBSD-3-Clause
messageformat01,762172 kB166 months agoApache-2.0

Internationalization Message Formatting: intl-messageformat vs messageformat

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.

🧩 Core Architecture: Runtime Parser vs Precompiled Functions

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."

🌍 Locale Support and Intl Dependency

intl-messageformat depends directly on the browser's or Node.js environment's Intl implementation. This means:

  • No extra polyfills needed if targeting modern environments.
  • Behavior may vary slightly across browsers if Intl implementations differ.
  • Cannot customize number/date formatting logic beyond what 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');

⚙️ Build-Time Integration and Tooling

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' });

🔁 Message Syntax and ICU Compliance

Both libraries support the core ICU MessageFormat syntax, including:

  • Simple placeholders ({name})
  • Pluralization ({count, plural, one {...} other {...}})
  • Select (gender/enum-based choices: {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().

📦 Bundle Size and Performance Trade-offs

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);
}

🛠️ Error Handling and Debugging

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.

🌐 Real-World Usage Scenarios

Scenario 1: Large-Scale Application with Hundreds of Messages

  • Best choice: messageformat
  • Why? Precompilation ensures consistent performance regardless of message count, and build-time validation catches errors early.

Scenario 2: Lightweight App Targeting Modern Browsers

  • Best choice: intl-messageformat
  • Why? Smaller bundle size and reliance on native Intl reduce overhead, especially if message count is low.

Scenario 3: Need Custom Formatters (e.g., markdown, truncation)

  • Best choice: messageformat
  • Why? Only messageformat allows registering custom formatting functions within the ICU string.

Scenario 4: Server-Side Rendering with Node.js

  • Both work, but:
    • Use intl-messageformat if you trust Node.js’s Intl support (v13+ has full ICU).
    • Use messageformat if you need guaranteed consistency across Node versions or want to avoid Intl polyfills.

📌 Summary Table

Featureintl-messageformatmessageformat
ParsingRuntimeBuild-time (precompiled functions)
Intl DependencyYes (uses native Intl)No (bundles its own locale data)
Custom Formatters❌ Not supported✅ Supported
Bundle ImpactSmall library, CPU cost per renderLarger bundles, zero runtime parse
Build ToolingMinimalRich (CLI, bundler plugins)
Error DetectionRuntime (unless cached)Build-time (if precompiled)

💡 Final Recommendation

Choose intl-messageformat if you:

  • Target modern environments with solid Intl support
  • Prefer smaller bundles over runtime CPU cost
  • Don’t need custom formatters inside messages

Choose messageformat if you:

  • Want maximum runtime performance via precompilation
  • Need custom formatting logic within ICU strings
  • Prefer catching message syntax errors at build time
  • Require consistent behavior across all JavaScript environments

Both are mature, well-maintained solutions — the right pick depends on whether you prioritize bundle size (intl-messageformat) or runtime performance and flexibility (messageformat).

How to Choose: intl-messageformat vs messageformat

  • intl-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.

  • messageformat:

    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.

README for intl-messageformat

Intl MessageFormat

We've migrated the docs to https://formatjs.github.io/docs/intl-messageformat.