icu-minify review
icu-minify 4.13.7 compiles ICU MessageFormat strings into compact JSON arrays, then formats those arrays with native Intl objects at runtime. Its two public subpaths split the expensive parser from the small formatter: use icu-minify/compile during a build and ship icu-minify/format with the catalog. Arguments, select branches, cardinal and ordinal plurals, number and date styles, time values, and rich-text tags are supported. Our root-entry checks failed for both require() and ESM import because version 4.13.7 exports subpaths only; a bare icu-minify import is not part of its API.
icu-minify 4.13.7 installed in 1.1 seconds and left 3 packages, but bare require() and ESM import both failed in our sandbox because only subpath exports exist. Choose it for build-time ICU compilation into JSON; choose a full i18n package when you also need catalog loading, framework hooks, or remote-message handling.
We installed it
| Install | ✓ · 1.1s | 3 packages on disk · 1 MB |
| Import | ✗ | ESM import fails · require() fails · ESM package with exports map |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does icu-minify install cleanly?
Yes. In a fresh container with an empty cache, npm install icu-minify finished in 1 seconds, leaving 3 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
Can icu-minify run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does icu-minify work with both ESM and CommonJS?
Neither plain import nor require succeeded in our sandbox, so it needs a bundler or extra setup.
Does icu-minify include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
icu-minify or intl-messageformat: which should you use?
intl-messageformat: Use it when runtime parsing and a mature direct formatter matter more than a precompiled JSON catalog. icu-minify 4.13.7 installed in 1.1 seconds and left 3 packages, but bare require() and ESM import both failed in our sandbox because only subpath exports exist.
When should you not use icu-minify?
Messages arrive from a CMS at request time and no server-side compilation cache is available. Shipping the compile subpath to browsers defeats the package's design.
Use it if
- Your ICU catalogs can be compiled before deployment, allowing the browser to receive JSON arrays instead of a message parser.
- Compiled messages must cross a server-to-client data boundary as serializable values rather than JavaScript functions.
- You already own locale loading and message lookup and need only the compiler and formatter layers.
- Client runtime weight matters enough to add a catalog compilation step and an Intl formatter cache.
- Messages arrive from a CMS at request time and no server-side compilation cache is available. Shipping the compile subpath to browsers defeats the package's design.
- Your messages use ICU plural offsets. The compiler source rejects offsets instead of encoding them.
- You need routing, React hooks, message extraction, locale negotiation, or translation management. The package exposes only compile and format subpaths.
- A package must work through its root import. In our Node 22.23.2 sandbox, both require('icu-minify') and import('icu-minify') failed.
- You want a stable standalone release narrative. Version 4.13.7 follows next-intl's monorepo version, and its changelog item is an extractor-plugin SWC pin rather than a change to these two functions.
Setup reality
We installed icu-minify 4.13.7 in a fresh Node 22 Bookworm sandbox in 1.1 seconds. It left 3 packages and 1 MB on disk, with 0 known vulnerabilities from npm audit. The package has 1 direct dependency, 0 peer dependencies, 88 KB unpacked, and an MIT license. Our scanner found no TypeScript types. Node 22.23.2 failed both root require() and root ESM import because the exports map contains only ./compile and ./format.
Import icu-minify/compile in a build script and icu-minify/format in runtime code. There are no credentials, native builds, or required config files. You must create the catalog transform yourself when you use this package directly. Store the compiler output as JSON, version it with the source catalog if reproducibility matters, and fail CI on malformed strings.
The formatter requires factories for Intl.DateTimeFormat, Intl.NumberFormat, and Intl.PluralRules. Cache those objects by locale and options in a busy service; constructing them for every message wastes the size win. Named number or date styles need matching entries in the options object, and formatted date values must be Date instances.
Our esbuild browser build failed, so we have no measured browser size for version 4.13.7. Development code throws for missing arguments, wrong plural values, absent fallback branches, and tag handlers. Some checks are conditional on NODE_ENV, which makes preproduction catalog tests important. Plural offsets remain unsupported, and remote catalogs need a trusted compilation step before formatting.
Patterns
Compile one message during the build compile-message
import compile from 'icu-minify/compile';
const message = compile('Hello {name}!');
console.log(JSON.stringify(message));Version 4.13.7 exposes compile only through the /compile subpath; a bare package import failed in our Node 22 test.
Provide cached Intl factories create-formatters
const cache = new Map();
const cached = (Ctor, args) => {
const key = `${Ctor.name}:${JSON.stringify(args)}`;
if (!cache.has(key)) cache.set(key, new Ctor(...args));
return cache.get(key);
};
const formatters = {
getDateTimeFormat: (...args) => cached(Intl.DateTimeFormat, args),
getNumberFormat: (...args) => cached(Intl.NumberFormat, args),
getPluralRules: (...args) => cached(Intl.PluralRules, args),
};The current formatter expects all 3 factory functions; caching avoids rebuilding Intl instances for each message.
Format a compiled argument format-message
import format from 'icu-minify/format';
const output = format(
['Hello ', ['name'], '!'],
'en',
{name: 'Mina'},
{formatters}
);The /format subpath consumes compiled data, a locale, values, and formatter options in version 4.13.7.
Transform a flat JSON catalog compile-catalog
const compiled = Object.fromEntries(
Object.entries(source).map(([key, value]) => {
if (typeof value !== 'string') throw new TypeError(`${key} is not a string`);
return [key, compile(value)];
})
);
await writeFile(outputPath, JSON.stringify(compiled));Compilation can throw on invalid ICU syntax, so fail the build instead of silently copying the source value.
Handle exact and cardinal plural branches format-plural
const files = compile(
'{count, plural, =0 {No files} one {# file} other {# files}}'
);
format(files, 'en', {count: 2}, {formatters});Every plural needs an other branch, and ICU plural offsets are unsupported by this compiler.
Select locale-aware ordinal rules format-ordinal
const rank = compile(
'{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
);
format(rank, 'en', {n: 21}, {formatters});Ordinal categories depend on the locale; these 4 suffix branches describe English only.
Choose a select fallback format-select
const role = compile(
'{role, select, admin {Administrator} editor {Editor} other {Member}}'
);
format(role, 'en', {role: 'guest'}, {formatters});Version 4.13.7 falls back to other for an unmatched string, and development mode reports a missing fallback.
Supply a named number style format-currency
const total = compile('Total: {amount, number, currency}');
const options = {
formatters,
formats: {number: {currency: {style: 'currency', currency: 'EUR'}}}
};
format(total, 'de-DE', {amount: 1234.5}, options);The style name currency must appear under formats.number or development formatting throws.
Format a Date in a fixed zone format-date
const published = compile('Published {at, date, short}');
const options = {
formatters,
timeZone: 'UTC',
formats: {dateTime: {short: {year: 'numeric', month: 'short', day: 'numeric'}}}
};
format(published, 'en-GB', {at: new Date('2026-08-26T12:00:00Z')}, options);Pass a Date instance for date and time nodes; an ISO string is the wrong runtime value type.
Map an ICU tag to an application value format-rich-text
const linked = compile('Read <link>{title}</link>');
const output = format(linked, 'en', {
title: 'the guide',
link: (chunks) => ({type: 'a', href: '/guide', children: chunks})
}, {formatters});A tag callback may return an object, so the formatter result can be a mixed array instead of one string.
Cache a compiled remote catalog compile-remote-catalog
const response = await fetch(catalogUrl);
if (!response.ok) throw new Error(`catalog HTTP ${response.status}`);
const source = await response.json();
const compiled = compileCatalog(source);
await cache.set(cacheKey, compiled);Remote source strings cannot benefit from build-time compilation unless your server fetches, compiles, and caches them before clients request the catalog.
Reject one bad locale in CI validate-all-locales
for (const [locale, catalog] of Object.entries(catalogs)) {
for (const [key, message] of Object.entries(catalog)) {
try { compile(message); }
catch (cause) { throw new Error(`${locale}.${key} is invalid`, {cause}); }
}
}Run this under a nonproduction NODE_ENV because several formatter checks are omitted from the production build.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| intl-messageformat | npm | Use it when runtime parsing and a mature direct formatter matter more than a precompiled JSON catalog. |
| @messageformat/core | npm | Use it when compiling messages into JavaScript functions fits your build and deployment model. |
| @lingui/core | npm | Use it when catalog extraction and application integrations should come from the same i18n toolkit. |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

