icu-minify
icu-minify splits ICU message formatting into two jobs. Its compile export parses an ICU string at build time and turns it into a compact, JSON-safe array representation; its format export evaluates that representation in the browser with native Intl formatters. This avoids shipping the much larger ICU parser to users while retaining arguments, select branches, cardinal and ordinal plurals, numbers, dates, times, and rich-text tags. It is a young ESM-only package from the next-intl monorepo, not a command-line minifier and not a complete translation workflow by itself.
A focused choice when compact, serializable ICU messages are the requirement and you are willing to own the compilation pipeline. Most applications should install a full i18n library or use next-intl's integration rather than call this low-level package directly.
Use it if
- You already have ICU message catalogs and can add a build step that compiles every message before it reaches the browser
- Client-side parse time and i18n runtime weight matter enough to justify maintaining a compiled catalog format
- You need compiled messages to remain plain JSON so they can cross a React Server Component boundary or be cached as data
- You want low-level ICU compilation and formatting primitives instead of a framework, extraction tool, or translation-management workflow
- You need plural offsets: the compiler source explicitly says they are unsupported, throws for them in development, and does not preserve the offset in its compiled representation
- You load messages from a CMS, TMS, or API at request time and cannot compile them before use; the project RFC says the normal catalog build integration cannot precompile remote messages
- You use next-intl's raw-message access for HTML, arrays, or other non-ICU data; the RFC says precompiled messages return the intermediate representation and parsing can reject non-ICU catalog values
- You want a batteries-included i18n library with locale loading, message lookup, extraction, React hooks, or routing; this package exposes only compile and format subpaths
- You need a mature standalone API contract: the package was first published in January 2026, follows next-intl's 4.x version, and its README format example currently omits the required formatter options present in the shipped type declaration
Setup reality
Installation is small in concept, but direct use needs more wiring than the README's short example shows. The package is ESM-only and exports only icu-minify/compile and icu-minify/format, so there is no root import, CommonJS require target, or CLI. The compile side depends on @formatjs/icu-messageformat-parser and belongs in a build script, loader, server process, or other trusted compilation stage. Save its result as JSON and ship only the format subpath to the client if bundle reduction is the goal. The current format type requires a fourth options argument containing factories for Intl.DateTimeFormat, Intl.NumberFormat, and Intl.PluralRules; the package tests create this adapter, while the README example leaves it out. Named number and date styles also require matching entries under options.formats, and dates must be actual Date objects at formatting time. Missing values, wrong value types, absent other branches, malformed ICU syntax, and missing tag callbacks throw useful errors in development. Plural offsets are unsupported. Production builds remove several validation branches, so compile catalogs in CI, exercise every locale there, and do not treat production formatting as a catalog validator. There are no peer dependencies, credentials, native builds, or config files, but you must own the build-time catalog transform and the boundary between source strings and compiled JSON.
Patterns
Compile one ICU message at build timecompile-message
import compile from 'icu-minify/compile';
const compiled = compile('Hello {name}!');
// ['Hello ', ['name'], '!']Keep this import in build or server code. The compile subpath pulls in the ICU parser, which defeats the client-size goal if bundled into the browser.
Create the required native Intl adaptercreate-formatters
import type {FormatOptions} from 'icu-minify/format';
export const formatters: FormatOptions['formatters'] = {
getDateTimeFormat: (...args) => new Intl.DateTimeFormat(...args),
getNumberFormat: (...args) => new Intl.NumberFormat(...args),
getPluralRules: (...args) => new Intl.PluralRules(...args)
};The fourth format argument is required by the 4.13.5 types even though the short README example omits it. Cache these Intl instances in a high-throughput application if profiling shows constructor cost.
Format a compiled argumentformat-argument
import format from 'icu-minify/format';
import {formatters} from './formatters.js';
const message = ['Hello ', ['name'], '!'];
const output = format(message, 'en', {name: 'Mina'}, {formatters});
// 'Hello Mina!'In TypeScript, keep the value returned by compile or type imported catalog data as CompiledMessage instead of relying on inference from a hand-written nested array.
Compile a flat locale catalogcompile-catalog
import compile from 'icu-minify/compile';
import {readFile, writeFile} from 'node:fs/promises';
const source = JSON.parse(await readFile('messages/en.json', 'utf8'));
const compiled = Object.fromEntries(
Object.entries(source).map(([key, message]) => [key, compile(String(message))])
);
await writeFile('dist/messages/en.json', JSON.stringify(compiled));This handles a flat string catalog. Add a recursive walk for nested namespaces, and fail the build when a value is not an ICU message string.
Format cardinal plurals and exact matchesformat-plural
const message = compile(
'{count, plural, =0 {No files} one {# file} other {# files}}'
);
format(message, 'en', {count: 0}, {formatters}); // 'No files'
format(message, 'en', {count: 2}, {formatters}); // '2 files'Always include an other branch. The compiler rejects a plural without one, and plural offsets are not supported.
Format locale-aware ordinalsformat-ordinal
const place = compile(
'{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
);
format(place, 'en', {n: 21}, {formatters}); // '21st'Ordinal categories vary by locale. Do not reuse English suffix branches for other languages without translator review.
Choose a branch with selectformat-select
const label = compile(
'{role, select, admin {Administrator} editor {Editor} other {Member}}'
);
format(label, 'en', {role: 'editor'}, {formatters}); // 'Editor'
format(label, 'en', {role: 'unknown'}, {formatters}); // 'Member'An other branch is required. Current code builds branch maps without Object.prototype, so values such as constructor safely fall back instead of selecting inherited properties.
Use a named currency formatformat-number
const price = compile('Total: {amount, number, currency}');
const options = {
formatters,
formats: {
number: {currency: {style: 'currency', currency: 'EUR'}}
}
};
format(price, 'de-DE', {amount: 1234.5}, options);A named ICU style such as currency must exist in options.formats.number. Development builds throw when the name is missing.
Format a Date with a fixed time zoneformat-date-time
const message = compile('Published {at, date, short} at {at, time, clock}');
const options = {
formatters,
timeZone: 'UTC',
formats: {
dateTime: {
short: {year: 'numeric', month: 'short', day: 'numeric'},
clock: {hour: '2-digit', minute: '2-digit', hour12: false}
}
}
};
format(message, 'en-GB', {at: new Date('2026-08-08T14:30:00Z')}, options);Date and time arguments must be Date instances, not ISO strings. A timeZone inside a named format takes precedence over the top-level timeZone.
Turn ICU tags into rich valuesformat-rich-text
const message = compile('Read <link>{title}</link>');
const output = format(
message,
'en',
{
title: 'the guide',
link: (chunks) => ({type: 'a', href: '/guide', children: chunks})
},
{formatters}
);Tag handlers can return non-strings, so the result may be one value or an array of strings and rich values. Handle that union in your renderer.
Compile a remote catalog before formattingcompile-remote-messages
const response = await fetch('https://cdn.example.com/messages/fr.json');
if (!response.ok) throw new Error(`catalog HTTP ${response.status}`);
const source = await response.json();
const messages = Object.fromEntries(
Object.entries(source).map(([key, value]) => [key, compile(String(value))])
);Remote catalogs cannot use the normal build-time integration. This puts the parser and its CPU cost back on the server or client doing the fetch, so cache the compiled result.
Fail CI on malformed ICU messagesvalidate-catalog
for (const [locale, catalog] of Object.entries(catalogs)) {
for (const [key, message] of Object.entries(catalog)) {
try {
compile(String(message));
} catch (error) {
throw new Error(`${locale}.${key}: ${error.message}`, {cause: error});
}
}
}Compile every locale under a development or test environment. Several runtime validation branches are conditioned on NODE_ENV and are removed or bypassed in production.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| intl-messageformat | npm | You prefer a mature direct ICU formatter and accept parsing messages at runtime |
| @messageformat/core | npm | You want messages compiled to JavaScript functions and do not need the result to remain plain JSON |
| @lingui/core | npm | You want the formatter as part of a larger extraction, catalog, and framework integration workflow |