Content bundle (.kbf.json)
The content bundle is the JSON serialization of neokapi's
content model: a set of Documents, each holding
Blocks whose content is a set of peer editions, each a flat sequence of
Runs: the source, and every translation or other edition of it. Its marker
segment and Go package are both kbf, for Kapi Bundle Format.
The normative schema comes first, followed by worked examples of every run kind. For where the bundle sits among kapi's other formats, start with the overview.
Authoring and exchange, not runtime
A content bundle is an authoring and exchange format, not a runtime
catalog. It is heavy by design: it carries the data shape that lets
translators work, that tools round-trip, and that a review platform can
persist. That richness is waste at runtime, where the only operation is
(scope, source) → translated. Compile it down to an established runtime
catalog format (gettext MO, compact per-locale JSON, ICU) for the running
application, and keep the bundle on the pipeline side.
Two implementations, one format
The bundle has two reference implementations kept byte-for-byte equivalent by shared golden fixtures:
- Go, canonical.
core/kbfin the framework module. Where the two differ, the Go source governs. - TypeScript, mirror.
@neokapi/kapi-format, used by the React extractor and the in-browser tooling.
You can watch the two engines agree, live, on the conformance page, which runs the spec conformance suite against both implementations in your browser.
Detection and determinism
A .kbf.json file is auto-detected by its extension and by sniffing the
document for the magic string "kapi-bundle". The compound suffix is
deliberate: the file is JSON, so the .json tail lets jq, GitHub, and an
editor treat it as JSON, while the .kbf segment still says which JSON it is.
The overview
covers the convention, and the filepath.Ext caveat it creates.
The serializer is deterministic: 2-space indent, no HTML escaping, a trailing newline, fields emitted in the struct-definition order pinned below, and all map keys sorted (edition keys, plural forms, and select cases). Deterministic output is what keeps the content hash stable across runs, machines, and the two implementations.
Versioning
The current schema version is 2.0, carried in the schemaVersion field of
every file. Versions follow a MAJOR.MINOR contract:
- Major means a breaking change. A consumer must reject a file whose major version it does not recognize.
- Minor means a forward-compatible addition. A consumer should accept unknown minor versions within a major it understands, ignoring fields it does not recognize.
A reader validates the envelope on load: it rejects any document whose kind is
not kapi-bundle and any schemaVersion whose major it does not read.
Both implementations read schema 1.0 as well as 2.0, and write 2.0. In
schema 1.0 a block carried its source runs under source, its translations
in a targets map from locale to runs, and how each was produced in a
targetOrigins map from locale to origin. A reader turns such a block into the
editions it describes:
sourcebecomes the source edition, under the empty key;- each
targetsentry becomes the edition under its locale, carrying thetargetOriginsentry for that locale as itsorigin; - a
targetsentry under the empty locale becomes theunlabellededition; - a
targetOriginsentry for a locale with no target describes nothing and is dropped.
The file then carries schemaVersion 2.0, so writing it again writes the
current schema. A block that carries both editions and source or targets
is refused. A writer stamps 2.0 on any file whose version is not a minor of
major 2, because the blocks it writes are in schema 2.
File envelope
The top-level File object is the .kbf.json document. Field order and shape are
normative, because deterministic serialization is what keeps the content hash
stable across the Go and TypeScript implementations.
| Field | Type | Required | Description |
|---|---|---|---|
schemaVersion | string | yes | Wire format version, e.g. "2.0". Subject to the versioning contract above. |
kind | string | yes | Magic string; must be "kapi-bundle". |
created | string | no | Creation timestamp (RFC 3339). Omitted when empty. |
generator | GeneratorInfo | yes | The extractor that produced the file. |
project | ProjectInfo | yes | The project the file belongs to. |
vocabulary | Vocabulary | no | Vocabulary packs this file's runs depend on. Omitted when absent. |
documents | Document[] | yes | One entry per extracted source file. |
GeneratorInfo
Identifies the extractor that produced the file.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Extractor identifier, e.g. "@neokapi/kapi-format-examples". |
version | string | yes | Extractor version. |
capabilities | string[] | no | Declared capabilities, e.g. ["extract", "preview"]. Omitted when empty. |
ProjectInfo
Identifies the project the file belongs to.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Project identifier. |
sourceLocale | string | yes | BCP-47 locale of every block's source edition. |
kapi keeps the project of a bundle it rewrites, so sourceLocale goes on
naming the language of the source editions. A bundle kapi writes from another
format names the source language that format was read in.
Vocabulary
Lists the vocabulary packs whose span-type entries the file's runs expect a consumer to have loaded.
| Field | Type | Required | Description |
|---|---|---|---|
extends | string[] | no | Names of vocabulary packs, e.g. ["common-formatting", "rich-jsx"]. |
Document
A Document is one source file's worth of extracted content.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Stable identifier for the document within the file. |
documentType | string | yes | Source format discriminator. Both implementations define "jsx". |
path | string | yes | Path of the source file the content was extracted from. |
sourceHash | string | no | Content hash of the original source file. Omitted when empty. |
skeleton | Skeleton | no | Reference to the opaque skeleton that reconstructs the source on merge. |
blocks | Block[] | yes | The translatable blocks extracted from this document. |
Skeleton
A reference to the opaque skeleton payload a merge step consumes to reconstruct the original source file with translated content spliced back in. At least one of the fields is present.
| Field | Type | Required | Description |
|---|---|---|---|
ref | string | no | Reference to an external skeleton payload. Omitted when empty. |
inline | string | no | Inline skeleton payload. Omitted when empty. |
Block
A Block is what translation tracking follows. Its content is a map of peer
editions (see Editions): the source, a flat sequence of Runs in
the project's source locale, under the empty key, and every translation, tone
or channel edition under its own edition key.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Stable identifier within the document. |
hash | string | yes | Content hash of the block, used for deduplication and change detection. |
translatable | bool | yes | Whether the block should be offered for translation. |
type | string | yes | Coarse block classification: "jsx:element", "jsx:attribute", or "js:t". |
editions | map<key, Edition> | yes | Every edition of the block, keyed by edition key. The source edition, under "", is always present. |
unlabelled | Edition | no | A translation filed under no language (see Editions). Omitted when there is none. |
placeholders | Placeholder[] | yes | Metadata for every placeholder, paired code, and ICU pivot referenced by the runs. |
properties | BlockProperties | yes | Provenance metadata (source location, component, JSX path). |
preview | BlockPreviewHints | no | Optional hints for rendering a preview of the block. Omitted when absent. |
BlockProperties
Provenance metadata describing where the block was extracted from.
| Field | Type | Description |
|---|---|---|
file | string | Source file path. |
line | int | Line number in the source file. |
component | string | Enclosing component name. |
jsxPath | string | Path to the element within the component tree. |
element | string | The element tag the content was extracted from. |
locNote | string | Translator note (ITS locNote). Omitted when empty. |
BlockPreviewHints
Hints for rendering a representative preview of a block.
| Field | Type | Description |
|---|---|---|
storyId | string | Storybook story identifier. Omitted when empty. |
snapshotPath | string | Path to a rendered snapshot. Omitted when empty. |
sampleValues | map<string, any> | Sample values for placeholders, keyed by placeholder name. Omitted when absent. |
Placeholder
A Placeholder is metadata about a variable, element, node, or ICU pivot that
the block's runs reference. Validators use this list to assert that targets
preserve every required placeholder.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Placeholder name. Matches the equiv of the corresponding run, or the pivot of a plural/select construct. |
kind | string | yes | One of "variable", "element", "node", or "icu-pivot". |
jsType | string | no | The JavaScript/TypeScript type of the value, e.g. "number", "string", "ReactNode". Omitted when empty. |
sourceExpr | string | yes | The source expression the placeholder was extracted from. |
optional | bool | no | When true, the placeholder may be dropped in a target (e.g. a conditional JSX node). Omitted when false. |
The Run model
A Run is one element in a block's flat inline content sequence. Runs are a
discriminated union: each run is a JSON object with exactly one discriminator
key. A run object with zero or more than one discriminator is rejected on decode.
The seven kinds are text, ph, pcOpen, pcClose, sub, plural, and
select.
Text is the one kind serialized flat, as {"text": "..."}, where the
discriminator's value is the string itself. Every other kind nests its object
under the discriminator key.
text: a plain text chunk
{ "text": "Files " }
ph: a self-closing placeholder
A ph run is a self-contained inline token: a variable interpolation, a
conditional JSX expression, a <br/>, an icon, a redaction, and so on.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Run identifier, unique within the block. |
type | string | yes | Producer-specific run type, e.g. "jsx:var", "jsx:node". |
subType | string | no | Finer classification, e.g. "number", "string". Omitted when empty. |
data | string | yes | The original token the reader captured, e.g. "{count}". |
equiv | string | yes | Equivalence name; the placeholder this run stands for. |
disp | string | no | Short display label for chips. Falls back to equiv. Omitted when empty. |
constraints | RunConstraints | no | Per-run editing constraints (see RunConstraints). Omitted when absent. |
{
"ph": {
"id": "2",
"type": "jsx:var",
"subType": "number",
"data": "{count}",
"equiv": "count",
"disp": "count"
}
}
pcOpen / pcClose: paired codes
A paired code is split into an opening run (pcOpen) and a closing run
(pcClose) that share the same id within a runs scope. They wrap the content
between them, the analogue of an opening and closing HTML/JSX tag.
pcOpen carries the same fields as ph (id, type, subType, data,
equiv, disp, constraints). pcClose carries id, type, subType,
data, and an optional equiv (repeated for locality so a renderer can render
the closing half on its own).
{
"pcOpen": {
"id": "1",
"type": "jsx:element",
"subType": "span",
"data": "<span className=\"muted\">",
"equiv": "muted",
"disp": "span"
}
}
{
"pcClose": {
"id": "1",
"type": "jsx:element",
"subType": "span",
"data": "</span>",
"equiv": "muted"
}
}
sub: a reference to a subblock
A sub run references another Block by id. It is emitted when an outer format
extracts a field whose value is itself a mini-document in another format: a
subfilter produces a separate block, and the outer block points at it.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Run identifier within the block. |
ref | string | yes | The id of the referenced subblock. |
equiv | string | yes | Equivalence name for the subblock token. |
{ "sub": { "id": "1", "ref": "subblock-7", "equiv": "body" } }
plural: a structured plural construct
A plural run carries an ICU plural construct as first-class structure: a
pivot variable that drives selection, and a forms map keyed by plural form.
Each form's value is itself a Run[], so inline markup and placeholders inside a
plural clause stay first-class.
| Field | Type | Required | Description |
|---|---|---|---|
pivot | string | yes | The variable driving plural selection. Declared in placeholders with kind icu-pivot. |
forms | map<form, Run[]> | yes | One run sequence per plural form. |
The form keys are the CLDR plural categories: zero, one, two, few,
many, other. On the wire the deterministic serializer emits them in sorted
order (so one, other, zero below); a renderer is free to re-order them into
CLDR order for display.
{
"plural": {
"pivot": "count",
"forms": {
"one": [{ "text": "1 item in your cart" }],
"other": [
{
"ph": {
"id": "1",
"type": "jsx:var",
"subType": "number",
"data": "{count}",
"equiv": "count",
"disp": "count"
}
},
{ "text": " items in your cart" }
],
"zero": [{ "text": "Your cart is empty" }]
}
}
}
select: a structured select construct
A select run is symmetric to plural, but its cases map is keyed by
arbitrary string values (such as male / female / other) rather than fixed
plural categories. Case keys are likewise emitted in sorted order on the wire.
| Field | Type | Required | Description |
|---|---|---|---|
pivot | string | yes | The variable driving selection. Declared in placeholders. |
cases | map<string, Run[]> | yes | One run sequence per select case. |
{
"select": {
"pivot": "gender",
"cases": {
"female": [{ "text": "She liked your post" }],
"male": [{ "text": "He liked your post" }],
"other": [{ "text": "They liked your post" }]
}
}
}
RunConstraints
Optional per-run editing constraints, present on ph and pcOpen runs when a
run overrides its vocabulary defaults.
| Field | Type | Description |
|---|---|---|
deletable | bool | Whether the run may be deleted in a target. |
cloneable | bool | Whether the run may be duplicated in a target. |
reorderable | bool | Whether the run may be moved within a target. |
Editions
A block's content is a set of peer editions in editions, a map from edition
key to Edition. The source edition, under the empty key "", holds the
content the block was extracted with, in the project's sourceLocale; it is
always present, empty if the block has no content. Every other edition sits
under its own key:
- a translation under its BCP-47 locale (
"fr","nb-NO"); - an edition with a tone or a channel under the locale followed by
;tone=<value>and;channel=<value>("fr;tone=formal","en;channel=short"), so a short English edition sits beside the English source.
Keys are sorted on the wire. A block with no edition in a language is untranslated in that language, and a writer leaves out any edition other than the source that holds no runs, whatever status it records.
A key carries a language, then at most one tone and one channel, each with a
value. A key with a dimension a reader does not know, such as one a later minor
version adds, or a key that names no language, names an edition that reader
cannot hold. kapi reads every other edition of the block, never files that one
under the shorter key its text would leave (nb;audience=kids is never nb),
and writes it back unchanged when it rewrites the bundle.
Edition
| Field | Type | Required | Description |
|---|---|---|---|
runs | Run[] | yes | The edition's content as a run sequence, using the same run primitives as every other edition. Written as [] when empty. |
status | string | no | Lifecycle state: written or established for a source; draft, translated or established for a translation. established is the state kapi status shows as approved. Omitted when none is recorded. |
origin | Origin | no | How the content was produced and what governed it. Omitted when it records nothing. |
score | number | no | The producer's quality score for a translation or other derived edition. Omitted when zero. The source carries none. |
derived | Derivation | no | The edition this one was made from. Omitted for an edition nothing derived it from. |
Derivation
| Field | Type | Required | Description |
|---|---|---|---|
from | string | yes | Key of the edition this one was made from: "" for the source, whatever its language. A key in the source language names a same-language edition beside the source. |
rev | string | yes | That edition's revision when this one was made, r: and 16 hex digits of a hash of its key and runs. |
Comparing rev with the current revision of the edition named in from tells
whether the derived edition is stale. A revision of the source is taken under
the language a reader files it by, which may be the bundle's source language,
the source language of the project that read it, or none. The runs are the
same under each, so kapi reads a derivation from the source as current while
the revision of the source runs under any of these keys equals rev.
Origin
Every field is optional and omitted when empty. The last two keep the spelling the content model gives them.
| Field | Type | Description |
|---|---|---|
kind | string | human, agent, memory, mt, ai, ocr, asr, or llm-refined. |
engine | string | The engine that produced the content. |
tool | string | The tool that produced it. |
reference | string | A batch id, content-memory entry, or session. |
timestamp | string | When it was produced (RFC 3339). |
confidence | number | A recognizer's confidence in [0, 1], for recognized content. |
profile | string | The context profile that governed production. |
profile_version | string | The revision of that profile in force. |
context_fingerprint | string | A hash of the governing context as it reached the producer, so a later change to it can be seen. |
{
"id": "files-heading",
"hash": "2xykvb",
"translatable": true,
"type": "jsx:element",
"editions": {
"": {
"runs": [{ "text": "Files matched" }]
},
"de": {
"runs": [{ "text": "Gefundene Dateien" }]
},
"en;channel=short": {
"runs": [{ "text": "Matches" }],
"derived": { "from": "", "rev": "r:3f9a1c0e7b2d4a55" }
},
"fr": {
"runs": [{ "text": "Fichiers trouvés" }],
"status": "translated",
"origin": { "kind": "ai", "engine": "claude", "tool": "translate" },
"derived": { "from": "", "rev": "r:3f9a1c0e7b2d4a55" }
}
},
"placeholders": [],
"properties": {
"file": "src/FilesHeading.tsx",
"line": 4,
"component": "FilesHeading",
"jsxPath": "FilesHeading > h2",
"element": "h2"
}
}
A translation may add content, restructure text, or re-wrap paired codes, but it must preserve every required placeholder from the source.
A translation that a reader filed under no language, such as an xcstrings
entry keyed by the empty string, has no key of its own, because the
empty key names the source. It travels in the block's unlabelled field, with
the same shape as any other edition.
Validation
The validator reports problems as typed errors, each carrying a machine-readable
kind. Two checks are exposed: a structural well-formedness check over the runs
of every edition of a block (ValidateBlock), and a placeholder-preservation
check of a translation against its source (ValidateTargetAgainstSource).
Validation walks into nested plural forms and select cases as their own scopes.
| Kind | Meaning |
|---|---|
missing-placeholder | A target dropped a required (non-optional) source placeholder. |
extra-placeholder | A target introduced a placeholder not present in the source. |
malformed-runs | A run is not a well-formed discriminated union: it has zero or more than one discriminator key set. |
unknown-placeholder | A source run references a placeholder (or plural/select pivot) not declared in the block's placeholders list. |
unclosed-paired-code | A pcOpen has no matching pcClose in the same runs scope. |
unmatched-close-code | A pcClose has no matching pcOpen. |
duplicate-paired-code | Two pcOpen runs share the same id within a single runs scope. |
You can exercise every one of these rules against both reference implementations on the KBF Tests page.
Companion annotation file (.overlays.jsonl)
Stand-off annotations (review status, protected terms, MT confidence,
terminology matches) live in a companion .overlays.jsonl file, outside the
.kbf.json itself. A .overlays.jsonl file is JSON-Lines: each line
is one compact JSON record (no indentation, LF-terminated), which keeps it
grep- and diff-friendly. The first
non-empty line is a header record ("type": "header"); every subsequent non-empty
line is an annotation record ("type": "annotation").
The header carries annotationType, annotationVersion, a producer (id +
version), a created timestamp, and targetArchive, the archive state the
annotations were produced against. Each annotation record carries a type, an
id, the block it is about, an anchor saying where inside that block, and a
producer-specific data payload (the framework imposes no schema on data).
An anchor is the same Anchor the engine uses everywhere else it records a
position, so a term match written here means what it means in an overlay or a
check finding. Its kind discriminates four shapes:
| Anchor kind | Meaningful fields | Targets |
|---|---|---|
block | n/a | Block-level metadata. |
run | path, runId | A specific run, identified by path and matched against runId. |
range | path, start, end | A half-open span of the run sequence at path. |
form | path, key | A specific plural form or select case, selected by key. |
The path is a list of steps into a block's nested run structure: a bare number
indexes into a Run[], an object {"plural": "<form>"} descends into a plural
form, and {"select": "<value>"} descends into a select case. An empty path
refers to the top-level runs of the block's source edition.
start and end are each a run position: {"run": <index>, "offset": <runes>}, where offset is a rune offset into that run's text and defaults to
0. A range bounded by run positions can cross run boundaries, which a flat
character offset cannot. A phrase running through a bold segment is then one
range, and a boundary can sit either side of a placeholder, which carries no
text of its own.
Resolving an anchor against a block either succeeds or returns one of six machine-readable failure reasons:
| Reason | Meaning |
|---|---|
block-not-found | The anchored block id does not match. |
path-out-of-bounds | A path step indexes past the end of a run sequence. |
path-wrong-kind | The path lands on a run of the wrong kind for the anchor. |
run-id-mismatch | The resolved run's id differs from the recorded runId (a likely orphan). |
range-out-of-bounds | The range addresses positions outside the run sequence it names. |
form-not-found | The anchored plural form or select case does not exist on the block. |
An example header and run-level annotation record:
{"type":"header","annotationType":"@neokapi/example","annotationVersion":"1.0.0","producer":{"id":"@neokapi/kapi-format-examples","version":"0.0.1"},"created":"2026-04-15T12:00:00Z","targetArchive":"sha256:deadbeef"}
{"type":"annotation","id":"term-1","block":"tag-chip","anchor":{"kind":"run","path":[2],"runId":"2"},"data":{"kind":"protected-term","term":"label","action":"preserve-placeholder","confidence":1.0}}
The KBF anatomy page lets you edit a .overlays.jsonl overlay and watch each
anchor resolve against a live document, or fail with one of the reasons above.
Worked examples
These examples walk through the run model one kind at a time, then show a complete document and its annotation overlay. The schema above is the normative reference; this half is the readable companion. Every example is real: they are the golden fixtures shared between the Go and TypeScript implementations.
A block is a flat run sequence
Translation works on a Block. Each of its editions is a flat Run[]:
inline markup, variables, and ICU constructs all live in the sequence, not in
the text. This is what lets a translation step rewrite the readable text while every
placeholder and tag stays first-class and accounted for.
Paired code wrapping a variable
files-heading extracts Files <span className="muted">({count} matched)</span>.
The <span> becomes a paired code, an opening pcOpen and a closing
pcClose that share an id, and {count} becomes a ph placeholder
between them:
{
"id": "files-heading",
"hash": "2xykvb",
"translatable": true,
"type": "jsx:element",
"editions": {
"": {
"runs": [
{ "text": "Files " },
{
"pcOpen": {
"id": "1",
"type": "jsx:element",
"subType": "span",
"data": "<span className=\"muted\">",
"equiv": "muted",
"disp": "span"
}
},
{ "text": "(" },
{
"ph": {
"id": "2",
"type": "jsx:var",
"subType": "number",
"data": "{count}",
"equiv": "count",
"disp": "count"
}
},
{ "text": " matched)" },
{
"pcClose": {
"id": "1",
"type": "jsx:element",
"subType": "span",
"data": "</span>",
"equiv": "muted"
}
}
]
}
},
"placeholders": [
{
"name": "muted",
"kind": "element",
"jsType": "ReactNode",
"sourceExpr": "<span className=\"muted\">...</span>"
},
{
"name": "count",
"kind": "variable",
"jsType": "number",
"sourceExpr": "count"
}
],
"properties": {
"file": "src/FilesHeading.tsx",
"line": 4,
"component": "FilesHeading",
"jsxPath": "FilesHeading > h2",
"element": "h2"
}
}
A French edition re-orders the readable text but keeps both the muted paired
code and the count variable, so it validates clean:
{
"fr": {
"runs": [
{
"pcOpen": {
"id": "1",
"type": "jsx:element",
"subType": "span",
"data": "<span className=\"muted\">",
"equiv": "muted"
}
},
{ "text": "(" },
{
"ph": {
"id": "2",
"type": "jsx:var",
"subType": "number",
"data": "{count}",
"equiv": "count"
}
},
{ "text": " trouvés)" },
{
"pcClose": {
"id": "1",
"type": "jsx:element",
"subType": "span",
"data": "</span>",
"equiv": "muted"
}
}
]
}
}
Optional nodes
tag-chip carries two optional jsx:node placeholders (conditional JSX
expressions that may or may not render) around a required {label} variable.
Because badge and required are declared "optional": true, a target may drop
them; it may not drop label:
{
"editions": {
"": {
"runs": [
{
"ph": {
"id": "1",
"type": "jsx:node",
"subType": "logical-and",
"data": "index !== undefined && <span className=\"badge\">{index}</span>",
"equiv": "badge",
"disp": "⟨badge⟩"
}
},
{ "text": " " },
{
"ph": {
"id": "2",
"type": "jsx:var",
"subType": "string",
"data": "{label}",
"equiv": "label",
"disp": "label"
}
},
{ "text": " " },
{
"ph": {
"id": "3",
"type": "jsx:node",
"subType": "logical-and",
"data": "!deletable && <span className=\"required\">*</span>",
"equiv": "required",
"disp": "⟨required⟩"
}
}
]
}
},
"placeholders": [
{
"name": "badge",
"kind": "node",
"jsType": "ReactNode",
"sourceExpr": "index !== undefined && <span className=\"badge\">{index}</span>",
"optional": true
},
{
"name": "label",
"kind": "variable",
"jsType": "string",
"sourceExpr": "label"
},
{
"name": "required",
"kind": "node",
"jsType": "ReactNode",
"sourceExpr": "!deletable && <span className=\"required\">*</span>",
"optional": true
}
]
}
Plural
A plural run carries an ICU plural construct as structure: a pivot and a
forms map, where each form is its own Run[], so markup and placeholders
inside a clause stay first-class. Form keys are emitted in sorted order on the
wire (one, other, zero):
{
"plural": {
"pivot": "count",
"forms": {
"one": [{ "text": "1 item in your cart" }],
"other": [
{
"ph": {
"id": "1",
"type": "jsx:var",
"subType": "number",
"data": "{count}",
"equiv": "count",
"disp": "count"
}
},
{ "text": " items in your cart" }
],
"zero": [{ "text": "Your cart is empty" }]
}
}
}
The count pivot is declared in placeholders with kind icu-pivot.
Select
A select run is symmetric to plural, but its cases map is keyed by
arbitrary values rather than CLDR plural categories:
{
"select": {
"pivot": "gender",
"cases": {
"female": [{ "text": "She liked your post" }],
"male": [{ "text": "He liked your post" }],
"other": [{ "text": "They liked your post" }]
}
}
}
Subblock
When an outer format extracts a field whose value is itself a mini-document in
another format, the subfilter produces a separate Block and the outer block
points at it with a sub run:
{
"id": "email-body",
"editions": {
"": {
"runs": [
{ "text": "Thanks for signing up. " },
{ "sub": { "id": "1", "ref": "email-cta", "equiv": "cta" } }
]
}
},
"placeholders": [
{
"name": "cta",
"kind": "node",
"jsType": "ReactNode",
"sourceExpr": "<CallToAction/>"
}
]
}
A complete document with an annotation overlay
A full .kbf.json bundles documents and blocks under the envelope; a companion
.overlays.jsonl overlay attaches stand-off annotations by anchor. The header below is one
line; each annotation is one line. The term-1 record anchors to the label
run of tag-chip; entity-1 anchors to a character range; check-1 anchors to the
one plural form; orphan-1 carries a stale runId and will not resolve:
{"type":"header","annotationType":"@neokapi/example","annotationVersion":"1.0.0","producer":{"id":"@neokapi/kapi-format-examples","version":"0.0.1"},"created":"2026-04-15T12:00:00Z","targetArchive":"sha256:deadbeef"}
{"type":"annotation","id":"review-1","block":"files-heading","anchor":{"kind":"block"},"data":{"kind":"review-status","status":"approved"}}
{"type":"annotation","id":"term-1","block":"tag-chip","anchor":{"kind":"run","path":[2],"runId":"2"},"data":{"kind":"protected-term","term":"label"}}
{"type":"annotation","id":"entity-1","block":"files-heading","anchor":{"kind":"range","path":[],"start":{"run":4,"offset":1},"end":{"run":4,"offset":8}},"data":{"kind":"named-entity","entity":"matched"}}
{"type":"annotation","id":"check-1","block":"shopping-cart-plural","anchor":{"kind":"form","path":[0],"key":"one"},"data":{"kind":"qa","note":"singular form reviewed"}}
{"type":"annotation","id":"orphan-1","block":"tag-chip","anchor":{"kind":"run","path":[2],"runId":"99"},"data":{"kind":"protected-term","term":"label"}}
Try it live
The lab below runs the real core/kbf engine in your browser. Pick a sample or
edit the .kbf.json directly. It re-parses, canonicalizes, renders each block, and
validates the run structure as you type. Edit the .overlays.jsonl overlay and click an
annotation to resolve it against the live document and see exactly what it points
at (or why it does not resolve).
For the cross-engine conformance suite, where these same operations run against both the Go engine and the TypeScript mirror, see the KBF Tests page.
See also
- Overview: how the bundle relates to kapi's other formats.
- Overlays: the
.overlays.jsonlsidecar specified in full. - Choosing a format: the mapping onto XLIFF 2.x, and when to use it.
- Content model: the in-memory
Block/Runmodel this serializes. - Project file: the
kapi.yamlrecipe driving extraction and merge.