Skip to main content
The native i18next-v4 syntax adapter (@astilba/adapter-i18next-v4) is the adapter that plugs i18next into @astilba/core. It supplies the FormatAdapter (parse/export) and the RenderOracle (vector enumeration + render) that the round-trip driver needs.
Status: in-tree, not yet published. The adapter lives in the repository at packages/adapter-i18next-v4 and is marked "private": true in its package.json. It drives the fidelity test suite, but it is not yet released to npm as its own installable package. The public, installable package today is @astilba/core.

What it does

The adapter maps i18next resource bundles to and from the canonical model:
  • parse — flattens nested JSON to dotted keys, rejects ICU values loudly, peels plural suffixes language-awarely, resolves base/context per the disambiguation rule, and assembles structural PluralSets. Optionally validates $t() nesting refs resolve.
  • exportModel — writes the model back to nested JSON, re-deriving the suffix set from the target language and writing Value.raw byte-exact. Output is sorted and may renormalise (nested ↔ flat, key ordering) — byte equality is not the fidelity test.
  • render — a faithful, host-independent re-implementation of i18next’s native v4 lookup + interpolation + nesting, used as the round-trip render oracle.
  • roundTrip / assertLossless — the ergonomic i18next binding of the generic runRoundTrip driver.
  • mask / validatePlaceholders — the i18next tokenizer pre-bound to core’s masking utilities.

Parsing options

parse takes a ParseInput with a language, the resources (namespace → nested JSON), and an optional options object:
OptionDefaultEffect
keySeparator"."i18next key separator. false = flat keys, dots are literal.
declaredContexts{}Declared context values per namespace (preferred disambiguation).
strictNestingfalseOpt in to dangling $t() reference detection.
inferContextsfalseOpt in to context-set inference (off by default — see note).
allowUnsupportedLanguagefalseParse a language with no vendored CLDR rules as an opaque other-only variant.
inferContexts is off by default. On real corpora, shared-prefix keys (like enter_password / enter_username) are indistinguishable from contexts and would be mis-inferred and falsely rejected. Without declared contexts, every non-plural leaf is its own atomic key — which is still lossless.

The disambiguation rule

i18next overloaded the _suffix slot for both plural categories and context values, so the parser must decide, for a group of sibling keys sharing a parent path, where the base/context boundary lies. It does this with a three-step rule, language-awarely (the peeling step uses the vendored CLDR table). When it cannot choose safely — for example a context value that collides with a CLDR category name — it rejects loudly (AMBIGUOUS_KEY or CONTEXT_CATEGORY_COLLISION) and asks you to declare the contexts in project config.

Error codes

The adapter defines these AstilbaError codes (each factory names the offending key and explains the fix):
CodeRaised when
ICU_NOT_SUPPORTEDA value uses ICU MessageFormat. Phase 0 is native-only.
AMBIGUOUS_KEYSet-inference cannot resolve a base/context boundary.
CONTEXT_CATEGORY_COLLISIONA context value equals a CLDR category name.
DANGLING_NESTING_REFA $t() ref resolves to no key (opt-in via strictNesting).
INVALID_RESOURCEe.g. a non-string value, or both cardinal and ordinal on one key.
UNSUPPORTED_LANGUAGEA language with no vendored CLDR rules, not opted into.

Known Phase-0 boundaries

  • One plural kind per (key, context). A key used as both cardinal and ordinal is valid i18next but rejected here (INVALID_RESOURCE), because the model holds one kind per cell.
  • Vendored CLDR coverage is the corpus-plan set; other languages reject loudly unless allowUnsupportedLanguage is set.

Running it locally

Because the adapter is in the repo, you can use it by cloning and linking:
git clone https://github.com/astilbahq/astilba.git
cd astilba
vp install
vp test run
The test suite is the fidelity matrix — it exercises parse / exportModel / render end-to-end through assertLossless. See CONTRIBUTING.md for the full dev loop.