anymany
v2.0.1
Published
Smart list formatter — sort and join arrays of strings in any Intl locale, one function.
Maintainers
Readme
One export. 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"install
npm install anymanyusage
anymany(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([]); // ""anymany.parts() takes the same arguments and returns { type, value } parts
instead of a string — style the items apart from the separators.
anymany.parts(["a", "b"]);
// [{ type: "element", value: "a" }, { type: "literal", value: " and " }, { type: "element", value: "b" }]
anymany.parts(tags).map((p, i) =>
p.type === "element" ? <b key={i}>{p.value}</b> : p.value,
);recipes
Copy, paste, move on.
// Tag list
anymany(post.tags, { locale: "en" });
// "design, typography, and color"
// Sizes / options — "or" instead of "and"
anymany(product.sizes, { type: "disjunction" });
// "S, M, or L"
// Plain comma list, no joiner word
anymany(["4 kg", "2 m"], { type: "unit" });
// "4 kg, 2 m"
// Alphabetical the way the language actually orders letters
anymany(names, { sort: true, locale: "de" });
// "Apfel, Öl und Zebra" ← Ö sorts after A, not after Z
// Filenames with numbers, ordered by value
anymany(files, { sort: "numeric" });
// "file2 and file10"
// Cap a long list
anymany(participants, { max: 3 });
// "Ann, Bob, Cy, and +4"
// …with your own overflow wording
anymany(participants, { max: 3, overflow: (n) => `${n} more` });
// "Ann, Bob, Cy, and 2 more"anymany is pure — same input, same output, no clocks, no DOM. Pass an explicit
locale and server and client render identically.
sorting
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() puts "Banana" first
anymany(["file10", "file2"], { sort: "numeric" });
// "file2 and file10" ← numbers compared by value
anymany(["a", "A"], { sort: { caseFirst: "upper" } });
// "A and a" ← any Intl.CollatorOptions→ Sorting, max and overflow in detail
options
| 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.
→ What each option does, with examples
locales
Joiner words, collation and overflow digits all come from Intl. Pass any valid
BCP 47 tag, including regional variants and fallback arrays. When omitted,
native Intl uses the runtime locale.
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(["a", "b", "c", "d", "e", "f", "g"], { max: 3, locale: "ar-EG" });
// "a وb وc و+٤" ← localized overflow digitswhy no pluralization
By design. Intl ships no word data, and anymany ships zero language
dictionaries — that is what keeps it small and correct in every locale. The
overflow counter is "+N" with localized digits rather than "and N more" for
the same reason; pass your own wording through overflow when you need words.
Counting things in a sentence is anyplural's job, not this one's.
stability
anymany follows semver. The public API is a single
export — anymany, with anymany.parts on it — plus AnymanyOptions and the
exported types. It 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.
migrating from 1.x
2.0 removed the separate anymanyParts export. It is the same function, now
reached through the one name the package exports:
- import { anymany, anymanyParts } from "anymany";
+ import { anymany } from "anymany";
- anymanyParts(items);
+ anymany.parts(items);Arguments, return value and throwing behaviour are unchanged. Every any*
package follows this shape from 2.0 on: the bare call does the job, everything
else hangs off the same name.
compatibility
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.
the any family
anymany is part of any family — tiny, zero-dependency wrappers over native
Intl, one API per package.
| | | |
| --- | --- | --- |
| anywhen | dates & relative time | Intl.DateTimeFormat |
| anyamount | numbers, currency, units | Intl.NumberFormat |
| anymany | lists | Intl.ListFormat |
| anyaround | names & flags | Intl.DisplayNames |
| anylong | durations | Intl.DurationFormat |
| anyplural | plurals | Intl.PluralRules |
| anyword | words & graphemes | Intl.Segmenter |
Want all of them? anyfamily is one
install for the lot, and anyfamily-react
wraps each as a hook with a shared locale provider.
npm install anyfamilyMIT © kirilinsky
