Install the in-context editor
Add the in-context editor to your app with the React adapter or the framework-agnostic core, and mark the strings reviewers can edit.
There are two packages:
| Package | Use it for |
|---|---|
@transglot/react | React 18 and 19 apps. A TransglotProvider component and a useIncontextT hook. |
@transglot/incontext | Any other page: Vue, Svelte, Angular, server-rendered HTML or plain scripts. mountIncontextEditor and wrapT. The React adapter is a thin layer over it. |
Both need a publishable key whose allowed origins include the page's origin.
Not the runtime SDK
@transglot/react is the in-context editor. To deliver published translations to your users, use @transglot/react-i18n.
Install
npm install @transglot/react@transglot/incontext is installed with it. React and React DOM 18 or 19 are peer dependencies.
To capture screenshots for the screenshot library when a reviewer asks for a suggestion, also install the optional peer dependency:
npm install html2canvasWithout it, everything else works and suggestions use the text alone.
React
Render the provider only for people who may review translations, and mark strings with the t from useIncontextT():
import { TransglotProvider } from '@transglot/react';
import type { ReactNode } from 'react';
import { i18n } from './i18n';
export function ReviewProvider({ user, locale, children }: { user: User; locale: string; children: ReactNode }) {
if (!user.canReviewTranslations) {
return children;
}
return (
<TransglotProvider
publishableKey="taip_your_publishable_key"
url="https://api.transglot.ai"
locale={locale}
hostT={(key, ...args) => i18n.t(key, ...args)}
reviewer={user.email}
writableKeys={user.reviewableKeys}
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)
}
enterOnMount
>
{children}
</TransglotProvider>
);
}import { useIncontextT } from '@transglot/react';
export function Hero() {
const t = useIncontextT();
return <h1>{t('home.hero.title')}</h1>;
}useIncontextT() calls your hostT and adds the invisible watermark to every string it returns. The visible text is the same. It throws useIncontextT() must be used inside a <TransglotProvider>. outside the provider, so when the provider is rendered only for reviewers, read the context yourself and fall back to your own t for everyone else:
import { TransglotContext } from '@transglot/react';
import { useContext } from 'react';
import { i18n } from './i18n';
export function useT() {
const incontext = useContext(TransglotContext);
return incontext ? incontext.t : i18n.t;
}getNonce is what lets reviewers save; without it the editor is read-only. It calls an endpoint on your own server, which you write: see Your backend: the nonce endpoint.
TransglotProvider props
publishableKey, url, getNonce, writableKeys, fetchImpl and client are read once, on the first render. enterOnMount, theme, consoleUrl and captureElement are read when the editor mounts. Changing locale switches the editor's locale.
Prop
Type
Other exports
useTransglotContext(): the provider's context value ({ client, registry, t, locale, reviewer }). Throws outside the provider.TransglotContext: the React context itself,nulloutside the provider.createEditingSession, and the typesTransglotProviderProps,TransglotContextValue,EditingSession,EditingSessionOptions,EditingState,LocaleSummary, re-exported from@transglot/incontext.
Any framework
mountIncontextEditor builds the whole editor. wrapT marks the strings your app renders, against the registry the editor returns:
import { mountIncontextEditor, wrapT } from '@transglot/incontext';
import { messages, render } from './app';
let locale = 'fr';
const editor = mountIncontextEditor({
url: 'https://api.transglot.ai',
publishableKey: 'taip_your_publishable_key',
locale,
enterOnMount: currentUser.canReviewTranslations,
reviewer: () => currentUser.email,
writableKeys: currentUser.reviewableKeys,
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),
onLocaleChange: (next) => {
locale = next;
render();
},
});
export const t = wrapT(
(key: string) => messages[locale]?.[key] ?? key,
editor.registry,
() => editor.session.state().locale,
);Use the wrapped t wherever your app renders text. Only strings that went through it can be opened with ALT+click.
Better still, load this module only for reviewers, so visitors never download the editor.
Without a bundler
The package is published as ES modules, so a page can load it from a CDN that serves npm packages as ESM, for example jsDelivr:
<h1 id="title"></h1>
<script type="module">
import { mountIncontextEditor, wrapT } from 'https://cdn.jsdelivr.net/npm/@transglot/incontext@0.2.0/+esm';
const messages = { fr: { 'home.title': 'Bienvenue' } };
const editor = mountIncontextEditor({
url: 'https://api.transglot.ai',
publishableKey: 'taip_your_publishable_key',
locale: 'fr',
onLocaleChange: () => render(),
});
const t = wrapT((key) => messages[editor.session.state().locale]?.[key] ?? key, editor.registry, () =>
editor.session.state().locale,
);
function render() {
document.querySelector('#title').textContent = t('home.title');
}
render();
editor.session.enter();
</script>Pin the version in the URL so an update never reaches your page unannounced.
mountIncontextEditor options
Prop
Type
The returned handle
| Member | Description |
|---|---|
session | The editing session (below). |
registry | The registry to pass to wrapT. |
client | The API client the editor uses. |
shadowRoot() | The shadow root the editor's bar, panel and highlight render into, isolated from your page's styles. |
destroy() | Removes the editor and its listeners and leaves editing mode. Strings keep their watermarks, so a later mount can resolve them again. Safe to call more than once. |
The session lets you build your own controls:
editor.session.enter(); // turn editing mode on
editor.session.leave(); // turn it off
editor.session.state(); // { active, locale, sourceLocale, locales, canWrite, canAddLocale, ... }
const unsubscribe = editor.session.subscribe((state) => updateMyToolbar(state));
await editor.session.setLocale('de');wrapT(t, registry, locale)
wrapT(t: HostT, registry: Registry, locale: () => string): WrappedT returns a function with the same arguments as t. Each string it returns is registered as a key and locale pair and gets the invisible watermark. A non-string return value passes through unmarked. locale is called on every call, so pass a getter that reads the editor's current locale, as in the examples.
createRegistry() creates a registry yourself, if you want one before mounting; pass it as registry.
Your backend: the nonce endpoint
The publishable key ships in your page, so on its own it opens a read-only session. To let a reviewer save, the browser needs a single-use nonce that only your server can obtain:
- The editor calls your
getNonce(keys)with the keys inwritableKeys, when the session starts and each time it is renewed. - Your
getNoncecalls an endpoint on your own server, for examplePOST /api/transglot-nonce. - That endpoint checks the signed-in user may review, then calls
POST https://api.transglot.ai/v1/incontext/session-noncewith a project API token that has thewriteability (a secret that stays on your server), the publishable key, and the key names this user may change. - transglot answers
{ "nonce": "...", "expires_at": "..." }. Your endpoint returns the nonce to the browser, andgetNonceresolves with it. - The editor sends the nonce with the publishable key when it opens the session, and the session can then save exactly those keys.
app.post('/api/transglot-nonce', requireReviewer, async (req, res) => {
const keys = keysThisUserMayEdit(req.user, req.body.keys);
const response = await fetch('https://api.transglot.ai/v1/incontext/session-nonce', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TRANSGLOT_TOKEN}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
publishable_key: 'taip_your_publishable_key',
keys,
}),
});
if (!response.ok) {
res.json({ nonce: null });
return;
}
const { nonce } = await response.json();
res.json({ nonce });
});When getNonce resolves with null, the session is read-only: Save is disabled with the reason shown, and no screenshots are captured. A nonce can be used once and expires about a minute after it is issued, which is why the editor asks for a new one on every session. The request fields, the add_locale grant and the error responses are in The nonce request.
What can be edited in place
A watermark is appended to the string your t returns, so it only works while that string renders as one text node.
- A translation with inline markup (
Stamped with <strong>the carrier time</strong>) renders as several nodes. Reviewers can still save it, but the page is not updated afterwards; the editor tells the reviewer so. Split such sentences into one key per element if you want every part to update live. - Attribute text (
placeholder,title,alt,aria-label) has no text node, so it is never marked and cannot be opened with ALT+click.
While editing mode is on, ALT+click belongs to the editor: a click that does not land on a marked string opens nothing and does not reach your page's own handlers. The editor's bar and panel are exempt, and leaving editing mode gives the gesture back.
Errors
Failures are IncontextError instances with a stable slug, a message, an optional hint (what to do), and retryAfterSeconds when the server sent Retry-After. Common slugs:
slug | Meaning |
|---|---|
origin-not-allowed | The publishable key is unknown or deleted, the page's origin is not in its allowed origins, or the write nonce was already used or has expired. The server gives the same answer for all of these on purpose. |
plan-limit, plan-feature-required | The account's plan does not include the in-context editor. |
network-error | transglot could not be reached. |
rate-limited | Still rate limited after retries. |
invalid-url | url is not an https URL (or http on localhost). Thrown when the client is created. |
A save refused because someone else saved the same string first is an IncontextConflictError, a subclass with the slug row-changed and a row property holding the version now on the server. The editor handles it by showing Their version to the reviewer.
With mountIncontextEditor, pass onError to route failures into your own UI. The React provider does not take onError; the editor shows failures in its own UI.
Related
In-context editor
Let reviewers ALT+click any translated string in your running app and edit it in place, with the source, checks and a Glossa suggestion beside it.
Publishable keys
The browser-safe key the in-context editor starts its session with, the origins it works from, and how your backend grants write access.