npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@transglot/incontext

v0.2.0

Published

Zero-dependency runtime SDK for the transglot in-context editor: watermarking, ALT+click overlay, and the scoped-session API client.

Readme

@transglot/incontext

Zero-dependency runtime SDK core for the transglot in-context editor. It watermarks rendered strings, tracks them per page, survives host re-renders by re-walking those marks rather than by holding element references, opens the ALT+click Shadow-DOM overlay, optionally captures a screenshot, and talks to the scoped-session API.

It is also the whole editor: mountIncontextEditor() assembles every part below into a working editing surface in one call, with no framework involved. The framework adapters (for example @transglot/react) are thin layers over that same call, so a Vue, Svelte, Angular or plain-<script> host gets the identical editor without waiting for an adapter to exist.

This is NOT the runtime i18n loader. For loading published bundles at runtime, see @transglot/runtime and its createClient contract.

Install

npm install @transglot/incontext

Mount the editor

Two things have to happen on a page: the editor has to be mounted, and the strings the host renders have to carry a watermark so an ALT+click can resolve them back to a key. mountIncontextEditor does the first; wrapT does the second, against the registry the mount hands back.

import { createRegistry, mountIncontextEditor, wrapT } from '@transglot/incontext';

let locale = 'fr';
const registry = createRegistry();

const editor = mountIncontextEditor({
  url: 'https://app.example.com',
  publishableKey: 'taip_…',
  registry,
  locale,
  // Editing mode is OFF until you ask for it. Ask only for a reviewer: see
  // "Who should see this" below.
  enterOnMount: currentUser.mayReviewTranslations,
  // The SDK cannot re-render your tree, so it tells you when to.
  onLocaleChange: (next) => {
    locale = next;
    render();
  },
});

// Wrap YOUR translate function. Every string it returns is registered as a
// (key, locale) pair and suffixed with an invisible mark: the visible UI is
// unchanged, and ALT+click on any rendered string resolves back to its key.
const t = wrapT((key) => bundles[locale][key] ?? key, registry, () => editor.session.state().locale);

function render() {
  document.querySelector('#title').textContent = t('home.title');
}

render();

That is the entire integration. dev/vanilla.html is a runnable version of it (open it after npm run build); dev/harness.html is the narrower overlay-only harness.

Who should see this

The editor is for reviewers. Do not mount it for the public.

Editing mode is not a quiet developer tool. It paints a floating bar on the page carrying your project's per-language completion percentages, with the human-versus-machine texture behind each one, plus a link to the transglot console. ALT+click then opens any string on the page for editing. That is business information about your product plans, and on a public page anyone can read it.

The SDK cannot help you here: it has no way to tell a reviewer from a visitor, and no way to tell your production site from your staging one. Only your app knows. So mounting is inert by default, and turning editing mode on is something a host asks for, once, on purpose:

  • enterOnMount: true when the person looking at the page may review translations, or
  • leave it off and call editor.session.enter() from your own control (an internal-only toolbar, a query flag your staff use, a keyboard shortcut behind your own permission check).

Better still, gate the mount itself, so a visitor's page never loads the editor at all. getNonce is a separate decision about WRITING, not about seeing: a session with no nonce is read-only, and its bar still reports your progress.

What the mount owns

  • the ONE shared shadow root, with the single stylesheet installed into it, so the overlay, the bottom bar and the editing dialog cannot restyle each other or the host page,
  • the ALT-hold highlight and ALT+click resolution, and opening the dialog for whatever was clicked,
  • the dialog's write and suggest capability, kept in lock-step with the session as the scoped token resolves,
  • the bar: locale switching, adding a language, and leaving editing mode,
  • repainting the host's own light DOM as a backfill lands, and after a save is refused because someone else got there first,
  • best-effort screenshot capture and upload before an AI suggestion,
  • a teardown that stops the poll, releases every listener, and leaves the page as a non-editing visit would have left it.

Options

| option | required | meaning | | --- | --- | --- | | url + publishableKey | yes, unless client | the transglot app origin and the page's publishable key | | client | no | a pre-built IncontextClient, instead of url + publishableKey | | locale | yes | the locale being reviewed on this page | | enterOnMount | no | enter editing mode once the session mints (default false, see "Who should see this") | | registry | no | the registry your wrapT marks against; one is created when omitted | | reviewer | no | label recorded on every write; accepts a getter for a value that changes | | getNonce + writableKeys | no | how a session obtains WRITE (see below); read-only without them | | theme | no | 'light' \| 'dark' \| 'auto' (default 'auto'), resolved once at mount | | consoleUrl | no | where a reviewer who cannot add a language is pointed | | onLocaleChange | no | re-render your own i18n after a switch | | onRowsLanded | no | rows read on each backfill tick and on each locale switch, after the SDK has repainted what it can see | | onReady | no | called with the session before any surface installs | | onError | no | told about a failed session mint, a failed ALT+click read, or a locale list that would not load; supply it to route those into your own UI, omit it and the editor says so in its own chrome | | captureElement | no | supply your own screenshot pipeline instead of the optional html2canvas peer | | document | no | injectable document (tests, iframes) | | fetchImpl | no | injected fetch (tests / non-browser) |

The returned handle exposes session, registry, client, shadowRoot() and destroy(). session is how a host builds its own affordances:

editor.session.enter();                    // turn editing mode on from your own button
editor.session.subscribe((state) => …);    // active, locale, canWrite, backfill progress
await editor.session.setLocale('de');
editor.destroy();                          // idempotent

One key per element, when a sentence needs markup

A watermark is an invisible run of characters appended to the string your t() returned, so it only stays meaningful while that string renders as one text node. A translation that carries inline markup does not:

<!-- One key. Parses into three nodes, and the mark lands in whichever one is last. -->
<p>Stamped with <strong>the carrier time</strong> and your local time.</p>

The SDK will not update that string on the page. Both writers (the backfill repaint and the re-mark after a save) check the element first, and decline it when it holds more than one text node or any child element, because neither the mark nor the DOM can say which node is "the string". Declining leaves a stale but correct sentence; guessing would leave a mangled one. repaintLandedRows reports those as skipped, and reWatermark returns a notice to put in front of the reviewer: their save landed and the row is correct, the page simply did not move under it.

Split the sentence into one key per element instead, and every part stays independently editable and live-updating:

<p><span>{t('ship.stampedWith')}</span><strong>{t('ship.carrierTime')}</strong><span>{t('ship.andLocal')}</span></p>

Attribute strings (placeholder, title, alt, aria-label) have no text node at all, so they never carry a mark and are not editable in place either.

Read-only by default, and why

The publishable key is public: it ships in your page source, so it grants read and nothing else. To let a reviewer edit, your own backend calls POST /v1/incontext/session-nonce with a project token that has the write ability, naming the keys this session may change, and returns the nonce:

mountIncontextEditor({
  url: 'https://app.example.com',
  publishableKey: 'taip_…',
  locale: 'fr',
  writableKeys: ['home.title', 'home.subtitle'],
  getNonce: (keys) => fetch('/api/transglot-nonce', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ keys }),
  }).then((response) => response.json()).then((body) => body.nonce),
});

Omit getNonce and the editor is read-only, which is the right default for a viewer. The dialog renders Save disabled with a stated reason rather than offering a control that can only fail at submit time.

The client contract

createIncontextClient(options) returns the IncontextClient the mount uses. Build one yourself only when you need to hold it (to share it, or to inject a double in tests); otherwise pass url + publishableKey to the mount and let it build one.

| option | required | meaning | | --- | --- | --- | | url | yes | origin of the transglot app, e.g. https://app.example.com | | publishableKey | yes | the publishable in-context key, exchanged for a scoped session token | | getNonce | no | supplies the single-use write nonce (see above) | | writableKeys | no | the keys this session may write, passed to getNonce | | fetchImpl | no | injected fetch (tests / non-browser) |

The session token is held in a closure variable only, never in localStorage/sessionStorage.

import { createIncontextClient, mountIncontextEditor } from '@transglot/incontext';

const client = createIncontextClient({ url: 'https://app.example.com', publishableKey: 'taip_…' });
const editor = mountIncontextEditor({ client, locale: 'fr' });

The parts

mountIncontextEditor is an assembly, not a wall: every part it uses is exported and usable on its own. See src/index.ts for the full surface, including encode/decode watermarking, createRegistry, wrapT, forEachWatermarkedText, createOverlay, createDialog, createBar, createEditingSession, repaintLandedRows, and the checkQa mirror.