In-context editor
With an i18n instance from the Quick start, a Comvi project from
Connect to Comvi and an API key, let a translator click any
rendered text and edit it in the browser. @comvi/plugin-in-context-editor injects invisible
key markers into t() output and watches the DOM for them. Saves go to the Comvi platform
over its API, so it needs a project and an API key; it has no offline mode.
Install
Section titled “Install”npm install @comvi/plugin-in-context-editorpnpm add @comvi/plugin-in-context-editoryarn add @comvi/plugin-in-context-editorbun add @comvi/plugin-in-context-editorSet up
Section titled “Set up”import { createI18n } from '@comvi/core';import { cdnLoader } from '@comvi/plugin-fetch-loader';import { inContextEditor } from '@comvi/plugin-in-context-editor';
export const i18n = createI18n({ locale: 'en', apiKey: import.meta.env.DEV ? import.meta.env.VITE_COMVI_EDITOR_API_KEY : undefined,}) .use(cdnLoader({ cdnUrl: import.meta.env.VITE_COMVI_CDN_URL })) .use(inContextEditor());
await i18n.init();Create an API key in the project’s settings with project read plus translation read and
write; read alone loads the editor but every save fails. The editor reads the key off
i18n.apiKey, the same option the fetch loader reads for API mode. Compose a loader or an
inline messages catalog next to it: the editor can only edit text the app rendered
through t().
An API key in a browser bundle is readable by every visitor, and this one can overwrite
translations, so the editor key must never be in a production build or a public preview
build. The import.meta.env.DEV test is what keeps it out: Vite replaces it with false
in a build, and the bundler drops the key with the dead branch.
inContextEditor() takes the i18n instance, composes the devtools and plugin capabilities
onto it, queues inContextEditorPlugin() and returns the instance with its type unchanged.
The plugin runs inside init(): on the server it registers only the marker post-processor, so
server-rendered HTML carries the markers the client expects; in the browser it also starts the
DOM observer and the UI. Compose it before init().
| Do this | And |
|---|---|
Hold Alt (Option on macOS) and hover |
translated elements light up with an overlay naming the key |
Alt + click a highlighted element |
the edit modal opens on that key |
Cmd or Ctrl + S in the modal |
saves without closing |
Escape |
closes the modal, or asks about unsaved edits |
Nothing highlights on hover alone. Release the modifier, or switch windows, and the overlay
disappears. With no key configured the plugin starts in demo mode: the console says
[@comvi/plugin-in-context-editor] Running in demo mode - API key not configured. Changes cannot be saved.
once, and every click opens one panel naming the key, with no translation list.
Gate it out of production
Section titled “Gate it out of production”import { createI18n } from '@comvi/core';import { cdnLoader } from '@comvi/plugin-fetch-loader';import { inContextEditor } from '@comvi/plugin-in-context-editor';
const editorEnabled = import.meta.env.DEV;
const base = createI18n({ locale: 'en', fallbackLocale: 'en', apiKey: editorEnabled ? import.meta.env.VITE_COMVI_EDITOR_API_KEY : undefined,}).use(cdnLoader({ cdnUrl: import.meta.env.VITE_COMVI_CDN_URL }));
export const i18n = editorEnabled ? base.use(inContextEditor()) : base;
await i18n.init();Gating the key is required. Two things keep the editor code out of a production build: the
package’s production export condition resolves to an entry whose installer is an identity
function, with no editor bytes, and inContextEditorPlugin() returns a no-op when
process.env.NODE_ENV is "production". Neither touches your own createI18n call, so an
ungated apiKey still ships in dist/assets, where anyone can read it and overwrite
translations with it. Put the key and the installer behind the same condition, and never
widen that condition with a flag a deployed build can set: a preview deployment is a
production build, so the editor would not run there and the key would still ship. Both
branches of the ternary have the same type. The Comvi browser extension is the other way to
run the editor: it injects the standalone editor runtime into the page and keeps the key in
the extension.
Options
Section titled “Options”| Option | Default | Behaviour |
|---|---|---|
targetElement |
document.body |
The DOM subtree to watch. |
tagAttributes |
built-in map | HTML tag name to the attributes treated as translated values. Replaces the built-in map. |
debug |
false |
Verbose console logging. |
highlightStyle |
{ borderColor: '#D97706', backgroundColor: 'rgba(217, 119, 6, 0.12)', borderWidth: 2 } |
The overlay. Its z-index: 10000 is fixed. |
collectContext |
true |
Passive context collection while the editor runs. |
screenGroupResolver |
none | () => '/users/:id', a PII-free route template shown instead of a URL digest; return null to keep the digest. |
apiKeyOverride |
none | Overrides i18n.apiKey for this plugin only; used by the extension. |
Context collection sends the keys it sees, layout hints and screen groups to your project to
improve suggestions. The plugin option reaches the editor you composed; the
collectContext: false option on createI18n outranks it and also covers the extension.
Collection is off in demo mode.
Extension discovery
Section titled “Extension discovery”import { createI18n } from '@comvi/core';import { devtools } from '@comvi/core/devtools';import { inContextEditor } from '@comvi/plugin-in-context-editor';
export const i18n = createI18n({ locale: 'en' }) .use(devtools({ instanceId: 'checkout' })) .use(inContextEditor());The Comvi browser extension finds an app through window.__COMVI__, which an i18n instance
publishes only after composing @comvi/core/devtools. inContextEditor() composes it for
you with a generated comvi-<n> id; to name the instance, compose devtools({ instanceId })
first, because a later call configures nothing. Composing devtools() alone is enough for
extension visibility; the package is for the in-page UI. Discovery is removed on destroy().
The extension loads @comvi/plugin-in-context-editor/standalone, an IIFE that installs the
hook and exposes window.ComviInContextEditor with activate(), deactivate(),
isActive() and getStatus(). Its activate() takes EditorOptions plus apiKey, a
transport proxy so the credential stays in the extension, instanceId,
refreshMessages and onLifecycle, and returns null with a warning when a session is
already running. Activation is announced on window as a credential-free
comvi-in-context-editor:lifecycle CustomEvent.
Skip the markers
Section titled “Skip the markers”import { i18n } from './i18n';
export function drawLabel(ctx: CanvasRenderingContext2D, x: number, y: number) { ctx.fillText(i18n.t('chart.label', { markers: false }), x, y);}Pass markers: false when the invisible markers cause a problem for one call: a canvas or
a font that draws them as boxes, a width-sensitive layout, a comparison or sort that sees the
extra characters. The marker injector is the only reader of the option, so the text is
otherwise the same t() result, and that one string is not editable in context. <T> takes
the same option as a prop, markers={false} or :markers="false" in Vue.
The plugin returns a cleanup function, so destroy() unhooks everything it registered.
Read next
Section titled “Read next”- Overview: installers, plugin factories and registration options.
- Fetch loader: the loader the editor edits on top of.
- Devtools API:
devtools(),instanceIdandexposeGlobal.