> ## 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.

# @astilba/core API

> The published surface of @astilba/core: the canonical model, vendored CLDR plural rules, AstilbaError, the round-trip driver and contracts, and MT masking.

`@astilba/core` is the format-neutral heart of astilba. It depends on no syntax adapter; a
syntax adapter (like `@astilba/adapter-i18next-v4`) depends on *it*. This page documents the
actual public surface as it ships today, grouped by module.

<Callout type="warning">
  astilba is **pre-1.0**. The public API may change before 1.0. This page reflects the
  current exports, not a frozen contract.
</Callout>

## Install

```bash theme={null}
pnpm add @astilba/core
```

Two entry points:

| Import path          | Contents                             |
| -------------------- | ------------------------------------ |
| `@astilba/core`      | The full API (everything below).     |
| `@astilba/core/cldr` | Just the vendored CLDR plural rules. |

## The canonical model (`model.ts`)

```ts theme={null}
type CLDRCategory = "zero" | "one" | "two" | "few" | "many" | "other";

const ALL_CLDR_CATEGORIES: readonly CLDRCategory[]; // ["zero","one","two","few","many","other"]

type PluralKind = "none" | "cardinal" | "ordinal";

type ValueToken =
  | { type: "text"; raw: string }
  | { type: "interpolation"; raw: string; variable: string; format?: string }
  | { type: "nesting"; raw: string; ref: string; options?: string }
  | { type: "markup"; raw: string };

interface Value {
  raw: string;        // byte-exact source — the source of truth
  tokens: ValueToken[]; // derived view; join(.raw) === raw exactly
}

interface PluralSet {
  kind: PluralKind;
  values: Map<CLDRCategory, Value>;
  bare?: Value;       // rare: suffix-less form alongside plural forms
}

interface Key {
  namespace: string;
  base: string;       // key path without namespace, without plural/context suffixes
  contexts: Map<string, PluralSet>; // "" === no context
}

interface CanonicalModel {
  language: string;   // BCP-47
  keys: Map<string, Key>; // `${namespace}:${base}` -> Key
}

function keyId(namespace: string, base: string): string; // `${namespace}:${base}`
```

See [the canonical model](/concepts/canonical-model) for the invariants (value bytes
preserved exactly, plurals structural, one kind per cell, in-memory only).

## CLDR plural rules (`cldr.ts`)

```ts theme={null}
const SUPPORTED_LANGUAGES: string[]; // ["ar","de","en","fr","ja","ko","pl","ru","zh"]

interface PluralRule {
  categories: CLDRCategory[];
  select: (n: number) => CLDRCategory;
  representative: Partial<Record<CLDRCategory, number>>;
}

interface LanguagePlurals {
  cardinal: PluralRule;
  ordinal: PluralRule;
}

function getPlurals(language: string): LanguagePlurals;           // unknown -> other-only
function categoriesFor(language: string, kind: PluralKind): CLDRCategory[];
function allCategoriesFor(language: string): Set<CLDRCategory>;   // cardinal ∪ ordinal
function selectCategory(language: string, count: number, ordinal: boolean): CLDRCategory;
function representativeCount(language: string, kind: "cardinal" | "ordinal", category: CLDRCategory): number;
function isSupportedLanguage(language: string): boolean;
function primarySubtag(language: string): string;                 // "en-US" -> "en"
function operands(input: number): { n: number; i: number; v: number; f: number; t: number }; // CLDR operand decomposition (the return type is internal, not exported)
```

See [CLDR plural rules](/concepts/plural-rules) for why the table is vendored and how the
fallback behaves.

## Errors (`errors.ts`)

```ts theme={null}
type AstilbaErrorCode = string; // open string — adapters define their own codes

class AstilbaError extends Error {
  readonly code: string;
  readonly key?: string;                          // fully-qualified key, if any
  readonly details?: Record<string, unknown>;
  constructor(code: string, message: string, opts?: { key?: string; details?: Record<string, unknown> });
}
```

There is one error class. `code` is an **open string**, so each adapter owns its own code
set (for example `ICU_NOT_SUPPORTED`, `UNSUPPORTED_LANGUAGE`) without coupling core to any
one syntax. Catch with `if (e instanceof AstilbaError) e.code`.

## Round-trip harness (`harness.ts`)

```ts theme={null}
interface FormatAdapter<Files = unknown, Input = unknown> {
  readonly id: string;
  parse: (input: Input) => CanonicalModel;
  export: (model: CanonicalModel) => Files;
}

interface RenderOracle<Files = unknown> {
  vectors: (model: CanonicalModel) => Vector[];
  render: (files: Files, vector: Vector, language: string) => string | undefined;
}

interface Vector {
  key: Key;
  context: string;
  kind: PluralSet["kind"];
  category?: string;
  args: unknown; // opaque to the driver; the adapter's render shape
}

interface RoundTripMismatch {
  namespace: string;
  base: string;
  context: string;
  kind: PluralSet["kind"];
  category?: string;
  args: unknown;
  fromSource: string | undefined;
  fromExported: string | undefined;
  note?: string;
}

interface RoundTripReport<Files = unknown> {
  language: string;
  verdict: "lossless" | "mismatch";
  vectorCount: number;
  mismatches: RoundTripMismatch[];
  canonical: CanonicalModel;
  exported: Files;
}

function runRoundTrip<Files, Input extends { resources: Files }>(
  adapter: FormatAdapter<Files, Input>,
  oracle: RenderOracle<Files>,
  input: Input
): RoundTripReport<Files>;

function formatMismatches(report: RoundTripReport): string;
```

See [round-trip fidelity](/concepts/fidelity) for how the driver uses these.

## MT masking (`mask.ts`)

```ts theme={null}
type Tokenizer = (raw: string) => ValueToken[];

interface MaskResult {
  masked: string;          // sentinels in place of non-text tokens
  parts: string[];         // original token raws, indexed by sentinel number
}

interface SentinelCheck { ok: boolean; errors: string[]; }
interface PlaceholderCheck { ok: boolean; errors: string[]; }

function sentinel(index: number): string;                  // "\uE000{index}\uE001"
function maskTokens(tokens: ValueToken[]): MaskResult;     // throws MASK_VALIDATION on reserved delimiter
function unmask(masked: string, parts: string[]): string;  // throws MASK_VALIDATION on unknown sentinel
function validateSentinels(translated: string, parts: string[], opts?: { requireOrder?: boolean }): SentinelCheck;
function validatePlaceholderTokens(source: ValueToken[], translated: ValueToken[]): PlaceholderCheck;
function validatePlaceholders(source: string, translated: string, tokenize: Tokenizer): PlaceholderCheck;
```

See [MT masking](/concepts/masking) for the mask → translate → validate flow.

## Related

* [Concepts](/concepts/canonical-model) — the why behind each module.
* [i18next-v4 adapter](/reference/adapter-i18next) — the in-tree adapter that supplies
  `parse` / `exportModel` / `render` / `assertLossless`.
