@khorsheed/dsh-reader 0.2.0

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.
Files changed (58) hide show
  1. package/README.en.md +90 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +90 -0
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +15119 -0
  6. package/lib/index.js +3660 -0
  7. package/lib/invariant.js +38 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +1738 -0
  10. package/lib/typert.remote-client.d.ts +82 -0
  11. package/lib/typert.remote-client.js +1115 -0
  12. package/lib/types/arxiv.d.ts +47 -0
  13. package/lib/types/arxiv.js +76 -0
  14. package/lib/types/client/ReaderPane.d.ts +71 -0
  15. package/lib/types/client/ReaderPane.js +3135 -0
  16. package/lib/types/client/contract.d.ts +254 -0
  17. package/lib/types/client/contract.js +1 -0
  18. package/lib/types/client/definition.d.ts +27 -0
  19. package/lib/types/client/definition.js +32 -0
  20. package/lib/types/client/extract-article.d.ts +102 -0
  21. package/lib/types/client/extract-article.js +1024 -0
  22. package/lib/types/client/icons.d.ts +27 -0
  23. package/lib/types/client/icons.js +11 -0
  24. package/lib/types/client/index.d.ts +34 -0
  25. package/lib/types/client/index.js +222 -0
  26. package/lib/types/client/locales.d.ts +391 -0
  27. package/lib/types/client/locales.js +382 -0
  28. package/lib/types/client/parse-rss.d.ts +69 -0
  29. package/lib/types/client/parse-rss.js +656 -0
  30. package/lib/types/client/quote.d.ts +89 -0
  31. package/lib/types/client/quote.js +119 -0
  32. package/lib/types/client/selectors.d.ts +167 -0
  33. package/lib/types/client/selectors.js +413 -0
  34. package/lib/types/client/session.d.ts +171 -0
  35. package/lib/types/client/session.js +223 -0
  36. package/lib/types/client/store.d.ts +181 -0
  37. package/lib/types/client/store.js +226 -0
  38. package/lib/types/client/translate.d.ts +379 -0
  39. package/lib/types/client/translate.js +873 -0
  40. package/lib/types/index.d.ts +48 -0
  41. package/lib/types/index.js +25 -0
  42. package/lib/types/invariant.d.ts +29 -0
  43. package/lib/types/invariant.js +38 -0
  44. package/lib/types/link-resolvers.d.ts +107 -0
  45. package/lib/types/link-resolvers.js +299 -0
  46. package/lib/types/remote.d.ts +224 -0
  47. package/lib/types/remote.js +275 -0
  48. package/lib/types/schedule.d.ts +78 -0
  49. package/lib/types/schedule.js +114 -0
  50. package/lib/types/service.d.ts +633 -0
  51. package/lib/types/service.js +1697 -0
  52. package/lib/types/store.d.ts +224 -0
  53. package/lib/types/store.js +916 -0
  54. package/lib/types/types.d.ts +710 -0
  55. package/lib/types/types.js +182 -0
  56. package/locale/en.json +6 -0
  57. package/locale/zh.json +6 -0
  58. package/package.json +128 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * On-device translation for the article body, through the browser's own
3
+ * Translator API (Chrome 138+ / Edge 148+, desktop only).
4
+ *
5
+ * Everything here is the BROWSER half, on purpose. The model runs inside the
6
+ * user's browser: the article text never reaches the host, is never written to
7
+ * `state.json`, and costs no tokens. That is also why this module has no
8
+ * Remote verb and no host counterpart — its only dependency is a global the
9
+ * page may or may not have, and {@link detectTranslator} is the whole
10
+ * degradation story (no API → no button, not a broken one).
11
+ *
12
+ * The unit of translation is a SENTENCE inside one text node — never a node,
13
+ * never a paragraph. Two reasons the granularity matters:
14
+ *
15
+ * - The reader wants ONE sentence's original while reading the translation. A
16
+ * paragraph-level unit cannot answer that after the fact: the model merges
17
+ * and splits sentences, so re-aligning the two sides later is a guess, and a
18
+ * wrong guess shows the wrong original. Segmenting first makes the pairing
19
+ * true by construction.
20
+ * - Inline markup survives because we only ever touch text nodes: a link
21
+ * inside a sentence stays a link, its href untouched. A sentence that
22
+ * CROSSES an inline element becomes two units (the text on each side) — what
23
+ * Chrome's own page translation does — and each side still reveals exactly
24
+ * its own original.
25
+ *
26
+ * Batches go out as one string with a separator between units, and the answer
27
+ * is only accepted when it comes back with the same number of parts. A mangled
28
+ * separator costs that batch its granularity — it is re-sent one unit at a
29
+ * time — never its correctness.
30
+ *
31
+ * @module @khorsheed/dsh-reader/client/translate
32
+ */
33
+ import type { ReaderSentenceLearn } from '../types.ts';
34
+ /** The three views the reader picks between (the globe's menu). */
35
+ export type TranslationView = 'trans' | 'both' | 'orig';
36
+ /** What the browser reports for a language pair. */
37
+ export type TranslationAvailability = 'available' | 'downloadable' | 'downloading' | 'unavailable';
38
+ /**
39
+ * The target tags to try, in order.
40
+ *
41
+ * Chrome's documented list spells Chinese `zh` (Simplified) and `zh-Hant`
42
+ * (Traditional), and `zh` is what works on the machines this was built on — but
43
+ * `Translator.create()` has been observed rejecting a pair that
44
+ * `availability()` had just blessed (`NotSupportedError`, "Unable to create
45
+ * translator for the given source and target language", macOS builds included).
46
+ * Trying the next spelling costs one rejected call and turns a dead button into
47
+ * a working one where a build prefers the other tag.
48
+ */
49
+ export declare const TARGET_CANDIDATES: readonly string[];
50
+ /** The unit separator inside a batch. Rare, short, and verified on the way back. */
51
+ export declare const UNIT_SEPARATOR = "\u27E6|\u27E7";
52
+ /** The structural slice of the Translator API this module actually uses. */
53
+ export interface TranslatorLike {
54
+ availability(request: {
55
+ sourceLanguage: string;
56
+ targetLanguage: string;
57
+ }): Promise<string>;
58
+ create(request: {
59
+ sourceLanguage: string;
60
+ targetLanguage: string;
61
+ monitor?: (monitor: DownloadMonitorLike) => void;
62
+ }): Promise<TranslatorSessionLike>;
63
+ }
64
+ /** The `monitor` object `create()` hands back; only progress is read. */
65
+ export interface DownloadMonitorLike {
66
+ addEventListener(type: 'downloadprogress', listener: (event: {
67
+ loaded: number;
68
+ }) => void): void;
69
+ }
70
+ /** One created translator. */
71
+ export interface TranslatorSessionLike {
72
+ readonly inputQuota: number;
73
+ measureInputUsage(text: string): Promise<number>;
74
+ translate(text: string): Promise<string>;
75
+ }
76
+ /** Read the page's Translator API, or null when this browser has none. */
77
+ export declare function detectTranslator(scope?: unknown): TranslatorLike | null;
78
+ /** The structural slice of the Language Detector API, when the page has one. */
79
+ export interface LanguageDetectorLike {
80
+ availability(): Promise<string>;
81
+ create(options?: {
82
+ monitor?: (monitor: DownloadMonitorLike) => void;
83
+ }): Promise<{
84
+ detect(text: string): Promise<readonly {
85
+ detectedLanguage: string;
86
+ confidence: number;
87
+ }[]>;
88
+ destroy?(): void;
89
+ }>;
90
+ }
91
+ /** Read the page's Language Detector, or null when this browser has none. */
92
+ export declare function detectLanguageDetector(scope?: unknown): LanguageDetectorLike | null;
93
+ /**
94
+ * The body's source language: the browser's detector when it has one, else the
95
+ * caller's script guess.
96
+ *
97
+ * The probe is only worth it because a WRONG source is fatal in a way the
98
+ * reader cannot see: `create()` rejects for a pair the device does not have, and
99
+ * the error says "given source and target language" without naming either. The
100
+ * heuristic that decides this in the pane (`isCjk`) is a script test, not a
101
+ * language test — it cannot tell German from English.
102
+ *
103
+ * @param text - the body's leading text (a sample is enough).
104
+ * @param fallback - the caller's guess, used when detection is unavailable or unsure.
105
+ * @returns the BCP-47 tag to ask the translator for.
106
+ */
107
+ export declare function detectSourceLanguage(text: string, fallback: string): Promise<string>;
108
+ /** One attempted language pair and why it did not produce a translator. */
109
+ export interface SessionAttempt {
110
+ readonly sourceLanguage: string;
111
+ readonly targetLanguage: string;
112
+ readonly reason: string;
113
+ }
114
+ /** The outcome of asking the browser for a translator, across candidate pairs. */
115
+ export type SessionOutcome = {
116
+ readonly ok: true;
117
+ readonly session: TranslatorSessionLike;
118
+ readonly sourceLanguage: string;
119
+ readonly targetLanguage: string;
120
+ } | {
121
+ readonly ok: false;
122
+ readonly attempts: readonly SessionAttempt[];
123
+ readonly unsupported: boolean;
124
+ };
125
+ /** True when the browser says this pair is not something it can ever build. */
126
+ export declare function isUnsupported(error: unknown): boolean;
127
+ /**
128
+ * Create a translator, trying every candidate pair in order.
129
+ *
130
+ * `availability()` is a hint, not a promise: this browser has been observed
131
+ * blessing a pair and then rejecting `create()` for it. So the pairs are tried
132
+ * for real, the failures are collected (the reader gets to see which pair was
133
+ * asked for), and `unsupported` says whether the browser rejected EVERY attempt
134
+ * as an unsupported language pair — which is a permanent condition, unlike a
135
+ * download or a quota failure.
136
+ *
137
+ * @param api - the page's Translator API.
138
+ * @param request - the source candidates, the target candidates, and the monitor.
139
+ * @returns the first working session, or the collected attempts.
140
+ */
141
+ export declare function createSession(api: TranslatorLike, request: {
142
+ readonly sources: readonly string[];
143
+ readonly targets: readonly string[];
144
+ readonly monitor?: (monitor: DownloadMonitorLike) => void;
145
+ }): Promise<SessionOutcome>;
146
+ /** One sentence inside a text run, with the whitespace that follows it. */
147
+ export interface SentencePiece {
148
+ /** The sentence plus its trailing spacing — pieces concatenate back to the run. */
149
+ readonly text: string;
150
+ /** The translatable part (`text` without the trailing spacing). */
151
+ readonly core: string;
152
+ }
153
+ /**
154
+ * Split one run of text into sentence pieces.
155
+ *
156
+ * Conservative by construction: a split needs a terminator followed by
157
+ * whitespace (or the end), a token that is neither a known abbreviation nor a
158
+ * single initial, and a next character that does not continue a number. When
159
+ * any rule is unsure it does NOT split — one oversized unit only costs
160
+ * granularity, while a wrong split hands the model two broken half-sentences.
161
+ *
162
+ * @param text - the run's core text (already trimmed at both ends).
163
+ * @returns the pieces in order; a run with no terminator comes back as one.
164
+ */
165
+ export declare function splitSentences(text: string): SentencePiece[];
166
+ /** The class names the article markup is decorated with (hashed by the caller). */
167
+ export interface TranslateClasses {
168
+ readonly unit: string;
169
+ readonly reveal: string;
170
+ readonly line: string;
171
+ }
172
+ /** One translated sentence, as it lives in the article DOM. */
173
+ interface BuiltSegment {
174
+ readonly span: HTMLElement;
175
+ /** The source sentence, trimmed: what the reveal shows and what we translate. */
176
+ readonly original: string;
177
+ /**
178
+ * The whitespace that PRECEDED this unit inside its run. It stays in the
179
+ * original text node (so the DOM is unchanged), but the flattened block text
180
+ * needs it: without it two runs join as "See thebest-studied domains".
181
+ */
182
+ readonly lead: string;
183
+ /** The whitespace that followed it INSIDE the span (between sentences). */
184
+ readonly tail: string;
185
+ /**
186
+ * The run's own trailing whitespace, which stays in the DOM as a text node
187
+ * after the last span. It is not part of any span, but the flattened block
188
+ * text needs it — this is where "See the " + "best-studied domains" keeps its
189
+ * space.
190
+ */
191
+ readonly gap: string;
192
+ translated: string | null;
193
+ open: boolean;
194
+ }
195
+ /** Where a block's reveal container belongs. */
196
+ type RevealPlacement = 'sibling' | 'inside' | 'inline';
197
+ /** One block (paragraph, list item, heading…) and the segments inside it. */
198
+ interface BuiltBlock {
199
+ readonly element: Element;
200
+ readonly placement: RevealPlacement;
201
+ readonly segments: BuiltSegment[];
202
+ /** The block's original prose flattened across its units. */
203
+ flat: string;
204
+ /** Sentence ranges inside `flat`, in order — the reveal's real granularity. */
205
+ sentences: {
206
+ readonly start: number;
207
+ readonly end: number;
208
+ }[];
209
+ reveal: HTMLElement | null;
210
+ }
211
+ /** What one original text node needs to be put back exactly as it was. */
212
+ interface RestoreRecord {
213
+ readonly node: Text;
214
+ readonly lead: string;
215
+ readonly core: string;
216
+ readonly tail: string;
217
+ readonly tailNode: Text;
218
+ readonly spans: readonly HTMLElement[];
219
+ }
220
+ /** The live segmentation of one rendered article. */
221
+ export interface BuiltArticle {
222
+ readonly root: Element;
223
+ readonly blocks: BuiltBlock[];
224
+ readonly restores: RestoreRecord[];
225
+ view: TranslationView;
226
+ }
227
+ /**
228
+ * The stable key fragment for one sentence (or a whole body).
229
+ *
230
+ * FNV-1a over 64 bits: synchronous, which the crypto API is not, and long
231
+ * enough that the memory's 50k entries never meet a collision (the birthday
232
+ * bound is ~2^32 entries). The host stores the digest opaquely — only the
233
+ * browser ever computes it.
234
+ */
235
+ export declare function translationHash(text: string): string;
236
+ /**
237
+ * Segment one rendered article into sentence spans.
238
+ *
239
+ * Idempotent: a second call for the same element returns the first result, so a
240
+ * view switch never double-wraps. Mutates the DOM under `root` (each prose text
241
+ * node is replaced by spans); {@link restoreArticle} undoes it completely.
242
+ *
243
+ * @param root - the container holding the normalized article markup.
244
+ * @param classes - the hashed class names the spans and reveals carry.
245
+ * @returns the segmentation, or null when there is nothing to translate.
246
+ */
247
+ export declare function buildArticle(root: Element, classes: TranslateClasses): BuiltArticle | null;
248
+ /** Take the segmentation back out, restoring every original text node. */
249
+ export declare function restoreArticle(root: Element): void;
250
+ /** Paint the spans and the reveals for one view. */
251
+ export declare function setView(built: BuiltArticle, view: TranslationView, classes: TranslateClasses): void;
252
+ /**
253
+ * Mark (or clear) the pair under the pointer.
254
+ *
255
+ * The page is deliberately CLEAN by default: an expanded original carries no
256
+ * standing colour or bar, because a reader who explores a few sentences would
257
+ * otherwise leave the whole article speckled — reported as "the page gets
258
+ * messy". The pairing shows up on hover instead, from either side: point at a
259
+ * translated sentence and its original line lights up, point at an original
260
+ * line and the sentence it belongs to lights up.
261
+ *
262
+ * @param built - the live segmentation.
263
+ * @param target - what the pointer is over (or just left).
264
+ * @param on - true on hover, false when the pointer leaves.
265
+ */
266
+ export declare function setPairHover(built: BuiltArticle, target: EventTarget | null, on: boolean): void;
267
+ /** The segment a click landed on, if any. */
268
+ export declare function segmentAt(built: BuiltArticle, target: EventTarget | null): BuiltSegment | null;
269
+ /** Flip one sentence's original open / closed (the click gesture). */
270
+ export declare function toggleSegment(built: BuiltArticle, segment: BuiltSegment, classes: TranslateClasses): void;
271
+ /**
272
+ * Record a translated unit: in the DOM, and in the translation memory.
273
+ *
274
+ * @param pair - the `<src>→<tgt>` label this translation belongs to.
275
+ */
276
+ export declare function applyTranslation(built: BuiltArticle, segment: BuiltSegment, translated: string, pair: string): void;
277
+ /** A remembered translation for this sentence in this pair, when one exists. */
278
+ export declare function remembered(pair: string, text: string): string | undefined;
279
+ /**
280
+ * Pour translations the host persisted into the page's memory.
281
+ *
282
+ * The pane calls this before a translation run spends the model: every primed
283
+ * sentence paints from memory instead of a request. Entries already in the page
284
+ * are left alone (a fresher in-page write wins over the host's copy of it).
285
+ *
286
+ * @param pair - the pair label the translations belong to.
287
+ * @param entries - the sentences and their translations.
288
+ * @returns how many were new to the page.
289
+ */
290
+ export declare function primeMemory(pair: string, entries: readonly {
291
+ text: string;
292
+ translation: string;
293
+ }[]): number;
294
+ /** Drop the translation memory (the specs call this between cases). */
295
+ export declare function clearMemory(): void;
296
+ /**
297
+ * True when a string is already (mostly) the target language, so translating it
298
+ * would be Chinese-into-Chinese.
299
+ *
300
+ * The wall is where this matters: a wall mixes Chinese and English entries, and
301
+ * one card can even mix them field by field (a Chinese title with an English
302
+ * summary). The ratio mirrors the pane's own script test, and it is deliberately
303
+ * the CHEAP synchronous answer — the alternative was a Language Detector round
304
+ * trip per field for a decision a character count already gets right.
305
+ *
306
+ * @param text - the field's text.
307
+ * @returns true when the field should be left alone.
308
+ */
309
+ export declare function isTargetLanguage(text: string): boolean;
310
+ /** The outcome of translating a list of strings. */
311
+ export interface TextsOutcome {
312
+ /** Original → translation, for every string that came back. */
313
+ readonly translated: ReadonlyMap<string, string>;
314
+ /** How many strings were already remembered (no request needed). */
315
+ readonly remembered: number;
316
+ /** How many strings could not be translated at all. */
317
+ readonly failed: number;
318
+ /**
319
+ * What the run newly learned (for the host's persistent tiers), as
320
+ * `{ hash, source, target }` triples — `hash` is `translationHash(source)`.
321
+ */
322
+ readonly learned: readonly ReaderSentenceLearn[];
323
+ /** What the run was served from memory (whose LRU stamps should move). */
324
+ readonly recalled: readonly ReaderSentenceLearn[];
325
+ }
326
+ /**
327
+ * Translate a list of whole strings — the wall's cards, where a field is the
328
+ * unit and there is no DOM to segment.
329
+ *
330
+ * The batching contract is the article's, unchanged: sentences joined by the
331
+ * separator, accepted only when the same number of parts comes back, and a
332
+ * rejected batch re-sent one string at a time so the fallback gives up
333
+ * granularity rather than correctness. Results land in the same translation
334
+ * memory the article uses, so a card title translated here is not translated
335
+ * again when the same entry is opened.
336
+ *
337
+ * @param texts - the strings to translate, in order.
338
+ * @param pair - the `<src>→<tgt>` label these translations belong to.
339
+ * @param session - a translator for this source language.
340
+ * @param cancelled - checked between batches.
341
+ * @param onProgress - called with (done, total) as results land.
342
+ * @returns the translations, the remembered count and the failures.
343
+ */
344
+ export declare function translateTexts(texts: readonly string[], pair: string, session: TranslatorSessionLike, cancelled: () => boolean, onProgress?: (done: number, total: number) => void): Promise<TextsOutcome>;
345
+ /** What the driver needs; everything is injected so the specs can fake it. */
346
+ export interface RunTranslationOptions {
347
+ readonly built: BuiltArticle;
348
+ readonly session: TranslatorSessionLike;
349
+ /** The `<src>→<tgt>` label this run's translations belong to. */
350
+ readonly pair: string;
351
+ /** Flipped by the Cancel gesture; checked between batches. */
352
+ readonly cancelled: () => boolean;
353
+ readonly onProgress?: (done: number, total: number) => void;
354
+ }
355
+ /** What one run produced, for the host's persistent tiers. */
356
+ export interface RunTranslationResult {
357
+ readonly done: number;
358
+ readonly failed: number;
359
+ readonly total: number;
360
+ /** Units the model translated this run (new to the memory). */
361
+ readonly learned: readonly ReaderSentenceLearn[];
362
+ /** Units the memory served (their LRU stamps should move). */
363
+ readonly recalled: readonly ReaderSentenceLearn[];
364
+ }
365
+ /** Every segment of a built article, in reading order. */
366
+ export declare function segmentsOf(built: BuiltArticle): BuiltSegment[];
367
+ /**
368
+ * Translate every segment of a built article, best effort.
369
+ *
370
+ * Remembered units paint immediately; the rest go out in batches of whole
371
+ * sentences. A batch whose separator comes back altered is re-sent one unit at
372
+ * a time — the fallback gives up granularity for that batch, never correctness.
373
+ *
374
+ * @param options - the built article, a created session, and the cancel flag.
375
+ * @returns how many units were translated, how many failed, and the total.
376
+ */
377
+ export declare function runTranslation(options: RunTranslationOptions): Promise<RunTranslationResult>;
378
+ export {};
379
+ //# sourceMappingURL=translate.d.ts.map