@shubhamdeol/locale-ota-core
v1.0.0
Published
Pure functions: flatten, hash, diff, compose, delta, plural forms, marker validation, batching.
Readme
@shubhamdeol/locale-ota-core
The pure layer of locale-ota: flatten, hash, diff, compose, delta, plural forms, marker
validation, batching and release resolution. No IO, no dependencies, no node: imports,
so the same code runs on the server, in the CLI, in a browser and in React Native.
Status: phase 1, plus the expandReleaseKeys of phase 3b and the fallback chain of
phase 4c. Version 0.0.0, unpublished.
Install
bun add @shubhamdeol/locale-ota-corePublic API
Everything below is exported from @shubhamdeol/locale-ota-core.
Keys
| Export | What it does |
| --- | --- |
| flatten(input, options?) | Flattens a resource file to Record<string, string>; arrays become items.0, numbers and booleans become strings, null and friends throw invalid_leaf. |
| unflatten(flat, options?) | Rebuilds the nested object; a node whose children are 0..n-1 becomes an array. |
Hashing
| Export | What it does |
| --- | --- |
| hashValue(value) | Lowercase SHA-256 hex through crypto.subtle. |
| buildKeySet(namespace, flat) | Hashes every value and returns KeySetEntry[] sorted by namespace then key. |
| keySetHash(entries, options?) | The name of a release, decision D19: SHA-256 over the namespaces, keys and values, in buckets, as plan 5.8 defines it. Takes an injected digest and a pause. |
| hashResources(resources, options?) | The same hash from a bundled file, per namespace, without per-value hashing: what a client computes. Flattens one subtree at a time and pauses every HASH_SLICE_KEYS keys and after every bucket. |
| compareStrings(a, b) | Byte-wise string order, the order every sort in this package uses. |
Diff
| Export | What it does |
| --- | --- |
| diffKeySets(previous, next) | Returns { added, changed, removed }; a key changed when its source hash did. |
Compose and delta
| Export | What it does |
| --- | --- |
| compose(releaseKeys, rows, options) | The file a client sees: the newest row per release key at or below the cutoff with a matching source hash, nested per namespace. |
| delta(releaseKeys, rows, options) | { set, remove } for a client that already holds a revision. |
| composeChain(releaseKeys, rowsByLanguage, options) | The same file along a fallback chain: { resources, resolvedBy }. |
| deltaChain(releaseKeys, rowsByLanguage, options) | { set, remove } along a fallback chain. |
| groupRowsByLanguage(rows) | Groups a flat row list by its own language field, ready for the two chain functions. |
| mergeResources(base, set, remove?, options?) | Applies one namespace of a delta to a nested object and returns a new one. |
The fallback chain
| Export | What it does |
| --- | --- |
| defaultParent(code) | The code without its last BCP 47 subtag: pt-BR gives pt, pt gives null. The kept prefix keeps its own casing. |
| resolveChain({ requested, languages, sourceLanguage }) | The requested language, then its enabled ancestors. |
| MAX_CHAIN_HOPS | 4: the most parent steps a chain takes, so a chain holds at most five codes. |
resolveChain stops before the source language, at a code it has already taken, at a
code that is disabled or not configured, and after four hops. A requested language that
is not configured, or is configured and disabled, has no chain at all: the list comes
back empty and the server answers 404 language_not_enabled. The source language is the
one exception, because it is served from its own source rows rather than from a chain:
asking for it gives the one-code chain [requested]. Codes are matched without regard to
case and come back spelled the way the languages rows spell them, so they can be handed
straight to a row query.
composeChain expands the key set for chain[0] with expandReleaseKeys first, then
takes for each key the first chain language with a row at or below the cutoff carrying the
release source hash, and the newest such row. resolvedBy maps
namespace + nsSeparator + flat key to the chain language that answered, so the
dashboard can show that a pt-BR screen is reading a pt value. nsSeparator defaults
to ":".
deltaChain takes as candidates the keys with any chain-language row in (from, cutoff]
and resolves each twice. set holds the keys whose resolved value changed or appeared.
remove holds the keys that resolved at from and do not resolve at the cutoff, and the
keys whose only rows in the window carry a source hash the release has moved past: the
English changed inside the release, so the value the client holds is stale. Rows are
append-only, so the first of those two rules can only fire when the second does; it is
written out because plan 5.3 step 5 names it.
compose and delta are the one-language cases of the two: every row given is a
candidate whatever its language field says, and the key set is taken as it is rather
than expanded.
Plurals
| Export | What it does |
| --- | --- |
| parsePluralKey(key) | { base, category, ordinal } for an i18next v4 suffix, else null. |
| pluralKey(base, category, options?) | Builds base_one or base_ordinal_one. |
| requiredPluralCategories(language, options?) | The categories Intl.PluralRules needs, in CLDR order; an unknown tag gets other alone. |
| pluralWork(releaseKeys, language) | Groups plural keys by namespace and base, with the source forms and the target keys the language needs. |
| expandReleaseKeys(releaseKeys, language) | The release key set plus the plural forms the language needs and the source does not ship, each under the source hash of the English form it is translated from. |
| PLURAL_CATEGORIES | The CLDR order: zero, one, two, few, many, other. |
Markers and validation
| Export | What it does |
| --- | --- |
| extractMarkers(value, format?) | The interpolation markers of an i18next, icu or printf string, sorted and deduplicated. |
| extractTags(value) | The HTML tag names, closing tags as /name, sorted, duplicates kept. |
| validateTranslation(source, translation, options?) | { ok: true } or { ok: false, errors } with empty, marker_missing, marker_extra and tag_mismatch. |
Batching
| Export | What it does |
| --- | --- |
| batchWork(items, options?) | Splits a work list in order at 150 keys or 8 KB, an oversized item to its own batch. |
Errors
| Export | What it does |
| --- | --- |
| CoreError | The one error type: a code string and a details object. |
| isCoreError(value) | Narrows an unknown value to CoreError. |
| version | The published version of this package, "0.0.0" until it ships. |
Types
ResourceNode, ResourceValue, FlattenOptions, KeySetEntry, KeySetDiff, AddedKey,
ChangedKey, RemovedKey, ReleaseKey, TranslationRow, TranslationOrigin,
ComposedResources, ComposeOptions, DeltaOptions, DeltaResult, DeltaRemoval,
ChainLanguage, ResolveChainInput, ChainComposeOptions, ChainComposeResult,
ChainDeltaOptions, RowsByLanguage,
RemovalKey, PluralCategory, ParsedPluralKey, PluralOptions, PluralGroup,
PluralSourceForm, PluralTargetForm, MarkerFormat, ValidateOptions, ValidationError,
ValidationResult, BatchOptions, ReleaseLike, SemverVersion.
Rules this package keeps
- Zero dependencies, runtime and dev. Nothing from
node:. - Every sort is byte-wise, so a composed file is the same bytes on every machine.
- Hashing goes through
globalThis.crypto.subtle, which exists in Node 20, Bun, browsers and React Native 0.76 or later.
Tests
bun test packages/core # from the repository root
bun run typecheck # from this folder
bun run build # from this foldertest/fixtures/generate.ts builds a synthetic 4 MB English file of 40,000 keys from a seed,
and nextVersion makes the version after it. Nothing is written to disk. src/perf.test.ts
holds the phase 1 budgets: compose under 200 ms, diff under 500 ms.
