> ## Documentation Index
> Fetch the complete documentation index at: https://docs.astilba.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CLDR plural rules

> Why astilba vendors its own plural table instead of using Intl.PluralRules, which languages it covers, and how it falls back.

astilba ships a **vendored** copy of the CLDR plural rules in `@astilba/core`, rather than
reading from the host's `Intl.PluralRules`. This page explains why, what is covered, and what
happens for languages not in the table.

## Why vendor instead of `Intl.PluralRules`

Native i18next resolves plurals against the **end user's** ambient browser or host ICU. That
ICU version drifts across CLDR releases and across runtimes (Node, Bun, Deno, browsers,
Workers). If astilba's exporter emitted a suffix set that disagreed with what the client's
ICU asks for at runtime, the user would see a **silent miss** — the wrong form, or no form at
all.

Vendoring one table, used by **both** the exporter (which suffix keys to emit) **and** the
round-trip resolver (which suffix a given count selects), makes emission host-independent
and the fidelity harness fully deterministic. Every host agrees on what `_one` means.

## What's in the table

The vendored table covers the corpus-plan languages plus a few common ones, keyed by BCP-47
primary subtag:

```ts theme={null}
import { SUPPORTED_LANGUAGES } from "@astilba/core";

SUPPORTED_LANGUAGES;
// → ["ar", "de", "en", "fr", "ja", "ko", "pl", "ru", "zh"]
```

Each entry carries three things:

* **`categories`** — the ordered set of CLDR categories that apply to the language.
* **`select(n)`** — the CLDR rule, mapping a count to a category.
* **`representative`** — one count per category, used to build round-trip test vectors.

```ts theme={null}
import { getPlurals } from "@astilba/core";

const en = getPlurals("en");
en.cardinal.categories;          // → ["one", "other"]
en.cardinal.select(1);           // → "one"
en.cardinal.representative;      // → { one: 1, other: 2 }

getPlurals("ru").cardinal.categories;   // → ["one", "few", "many", "other"]
getPlurals("ar").cardinal.categories;   // → ["zero","one","two","few","many","other"]
getPlurals("ja").cardinal.categories;   // → ["other"]   (no plural distinction)
```

## Selecting a category and a representative count

Two helpers cover the common operations:

```ts theme={null}
import { selectCategory, representativeCount } from "@astilba/core";

selectCategory(language, count, ordinal);
// selectCategory("en", 1, false)  → "one"
// selectCategory("en", 21, true)  → "one"   (21st — ordinal)

representativeCount(language, kind, category);
// representativeCount("ru", "cardinal", "few")  → 2
// representativeCount("ar", "cardinal", "zero") → 0
```

`representativeCount` has one i18next-specific quirk worth knowing: for **cardinal** `zero`,
it returns `0` for *every* language, because i18next's `_zero` special-case is reached at
count 0 even for languages whose CLDR set has no `zero` category (like English).

## Fallback for unsupported languages

Languages with no vendored rules fall back to **`other`-only** — a graceful single-category
rule. This is safe (nothing crashes), but it *would* mis-model a real language that has more
categories. So the i18next adapter, when parsing, **rejects an unsupported language loudly**
(`UNSUPPORTED_LANGUAGE`) rather than silently approximating — unless you explicitly opt in
with `allowUnsupportedLanguage` (useful for opaque variant tags like Weblate's `@pirate`).

Check ahead of time when you need to:

```ts theme={null}
import { isSupportedLanguage } from "@astilba/core";

isSupportedLanguage("en");     // → true
isSupportedLanguage("en-US");  // → true   (region is collapsed to the primary subtag)
isSupportedLanguage("xh");     // → false  (falls back to "other"-only)
```

## Known limitations

* **Region collapse.** `primarySubtag` reduces `pt-BR` and `pt-PT` both to `pt`. A few
  languages have region-specific plural rules (most notably `pt-PT` differs from Brazilian
  Portuguese). This is harmless while the vendored table is small — those languages aren't in
  it, so both fall back to `other`-only — but when the full `make-plural` table is vendored,
  region-specific entries must be looked up *before* the primary subtag. That's a road-to-1.0
  item.

## Related

* [Round-trip fidelity](/concepts/fidelity) — how representative counts feed probe vectors.
* [The canonical model](/concepts/canonical-model) — `PluralSet.kind` and category maps.
