Smart list formatter built on native Intl.
Turn arrays into "banana, apple, and cherry", "a, b или c", or "a、b、c" — sorted right, joined right.
One function. Smart defaults. Any locale. ~0.5kb gzip. Zero dependencies.
Intl is powerful. anymany makes it usable.
Built for tags, participants, file lists, permissions, and search filters —
anywhere array.join(", ") should read like a person wrote it. No locale
files. No plugins. No config.
import { anymany } from "anymany";
anymany(["banana", "apple", "cherry"]);
// "banana, apple, and cherry"
anymany(["S", "M", "L"], { type: "disjunction" });
// "S, M, or L"
anymany(["cherry", "apple", "Banana"], { sort: true });
// "apple, Banana, and cherry"
anymany(["x", "y", "z", "a", "b", "c", "d"], { max: 3 });
// "x, y, z, and +4"npm install anymanyanymany(items);
anymany(items, options);items is an array of strings. Non-string items are coerced via String().
An empty array returns ""; a single item is returned as-is.
anymany(["read", "write"]); // "read and write"
anymany(["read"]); // "read"
anymany([]); // ""Default is "conjunction" (and) in "long" style. Both map straight to
Intl.ListFormat.
anymany(["a", "b", "c"]); // "a, b, and c"
anymany(["a", "b", "c"], { style: "short" }); // "a, b, & c"
anymany(["a", "b", "c"], { style: "narrow" }); // "a, b, c"type: "disjunction" joins with "or"; type: "unit" is a plain list with no
joiner word.
anymany(["S", "M", "L"], { type: "disjunction" }); // "S, M, or L"
anymany(["4 kg", "2 m"], { type: "unit" }); // "4 kg, 2 m"sort runs the items through Intl.Collator before joining — real
language-aware collation, not code-point order. The input array is never
mutated.
anymany(["cherry", "apple", "Banana"], { sort: true });
// "apple, Banana, and cherry" ← plain .sort() would put "Banana" firstsort: "numeric" compares embedded numbers by value, so 2 comes before
10. Pass any Intl.CollatorOptions object for full control.
anymany(["file10", "file2"], { sort: "numeric" });
// "file2 and file10"
anymany(["a", "A"], { sort: { caseFirst: "upper" } });
// "A and a"max caps the visible items (after sorting); the rest collapse into a
trailing "+N" counter. Digits come from Intl.NumberFormat, so they
localize — no words, locale-safe.
anymany(["x", "y", "z", "a", "b", "c", "d"], { max: 3 });
// "x, y, z, and +4"
anymany(["x", "y", "z", "a", "b"], { max: 3, overflow: (n) => `${n} more` });
// "x, y, z, and 2 more"The overflow item is just another list element, so the default type places
an "and" before it: "x, y, z, and +4". That is intentional — no hidden
joiner magic. If you prefer a plain comma list, combine max with
type: "unit".
anymanyParts() accepts the same arguments as anymany() and returns the
output as { type, value } parts — style the items apart from the
separators, or rebuild the string your own way.
import { anymanyParts } from "anymany";
anymanyParts(["a", "b"]);
// [
// { type: "element", value: "a" },
// { type: "literal", value: " and " },
// { type: "element", value: "b" },
// ]
// React: bold the items
anymanyParts(tags).map((p, i) =>
p.type === "element" ? <b key={i}>{p.value}</b> : p.value,
);Everything above works the same in any locale — joiner words, collation, and
overflow digits all come from Intl. Pass any valid BCP 47 tag, including
regional variants like en-GB, zh-TW, pt-BR; fallback arrays also work.
anymany(["a", "b", "c"], { locale: "ru" }); // "a, b и c"
anymany(["a", "b", "c"], { locale: "de" }); // "a, b und c"
anymany(["a", "b", "c"], { locale: "ja" }); // "a、b、c"
anymany(["a", "b", "c"], { type: "disjunction", locale: "ru" }); // "a, b или c"
anymany(["Öl", "Zebra", "Apfel"], { sort: true, locale: "de" });
// "Apfel, Öl und Zebra" ← Ö sorts after A, not after Z
anymany(["файл10", "файл2"], { sort: "numeric", locale: "ru" });
// "файл2 и файл10"
anymany(["a", "b", "c", "d", "e", "f", "g"], { max: 3, locale: "ar-EG" });
// "a وb وc و+٤" ← localized overflow digits
anymany(["a", "b"], { locale: ["sr-Latn-RS", "en"] });When omitted, native Intl uses the runtime locale.
| Option | Type | Default |
|---|---|---|
locale |
string | string[] |
runtime locale |
type |
"conjunction" | "disjunction" | "unit" |
"conjunction" |
style |
"long" | "short" | "narrow" |
"long" |
sort |
boolean | "numeric" | Intl.CollatorOptions |
no sorting |
max |
number (positive integer) |
no limit |
overflow |
(hidden: number) => string |
`+${N}` |
max throws a RangeError when it is zero, negative, or fractional.
anymany is part of a family — same author, same DNA: one function, smart defaults, any Intl locale, ~1kb gzip, zero dependencies, SSR-safe, dual ESM+CJS.
- anywhen — humanizes dates:
"yesterday, 2:35 PM","через 3 часа" - anyamount — humanizes amounts and units
- anymany — humanizes arrays of strings (this package)
Why no pluralization ("1 file" / "2 files")?
By design. Intl ships no word data, and anymany ships zero language
dictionaries — that is what keeps it lightweight and correct in every locale. The
overflow counter is "+N" (localized digits) instead of "and N more" for
the same reason. If a feature would require per-language words, it is out of
scope.
anymany follows semver. Since 1.0.0 the public API —
anymany, anymanyParts, AnymanyOptions, and the exported types — only
changes shape in a major release. New options arrive in minors; exact
formatted strings come from Intl and may vary between ICU versions, so
never assert on them across environments.
Node.js 18+ · Chrome 72+ · Firefox 78+ · Safari 14.1+ · Edge Runtime · Cloudflare Workers · Deno
CI runs the full suite on Node 20, 22, and 24. Older runtimes down to Node 18 work but are not tested on every release.
