{"_id":"@defensestation/yjs-local-translator","_rev":"3-61a51683a82738e6f450dd52472ab55a","name":"@defensestation/yjs-local-translator","dist-tags":{"latest":"0.3.0","dev":"0.3.0-dev.5.27558fb"},"versions":{"0.3.0-dev.2.d2befbc":{"name":"@defensestation/yjs-local-translator","version":"0.3.0-dev.2.d2befbc","keywords":["yjs","blocknote","translation","transformers.js","local-first","offline","web-worker","collaboration"],"author":"","license":"MIT","_id":"@defensestation/yjs-local-translator@0.3.0-dev.2.d2befbc","maintainers":[{"name":"gsingh-ds","email":"gsingh@defensestation.ca"}],"dist":{"shasum":"93f8c7138cf8e335649de584a39e6b4761ec12ba","tarball":"https://registry.npmjs.org/@defensestation/yjs-local-translator/-/yjs-local-translator-0.3.0-dev.2.d2befbc.tgz","fileCount":47,"integrity":"sha512-BnHY5t0C32PgaAyk5TTusED+drhgmLq487xyRpqZxuO7bdWO9U2X7F1ZAyw4VL1YvLfgwGMzHAv/xoEmK50IzA==","signatures":[{"sig":"MEYCIQDjAmoJ/xPgXJGSR5bZI9IhQru9LeKK8dFdX/QDuxUbkwIhANH5Waj+yZcq/cR98pWq/HBoXgRmOa8t81w62bYg9XFD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":178337},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./yjs":{"types":"./dist/yjs.d.ts","import":"./dist/yjs.js"},"./worker":{"types":"./dist/translator.worker.d.ts","import":"./dist/translator.worker.js"},"./blocknote":{"types":"./dist/blocknote.d.ts","import":"./dist/blocknote.js"}},"gitHead":"d2befbc0d4dcdcbea817be251d29cf8dfee3a538","scripts":{"test":"vitest run","build":"rm -rf dist && tsc -p tsconfig.build.json","check":"rm -rf dist && tsc -p tsconfig.build.json && vitest run","clean":"rm -rf dist coverage *.tgz","pack:local":"rm -rf dist && tsc -p tsconfig.build.json && vitest run && yarn pack"},"_npmUser":{"name":"gsingh-ds","email":"gsingh@defensestation.ca"},"_npmVersion":"10.8.2","description":"Private in-browser translation with streamed previews and single-transaction Yjs and BlockNote commits.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"packageManager":"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e","devDependencies":{"yjs":"13.6.31","vitest":"4.1.10","typescript":"5.9.3","@blocknote/core":"0.51.4"},"peerDependencies":{"yjs":">=13.6.0 <14","@tiptap/pm":">=3.0.0 <4","@blocknote/core":">=0.51.0 <1"},"peerDependenciesMeta":{"yjs":{"optional":true},"@tiptap/pm":{"optional":true},"@blocknote/core":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/yjs-local-translator_0.3.0-dev.2.d2befbc_1784243084312_0.813489664381245","host":"s3://npm-registry-packages-npm-production"}},"0.3.0-dev.5.27558fb":{"name":"@defensestation/yjs-local-translator","version":"0.3.0-dev.5.27558fb","keywords":["yjs","blocknote","translation","transformers.js","local-first","offline","web-worker","collaboration"],"author":"","license":"MIT","_id":"@defensestation/yjs-local-translator@0.3.0-dev.5.27558fb","maintainers":[{"name":"gsingh-ds","email":"gsingh@defensestation.ca"}],"dist":{"shasum":"feb81038f2fe84e794674c9519de2b312fb81668","tarball":"https://registry.npmjs.org/@defensestation/yjs-local-translator/-/yjs-local-translator-0.3.0-dev.5.27558fb.tgz","fileCount":47,"integrity":"sha512-MMasdVNcukLmQbGy5DsnOItSdfO/sL4eOLApyienA5UGsBlDnJXB+rkcQqwXsDcUCoshcIrSgo1g6PxlcUJzvw==","signatures":[{"sig":"MEQCIEy1TbBBQMQ0lebYNKQ0I51ippp+ZtXJDXIACUe5royZAiAWqchagijSq0geZhmKMIcHQe1JRqwieORXmmLs5/BdyA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":178341},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./yjs":{"types":"./dist/yjs.d.ts","import":"./dist/yjs.js"},"./worker":{"types":"./dist/translator.worker.d.ts","import":"./dist/translator.worker.js"},"./blocknote":{"types":"./dist/blocknote.d.ts","import":"./dist/blocknote.js"}},"gitHead":"27558fbbfd473e7193f02f49846424141811fef0","scripts":{"test":"vitest run","build":"rm -rf dist && tsc -p tsconfig.build.json","clean":"rm -rf dist coverage *.tgz","typecheck":"rm -rf dist && tsc -p tsconfig.build.json && vitest run","pack:local":"rm -rf dist && tsc -p tsconfig.build.json && vitest run && yarn pack"},"_npmUser":{"name":"gsingh-ds","email":"gsingh@defensestation.ca"},"_npmVersion":"10.8.2","description":"Private in-browser translation with streamed previews and single-transaction Yjs and BlockNote commits.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"packageManager":"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e","readmeFilename":"README.md","devDependencies":{"yjs":"13.6.31","vitest":"4.1.10","typescript":"5.9.3","@blocknote/core":"0.51.4"},"peerDependencies":{"yjs":">=13.6.0 <14","@tiptap/pm":">=3.0.0 <4","@blocknote/core":">=0.51.0 <1"},"peerDependenciesMeta":{"yjs":{"optional":true},"@tiptap/pm":{"optional":true},"@blocknote/core":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/yjs-local-translator_0.3.0-dev.5.27558fb_1784244105546_0.9538108639971505","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@defensestation/yjs-local-translator","version":"0.3.0","description":"Private in-browser translation with streamed previews and single-transaction Yjs and BlockNote commits.","type":"module","license":"MIT","author":"","keywords":["yjs","blocknote","translation","transformers.js","local-first","offline","web-worker","collaboration"],"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./yjs":{"types":"./dist/yjs.d.ts","import":"./dist/yjs.js"},"./blocknote":{"types":"./dist/blocknote.d.ts","import":"./dist/blocknote.js"},"./worker":{"types":"./dist/translator.worker.d.ts","import":"./dist/translator.worker.js"}},"sideEffects":false,"scripts":{"clean":"rm -rf dist coverage *.tgz","build":"rm -rf dist && tsc -p tsconfig.build.json","test":"vitest run","typecheck":"rm -rf dist && tsc -p tsconfig.build.json && vitest run","pack:local":"rm -rf dist && tsc -p tsconfig.build.json && vitest run && yarn pack"},"peerDependencies":{"@blocknote/core":">=0.51.0 <1","@tiptap/pm":">=3.0.0 <4","yjs":">=13.6.0 <14"},"peerDependenciesMeta":{"@blocknote/core":{"optional":true},"@tiptap/pm":{"optional":true},"yjs":{"optional":true}},"devDependencies":{"@blocknote/core":"0.51.4","typescript":"5.9.3","vitest":"4.1.10","yjs":"13.6.31"},"engines":{"node":">=20"},"packageManager":"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e","_id":"@defensestation/yjs-local-translator@0.3.0","gitHead":"9c78595f6546fefd4899906710e13b6417a5e070","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-24LSQmgdv0deFZJe1fIYd4vfoTqBPmWUxwAFDJNttch9hki4IiN6y1WVlNLTYOqqej9XBQ3rERrvaZdOQaZ+pQ==","shasum":"15a03518e9d639a8ba79e408f4d3a5cc020b426f","tarball":"https://registry.npmjs.org/@defensestation/yjs-local-translator/-/yjs-local-translator-0.3.0.tgz","fileCount":47,"unpackedSize":178327,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD9kmxgdyH6331JPdzxvuzIsZKODdTwQlKRdc3v4W+uRQIhAIgO9aY7E65XaKOYZ2+uKX6MLJakRSPzPEX3nRWOvyL2"}]},"_npmUser":{"name":"gsingh-ds","email":"gsingh@defensestation.ca"},"directories":{},"maintainers":[{"name":"gsingh-ds","email":"gsingh@defensestation.ca"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/yjs-local-translator_0.3.0_1784244227657_0.6006829817313455"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-16T23:04:44.134Z","modified":"2026-07-16T23:23:47.959Z","0.3.0-dev.2.d2befbc":"2026-07-16T23:04:44.455Z","0.3.0-dev.5.27558fb":"2026-07-16T23:21:45.683Z","0.3.0":"2026-07-16T23:23:47.794Z"},"license":"MIT","keywords":["yjs","blocknote","translation","transformers.js","local-first","offline","web-worker","collaboration"],"description":"Private in-browser translation with streamed previews and single-transaction Yjs and BlockNote commits.","maintainers":[{"name":"gsingh-ds","email":"gsingh@defensestation.ca"}],"readme":"# yjs-local-translator\n\nPrivate, local-first browser translation with:\n\n- built-in browser translation (Chrome's `Translator` API) when available, with\n  automatic fallback to Transformers.js\n- lazy direction-specific model loading\n- Web Worker inference\n- sentence-aware chunking so long documents survive model input limits\n- a translation memory that makes retranslation of edited documents\n  incremental: only changed sentences reach the model\n- staleness tracking, so the app knows when a stored translation no longer\n  matches its source\n- watchers that keep translations up to date automatically as the source\n  changes\n- full-input output streaming through `TextStreamer`\n- Yjs adapters that commit the final result as one minimal edit\n- BlockNote adapters that group final edits into one transaction\n- conflict detection so collaborator edits are not silently overwritten\n\nThe streamed text is intended for a local preview, popover, or side panel. It is\nnot written token-by-token into the shared document. When inference completes,\nthe final translation is written once.\n\n## Install\n\n```bash\nyarn add yjs-local-translator\n```\n\nInstall the adapter peer dependency you use:\n\n```bash\nyarn add yjs\n# and/or\nyarn add @blocknote/core\n```\n\n## Core browser translator\n\n```ts\nimport { BrowserTranslator } from \"yjs-local-translator\";\n\nconst translator = new BrowserTranslator();\n\nconst result = await translator.translate(\"Hello, how are you?\", {\n  sourceLanguage: \"en\",\n  targetLanguage: \"fr\",\n  onDelta(delta, streamedText) {\n    preview.textContent = streamedText;\n  },\n  onProgress(progress) {\n    console.log(progress.message, progress.progress);\n  }\n});\n\nconsole.log(result.text);\nconsole.log(result.engine); // \"native\", \"transformers\", \"memory\", or \"none\"\n```\n\nStreaming only exposes decoded output while generation is already running.\n\nLanguage tags are normalized to their lowercase primary subtag, so `\"EN\"`,\n`\"en-US\"`, and `\"en\"` are equivalent.\n\n## Built-in browser translation\n\nWhen the page runs in a browser that exposes the built-in `Translator` API\n(Chrome 138+, and Chromium-based browsers that ship it), the requested language\npair is first offered to the browser's own on-device translator. The language\npacks are downloaded and managed by the browser, nothing leaves the machine,\nand no Transformers.js worker is started. If the API is missing, the pair is\nunsupported, or creation fails, translation falls back to the Transformers.js\nworker without any change in behavior for the caller.\n\nThe result's `engine` field reports which engine produced the translation.\n\nTo always use the Transformers.js worker:\n\n```ts\nconst translator = new BrowserTranslator({ useNativeTranslation: false });\n```\n\n## Long text and chunking\n\nMarian/OPUS models truncate input past roughly 512 tokens. Before inference,\ninput is split on paragraph and sentence boundaries into chunks of at most\n`maxChunkLength` characters (default 500), translated chunk by chunk, and\nreassembled with the original paragraph spacing. Streaming callbacks receive\nthe concatenated output across chunks.\n\n```ts\nconst translator = new BrowserTranslator({ maxChunkLength: 800 });\n```\n\n## Translation memory: incremental retranslation\n\nWithout a memory, every call translates the full input. With a memory, the\ninput is split into sentence chunks and each chunk is looked up in a cache\nbefore it is sent to a model. Editing one sentence in a long document then\ncosts one model call on the next translation instead of a full run.\n\n```ts\nimport {\n  BrowserTranslator,\n  InMemoryTranslationMemory\n} from \"yjs-local-translator\";\n\nconst translator = new BrowserTranslator({\n  translationMemory: new InMemoryTranslationMemory()\n});\n```\n\n`InMemoryTranslationMemory` keeps up to 5000 entries (configurable) with\nleast-recently-used eviction and lasts for the session. To persist the cache\nacross page loads, use the IndexedDB adapter:\n\n```ts\nimport { IndexedDBTranslationMemory } from \"yjs-local-translator\";\n\nconst translator = new BrowserTranslator({\n  translationMemory: new IndexedDBTranslationMemory()\n});\n```\n\nDetails worth knowing:\n\n- Cache keys contain the engine identity (native pair or model route), the\n  language pair, and the chunk text. Changing the model catalog or switching\n  engines produces different keys, so a hit can never return output from a\n  different model.\n- When every chunk is served from the cache, the result's `engine` is\n  `\"memory\"` and the translation is close to instant.\n- Streaming callbacks still fire in document order; cached chunks arrive as\n  one delta each.\n- A custom store only needs `get(key)` and `set(key, value)` (sync or async)\n  to satisfy the `TranslationMemory` interface.\n- Cache failures are swallowed: a broken IndexedDB never fails a translation,\n  it only loses the speedup.\n\n## Detecting stale translations\n\nWhen a translation is stored beside its source with `translateYMapString`, a\nmetadata record is written under `\"<targetKey>:meta\"` in the same\ntransaction. It contains a hash of the exact source text that was translated,\nthe language pair, the models used, and a timestamp.\n\n```ts\nimport {\n  isYMapTranslationStale,\n  translateYMapString\n} from \"yjs-local-translator/yjs\";\n\nawait translateYMapString(translator, fields, \"title\", {\n  sourceLanguage: \"en\",\n  targetLanguage: \"de\",\n  targetKey: \"title:de\"\n});\n\nisYMapTranslationStale(fields, \"title\", \"title:de\"); // false\n\nfields.set(\"title\", \"New content\");\n\nisYMapTranslationStale(fields, \"title\", \"title:de\"); // true\n```\n\nUse this to render an \"outdated translation\" badge or to decide whether a\nretranslation is needed at all. Metadata is skipped when the source is\nreplaced in place (there is nothing left to compare), and can be turned off\nwith `storeMetadata: false` or moved with `metadataKey`.\n\nFor a whole-document `Y.Text`, pass a map and key to receive the record:\n\n```ts\nimport { translateYText } from \"yjs-local-translator/yjs\";\n\nawait translateYText(translator, sourceText, {\n  sourceLanguage: \"en\",\n  targetLanguage: \"de\",\n  target: germanText,\n  metadataMap: doc.getMap(\"translation-meta\"),\n  metadataKey: \"content:de\"\n});\n```\n\nThe lower-level pieces are exported too: `hashText(text)` and\n`isTranslationStale(sourceText, metadata)`.\n\n## Keeping translations up to date automatically\n\nWatchers observe a source, wait for typing to settle, and retranslate. They\npair naturally with a translation memory, which keeps each rerun cheap.\n\n```ts\nimport { watchYMapString } from \"yjs-local-translator/yjs\";\n\nconst stop = watchYMapString(translator, fields, \"title\", {\n  sourceLanguage: \"en\",\n  targetLanguage: \"de\",\n  targetKey: \"title:de\",\n  debounceMs: 2000,\n  onTranslated(result) {\n    console.log(\"updated:\", result.text);\n  },\n  onError(error) {\n    console.warn(error);\n  }\n});\n\n// when the component unmounts or the feature is turned off\nstop();\n```\n\n`watchYText` does the same for a pair of Y.Text values:\n\n```ts\nimport { watchYText } from \"yjs-local-translator/yjs\";\n\nconst stop = watchYText(translator, englishText, {\n  sourceLanguage: \"en\",\n  targetLanguage: \"fr\",\n  target: frenchText,\n  immediate: true\n});\n```\n\nOptions:\n\n- `debounceMs` (default 1500): idle time after the last edit before reacting.\n  Bursts of keystrokes collapse into one translation.\n- `autoTranslate` (default true): set to false to only receive `onStale`\n  callbacks and trigger translation yourself.\n- `localOnly` (default true): react only to edits made by this client. In a\n  collaborative document this means exactly one client, the author of the\n  edit, retranslates. Every other client receives the translated result\n  through sync instead of racing to produce it.\n- `immediate` (default false): run once right after the watcher starts, which\n  is useful to repair translations that went stale while the app was closed.\n- `onStale(sourceText)`, `onTranslated(result)`, `onError(error)`.\n\nWatchers never react to their own writes: transactions tagged with the\ntranslation origin are ignored, so there is no feedback loop. If the source\nchanges while a translation is running, the conflicting result is dropped and\na fresh run is scheduled with the newer text.\n\nThe target must be different from the source (a separate `Y.Text` or a\ndifferent map key). Watching a value onto itself would retranslate its own\noutput.\n\n## Recommended production setup\n\n```ts\nimport {\n  BrowserTranslator,\n  IndexedDBTranslationMemory\n} from \"yjs-local-translator\";\nimport { watchYMapString } from \"yjs-local-translator/yjs\";\n\nconst translator = new BrowserTranslator({\n  translationMemory: new IndexedDBTranslationMemory()\n});\n\nconst stop = watchYMapString(translator, fields, \"title\", {\n  sourceLanguage: \"en\",\n  targetLanguage: \"de\",\n  targetKey: \"title:de\",\n  immediate: true\n});\n```\n\nOn startup, `immediate: true` repairs anything that drifted while the app was\noffline; the memory makes that repair skip every unchanged sentence; and the\nwatcher keeps the pair in sync from then on.\n\n## Yjs\n\n### Replace a Y.Text once\n\n```ts\nimport * as Y from \"yjs\";\nimport { BrowserTranslator } from \"yjs-local-translator\";\nimport { translateYText } from \"yjs-local-translator/yjs\";\n\nconst doc = new Y.Doc();\nconst text = doc.getText(\"content\");\ntext.insert(0, \"Hello world\");\n\nconst translator = new BrowserTranslator();\n\nawait translateYText(translator, text, {\n  sourceLanguage: \"en\",\n  targetLanguage: \"es\",\n  onDelta(_delta, streamedText) {\n    localPreview.textContent = streamedText;\n  }\n});\n```\n\nThe preview streams locally. The Y.Text changes in one final Yjs transaction,\napplied as a minimal edit: the longest common prefix and suffix are left\nuntouched so collaborator cursors and relative positions outside the changed\nregion survive. By default, the operation aborts if the source changes during\ntranslation.\n\n### Store a translation beside the source\n\n```ts\nimport { translateYMapString } from \"yjs-local-translator/yjs\";\n\nconst fields = doc.getMap(\"fields\");\nfields.set(\"title\", \"Local-first collaboration\");\n\nawait translateYMapString(translator, fields, \"title\", {\n  sourceLanguage: \"en\",\n  targetLanguage: \"de\",\n  targetKey: \"title:de\"\n});\n```\n\n## BlockNote\n\n### Translate the current text selection\n\n```ts\nimport { BrowserTranslator } from \"yjs-local-translator\";\nimport {\n  translateBlockNoteSelection\n} from \"yjs-local-translator/blocknote\";\n\nconst translator = new BrowserTranslator();\nlet preview = \"\";\n\nawait translateBlockNoteSelection(editor, translator, {\n  sourceLanguage: \"en\",\n  targetLanguage: \"fr\",\n  onDelta(_delta, streamedText) {\n    preview = streamedText;\n    renderPreview(preview);\n  }\n});\n```\n\nThe selection is translated as one complete input. The final replacement is a\nsingle BlockNote transaction and preserves inline styles, links, and custom\ninline wrappers. If the user or a collaborator changes the selection while\ntranslation is running, the default conflict policy aborts.\n\n### Translate the whole document\n\n```ts\nimport {\n  translateBlockNoteDocument\n} from \"yjs-local-translator/blocknote\";\n\nawait translateBlockNoteDocument(editor, translator, {\n  sourceLanguage: \"en\",\n  targetLanguage: \"es\",\n  onBlockDelta({ blockId, streamedText }) {\n    renderBlockPreview(blockId, streamedText);\n  }\n});\n```\n\nEvery root and nested text block is translated in document order. The finished\nblock changes are grouped in one BlockNote transaction and therefore one undo\nstep. Non-text blocks are skipped. `translateBlockNoteBlocks` remains\navailable when you need to translate an explicit set of blocks.\n\n## Collaboration behavior\n\nThe default conflict policy is `\"abort\"`. This prevents a completed translation\nfrom overwriting content that changed while the model was running.\n\nFor explicit overwrite behavior:\n\n```ts\nawait translateYText(translator, text, {\n  sourceLanguage: \"en\",\n  targetLanguage: \"fr\",\n  conflictPolicy: \"overwrite\"\n});\n```\n\nBlock translation additionally supports `\"skip\"`, which commits unchanged\nblocks and leaves concurrently edited blocks alone.\n\n## Formatting behavior\n\n`translateBlockNoteSelection` replaces the selected inline content with plain\ntranslated text. `translateBlockNoteDocument` and\n`translateBlockNoteBlocks` preserve block IDs, block types, properties,\nchildren, document position, and inline text styles. Because translation can\nchange word order and length, styles across multiple text spans are assigned\nproportionally in the translated text.\n\nNested blocks are left in place and table cells are translated individually,\npreserving table dimensions, headers, widths, merged-cell properties, cell\nstyles, links, and other inline wrappers. Use `createUpdate` when your\napplication needs a different mapping strategy.\n\n```ts\nawait translateBlockNoteBlocks(editor, translator, {\n  sourceLanguage: \"en\",\n  targetLanguage: \"fr\",\n  createUpdate(block, translation) {\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: translation,\n          styles: { bold: block.type === \"heading\" }\n        }\n      ]\n    };\n  }\n});\n```\n\n## Models and download size\n\nThe default catalog uses direction-specific OPUS/Marian models. English pairs\nload one model. A pair such as French to Spanish routes through English and\nloads two models.\n\nQuality caveat: the default English to Japanese model\n(`Xenova/opus-mt-en-jap`) was trained on a Bible corpus and produces stilted\noutput for general text. Prefer the browser's built-in translator for this\npair, or replace the catalog entry with a model that fits your content.\n\nThe default `dtype` is `int8` to reduce downloads. ONNX graph optimization is\ndisabled by default because current ONNX Runtime Web releases can fail while\nrewriting the quantized OPUS/Marian graphs. To request full precision and\nre-enable optimization:\n\n```ts\nconst translator = new BrowserTranslator({\n  dtype: \"fp32\",\n  graphOptimizationLevel: \"all\"\n});\n```\n\nStreaming does not affect model quality. Quantization, model choice, and pivot\ntranslation can affect quality.\n\n## Transformers.js loading and CSP\n\nBy default, the worker imports Transformers.js from jsDelivr. For production\nuse, self-hosting the browser build is recommended. It removes the runtime CDN\ndependency, keeps the deployment fully first-party, and works under a\nrestrictive Content Security Policy:\n\n```ts\nconst translator = new BrowserTranslator({\n  transformersUrl: new URL(\n    \"/vendor/transformers.min.js\",\n    window.location.origin\n  ).href\n});\n```\n\nYour CSP must permit the worker, the Transformers.js module URL, Hugging Face\nmodel downloads, and WebAssembly execution.\n\n## Add or replace language models\n\n```ts\nimport {\n  BrowserTranslator,\n  DEFAULT_MODEL_CATALOG\n} from \"yjs-local-translator\";\n\nconst translator = new BrowserTranslator({\n  modelCatalog: {\n    ...DEFAULT_MODEL_CATALOG,\n    \"en>nl\": {\n      id: \"Xenova/opus-mt-en-nl\",\n      label: \"English → Dutch\"\n    },\n    \"nl>en\": {\n      id: \"Xenova/opus-mt-nl-en\",\n      label: \"Dutch → English\"\n    }\n  }\n});\n```\n\n## Example app\n\nA runnable BlockNote editor with translation controls lives in\n[`examples/blocknote`](examples/blocknote). Build the package, then install and\nstart the example:\n\n```bash\ncorepack enable   # once per machine; provisions the pinned yarn version\nyarn install\nyarn build\ncd examples/blocknote\nyarn install\nyarn dev\n```\n\nSee the example's README for the yarn link setup that keeps library rebuilds\nvisible to the example without reinstalling.\n\n## Development\n\n```bash\nyarn install\nyarn check\nyarn pack\n```\n\nBefore publishing, confirm the package name is still available on the npm\nregistry and fill in the author, repository, bugs, and homepage fields in\n`package.json`:\n\n```bash\nyarn publish --access public\n```\n","readmeFilename":"README.md"}