@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
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)
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.