@defensestation/yjs-local-translator 0.3.0-dev.2.d2befbc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +519 -0
- package/dist/blocknote.d.ts +62 -0
- package/dist/blocknote.d.ts.map +1 -0
- package/dist/blocknote.js +379 -0
- package/dist/blocknote.js.map +1 -0
- package/dist/browser-translator.d.ts +81 -0
- package/dist/browser-translator.d.ts.map +1 -0
- package/dist/browser-translator.js +452 -0
- package/dist/browser-translator.js.map +1 -0
- package/dist/chunking.d.ts +16 -0
- package/dist/chunking.d.ts.map +1 -0
- package/dist/chunking.js +95 -0
- package/dist/chunking.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata.d.ts +26 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +41 -0
- package/dist/metadata.js.map +1 -0
- package/dist/model-catalog.d.ts +14 -0
- package/dist/model-catalog.d.ts.map +1 -0
- package/dist/model-catalog.js +41 -0
- package/dist/model-catalog.js.map +1 -0
- package/dist/native-translation.d.ts +37 -0
- package/dist/native-translation.d.ts.map +1 -0
- package/dist/native-translation.js +80 -0
- package/dist/native-translation.js.map +1 -0
- package/dist/protocol.d.ts +59 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/translation-memory.d.ts +50 -0
- package/dist/translation-memory.d.ts.map +1 -0
- package/dist/translation-memory.js +117 -0
- package/dist/translation-memory.js.map +1 -0
- package/dist/translator.worker.d.ts +2 -0
- package/dist/translator.worker.d.ts.map +1 -0
- package/dist/translator.worker.js +304 -0
- package/dist/translator.worker.js.map +1 -0
- package/dist/yjs.d.ts +98 -0
- package/dist/yjs.d.ts.map +1 -0
- package/dist/yjs.js +291 -0
- package/dist/yjs.js.map +1 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# yjs-local-translator
|
|
2
|
+
|
|
3
|
+
Private, local-first browser translation with:
|
|
4
|
+
|
|
5
|
+
- built-in browser translation (Chrome's `Translator` API) when available, with
|
|
6
|
+
automatic fallback to Transformers.js
|
|
7
|
+
- lazy direction-specific model loading
|
|
8
|
+
- Web Worker inference
|
|
9
|
+
- sentence-aware chunking so long documents survive model input limits
|
|
10
|
+
- a translation memory that makes retranslation of edited documents
|
|
11
|
+
incremental: only changed sentences reach the model
|
|
12
|
+
- staleness tracking, so the app knows when a stored translation no longer
|
|
13
|
+
matches its source
|
|
14
|
+
- watchers that keep translations up to date automatically as the source
|
|
15
|
+
changes
|
|
16
|
+
- full-input output streaming through `TextStreamer`
|
|
17
|
+
- Yjs adapters that commit the final result as one minimal edit
|
|
18
|
+
- BlockNote adapters that group final edits into one transaction
|
|
19
|
+
- conflict detection so collaborator edits are not silently overwritten
|
|
20
|
+
|
|
21
|
+
The streamed text is intended for a local preview, popover, or side panel. It is
|
|
22
|
+
not written token-by-token into the shared document. When inference completes,
|
|
23
|
+
the final translation is written once.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn add yjs-local-translator
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Install the adapter peer dependency you use:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
yarn add yjs
|
|
35
|
+
# and/or
|
|
36
|
+
yarn add @blocknote/core
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Core browser translator
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { BrowserTranslator } from "yjs-local-translator";
|
|
43
|
+
|
|
44
|
+
const translator = new BrowserTranslator();
|
|
45
|
+
|
|
46
|
+
const result = await translator.translate("Hello, how are you?", {
|
|
47
|
+
sourceLanguage: "en",
|
|
48
|
+
targetLanguage: "fr",
|
|
49
|
+
onDelta(delta, streamedText) {
|
|
50
|
+
preview.textContent = streamedText;
|
|
51
|
+
},
|
|
52
|
+
onProgress(progress) {
|
|
53
|
+
console.log(progress.message, progress.progress);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(result.text);
|
|
58
|
+
console.log(result.engine); // "native", "transformers", "memory", or "none"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Streaming only exposes decoded output while generation is already running.
|
|
62
|
+
|
|
63
|
+
Language tags are normalized to their lowercase primary subtag, so `"EN"`,
|
|
64
|
+
`"en-US"`, and `"en"` are equivalent.
|
|
65
|
+
|
|
66
|
+
## Built-in browser translation
|
|
67
|
+
|
|
68
|
+
When the page runs in a browser that exposes the built-in `Translator` API
|
|
69
|
+
(Chrome 138+, and Chromium-based browsers that ship it), the requested language
|
|
70
|
+
pair is first offered to the browser's own on-device translator. The language
|
|
71
|
+
packs are downloaded and managed by the browser, nothing leaves the machine,
|
|
72
|
+
and no Transformers.js worker is started. If the API is missing, the pair is
|
|
73
|
+
unsupported, or creation fails, translation falls back to the Transformers.js
|
|
74
|
+
worker without any change in behavior for the caller.
|
|
75
|
+
|
|
76
|
+
The result's `engine` field reports which engine produced the translation.
|
|
77
|
+
|
|
78
|
+
To always use the Transformers.js worker:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const translator = new BrowserTranslator({ useNativeTranslation: false });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Long text and chunking
|
|
85
|
+
|
|
86
|
+
Marian/OPUS models truncate input past roughly 512 tokens. Before inference,
|
|
87
|
+
input is split on paragraph and sentence boundaries into chunks of at most
|
|
88
|
+
`maxChunkLength` characters (default 500), translated chunk by chunk, and
|
|
89
|
+
reassembled with the original paragraph spacing. Streaming callbacks receive
|
|
90
|
+
the concatenated output across chunks.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const translator = new BrowserTranslator({ maxChunkLength: 800 });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Translation memory: incremental retranslation
|
|
97
|
+
|
|
98
|
+
Without a memory, every call translates the full input. With a memory, the
|
|
99
|
+
input is split into sentence chunks and each chunk is looked up in a cache
|
|
100
|
+
before it is sent to a model. Editing one sentence in a long document then
|
|
101
|
+
costs one model call on the next translation instead of a full run.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import {
|
|
105
|
+
BrowserTranslator,
|
|
106
|
+
InMemoryTranslationMemory
|
|
107
|
+
} from "yjs-local-translator";
|
|
108
|
+
|
|
109
|
+
const translator = new BrowserTranslator({
|
|
110
|
+
translationMemory: new InMemoryTranslationMemory()
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`InMemoryTranslationMemory` keeps up to 5000 entries (configurable) with
|
|
115
|
+
least-recently-used eviction and lasts for the session. To persist the cache
|
|
116
|
+
across page loads, use the IndexedDB adapter:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { IndexedDBTranslationMemory } from "yjs-local-translator";
|
|
120
|
+
|
|
121
|
+
const translator = new BrowserTranslator({
|
|
122
|
+
translationMemory: new IndexedDBTranslationMemory()
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Details worth knowing:
|
|
127
|
+
|
|
128
|
+
- Cache keys contain the engine identity (native pair or model route), the
|
|
129
|
+
language pair, and the chunk text. Changing the model catalog or switching
|
|
130
|
+
engines produces different keys, so a hit can never return output from a
|
|
131
|
+
different model.
|
|
132
|
+
- When every chunk is served from the cache, the result's `engine` is
|
|
133
|
+
`"memory"` and the translation is close to instant.
|
|
134
|
+
- Streaming callbacks still fire in document order; cached chunks arrive as
|
|
135
|
+
one delta each.
|
|
136
|
+
- A custom store only needs `get(key)` and `set(key, value)` (sync or async)
|
|
137
|
+
to satisfy the `TranslationMemory` interface.
|
|
138
|
+
- Cache failures are swallowed: a broken IndexedDB never fails a translation,
|
|
139
|
+
it only loses the speedup.
|
|
140
|
+
|
|
141
|
+
## Detecting stale translations
|
|
142
|
+
|
|
143
|
+
When a translation is stored beside its source with `translateYMapString`, a
|
|
144
|
+
metadata record is written under `"<targetKey>:meta"` in the same
|
|
145
|
+
transaction. It contains a hash of the exact source text that was translated,
|
|
146
|
+
the language pair, the models used, and a timestamp.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import {
|
|
150
|
+
isYMapTranslationStale,
|
|
151
|
+
translateYMapString
|
|
152
|
+
} from "yjs-local-translator/yjs";
|
|
153
|
+
|
|
154
|
+
await translateYMapString(translator, fields, "title", {
|
|
155
|
+
sourceLanguage: "en",
|
|
156
|
+
targetLanguage: "de",
|
|
157
|
+
targetKey: "title:de"
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
isYMapTranslationStale(fields, "title", "title:de"); // false
|
|
161
|
+
|
|
162
|
+
fields.set("title", "New content");
|
|
163
|
+
|
|
164
|
+
isYMapTranslationStale(fields, "title", "title:de"); // true
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use this to render an "outdated translation" badge or to decide whether a
|
|
168
|
+
retranslation is needed at all. Metadata is skipped when the source is
|
|
169
|
+
replaced in place (there is nothing left to compare), and can be turned off
|
|
170
|
+
with `storeMetadata: false` or moved with `metadataKey`.
|
|
171
|
+
|
|
172
|
+
For a whole-document `Y.Text`, pass a map and key to receive the record:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { translateYText } from "yjs-local-translator/yjs";
|
|
176
|
+
|
|
177
|
+
await translateYText(translator, sourceText, {
|
|
178
|
+
sourceLanguage: "en",
|
|
179
|
+
targetLanguage: "de",
|
|
180
|
+
target: germanText,
|
|
181
|
+
metadataMap: doc.getMap("translation-meta"),
|
|
182
|
+
metadataKey: "content:de"
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The lower-level pieces are exported too: `hashText(text)` and
|
|
187
|
+
`isTranslationStale(sourceText, metadata)`.
|
|
188
|
+
|
|
189
|
+
## Keeping translations up to date automatically
|
|
190
|
+
|
|
191
|
+
Watchers observe a source, wait for typing to settle, and retranslate. They
|
|
192
|
+
pair naturally with a translation memory, which keeps each rerun cheap.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { watchYMapString } from "yjs-local-translator/yjs";
|
|
196
|
+
|
|
197
|
+
const stop = watchYMapString(translator, fields, "title", {
|
|
198
|
+
sourceLanguage: "en",
|
|
199
|
+
targetLanguage: "de",
|
|
200
|
+
targetKey: "title:de",
|
|
201
|
+
debounceMs: 2000,
|
|
202
|
+
onTranslated(result) {
|
|
203
|
+
console.log("updated:", result.text);
|
|
204
|
+
},
|
|
205
|
+
onError(error) {
|
|
206
|
+
console.warn(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// when the component unmounts or the feature is turned off
|
|
211
|
+
stop();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`watchYText` does the same for a pair of Y.Text values:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { watchYText } from "yjs-local-translator/yjs";
|
|
218
|
+
|
|
219
|
+
const stop = watchYText(translator, englishText, {
|
|
220
|
+
sourceLanguage: "en",
|
|
221
|
+
targetLanguage: "fr",
|
|
222
|
+
target: frenchText,
|
|
223
|
+
immediate: true
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Options:
|
|
228
|
+
|
|
229
|
+
- `debounceMs` (default 1500): idle time after the last edit before reacting.
|
|
230
|
+
Bursts of keystrokes collapse into one translation.
|
|
231
|
+
- `autoTranslate` (default true): set to false to only receive `onStale`
|
|
232
|
+
callbacks and trigger translation yourself.
|
|
233
|
+
- `localOnly` (default true): react only to edits made by this client. In a
|
|
234
|
+
collaborative document this means exactly one client, the author of the
|
|
235
|
+
edit, retranslates. Every other client receives the translated result
|
|
236
|
+
through sync instead of racing to produce it.
|
|
237
|
+
- `immediate` (default false): run once right after the watcher starts, which
|
|
238
|
+
is useful to repair translations that went stale while the app was closed.
|
|
239
|
+
- `onStale(sourceText)`, `onTranslated(result)`, `onError(error)`.
|
|
240
|
+
|
|
241
|
+
Watchers never react to their own writes: transactions tagged with the
|
|
242
|
+
translation origin are ignored, so there is no feedback loop. If the source
|
|
243
|
+
changes while a translation is running, the conflicting result is dropped and
|
|
244
|
+
a fresh run is scheduled with the newer text.
|
|
245
|
+
|
|
246
|
+
The target must be different from the source (a separate `Y.Text` or a
|
|
247
|
+
different map key). Watching a value onto itself would retranslate its own
|
|
248
|
+
output.
|
|
249
|
+
|
|
250
|
+
## Recommended production setup
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import {
|
|
254
|
+
BrowserTranslator,
|
|
255
|
+
IndexedDBTranslationMemory
|
|
256
|
+
} from "yjs-local-translator";
|
|
257
|
+
import { watchYMapString } from "yjs-local-translator/yjs";
|
|
258
|
+
|
|
259
|
+
const translator = new BrowserTranslator({
|
|
260
|
+
translationMemory: new IndexedDBTranslationMemory()
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const stop = watchYMapString(translator, fields, "title", {
|
|
264
|
+
sourceLanguage: "en",
|
|
265
|
+
targetLanguage: "de",
|
|
266
|
+
targetKey: "title:de",
|
|
267
|
+
immediate: true
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
On startup, `immediate: true` repairs anything that drifted while the app was
|
|
272
|
+
offline; the memory makes that repair skip every unchanged sentence; and the
|
|
273
|
+
watcher keeps the pair in sync from then on.
|
|
274
|
+
|
|
275
|
+
## Yjs
|
|
276
|
+
|
|
277
|
+
### Replace a Y.Text once
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import * as Y from "yjs";
|
|
281
|
+
import { BrowserTranslator } from "yjs-local-translator";
|
|
282
|
+
import { translateYText } from "yjs-local-translator/yjs";
|
|
283
|
+
|
|
284
|
+
const doc = new Y.Doc();
|
|
285
|
+
const text = doc.getText("content");
|
|
286
|
+
text.insert(0, "Hello world");
|
|
287
|
+
|
|
288
|
+
const translator = new BrowserTranslator();
|
|
289
|
+
|
|
290
|
+
await translateYText(translator, text, {
|
|
291
|
+
sourceLanguage: "en",
|
|
292
|
+
targetLanguage: "es",
|
|
293
|
+
onDelta(_delta, streamedText) {
|
|
294
|
+
localPreview.textContent = streamedText;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The preview streams locally. The Y.Text changes in one final Yjs transaction,
|
|
300
|
+
applied as a minimal edit: the longest common prefix and suffix are left
|
|
301
|
+
untouched so collaborator cursors and relative positions outside the changed
|
|
302
|
+
region survive. By default, the operation aborts if the source changes during
|
|
303
|
+
translation.
|
|
304
|
+
|
|
305
|
+
### Store a translation beside the source
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { translateYMapString } from "yjs-local-translator/yjs";
|
|
309
|
+
|
|
310
|
+
const fields = doc.getMap("fields");
|
|
311
|
+
fields.set("title", "Local-first collaboration");
|
|
312
|
+
|
|
313
|
+
await translateYMapString(translator, fields, "title", {
|
|
314
|
+
sourceLanguage: "en",
|
|
315
|
+
targetLanguage: "de",
|
|
316
|
+
targetKey: "title:de"
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## BlockNote
|
|
321
|
+
|
|
322
|
+
### Translate the current text selection
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { BrowserTranslator } from "yjs-local-translator";
|
|
326
|
+
import {
|
|
327
|
+
translateBlockNoteSelection
|
|
328
|
+
} from "yjs-local-translator/blocknote";
|
|
329
|
+
|
|
330
|
+
const translator = new BrowserTranslator();
|
|
331
|
+
let preview = "";
|
|
332
|
+
|
|
333
|
+
await translateBlockNoteSelection(editor, translator, {
|
|
334
|
+
sourceLanguage: "en",
|
|
335
|
+
targetLanguage: "fr",
|
|
336
|
+
onDelta(_delta, streamedText) {
|
|
337
|
+
preview = streamedText;
|
|
338
|
+
renderPreview(preview);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The selection is translated as one complete input. The final replacement is a
|
|
344
|
+
single BlockNote transaction and preserves inline styles, links, and custom
|
|
345
|
+
inline wrappers. If the user or a collaborator changes the selection while
|
|
346
|
+
translation is running, the default conflict policy aborts.
|
|
347
|
+
|
|
348
|
+
### Translate the whole document
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import {
|
|
352
|
+
translateBlockNoteDocument
|
|
353
|
+
} from "yjs-local-translator/blocknote";
|
|
354
|
+
|
|
355
|
+
await translateBlockNoteDocument(editor, translator, {
|
|
356
|
+
sourceLanguage: "en",
|
|
357
|
+
targetLanguage: "es",
|
|
358
|
+
onBlockDelta({ blockId, streamedText }) {
|
|
359
|
+
renderBlockPreview(blockId, streamedText);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Every root and nested text block is translated in document order. The finished
|
|
365
|
+
block changes are grouped in one BlockNote transaction and therefore one undo
|
|
366
|
+
step. Non-text blocks are skipped. `translateBlockNoteBlocks` remains
|
|
367
|
+
available when you need to translate an explicit set of blocks.
|
|
368
|
+
|
|
369
|
+
## Collaboration behavior
|
|
370
|
+
|
|
371
|
+
The default conflict policy is `"abort"`. This prevents a completed translation
|
|
372
|
+
from overwriting content that changed while the model was running.
|
|
373
|
+
|
|
374
|
+
For explicit overwrite behavior:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
await translateYText(translator, text, {
|
|
378
|
+
sourceLanguage: "en",
|
|
379
|
+
targetLanguage: "fr",
|
|
380
|
+
conflictPolicy: "overwrite"
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Block translation additionally supports `"skip"`, which commits unchanged
|
|
385
|
+
blocks and leaves concurrently edited blocks alone.
|
|
386
|
+
|
|
387
|
+
## Formatting behavior
|
|
388
|
+
|
|
389
|
+
`translateBlockNoteSelection` replaces the selected inline content with plain
|
|
390
|
+
translated text. `translateBlockNoteDocument` and
|
|
391
|
+
`translateBlockNoteBlocks` preserve block IDs, block types, properties,
|
|
392
|
+
children, document position, and inline text styles. Because translation can
|
|
393
|
+
change word order and length, styles across multiple text spans are assigned
|
|
394
|
+
proportionally in the translated text.
|
|
395
|
+
|
|
396
|
+
Nested blocks are left in place and table cells are translated individually,
|
|
397
|
+
preserving table dimensions, headers, widths, merged-cell properties, cell
|
|
398
|
+
styles, links, and other inline wrappers. Use `createUpdate` when your
|
|
399
|
+
application needs a different mapping strategy.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
await translateBlockNoteBlocks(editor, translator, {
|
|
403
|
+
sourceLanguage: "en",
|
|
404
|
+
targetLanguage: "fr",
|
|
405
|
+
createUpdate(block, translation) {
|
|
406
|
+
return {
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
409
|
+
type: "text",
|
|
410
|
+
text: translation,
|
|
411
|
+
styles: { bold: block.type === "heading" }
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Models and download size
|
|
420
|
+
|
|
421
|
+
The default catalog uses direction-specific OPUS/Marian models. English pairs
|
|
422
|
+
load one model. A pair such as French to Spanish routes through English and
|
|
423
|
+
loads two models.
|
|
424
|
+
|
|
425
|
+
Quality caveat: the default English to Japanese model
|
|
426
|
+
(`Xenova/opus-mt-en-jap`) was trained on a Bible corpus and produces stilted
|
|
427
|
+
output for general text. Prefer the browser's built-in translator for this
|
|
428
|
+
pair, or replace the catalog entry with a model that fits your content.
|
|
429
|
+
|
|
430
|
+
The default `dtype` is `int8` to reduce downloads. ONNX graph optimization is
|
|
431
|
+
disabled by default because current ONNX Runtime Web releases can fail while
|
|
432
|
+
rewriting the quantized OPUS/Marian graphs. To request full precision and
|
|
433
|
+
re-enable optimization:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
const translator = new BrowserTranslator({
|
|
437
|
+
dtype: "fp32",
|
|
438
|
+
graphOptimizationLevel: "all"
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Streaming does not affect model quality. Quantization, model choice, and pivot
|
|
443
|
+
translation can affect quality.
|
|
444
|
+
|
|
445
|
+
## Transformers.js loading and CSP
|
|
446
|
+
|
|
447
|
+
By default, the worker imports Transformers.js from jsDelivr. For production
|
|
448
|
+
use, self-hosting the browser build is recommended. It removes the runtime CDN
|
|
449
|
+
dependency, keeps the deployment fully first-party, and works under a
|
|
450
|
+
restrictive Content Security Policy:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
const translator = new BrowserTranslator({
|
|
454
|
+
transformersUrl: new URL(
|
|
455
|
+
"/vendor/transformers.min.js",
|
|
456
|
+
window.location.origin
|
|
457
|
+
).href
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Your CSP must permit the worker, the Transformers.js module URL, Hugging Face
|
|
462
|
+
model downloads, and WebAssembly execution.
|
|
463
|
+
|
|
464
|
+
## Add or replace language models
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import {
|
|
468
|
+
BrowserTranslator,
|
|
469
|
+
DEFAULT_MODEL_CATALOG
|
|
470
|
+
} from "yjs-local-translator";
|
|
471
|
+
|
|
472
|
+
const translator = new BrowserTranslator({
|
|
473
|
+
modelCatalog: {
|
|
474
|
+
...DEFAULT_MODEL_CATALOG,
|
|
475
|
+
"en>nl": {
|
|
476
|
+
id: "Xenova/opus-mt-en-nl",
|
|
477
|
+
label: "English → Dutch"
|
|
478
|
+
},
|
|
479
|
+
"nl>en": {
|
|
480
|
+
id: "Xenova/opus-mt-nl-en",
|
|
481
|
+
label: "Dutch → English"
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Example app
|
|
488
|
+
|
|
489
|
+
A runnable BlockNote editor with translation controls lives in
|
|
490
|
+
[`examples/blocknote`](examples/blocknote). Build the package, then install and
|
|
491
|
+
start the example:
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
corepack enable # once per machine; provisions the pinned yarn version
|
|
495
|
+
yarn install
|
|
496
|
+
yarn build
|
|
497
|
+
cd examples/blocknote
|
|
498
|
+
yarn install
|
|
499
|
+
yarn dev
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
See the example's README for the yarn link setup that keeps library rebuilds
|
|
503
|
+
visible to the example without reinstalling.
|
|
504
|
+
|
|
505
|
+
## Development
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
yarn install
|
|
509
|
+
yarn check
|
|
510
|
+
yarn pack
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Before publishing, confirm the package name is still available on the npm
|
|
514
|
+
registry and fill in the author, repository, bugs, and homepage fields in
|
|
515
|
+
`package.json`:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
yarn publish --access public
|
|
519
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Block, BlockNoteEditor, PartialBlock } from "@blocknote/core";
|
|
2
|
+
import type { BrowserTranslator, TranslateOptions, TranslationResult } from "./index.js";
|
|
3
|
+
import { type ConflictPolicy } from "./yjs.js";
|
|
4
|
+
type AnyBlock = Block<any, any, any>;
|
|
5
|
+
type AnyPartialBlock = PartialBlock<any, any, any>;
|
|
6
|
+
type AnyEditor = BlockNoteEditor<any, any, any>;
|
|
7
|
+
export interface BlockTranslationDelta {
|
|
8
|
+
blockId: string;
|
|
9
|
+
blockIndex: number;
|
|
10
|
+
blockCount: number;
|
|
11
|
+
delta: string;
|
|
12
|
+
streamedText: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TranslatedBlock {
|
|
15
|
+
blockId: string;
|
|
16
|
+
sourceText: string;
|
|
17
|
+
translation: string;
|
|
18
|
+
result: TranslationResult;
|
|
19
|
+
}
|
|
20
|
+
export interface TranslateBlockNoteBlocksOptions extends Omit<TranslateOptions, "onDelta"> {
|
|
21
|
+
blocks?: AnyBlock[];
|
|
22
|
+
conflictPolicy?: ConflictPolicy | "skip";
|
|
23
|
+
commit?: boolean;
|
|
24
|
+
onBlockDelta?: (event: BlockTranslationDelta) => void;
|
|
25
|
+
onUnsupportedBlock?: (block: AnyBlock) => void;
|
|
26
|
+
createUpdate?: (block: AnyBlock, translatedText: string) => AnyPartialBlock;
|
|
27
|
+
}
|
|
28
|
+
/** Options for translating every text block in a BlockNote document. */
|
|
29
|
+
export interface TranslateBlockNoteDocumentOptions extends TranslateBlockNoteBlocksOptions {
|
|
30
|
+
}
|
|
31
|
+
export interface TranslateBlockNoteSelectionOptions extends TranslateOptions {
|
|
32
|
+
conflictPolicy?: ConflictPolicy;
|
|
33
|
+
commit?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function blockNoteBlockToPlainText(block: AnyBlock): string;
|
|
36
|
+
/**
|
|
37
|
+
* Returns every block in document order, including nested child blocks.
|
|
38
|
+
*
|
|
39
|
+
* BlockNote's `editor.document` contains only root blocks, so children must
|
|
40
|
+
* be traversed explicitly when operating on the whole document.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getBlockNoteDocumentBlocks(editor: AnyEditor): AnyBlock[];
|
|
43
|
+
/**
|
|
44
|
+
* Translates selected or explicit inline-content blocks. Every block is sent
|
|
45
|
+
* to the model as one complete input. Streaming remains local; all finished
|
|
46
|
+
* block updates are grouped in one BlockNote transaction.
|
|
47
|
+
*/
|
|
48
|
+
export declare function translateBlockNoteBlocks(editor: AnyEditor, translator: BrowserTranslator, options: TranslateBlockNoteBlocksOptions): Promise<TranslatedBlock[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Translates every supported text block in the BlockNote document. Root and
|
|
51
|
+
* nested blocks are translated in document order, then committed together in
|
|
52
|
+
* one BlockNote transaction. Use this instead of `translateBlockNoteBlocks`
|
|
53
|
+
* when selection and cursor position should not affect the translation scope.
|
|
54
|
+
*/
|
|
55
|
+
export declare function translateBlockNoteDocument(editor: AnyEditor, translator: BrowserTranslator, options: TranslateBlockNoteDocumentOptions): Promise<TranslatedBlock[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Translates the current text selection as one complete input and replaces it
|
|
58
|
+
* once when finished. It aborts if the selection or selected text changes.
|
|
59
|
+
*/
|
|
60
|
+
export declare function translateBlockNoteSelection(editor: AnyEditor, translator: BrowserTranslator, options: TranslateBlockNoteSelectionOptions): Promise<TranslationResult>;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=blocknote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocknote.d.ts","sourceRoot":"","sources":["../src/blocknote.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,eAAe,EACf,YAAY,EACb,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,UAAU,CAAC;AAElB,KAAK,QAAQ,GAAG,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AACrC,KAAK,eAAe,GAAG,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AACnD,KAAK,SAAS,GAAG,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAShD,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,MAAM,WAAW,+BACf,SAAQ,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC;IACzC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC;IACzC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACtD,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC/C,YAAY,CAAC,EAAE,CACb,KAAK,EAAE,QAAQ,EACf,cAAc,EAAE,MAAM,KACnB,eAAe,CAAC;CACtB;AAED,wEAAwE;AACxE,MAAM,WAAW,iCACf,SAAQ,+BAA+B;CAAG;AAE5C,MAAM,WAAW,kCAAmC,SAAQ,gBAAgB;IAC1E,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAuLD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAqBjE;AAyGD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,EAAE,CAexE;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,iBAAiB,EAC7B,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,eAAe,EAAE,CAAC,CAwH5B;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,iBAAiB,EAC7B,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,eAAe,EAAE,CAAC,CAK5B;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,iBAAiB,EAC7B,OAAO,EAAE,kCAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAyC5B"}
|