The pieces
Source language
Source language
A source language decides which files it can read and creates a file narrator for each one.
@usenarrator/lang-ts is the only source language today. It handles .ts, .tsx, .js, .jsx, .mjs and .cjs, and skips .d.ts files. See Adding a source language.Parser
Parser
The TypeScript language does not parse code itself. You hand it a parser:
oxcParser uses the native oxc binding on Node and Bun, and createWasmParser() loads the WASM build of the same oxc version for browsers, workers and edge runtimes. Both produce the same ESTree-shaped tree, so narration is identical. See Embedding in your app.Engine
Engine
The engine walks the tree. For each call it looks for a plugin rule keyed by the callee, such as
*.filter or z.object, and it does the same for property reads, tagged templates and JSX. If no rule claims a node, it falls back to generic phrasing built from the identifier names. See the Engine API.Plugins
Plugins
A plugin is a bundle of rules, vocabulary and phrases for one library or codebase. The built-in
std plugin covers JavaScript itself: arrays, strings, Math, JSON, Promise, Map, Set and the console. Later plugins win when two rules match the same call. See Plugins.Dependencies
Dependencies
Before narrating a file, the engine learns what its package depends on from the nearest
package.json. Plugins combine that with the file’s imports to decide whether they apply, and which major version of their library to read. See Dependency detection.Locale and phrasebooks
Locale and phrasebooks
A locale supplies grammar (lists, articles, plurals, possessives) and every core phrase. Each plugin ships its own phrasebook per locale, and a locale can override any plugin’s phrases. See Translating Narrator.
Lines
Lines
The result of narrating a file is an array of
Line objects. Each line has an indent depth, a kind, the source offsets it describes, and text with inline markup that marks names, functions, types and literals.Units and diffs
Units and diffs
To compare two versions of a file, Narrator splits each version into units, one per top-level declaration, and diffs the English of matching units. See Units and diffs.
One statement, step by step
Take this line:English
std plugin has rules for *.filter and *.map, and because the calls form a pipeline over the same list, it reads them as numbered steps instead of one long nested phrase. The property paidAt is split into “paid at”, and !inv.paidAt becomes “is missing” rather than “is not truthy”, because that is what the check means in practice.
None of this involves guessing. Each of those choices is a rule you can read in the source, and a plugin can override any of them.
Names carry a lot of meaning
Most of the meaning in real code lives in identifiers, so Narrator works hard to read them well. It splits camelCase and snake_case, expands abbreviations through a glossary, and looks at the first word to decide what kind of thing a name is.English
isTrialing starts with “is”, so it reads as a yes or no question. buildInvoice starts with a verb, so it reads as an action that produces something. A plugin can add its own verbs, glossary entries and acronyms through its vocabulary field.