> ## Documentation Index
> Fetch the complete documentation index at: https://narrator.ami.rip/llms.txt
> Use this file to discover all available pages before exploring further.

# Narrator

> Read TypeScript and JavaScript as plain English sentences, generated deterministically without an LLM.

export const showcase = [{
  id: "plain",
  label: "TypeScript",
  file: "renew.ts",
  code: [[["kw", "export"], ["", " "], ["kw", "async"], ["", " "], ["kw", "function"], ["", " "], ["fn", "renew"], ["", "(account: "], ["ty", "Account"], ["", ", now: "], ["ty", "Date"], ["", ") {"]], [["", "  "], ["kw", "if"], ["", " (account.status === "], ["st", '"canceled"'], ["", ") "], ["kw", "return"], ["", ";"]], [["", "  "], ["kw", "const"], ["", " plan = plans."], ["fn", "find"], ["", "((p) => p.id === account.planId);"]], [["", "  "], ["kw", "if"], ["", " (!plan) "], ["kw", "throw"], ["", " "], ["kw", "new"], ["", " "], ["fn", "NotFoundError"], ["", "("], ["st", '"Unknown plan"'], ["", ");"]], [["", "  "], ["kw", "for"], ["", " ("], ["kw", "const"], ["", " seat "], ["kw", "of"], ["", " account.seats) {"]], [["", "    "], ["kw", "if"], ["", " (seat.expiresAt < now) "], ["kw", "await"], ["", " "], ["fn", "releaseSeat"], ["", "(seat);"]], [["", "  }"]], [["", "  "], ["kw", "await"], ["", " "], ["fn", "sendReceipt"], ["", "({ to: account.ownerEmail, amount: plan.price });"]], [["", "}"]]],
  english: [{
    d: 0,
    kind: "head",
    from: 1,
    to: 9,
    s: [["f", "To renew"], ["", " (async), given "], ["v", "account"], ["", " "], ["t", "(Account)"], ["", " and "], ["v", "now"], ["", " "], ["t", "(Date)"], ["", ":"]]
  }, {
    d: 1,
    kind: "stmt",
    from: 2,
    to: 2,
    s: [["", "If "], ["v", "account"], ["", "'s "], ["v", "status"], ["", " is "], ["l", '"canceled"'], ["", ", stop here."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 3,
    to: 3,
    s: [["", "Let "], ["v", "plan"], ["", " be the first "], ["v", "p"], ["", " in "], ["v", "plans"], ["", " where "], ["v", "p"], ["", "'s "], ["v", "ID"], ["", " is "], ["v", "account"], ["", "'s "], ["v", "plan ID"], ["", "."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 4,
    to: 4,
    s: [["", "If "], ["v", "plan"], ["", " is missing, fail with "], ["f", "a not found error"], ["", ": "], ["l", '"Unknown plan"'], ["", "."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 5,
    to: 7,
    s: [["", "For each "], ["v", "seat"], ["", " in "], ["v", "account"], ["", "'s "], ["v", "seats"], ["", ":"]]
  }, {
    d: 2,
    kind: "stmt",
    from: 6,
    to: 6,
    s: [["", "If "], ["v", "seat"], ["", "'s "], ["v", "expires at"], ["", " is before "], ["v", "now"], ["", ", "], ["f", "release seat"], ["", " ("], ["v", "seat"], ["", ")."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 8,
    to: 8,
    s: [["f", "Send receipt"], ["", " ("], ["v", "to"], ["", ": "], ["v", "account"], ["", "'s "], ["v", "owner email"], ["", " and "], ["v", "amount"], ["", ": "], ["v", "plan"], ["", "'s "], ["v", "price"], ["", ")."]]
  }]
}, {
  id: "react",
  label: "React",
  file: "Cart.tsx",
  code: [[["kw", "import"], ["", " { useState } "], ["kw", "from"], ["", " "], ["st", '"react"'], ["", ";"]], [], [["kw", "export"], ["", " "], ["kw", "function"], ["", " "], ["fn", "Cart"], ["", "({ items, onCheckout }) {"]], [["", "  "], ["kw", "const"], ["", " [coupon, setCoupon] = "], ["fn", "useState"], ["", "("], ["st", '""'], ["", ");"]], [["", "  "], ["kw", "const"], ["", " total = items."], ["fn", "reduce"], ["", "((sum, item) => sum + item.price, "], ["nu", "0"], ["", ");"]], [["", "  "], ["kw", "if"], ["", " (items.length === "], ["nu", "0"], ["", ") "], ["kw", "return"], ["", " <p>"], ["ty", "Your"], ["", " cart is empty.</p>;"]], [["", "  "], ["kw", "return"], ["", " ("]], [["", "    <form onSubmit={() => "], ["fn", "onCheckout"], ["", "(coupon)}>"]], [["", "      <input value={coupon} onChange={(e) => "], ["fn", "setCoupon"], ["", "(e.target.value)} />"]], [["", "      <button type="], ["st", '"submit"'], ["", " disabled={total === "], ["nu", "0"], ["", "}>"], ["ty", "Pay"], ["", "</button>"]], [["", "    </form>"]], [["", "  );"]], [["", "}"]]],
  english: [{
    d: 0,
    kind: "head",
    from: 3,
    to: 13,
    s: [["f", "Cart"], ["", ", a component that takes "], ["v", "items"], ["", " and "], ["v", "on checkout"], ["", ":"]]
  }, {
    d: 1,
    kind: "stmt",
    from: 4,
    to: 4,
    s: [["", "Keep track of "], ["v", "coupon"], ["", " (starts as "], ["l", '""'], ["", ")."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 5,
    to: 5,
    s: [["", "Let "], ["v", "total"], ["", " be the total "], ["v", "price"], ["", " across "], ["v", "items"], ["", "."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 6,
    to: 6,
    s: [["", "If "], ["v", "items"], ["", " are empty, show a paragraph "], ["l", '"Your cart is empty."'], ["", "."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 7,
    to: 12,
    s: [["", "Show a form (when submitted, trigger "], ["f", "on checkout"], ["", " ("], ["v", "coupon"], ["", ")) containing:"]]
  }, {
    d: 2,
    kind: "bullet",
    from: 7,
    to: 12,
    s: [["", "• an input ("], ["v", "value"], ["", ": "], ["v", "coupon"], ["", "; when changed, set "], ["v", "coupon"], ["", " to "], ["v", "e"], ["", "'s "], ["v", "target"], ["", "'s "], ["v", "value"], ["", ")"]]
  }, {
    d: 2,
    kind: "bullet",
    from: 7,
    to: 12,
    s: [["", "• a submit button "], ["l", '"Pay"'], ["", " ("], ["v", "disabled"], ["", " when "], ["v", "total"], ["", " is "], ["l", "0"], ["", ")"]]
  }]
}, {
  id: "drizzle",
  label: "Drizzle",
  file: "users.ts",
  code: [[["kw", "export"], ["", " "], ["kw", "async"], ["", " "], ["kw", "function"], ["", " "], ["fn", "findActiveUsers"], ["", "(orgId: string) {"]], [["", "  "], ["kw", "return"], ["", " db"]], [["", "    ."], ["fn", "select"], ["", "()"]], [["", "    ."], ["kw", "from"], ["", "(users)"]], [["", "    ."], ["fn", "where"], ["", "("], ["fn", "and"], ["", "("], ["fn", "eq"], ["", "(users.orgId, orgId), "], ["fn", "isNull"], ["", "(users.deletedAt)))"]], [["", "    ."], ["fn", "orderBy"], ["", "("], ["fn", "desc"], ["", "(users.createdAt))"]], [["", "    ."], ["fn", "limit"], ["", "("], ["nu", "50"], ["", ");"]], [["", "}"]]],
  english: [{
    d: 0,
    kind: "head",
    from: 1,
    to: 8,
    s: [["f", "To find active users"], ["", " (async), given "], ["v", "org ID"], ["", ":"]]
  }, {
    d: 1,
    kind: "stmt",
    from: 2,
    to: 7,
    s: [["", "Give back the rows of "], ["v", "users"], ["", ":"]]
  }, {
    d: 2,
    kind: "bullet",
    from: 2,
    to: 7,
    s: [["", "• where all of these are true:"]]
  }, {
    d: 3,
    kind: "bullet",
    from: 2,
    to: 7,
    s: [["", "• "], ["v", "users"], ["", "' "], ["v", "org ID"], ["", " is "], ["v", "org ID"]]
  }, {
    d: 3,
    kind: "bullet",
    from: 2,
    to: 7,
    s: [["", "• "], ["v", "users"], ["", "' "], ["v", "deleted at"], ["", " is empty"]]
  }, {
    d: 2,
    kind: "bullet",
    from: 2,
    to: 7,
    s: [["", "• sorted by "], ["v", "users"], ["", "' "], ["v", "created at"], ["", " (newest first)"]]
  }, {
    d: 2,
    kind: "bullet",
    from: 2,
    to: 7,
    s: [["", "• at most "], ["l", "50"], ["", " rows"]]
  }]
}, {
  id: "hono",
  label: "Hono",
  file: "api.ts",
  code: [[["kw", "import"], ["", " { "], ["ty", "Hono"], ["", " } "], ["kw", "from"], ["", " "], ["st", '"hono"'], ["", ";"]], [], [["kw", "const"], ["", " app = "], ["kw", "new"], ["", " "], ["fn", "Hono"], ["", "();"]], [], [["", "app."], ["fn", "get"], ["", "("], ["st", '"/users/:id"'], ["", ", "], ["kw", "async"], ["", " (c) => {"]], [["", "  "], ["kw", "const"], ["", " id = c.req."], ["fn", "param"], ["", "("], ["st", '"id"'], ["", ");"]], [["", "  "], ["kw", "const"], ["", " user = "], ["kw", "await"], ["", " "], ["fn", "findUser"], ["", "(id);"]], [["", "  "], ["kw", "if"], ["", " (!user) "], ["kw", "return"], ["", " c."], ["fn", "notFound"], ["", "();"]], [["", "  "], ["kw", "return"], ["", " c."], ["fn", "json"], ["", "(user);"]], [["", "});"]], [], [["kw", "export"], ["", " default app;"]]],
  english: [{
    d: 0,
    kind: "stmt",
    from: 3,
    to: 3,
    s: [["", "Create a Hono app as "], ["v", "app"], ["", "."]]
  }, null, {
    d: 0,
    kind: "stmt",
    from: 5,
    to: 10,
    s: [["", "Answer "], ["l", "GET"], ["", " "], ["c", "/users/:id"], ["", ":"]]
  }, {
    d: 1,
    kind: "stmt",
    from: 6,
    to: 6,
    s: [["", "Let "], ["v", "ID"], ["", " be the "], ["c", "id"], ["", " path parameter."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 7,
    to: 7,
    s: [["f", "Find user"], ["", " ("], ["v", "ID"], ["", ")."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 8,
    to: 8,
    s: [["", "If "], ["v", "user"], ["", " is missing, reply with the not-found response."]]
  }, {
    d: 1,
    kind: "stmt",
    from: 9,
    to: 9,
    s: [["", "Reply with "], ["v", "user"], ["", " as JSON."]]
  }, null, {
    d: 0,
    kind: "stmt",
    from: 12,
    to: 12,
    s: [["", "Export "], ["v", "app"], ["", " as this module's request handler."]]
  }]
}];

export const Showcase = ({examples}) => {
  const [active, setActive] = useState(0);
  const [hover, setHover] = useState(null);
  const ex = examples[active];
  const lineInRange = (n, l) => l && l.from !== null && n >= l.from && n <= l.to;
  const codeLit = n => hover && (hover.side === "code" ? hover.n === n : lineInRange(n, ex.english[hover.i]));
  const span = l => l.to - l.from;
  const narrowest = n => Math.min(...ex.english.filter(l => lineInRange(n, l)).map(span));
  const englishLit = i => hover && (hover.side === "english" ? hover.i === i : lineInRange(hover.n, ex.english[i]) && span(ex.english[i]) === narrowest(hover.n));
  const pick = i => {
    setActive(i);
    setHover(null);
  };
  return <div className="nr-showcase not-prose">
			<div className="nr-showcase-bar">
				<div className="nr-tabs" role="tablist">
					{examples.map((e, i) => <button key={e.id} role="tab" aria-selected={i === active} className={i === active ? "nr-tab nr-tab-on" : "nr-tab"} onClick={() => pick(i)}>
							{e.label}
						</button>)}
				</div>
				<span className="nr-file">{ex.file}</span>
			</div>
			<div className="nr-panes" onMouseLeave={() => setHover(null)}>
				<div>
					<div className="nr-pane-label">Code</div>
					<pre className="nr-code" aria-label="Source code">
						{ex.code.map((segs, i) => <div key={i} className={codeLit(i + 1) ? "nr-cl nr-lit" : "nr-cl"} onMouseEnter={() => setHover({
    side: "code",
    n: i + 1
  })}>
								<span className="nr-ln">{i + 1}</span>
								<span>
									{segs.map(([k, t], j) => <span key={j} className={k ? `nr-${k}` : undefined}>
											{t}
										</span>)}
									{segs.length === 0 ? " " : null}
								</span>
							</div>)}
					</pre>
				</div>
				<div className="nr-english" aria-label="Narration">
					<div className="nr-pane-label">English</div>
					{ex.english.map((l, i) => l === null ? <div key={i} className="nr-gap" /> : <div key={i} className={`nr-el nr-${l.kind} nr-d${Math.min(l.d, 5)}${englishLit(i) ? " nr-lit" : ""}`} onMouseEnter={() => setHover({
    side: "english",
    i
  })}>
								{l.s.map(([k, t], j) => <span key={j} className={k ? `nr-m-${k}` : undefined}>
										{t}
									</span>)}
							</div>)}
				</div>
			</div>
			<div className="nr-caption">Generated by Narrator from the code on the left. Hover a line to see where it came from.</div>
		</div>;
};

<div className="nr-hero not-prose">
  <h1>Read code in plain English</h1>
  <p>Narrator turns TypeScript and JavaScript into clear sentences, deterministically and without an LLM. It runs on your server or in the browser.</p>

  <div className="nr-cta">
    <a className="nr-btn nr-btn-primary" href="/getting-started">Get started</a>
    <a className="nr-btn" href="/guides/browser-extension">Browser extension</a>
  </div>
</div>

<Showcase examples={showcase} />

## What it is

Narrator is an SDK, published under the `@usenarrator/*` scope, that turns source files and diffs into English sentences. Every phrase comes from a syntax node and a rule, so the same code always reads the same way, and you can point at the rule that produced any sentence.

<Columns cols={3}>
  <Card title="Programmatic, not generative" icon="cog">
    Narrator uses rules, not a language model, so the same code always produces the same English. It is fast enough to narrate a whole pull request when the page loads.
  </Card>

  <Card title="Runs anywhere" icon="globe">
    It uses the native oxc parser on Node and Bun, and a WASM build of the same parser in browsers, extension workers and edge runtimes.
  </Card>

  <Card title="Pluggable on three axes" icon="blocks">
    You can add source languages (TypeScript and JavaScript today), output languages (English today), and library plugins. Twenty-five ship today, from React and Next.js to Drizzle, Stripe and Temporal.
  </Card>
</Columns>

## Why it exists

More and more code is written quickly, often with help from AI tools, and someone still has to review it. Reading a diff line by line is slow, and a summary written by a model can sound right while missing the one condition that matters. Narrator sits between the two. It reads every statement, so nothing is skipped, and it says what the code does in sentences you can skim. When the English looks wrong, the code usually is too.

It also helps people who read code less often than they write it: a product manager checking what a pricing rule really does, a new teammate finding their way around, or a reviewer working in a library they don't know well.

## A 30-second demo

The `narrator` CLI narrates files, directories and git diffs. Point it at a file:

```ts billing.ts theme={null}
export async function renewSubscription(subscription: Subscription, now: Date) {
  if (subscription.status === "canceled") return;
  const plan = plans.find((p) => p.id === subscription.planId);
  if (!plan) throw new NotFoundError("Unknown plan");
  for (const seat of subscription.seats) {
    if (seat.expiresAt < now) await releaseSeat(seat);
  }
  await sendReceipt({ to: subscription.ownerEmail, amount: plan.price });
}
```

```text narrator billing.ts --plain theme={null}
To renew subscription (async), given subscription (Subscription) and now (Date):
  If subscription's status is "canceled", stop here.
  Let plan be the first p in plans where p's ID is subscription's plan ID.
  If plan is missing, fail with a not found error: "Unknown plan".
  For each seat in subscription's seats:
    If seat's expires at is before now, release seat (seat).
  Send receipt (to: subscription's owner email and amount: plan's price).
```

A few things to notice: `ownerEmail` became "owner email" (identifiers are split into words, and abbreviations expand through a glossary), the `find` callback became "the first p in plans where…", and the guard clause became a single "If …, stop here." Nothing was summarized away, because Narrator narrates every statement instead of paraphrasing the whole function.

## Reading diffs on GitHub

The browser extension uses the same engine to replace GitHub's diff rows with English, so you can review what a pull request does before you read how it does it.

<Frame caption="The extension on a public pull request. Each changed declaration gets its own block, with added sentences in green.">
  <img src="https://mintcdn.com/narrator-e87f5984/eZy1xgjCws85ObLC/images/extension-diff.png?fit=max&auto=format&n=eZy1xgjCws85ObLC&q=85&s=cda380b80fd14f3077660449ab87019c" alt="A GitHub pull request diff where the code rows have been replaced by English sentences" width="1050" height="502" data-path="images/extension-diff.png" />
</Frame>

## Where to go next

<Columns cols={2}>
  <Card title="Getting started" icon="rocket" href="/getting-started">
    Narrate a file and a diff from your own code in five minutes.
  </Card>

  <Card title="How it works" icon="workflow" href="/concepts/how-it-works">
    Follow a statement from source code through the engine and plugins to the finished sentence.
  </Card>

  <Card title="Browser extension" icon="github" href="/guides/browser-extension">
    Read GitHub pull requests in English, inline, in either split or unified view.
  </Card>

  <Card title="Plugins" icon="puzzle" href="/plugins/overview">
    Add plugins so that calls to your libraries read naturally.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.