Skip to main content
Narrator reads code the way a careful colleague would read it out loud. It parses the file into a syntax tree, walks that tree statement by statement, and asks a set of rules how to say each piece. The words themselves live in a phrasebook, so the same rules can speak more than one human language.

The pieces

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.
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.
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.
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.
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.
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.
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.
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
The engine sees a variable declaration, so it uses the phrase “Let X be Y.” The value is a chain of calls on one list. The 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.