# prosemirror-transliterate
Development build 0.0.0. Provider-agnostic transliteration for ProseMirror.
# Quick start
Add transliteration to an existing ProseMirror editor in one plugin. Your schema and document format stay yours.
## Install locally
This checkout is a development build. Build and pack it before installing in another project:
```sh
# In this repository
pnpm install
pnpm build
npm pack
# In your application: replace the path with the generated tarball path
npm install /absolute/path/prosemirror-transliterate-0.0.0.tgz
npm install prosemirror-state prosemirror-view prosemirror-model prosemirror-schema-basic prosemirror-keymap prosemirror-commands
```
Once a release has been published to npm, replace the tarball installation with `npm install prosemirror-transliterate`.
## Create an editor
Add `
` to your page. Use a bundler that supports CSS imports, such as Vite.
```ts
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { schema } from 'prosemirror-schema-basic';
import { keymap } from 'prosemirror-keymap';
import { baseKeymap } from 'prosemirror-commands';
import { transliterationPlugin } from 'prosemirror-transliterate';
import 'prosemirror-view/style/prosemirror.css';
import 'prosemirror-transliterate/style.css';
const element = document.querySelector('#editor');
if (!element) throw new Error('Missing editor element');
const view = new EditorView(element, {
state: EditorState.create({
schema,
plugins: [
transliterationPlugin({ lang: 'hi', debounceMs: 150 }),
keymap(baseKeymap),
],
}),
attributes: {
role: 'textbox',
'aria-label': 'Document editor',
'aria-multiline': 'true',
},
});
// On route change or component unmount:
// view.destroy();
```
The default provider sends the active Latin word to Google Input Tools. For sensitive text or a production integration, choose and configure a provider appropriate for your application. See [custom providers](providers.md).
## Try a word
Focus the editor and type `namaste`. Once suggestions arrive, use ↑ / ↓ to select, Enter to insert, or Space to insert with a trailing space. Escape closes the menu and cancels a pending request. Mouse selection keeps focus in the editor.
When there are no suggestions, the plugin leaves normal key handling to the editor. It does not hold Space while a request is pending. If you type a space before suggestions arrive, your original text remains.
## Add to an existing editor
Put `transliterationPlugin()` before other keymaps that consume Enter, Space, or the arrow keys. Add it once per editor state. Include both the core ProseMirror stylesheet and the small popup stylesheet. Do not copy implementation source into your application.
In a React or Vue application, create the `EditorView` on mount and destroy it during cleanup. Importing the package is safe during SSR; do not instantiate an editor until the DOM is available. Preserve your application's existing state ownership and transaction dispatch pattern.
## Change language
Options are fixed for a plugin instance. Create a new plugin when the language or provider changes and reconfigure the state, preserving other plugins:
```ts
const previousPlugin = plugin; // Track this reference in your integration.
plugin = transliterationPlugin({ lang: 'bn' });
view.updateState(view.state.reconfigure({
plugins: view.state.plugins.map(item => item === previousPlugin ? plugin : item),
}));
```
This is a replacement pattern, not a complete snippet: initialize `plugin` when creating your editor. Reconfiguration destroys the old view resources and cancels pending work. In collaborative editors, remote document changes also invalidate the active suggestion range.
---
# API reference
All JavaScript exports are available from `prosemirror-transliterate`. Popup styles are available from `prosemirror-transliterate/style.css`.
## transliterationPlugin(options?)
Returns a `Plugin`. Install once per editor, before keymaps that consume its keyboard controls.
| Option | Default | Behavior |
| --- | --- | --- |
| `lang` | `'hi'` | Language code passed to the provider. See `Language`. |
| `numOptions` | `5` | Maximum candidate count; clamped to an integer from 1–10. |
| `debounceMs` | `150` | Delay after input or cursor changes; clamped to 0–10,000 ms. |
| `cacheSize` | `100` | Maximum words cached per editor; 0 disables caching, maximum 10,000. |
| `getSuggestions` | Google adapter | Synchronous or async provider returning a readonly array of strings. |
| `onError` | none | Receives custom provider failures; cancellation is ignored. |
Non-finite numeric options use their defaults. Empty or duplicate candidates are removed. Returned suggestions are capped by `numOptions`. Successful nonempty results use an in-memory LRU cache scoped to each editor. It has no time-based expiry; disable it for changing or context-dependent providers. Empty results and failures are not cached. Destruction clears the cache.
## SuggestionProvider
```ts
interface SuggestionContext {
lang: Language;
numOptions: number;
signal: AbortSignal;
}
type SuggestionProvider = (
word: string,
context: SuggestionContext,
) => readonly string[] | Promise;
```
The plugin passes the active word, never the full document. Pass `signal` into your network request. The plugin rejects stale responses even if your provider ignores cancellation. It handles thrown errors without blocking editing and invokes `onError` for current, non-aborted custom requests.
## getTransliterateSuggestions(word, options?)
A standalone helper for the unofficial Google Input Tools endpoint. You can call it without creating an editor.
```ts
const candidates = await getTransliterateSuggestions('namaste', {
lang: 'hi',
numOptions: 5,
showCurrentWordAsLastSuggestion: true,
signal: controller.signal,
});
```
Defaults are `lang: 'hi'`, `numOptions: 5`, and `showCurrentWordAsLastSuggestion: true`, including when only some options are provided. The original word, when enabled, is appended after the capped candidates, so the helper may return `numOptions + 1` items. Blank input and aborted requests return `[]`. HTTP errors, malformed data, and network failures return `[word]` if the original-word option is enabled, otherwise `[]`. Errors are not logged.
The plugin's built-in provider disables the original-word option; Escape is the way to retain the original input. The built-in adapter absorbs failures, so `onError` only observes errors thrown by a custom provider.
## Language
A TypeScript union preserving the original adapter's language identifiers:
```text
am ar bn be bg yue-hant zh zh-hant fr de el gu he hi it ja kn
ml mr ne or fa pt pa ru sa sr si es ta te ti uk ur vi
```
These are accepted configuration values, **not a verified support matrix**. Candidate quality and availability depend on your provider. The bundled website vocabulary demonstrates Hindi, Bengali, Tamil, and Telugu only.
## transliterationKey
Read the current plugin state without mutating it:
```ts
const state = transliterationKey.getState(view.state);
// { suggestions, index, from, to, word } | undefined
```
`word` is `null` when cleared. `from` and `to` are ProseMirror document positions. State is readonly; let the plugin handle updates. Internal transaction metadata is not a public command API.
## Keyboard behavior
| Key | While suggestions are open |
| --- | --- |
| ↑ / ↓ | Cycle through suggestions, wrapping at either end. |
| Enter | Replace the word with the highlighted suggestion. |
| Space | Replace with the highlighted suggestion and a trailing space in one transaction. |
| Escape | Dismiss suggestions and cancel any pending request. |
| Tab | Pass through to the host. |
Modified keys and IME composition keys pass through. Escape also cancels pending work before the menu opens. Undo grouping follows your host's `prosemirror-history` configuration; the replacement and its trailing space are one transaction, but can share a history group with preceding typing.
## Word boundaries and lifecycle
Only ASCII Latin words (`a-z`, `A-Z`) at a collapsed text cursor and at the end of a word are eligible. The plugin skips code blocks, code marks, numbers mixed into words, and suffixes of other Unicode words. It does not transliterate selected ranges or convert a whole pasted document. A pasted final eligible word can trigger suggestions.
Typing, deletion, selection changes, and external document changes invalidate old ranges immediately. Blur, Escape, composition start, and destruction cancel timers and in-flight requests. The popup uses fixed positioning and follows scroll/resize. The editor's document owns the popup. Ordinary DOM editors are the supported integration; custom Shadow DOM popup placement is not exposed.
The popup provides listbox/option roles, an active-descendant relationship, and a live status region. Label your host editor as shown in the quick start. Validate with your application's supported screen readers; the package does not claim a screen-reader certification.
## Styling and legacy export
Override `--transliteration-background`, `--transliteration-color`, `--transliteration-border`, and `--transliteration-selected` on `:root` or the document body. The popup is mounted under `body`, so editor-only variables do not inherit into it.
`NewtransliterationPlugin` remains a deprecated alias of `transliterationPlugin`. Existing calls work; prefer the new name in new code. The stylesheet now styles only the popup rather than changing the entire editor.
---
# Bring your own provider
The plugin handles the editor. Your provider handles the words. Swap in an offline dictionary, your API, or a transliteration service without changing the editor integration.
## An offline vocabulary
Useful for tests, a controlled vocabulary, or learning the API. This example is a dictionary, not a general transliteration engine.
```ts
const vocabulary: Record = {
namaste: ['नमस्ते', 'नमस्कार'],
duniya: ['दुनिया'],
};
const plugin = transliterationPlugin({
lang: 'hi',
getSuggestions: word => vocabulary[word.toLowerCase()] ?? [],
});
```
The website uses this pattern for its sample words. Unknown words deliberately return no suggestions.
## Your application endpoint
```ts
import type { SuggestionProvider } from 'prosemirror-transliterate';
const getSuggestions: SuggestionProvider = async (word, { lang, numOptions, signal }) => {
const response = await fetch('/api/transliterate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ word, lang, limit: numOptions }),
signal,
});
if (!response.ok) throw new Error(`Suggestion request failed: ${response.status}`);
const data: unknown = await response.json();
if (!Array.isArray(data) || !data.every(item => typeof item === 'string')) {
throw new Error('Expected a JSON array of strings');
}
return data;
};
const plugin = transliterationPlugin({
lang: 'hi',
getSuggestions,
onError: () => showSuggestionServiceUnavailable(), // Your application's status UI.
});
```
`/api/transliterate` and `showSuggestionServiceUnavailable` are application-owned examples; this repository does not implement that endpoint. Its response contract is a JSON string array. Add authentication, request limits, and provider credentials on your server if required. Do not put secrets in frontend configuration. Prefer an error indicator over logging typed text.
## Cancellation and caching
Use the supplied `AbortSignal` for every request. A response that arrives after the user changes their word or cursor is discarded. Providers that cannot cancel are supported, but their underlying work may continue.
Results are cached by exact word in the current editor. Set `cacheSize: 0` if results depend on time, user context, or mutable data. A new plugin instance starts a fresh cache. Debouncing and caching reduce provider calls; neither guarantees a provider quota.
## The built-in Google adapter
Calling `transliterationPlugin({ lang: 'hi' })` uses the unofficial `inputtools.google.com` endpoint. It sends the active Latin word and language, not the full document. Network failures, unsupported identifiers, unexpected responses, or CORS restrictions simply leave normal typing intact.
For live-demo troubleshooting, check your browser's network panel and the chosen language. Do not assume a successful build means the external service is available. Tests mock the network, and the offline playground works without Google.
---
# How the plugin works
A transliteration menu is a useful way to learn one of ProseMirror's central ideas: the document and plugin state change through transactions, while browser effects belong to the view.
## 1. Locate an eligible word
After a document or selection change, the view checks the text around the collapsed cursor in the current text block. An ASCII Latin word ending at that cursor becomes a candidate. Code content, mixed-script suffixes, and partial words are excluded.
The document is not serialized or scanned end to end. Only the current text block is inspected. Detection cost grows with the current block's length, not the full document. Very large single-paragraph documents can still make this work more expensive.
## 2. Delay and cancel work
The view waits 150 ms by default. Another edit cancels the timer and aborts any previous request. Each request captures a generation number and a word range. A returned result is only eligible if the generation, text, selection, and focus still match.
Why check both generation and range? A user can type a word, delete it, and type the same word at the same position before an old request finishes. Matching the text alone would accept the old result.
## 3. Commit suggestions through a transaction
The view dispatches metadata containing the candidates and their range. The plugin's `state.apply` returns a new state value. It never writes directly into an existing `EditorState`.
This keeps state transitions predictable. Arrow-key navigation also dispatches metadata, so readers of an older state see the older selection index. Suggestion-only updates are excluded from history.
## 4. Render without changing the document
A view-owned popup reads the plugin state. It renders candidates as text nodes, avoiding HTML interpretation of provider strings. Its listbox follows cursor coordinates, and its options identify the active suggestion for assistive technology.
The popup is created only when an `EditorView` exists. Importing the module on a server does not touch `document` or `window`.
## 5. Accept or dismiss
Enter, Space, and mouse selection validate the current range before inserting. Space and the selected candidate are inserted in one transaction. Escape discards suggestions. With no candidates, normal editor behavior continues.
## 6. Dispose everything the view owns
Timers, abort controllers, cache entries, DOM nodes, and event listeners belong to a single view and are cleaned up on destruction. A weak map connects event handlers to that view's controller. Two editors can share a plugin instance without sharing requests or popup state.
## What to read next
Read `src/plugin.ts` alongside `tests/index.test.ts`. Start with the stale-response test, then the shared-plugin test. These are useful patterns for mentions, autocomplete, inline search, and other async editor features.
The [official ProseMirror guide](https://prosemirror.net/docs/guide/) explains transactions, editor state, and plugin views. The [reference manual](https://prosemirror.net/docs/ref/) documents the underlying APIs.
---
# Integrate with a coding agent
Copy the prompt below into your coding agent from your application's repository. Replace the bracketed values first. The agent should inspect your existing editor before changing it.
```text
Integrate prosemirror-transliterate into this application.
Target editor/component: [path, or discover the existing ProseMirror editor]
Target language: [hi / bn / ta / te / another Language identifier]
Provider: [offline vocabulary / our endpoint URL / Google Input Tools]
Package source: [published version or absolute path to a built .tgz]
1. Read the repository instructions and inspect package.json, the existing
ProseMirror schema, plugin order, transaction dispatch, styles, tests, and
component lifecycle. Preserve the current architecture and package manager.
Do not replace the editor or create a second editor to demonstrate the feature.
2. Read the installed package README, TypeScript declarations, and docs/api.md.
Use transliterationPlugin, exported from prosemirror-transliterate.
NewtransliterationPlugin is a deprecated alias. Do not invent package APIs.
If no published release is available, use the supplied local tarball.
Resolve one compatible copy of each ProseMirror peer dependency.
3. Add the plugin once, before keymaps that consume Enter, Space, or arrow keys.
Import prosemirror-transliterate/style.css and retain the host's core
ProseMirror styles. Give the editor a meaningful accessible label.
4. Configure lang and the selected provider. A custom getSuggestions function
receives (word, { lang, numOptions, signal }) and returns a string array or
Promise of one. Pass signal to fetch, validate responses, and handle provider
failures without blocking typing. Keep credentials on the server. Do not
log document text. Google Input Tools is an unofficial external endpoint:
tell the application user when their active word will be sent to it.
An offline dictionary must be described as a limited vocabulary.
5. Preserve editor state across renders. Create the EditorView only after mount
in SSR applications and call view.destroy() on unmount. If language changes,
replace this plugin using state.reconfigure while preserving other plugins.
Do not mutate EditorState or plugin state directly.
6. Verify realistic behavior: type a supported Latin word, wait for suggestions,
navigate with arrows, accept with Enter and Space, dismiss with Escape,
select with the mouse, and undo. Confirm stale responses cannot replace text
after a cursor change, more typing, or editor destruction. Test failure,
IME composition, and two editors. Mock network calls in automated tests.
Do not assert support for languages your chosen provider has not verified.
7. Run this application's relevant type checks, tests, and production build.
Report changed files, how to try the feature, provider behavior, verification
results, and any concrete remaining limitations. Do not publish or deploy
unless that is included in my request.
```
## What the agent needs
A package version or local tarball, an existing ProseMirror editor, a language, and a provider decision. For a custom endpoint, supply the response contract and server authentication approach. Do not paste secrets into the prompt.
## Review the result
The final integration should preserve the existing schema and state ownership. Test input in the real browser, including the empty-result path. An offline word list proves integration behavior; it does not establish general language support or external-service reliability.
---
# Deploy on Cloudflare
The documentation site is static HTML, CSS, and JavaScript. It runs on Cloudflare Workers Static Assets with no database, server code, or provider secrets. The default offline playground makes no suggestion-service requests.
## Preview and verify
Run these commands from the repository root (the `package/` folder in the original workspace):
```sh
pnpm install --frozen-lockfile
pnpm check
pnpm site:preview
```
`pnpm site:build` builds the library, generates HTML guides from `docs/*.md`, and emits the website to `website/dist/`. The generated site also includes `/llms.txt`, `/llms-full.txt`, and `/agent-prompt.md` for coding agents.
## Deploy with Wrangler
```sh
pnpm site:check # Build and validate with wrangler deploy --dry-run
pnpm exec wrangler login
pnpm site:deploy # Build, then deploy the static assets
```
Wrangler prompts you to authenticate if needed. The deployment command prints your deployed URL. Set a unique Worker name in `wrangler.jsonc` if `prosemirror-transliterate` is already in use in your account.
The configuration uses `assets.directory: './website/dist'` and a recent compatibility date. A `404.html` page handles missing routes. Static assets use Cloudflare's documented clean HTML routing; `/docs/api` serves the generated API guide. No SPA fallback is needed because the guides are generated HTML.
## Cloudflare Git builds
Connect the repository in Cloudflare Workers & Pages and choose Workers. If you push the contents of this Git repository, use the repository root as the build root. If it is nested inside a larger repository, set the build root to its `package/` folder.
- Build command: `pnpm install --frozen-lockfile && pnpm site:build`
- Deploy command: `pnpm exec wrangler deploy`
- Node version: 22 or newer
The pinned `packageManager` field identifies pnpm. Ensure pnpm 10.12.1 is available in your build environment. Keep credentials in Cloudflare's CI configuration, never in Git.
## Cloudflare Pages alternative
For an existing Pages workflow, build with `pnpm site:build` and choose `website/dist` as the output directory. You can upload it using:
```sh
pnpm exec wrangler pages deploy website/dist --project-name YOUR_PAGES_PROJECT
```
Replace the project name with your own. `site:deploy` targets Workers Static Assets; the Pages command is an alternative deployment path.
## Before sharing
Check the playground, language changes, guide navigation, copy buttons, and `/404.html` on the deployed URL. Switching the playground to Google mode makes requests directly from the visitor's browser; verify availability there if you want to offer live suggestions. The package itself is published separately to npm.
See Cloudflare's [Static Assets guide](https://developers.cloudflare.com/workers/static-assets/get-started/) and [Wrangler configuration reference](https://developers.cloudflare.com/workers/wrangler/configuration/) for the current platform behavior.
---
# Release the package
The current version is `0.0.0`, retained from the initial repository. No npm publication is performed by a build or by the documentation deployment.
## Validate
```sh
pnpm install --frozen-lockfile
pnpm check
pnpm test:coverage
pnpm exec playwright install chromium
pnpm test:e2e
pnpm test:package
npm pack --dry-run
```
Inspect the tarball file list: JavaScript, source maps, declarations, popup CSS, docs, README, and MIT license should be present. Website build assets and test dependencies must not be shipped. Source maps include library source for debugging.
## Smoke-test the consumer package
```sh
pnpm build
npm pack
# Install the generated .tgz in a separate Vite + ProseMirror application.
```
Check JavaScript import resolution, TypeScript declarations, and `prosemirror-transliterate/style.css`. The browser build should resolve ProseMirror from your application's peer dependencies. The package is ESM-only; CommonJS require is not a supported entry point.
## Publish when ready
Confirm ownership and availability of the npm name, choose a version, update CHANGELOG.md, and review repository and author metadata. Replace the development-release notices in the README and website when the release is actually available. Run validation again after changing release metadata.
```sh
npm version 0.1.0
npm publish --access public
```
These are maintainer actions to perform deliberately, not part of CI. `prepublishOnly` runs the main checks before publishing. Release credentials belong in your registry login or trusted publishing setup, never in this repository.
---