Gå til hovedinnhold

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 flat sequence of Runs, with optional per-locale targets. Its marker segment and Go package are both kbf, for Kapi Bundle Format.

This page is the normative schema, followed by worked examples of every run kind. For where the bundle sits among kapi's other formats, start with the overview.

What it is not for

A content bundle is an authoring and exchange format, not a runtime catalog. It is intentionally heavy: 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/kbf in 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 (target locales, 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 1.0, carried in the schemaVersion field of every file. Versions follow a MAJOR.MINOR contract:

  • Major — a breaking change. A consumer must reject a file whose major version it does not recognize.
  • Minor — 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 does not match the version the build speaks.

File envelope

The top-level File object is the .kbf.json document. Field order and shape are normative — deterministic serialization is what keeps the content hash stable across the Go and TypeScript implementations.

FieldTypeRequiredDescription
schemaVersionstringyesWire format version, e.g. "1.0". Subject to the versioning contract above.
kindstringyesMagic string; must be "kapi-bundle".
createdstringnoCreation timestamp (RFC 3339). Omitted when empty.
generatorGeneratorInfoyesThe extractor that produced the file.
projectProjectInfoyesThe project the file belongs to.
vocabularyVocabularynoVocabulary packs this file's runs depend on. Omitted when absent.
documentsDocument[]yesOne entry per extracted source file.

GeneratorInfo

Identifies the extractor that produced the file.

FieldTypeRequiredDescription
idstringyesExtractor identifier, e.g. "@neokapi/kapi-format-examples".
versionstringyesExtractor version.
capabilitiesstring[]noDeclared capabilities, e.g. ["extract", "preview"]. Omitted when empty.

ProjectInfo

Identifies the project the file belongs to.

FieldTypeRequiredDescription
idstringyesProject identifier.
sourceLocalestringyesBCP-47 locale of the source runs.

Vocabulary

Lists the vocabulary packs whose span-type entries the file's runs expect a consumer to have loaded.

FieldTypeRequiredDescription
extendsstring[]noNames of vocabulary packs, e.g. ["common-formatting", "rich-jsx"].

Document

A Document is one source file's worth of extracted content.

FieldTypeRequiredDescription
idstringyesStable identifier for the document within the file.
documentTypestringyesSource format discriminator. The Go implementation defines "jsx".
pathstringyesPath of the source file the content was extracted from.
sourceHashstringnoContent hash of the original source file. Omitted when empty.
skeletonSkeletonnoReference to the opaque skeleton used to reconstruct the source on merge.
blocksBlock[]yesThe translatable units 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.

FieldTypeRequiredDescription
refstringnoReference to an external skeleton payload. Omitted when empty.
inlinestringnoInline skeleton payload. Omitted when empty.

Block

A Block is the unit of translation tracking. Its source is a flat sequence of Runs in the project's source locale; committed translations live in the optional per-locale targets map (see Targets).

FieldTypeRequiredDescription
idstringyesStable identifier within the document.
hashstringyesContent hash of the block, used for deduplication and change detection.
translatableboolyesWhether the block should be offered for translation.
typestringyesCoarse block classification: "jsx:element", "jsx:attribute", or "js:t".
sourceRun[]yesSource-locale content as a run sequence.
targetsmap<locale, Run[]>noCommitted translations, keyed by BCP-47 locale. Omitted when there are none.
placeholdersPlaceholder[]yesMetadata for every placeholder, paired code, and ICU pivot referenced by the runs.
propertiesBlockPropertiesyesProvenance metadata (source location, component, JSX path).
previewBlockPreviewHintsnoOptional hints for rendering a preview of the block. Omitted when absent.

BlockProperties

Provenance metadata describing where the block was extracted from.

FieldTypeDescription
filestringSource file path.
lineintLine number in the source file.
componentstringEnclosing component name.
jsxPathstringPath to the element within the component tree.
elementstringThe element tag the content was extracted from.
locNotestringTranslator note (ITS locNote). Omitted when empty.

BlockPreviewHints

Hints used to render a representative preview of a block.

FieldTypeDescription
storyIdstringStorybook story identifier. Omitted when empty.
snapshotPathstringPath to a rendered snapshot. Omitted when empty.
sampleValuesmap<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.

FieldTypeRequiredDescription
namestringyesPlaceholder name. Matches the equiv of the corresponding run, or the pivot of a plural/select construct.
kindstringyesOne of "variable", "element", "node", or "icu-pivot".
jsTypestringnoThe JavaScript/TypeScript type of the value, e.g. "number", "string", "ReactNode". Omitted when empty.
sourceExprstringyesThe source expression the placeholder was extracted from.
optionalboolnoWhen 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 — {"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.

FieldTypeRequiredDescription
idstringyesRun identifier, unique within the block.
typestringyesProducer-specific run type, e.g. "jsx:var", "jsx:node".
subTypestringnoFiner classification, e.g. "number", "string". Omitted when empty.
datastringyesThe original token the reader captured, e.g. "{count}".
equivstringyesEquivalence name; the placeholder this run stands for.
dispstringnoShort display label for chips. Falls back to equiv. Omitted when empty.
constraintsRunConstraintsnoPer-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.

FieldTypeRequiredDescription
idstringyesRun identifier within the block.
refstringyesThe id of the referenced subblock.
equivstringyesEquivalence 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.

FieldTypeRequiredDescription
pivotstringyesThe variable driving plural selection. Declared in placeholders with kind icu-pivot.
formsmap<form, Run[]>yesOne 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.

FieldTypeRequiredDescription
pivotstringyesThe variable driving selection. Declared in placeholders.
casesmap<string, Run[]>yesOne 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.

FieldTypeDescription
deletableboolWhether the run may be deleted in a target.
cloneableboolWhether the run may be duplicated in a target.
reorderableboolWhether the run may be moved within a target.

Targets

A block's committed translations live in targets, a map keyed by BCP-47 locale whose value is a Run[] — the translated content for that locale, using exactly the same run primitives as source. The field is omitted entirely when a block has no targets. Locale keys are sorted on the wire.

{
"id": "files-heading",
"hash": "2xykvb",
"translatable": true,
"type": "jsx:element",
"source": [{ "text": "Files matched" }],
"targets": {
"de": [{ "text": "Gefundene Dateien" }],
"fr": [{ "text": "Fichiers trouvés" }]
},
"placeholders": [],
"properties": {
"file": "src/FilesHeading.tsx",
"line": 4,
"component": "FilesHeading",
"jsxPath": "FilesHeading > h2",
"element": "h2"
}
}

A target may add content, restructure text, or re-wrap paired codes, but it must preserve every required placeholder from the source.

Validation

The validator reports problems as typed errors, each carrying a machine-readable kind. Two checks are exposed: a structural well-formedness check over a block's runs (ValidateBlock), and a placeholder-preservation check of a target against its source (ValidateTargetAgainstSource). Validation walks into nested plural forms and select cases as their own scopes.

KindMeaning
missing-placeholderA target dropped a required (non-optional) source placeholder.
extra-placeholderA target introduced a placeholder not present in the source.
malformed-runsA run is not a well-formed discriminated union — it has zero or more than one discriminator key set.
unknown-placeholderA source run references a placeholder (or plural/select pivot) not declared in the block's placeholders list.
unclosed-paired-codeA pcOpen has no matching pcClose in the same runs scope.
unmatched-close-codeA pcClose has no matching pcOpen.
duplicate-paired-codeTwo 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 rather than inside 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, an anchor, and a producer-specific data payload (the framework imposes no schema on data).

An anchor binds an annotation to a location in a block. Its kind discriminates four shapes:

Anchor kindMeaningful fieldsTargets
blockblockBlock-level metadata.
runblock, path, runIdA specific run, identified by path and matched against runId.
rangeblock, path, offset, lengthA character range inside a text run (offsets in UTF-16 code units).
formblock, path, keyA 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 block's top-level source runs.

Resolving an anchor against a block either succeeds or returns one of six machine-readable failure reasons:

ReasonMeaning
block-not-foundThe anchored block id does not match.
path-out-of-boundsA path step indexes past the end of a run sequence.
path-wrong-kindThe path lands on a run of the wrong kind for the anchor.
run-id-mismatchThe resolved run's id differs from the recorded runId (a likely orphan).
range-out-of-boundsThe character range exceeds the target text run.
form-not-foundThe 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","anchor":{"kind":"run","block":"tag-chip","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 — or fail with one of the reasons above — against a live document.

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

The unit of translation is a Block. Its source 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",
"source": [
{ "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 target re-orders the readable text but keeps both the muted paired code and the count variable — so it validates clean:

{
"fr": [
{
"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:

{
"source": [
{
"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",
"source": [
{ "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; qa-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","anchor":{"kind":"block","block":"files-heading"},"data":{"kind":"review-status","status":"approved"}}
{"type":"annotation","id":"term-1","anchor":{"kind":"run","block":"tag-chip","path":[2],"runId":"2"},"data":{"kind":"protected-term","term":"label"}}
{"type":"annotation","id":"entity-1","anchor":{"kind":"range","block":"files-heading","path":[4],"offset":1,"length":7},"data":{"kind":"named-entity","entity":"matched"}}
{"type":"annotation","id":"qa-1","anchor":{"kind":"form","block":"shopping-cart-plural","path":[0],"key":"one"},"data":{"kind":"qa","note":"singular form reviewed"}}
{"type":"annotation","id":"orphan-1","anchor":{"kind":"run","block":"tag-chip","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).

Loading the interactive KBF lab…

For the cross-engine conformance suite — 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.jsonl sidecar specified in full.
  • Choosing a format — the mapping onto XLIFF 2.x, and when to use it.
  • Content model — the in-memory Block/Run model this serializes.
  • Project file — the kapi.yaml recipe driving extraction and merge.