Skip to content
JavaScript Basics: Choose Count Labels with Intl.PluralRules

JavaScript Basics: Choose Count Labels with Intl.PluralRules

October 20, 2026

A count label needs a language-specific choice: one file, two files, or an ordinal such as 21st. Intl.PluralRules selects a grammatical category; your application supplies the words or suffixes for that category.

These examples were tested on Linux with Node.js v24.19.0. They use built-in APIs only, require no network access, and include assertions that check the results. See MDN’s Intl.PluralRules reference. These examples pass explicit locales and use the ICU data in the tested Node build; other runtime builds may support fewer locales.

Prepare a scratch directory

You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8; .mjs selects ES modules. Each example runs independently.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Select an English file label

Save this as label-file-counts.mjs:

import assert from 'node:assert/strict';

const rules = new Intl.PluralRules('en', { type: 'cardinal' });
const labels = { one: 'file', other: 'files' };
function fileCount(count) {
  if (!Number.isSafeInteger(count) || count < 0) {
    throw new RangeError('Expected a nonnegative integer count');
  }
  return `${count} ${labels[rules.select(count)]}`;
}
const results = [0, 1, 2].map(fileCount);
assert.deepEqual(results, ['0 files', '1 file', '2 files']);
assert.throws(() => fileCount(-1), RangeError);
assert.throws(() => fileCount(1.5), RangeError);
assert.throws(() => fileCount('1'), RangeError);
console.log(results.join('\n'));

Run it:

node label-file-counts.mjs

Output:

0 files
1 file
2 files

Cardinal rules describe quantities. In this English example, zero selects other, not a zero category. Validation belongs to the application: this helper allows whole, nonnegative file counts and rejects numeric strings. The API does not translate the noun or decide whether a negative count makes sense.

2. Build English ordinal labels

Save this as label-ordinal-positions.mjs:

import assert from 'node:assert/strict';

const rules = new Intl.PluralRules('en', { type: 'ordinal' });
const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' };
function position(value) {
  if (!Number.isSafeInteger(value) || value < 1) {
    throw new RangeError('Expected a positive integer position');
  }
  return `${value}${suffixes[rules.select(value)]}`;
}
const values = [1, 2, 3, 4, 11, 12, 13, 21, 22, 23];
const labels = values.map(position);
assert.deepEqual(labels,
  ['1st', '2nd', '3rd', '4th', '11th', '12th', '13th', '21st', '22nd', '23rd']);
assert.equal(position(111), '111th');
assert.throws(() => position(0), RangeError);
console.log(labels.join(', '));

Run it:

node label-ordinal-positions.mjs

Output:

1st, 2nd, 3rd, 4th, 11th, 12th, 13th, 21st, 22nd, 23rd

Ordinal rules describe positions. Selecting a suffix from the last digit alone would mishandle 11, 12, and 13. The English ordinal categories handle these cases. The suffix table is deliberately English-specific; changing only the locale does not make these suffixes correct in another language.

3. Account for more than two categories

Save this as inspect-arabic-plural-categories.mjs:

import assert from 'node:assert/strict';

assert.equal(Intl.PluralRules.supportedLocalesOf(['ar']).length, 1);
const rules = new Intl.PluralRules('ar', { type: 'cardinal' });
const categories = rules.resolvedOptions().pluralCategories;
const values = [0, 1, 2, 3, 11, 100];
const selected = values.map(value => rules.select(value));
assert.deepEqual(categories, ['zero', 'one', 'two', 'few', 'many', 'other']);
assert.deepEqual(selected, ['zero', 'one', 'two', 'few', 'many', 'other']);
assert.equal(rules.select(1.5), 'other');

// Check that a message catalog has an entry for every possible category.
const catalogKeys = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
assert.ok(categories.every(category => catalogKeys.has(category)));
console.log(values.map((value, index) => `${value}: ${selected[index]}`).join('\n'));
console.log('Message catalog covers every category');

Run it:

node inspect-arabic-plural-categories.mjs

Output:

0: zero
1: one
2: two
3: few
11: many
100: other
Message catalog covers every category

Arabic cardinal rules use all six category names in this runtime. They are grammatical labels, not universal numerical intervals. Inspecting pluralCategories helps a test detect missing catalog entries. A real localized interface still needs translated messages for those keys and locale-aware number formatting. The ECMAScript resolved-options definition specifies the available category list; the number examples above are checked by the script.

Clean up

In the same terminal, remove only the named example files, return to your original directory, and remove the empty scratch directory:

rm -- label-file-counts.mjs label-ordinal-positions.mjs inspect-arabic-plural-categories.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on