Skip to main content
@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.
astilba is pre-1.0. The public API may change before 1.0. This page reflects the current exports, not a frozen contract.

Install

pnpm add @astilba/core
Two entry points:
Import pathContents
@astilba/coreThe full API (everything below).
@astilba/core/cldrJust the vendored CLDR plural rules.

The canonical model (model.ts)

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 for the invariants (value bytes preserved exactly, plurals structural, one kind per cell, in-memory only).

CLDR plural rules (cldr.ts)

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 for why the table is vendored and how the fallback behaves.

Errors (errors.ts)

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)

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 for how the driver uses these.

MT masking (mask.ts)

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 for the mask → translate → validate flow.
  • Concepts — the why behind each module.
  • i18next-v4 adapter — the in-tree adapter that supplies parse / exportModel / render / assertLossless.