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:
| Option | Default | Effect |
|---|
keySeparator | "." | i18next key separator. false = flat keys, dots are literal. |
declaredContexts | {} | Declared context values per namespace (preferred disambiguation). |
strictNesting | false | Opt in to dangling $t() reference detection. |
inferContexts | false | Opt in to context-set inference (off by default — see note). |
allowUnsupportedLanguage | false | Parse 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):
| Code | Raised when |
|---|
ICU_NOT_SUPPORTED | A value uses ICU MessageFormat. Phase 0 is native-only. |
AMBIGUOUS_KEY | Set-inference cannot resolve a base/context boundary. |
CONTEXT_CATEGORY_COLLISION | A context value equals a CLDR category name. |
DANGLING_NESTING_REF | A $t() ref resolves to no key (opt-in via strictNesting). |
INVALID_RESOURCE | e.g. a non-string value, or both cardinal and ordinal on one key. |
UNSUPPORTED_LANGUAGE | A 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.