Our closet screen shows a simple line: how many pieces of clothing you own. In English this needs two strings, "1 item" and "5 items". When we added Polish, Russian, Ukrainian, Czech and Slovak, two strings were not enough any more.
This post shows how we handle plural forms in Wearton, a React Native app with 15 languages, with no i18n library and with TypeScript checking every dictionary.
The problem: "one" and "other" are not enough
The Unicode CLDR project gives every language a set of plural categories: one, few, many, other (and zero, two for some languages). English uses only one and other. Other languages use more:
| Language | 1 | 2 | 5 | 12 | 22 |
|---|---|---|---|---|---|
| English | one | other | other | other | other |
| Polish | one | few | many | many | few |
| Russian, Ukrainian | one | few | many | many | few |
| Czech, Slovak | one | few | other | other | other |
| French, Portuguese | one (also 0) | other | other | other | other |
| Chinese, Japanese | other | other | other | other | other |
Two details catch people:
- In Polish and Russian, 12, 13 and 14 are
many, but 22, 23 and 24 arefewagain. The rule looks at the last two digits, not only the last one. - In French and Portuguese, 0 is
one: "0 article", not "0 articles".
Here is the same closet line in Russian and Czech:
| Count | Russian | Czech |
|---|---|---|
| 1 | 1 вещь | 1 kousek |
| 3 | 3 вещи | 3 kousky |
| 5 | 5 вещей | 5 kousků |
Step 1: a typed dictionary per language
English is the source of truth. Its dictionary is a plain object with as const, and the type of every key comes from it:
export const en = {
'closet.count.one': '{count} item',
'closet.count.other': '{count} items',
// ...
} as const;
export type StringKey = keyof typeof en;
Every other language must have every English key:
type Dictionary = Record<StringKey, string>;
If a translator forgets a key, tsc fails. We find a missing string at build time, not on a user's screen.
Step 2: extra plural forms without changing English
Polish needs few and many, but English has no such strings. We did not want empty few keys in the English file. A template literal type lets a dictionary add them as optional keys:
type ExtraPluralKey = `${string}.few` | `${string}.many`;
type Dictionary = Record<StringKey, string> & {
[key: ExtraPluralKey]: string;
};
export const ru: Dictionary = {
'closet.count.one': '{count} вещь',
'closet.count.other': '{count} вещей',
'closet.count.few': '{count} вещи',
'closet.count.many': '{count} вещей',
// ...
};
English stays with two keys. Russian adds two more. TypeScript still checks that the required keys exist.
Step 3: pick the category, then the string
The function that formats a count does two things:
- It finds the plural category of the number for the current language.
- It looks for
<base>.fewor<base>.manyin the dictionary. If that key is missing, it falls back to<base>.other.
function formatCount(lang: Lang, count: number, base: string): string {
const form = pluralCategory(lang, count); // 'one' | 'few' | 'many' | 'other'
const dict = dictionaries[lang];
const key =
form === 'one' ? `${base}.one`
: dict[`${base}.${form}`] ? `${base}.${form}`
: `${base}.other`;
return dict[key].replace('{count}', String(count));
}
The fallback matters. A new screen can ship with only one and other in every language, and the Polish translator adds few and many later. Nothing breaks in between: the user sees the other form, which is often close enough.
Step 4: test the rules against Intl.PluralRules
We wrote the category rules for our 15 languages by hand. You can also use Intl.PluralRules, which is built into modern JavaScript engines:
new Intl.PluralRules('pl').select(22); // 'few'
new Intl.PluralRules('pl').select(12); // 'many'
new Intl.PluralRules('fr').select(0); // 'one'
new Intl.PluralRules('cs').select(1.5); // 'many'
Even if you write your own rules, Intl.PluralRules is a good test oracle. Loop over 0 to 200 for each language and compare your function with it. This catches the 12-14 and 22-24 cases at once.
The last line shows one more trap: Czech has a many form, but only for decimals. For whole numbers in a closet count, one, few and other are enough.
What we learned
- Start from CLDR categories, not from "singular and plural".
- Make English the typed source of keys, so a missing translation fails the build.
- Let languages add
fewandmanyas optional keys, with a fallback toother. - Test the numbers 0, 1, 2, 5, 11, 12, 21, 22 and 112 in every language.
About Wearton
Wearton is a personal stylist app. It finds your colour season from one selfie, keeps your closet and suggests what to wear for the weather and your plans. It runs on iPhone, Android, the web app, Chrome, Edge, Firefox, Safari, ChatGPT and Claude, in English, German, Spanish, French, Italian, Polish, Portuguese, Russian, Turkish, Ukrainian, Chinese, Japanese, Hungarian, Slovak and Czech.
Try it at wearton.app.
Top comments (0)