@internetarchive/bookreader 5.0.0-97 → 5.0.0-98

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-97",
3
+ "version": "5.0.0-98",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "type": "module",
6
6
  "files": [
@@ -31,7 +31,7 @@
31
31
  "homepage": "https://github.com/internetarchive/bookreader#readme",
32
32
  "private": false,
33
33
  "dependencies": {
34
- "@internetarchive/bergamot-translator": "0.4.9-ia.0",
34
+ "@internetarchive/bergamot-translator": "^0.4.9-ia.1",
35
35
  "@internetarchive/ia-activity-indicator": "^0.0.4",
36
36
  "@internetarchive/ia-item-navigator": "^2.1.2",
37
37
  "@internetarchive/icon-bookmark": "^1.3.4",
@@ -147,21 +147,25 @@ export const DEFAULT_OPTIONS = {
147
147
  **/
148
148
  plugins: {
149
149
  /** @type {Partial<import('../plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin['options'>]}*/
150
- archiveAnalytics: null,
150
+ archiveAnalytics: {},
151
151
  /** @type {Partial<import('../plugins/plugin.autoplay.js').AutoplayPlugin['options'>]}*/
152
- autoplay: null,
152
+ autoplay: {},
153
153
  /** @type {Partial<import('../plugins/plugin.chapters.js').ChaptersPlugin['options']>} */
154
- chapters: null,
154
+ chapters: {},
155
+ /** @type {Partial<import('../plugins/plugin.experiments.js').ExperimentsPlugin['options']>} */
156
+ experiments: {},
155
157
  /** @type {Partial<import('../plugins/plugin.iiif.js').IiifPlugin['options']>} */
156
- iiif: null,
158
+ iiif: {},
157
159
  /** @type {Partial<import('../plugins/plugin.resume.js').ResumePlugin['options']>} */
158
- resume: null,
160
+ resume: {},
159
161
  /** @type {Partial<import('../plugins/search/plugin.search.js').SearchPlugin['options']>} */
160
- search: null,
162
+ search: {},
161
163
  /** @type {Partial<import('../plugins/plugin.text_selection.js').TextSelectionPlugin['options']>} */
162
- textSelection: null,
164
+ textSelection: {},
165
+ /** @type {Partial<import('../plugins/translate/plugin.translate.js').TranslatePlugin['options']>} */
166
+ translate: {},
163
167
  /** @type {Partial<import('../plugins/tts/plugin.tts.js').TtsPlugin['options']>} */
164
- tts: null,
168
+ tts: {},
165
169
  },
166
170
 
167
171
  /**
package/src/BookReader.js CHANGED
@@ -49,10 +49,47 @@ import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet.js';
49
49
  * @constructor
50
50
  */
51
51
  export default function BookReader(overrides = {}) {
52
- const options = jQuery.extend(true, {}, BookReader.defaultOptions, overrides, BookReader.optionOverrides);
52
+ const options = BookReader.extendOptions({}, BookReader.defaultOptions, overrides, BookReader.optionOverrides);
53
53
  this.setup(options);
54
54
  }
55
55
 
56
+ /**
57
+ * Extend the default options for BookReader
58
+ * Accepts any number of option objects and merges them in order.
59
+ *
60
+ * Does a shallow merge of everything except plugin options. These are individually merged.
61
+ * @param {...Partial<BookReaderOptions>} newOptions
62
+ */
63
+ BookReader.extendOptions = function(...newOptions) {
64
+ if (newOptions.length <= 1) {
65
+ return newOptions[0];
66
+ }
67
+ /** @type {Array<keyof BookReaderOptions>} */
68
+ const shallowOverrides = ['onePage', 'twoPage', 'vars'];
69
+ /** @type {Array<keyof BookReaderOptions>} */
70
+ const mapOverrides = ['plugins', 'controls'];
71
+
72
+ const result = newOptions.shift();
73
+ for (const opts of newOptions) {
74
+ for (const [key, value] of Object.entries(opts)) {
75
+ if (shallowOverrides.includes(key)) {
76
+ result[key] ||= {};
77
+ result[key] = Object.assign(result[key], value);
78
+ } else if (mapOverrides.includes(key)) {
79
+ result[key] ||= {};
80
+ for (const [name, mapValue] of Object.entries(value)) {
81
+ result[key][name] ||= {};
82
+ Object.assign(result[key][name], mapValue);
83
+ }
84
+ } else {
85
+ result[key] = value;
86
+ }
87
+ }
88
+ }
89
+
90
+ return result;
91
+ };
92
+
56
93
  BookReader.version = PACKAGE_JSON.version;
57
94
 
58
95
  // Mode constants
@@ -72,12 +109,16 @@ BookReader.PLUGINS = {
72
109
  autoplay: null,
73
110
  /** @type {typeof import('./plugins/plugin.chapters.js').ChaptersPlugin | null}*/
74
111
  chapters: null,
112
+ /** @type {typeof import('./plugins/plugin.experiments.js').ExperimentsPlugin | null}*/
113
+ experiments: null,
75
114
  /** @type {typeof import('./plugins/plugin.resume.js').ResumePlugin | null}*/
76
115
  resume: null,
77
116
  /** @type {typeof import('./plugins/search/plugin.search.js').SearchPlugin | null}*/
78
117
  search: null,
79
118
  /** @type {typeof import('./plugins/plugin.text_selection.js').TextSelectionPlugin | null}*/
80
119
  textSelection: null,
120
+ /** @type {typeof import('./plugins/translate/plugin.translate.js').TranslatePlugin | null}*/
121
+ translate: null,
81
122
  /** @type {typeof import('./plugins/tts/plugin.tts.js').TtsPlugin | null}*/
82
123
  tts: null,
83
124
  };
@@ -138,9 +179,11 @@ BookReader.prototype.setup = function(options) {
138
179
  archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null,
139
180
  autoplay: BookReader.PLUGINS.autoplay ? new BookReader.PLUGINS.autoplay(this) : null,
140
181
  chapters: BookReader.PLUGINS.chapters ? new BookReader.PLUGINS.chapters(this) : null,
182
+ experiments: BookReader.PLUGINS.experiments ? new BookReader.PLUGINS.experiments(this) : null,
141
183
  search: BookReader.PLUGINS.search ? new BookReader.PLUGINS.search(this) : null,
142
184
  resume: BookReader.PLUGINS.resume ? new BookReader.PLUGINS.resume(this) : null,
143
185
  textSelection: BookReader.PLUGINS.textSelection ? new BookReader.PLUGINS.textSelection(this) : null,
186
+ translate: BookReader.PLUGINS.translate ? new BookReader.PLUGINS.translate(this) : null,
144
187
  tts: BookReader.PLUGINS.tts ? new BookReader.PLUGINS.tts(this) : null,
145
188
  };
146
189
 
@@ -365,6 +408,7 @@ BookReader.prototype.getActivePageContainerElements = function() {
365
408
  * Get the HTML Elements for the rendered page. Note there can be more than one, since
366
409
  * (at least as of writing) different modes can maintain different caches.
367
410
  * @param {PageIndex} pageIndex
411
+ * @returns {HTMLElement[]}
368
412
  */
369
413
  BookReader.prototype.getActivePageContainerElementsForIndex = function(pageIndex) {
370
414
  return [
Binary file
@@ -45,6 +45,7 @@
45
45
  // but they appear the same in the UI.
46
46
  .searchHiliteLayer, .ttsHiliteLayer {
47
47
  pointer-events: none;
48
+ z-index: 4;
48
49
 
49
50
  rect {
50
51
  // Note: Can't use fill-opacity ; safari inexplicably applies that to
@@ -88,21 +88,20 @@
88
88
  // These are Microsoft Edge specific fixed to make some of the
89
89
  // browsers features work well. These are for the in-place
90
90
  // translation.
91
+ .BRtextLayer:has([_istranslated="1"], msreadoutspan) {
92
+ mix-blend-mode: normal;
93
+ }
94
+
91
95
  .BRwordElement, .BRspace {
92
96
  &[_istranslated="1"], &[_msttexthash] {
93
- background-color: #e4dccd;
94
97
  color: black;
95
98
  letter-spacing: unset !important;
96
- background: #ccbfa7;
97
99
  }
98
100
  }
99
101
 
100
- .BRlineElement font[_mstmutation="1"] {
101
- background: #ccbfa7;
102
- }
103
-
104
102
  .BRlineElement:has([_istranslated="1"], [_msttexthash]) {
105
- background-color: #e4dccd;
103
+ background: rgba(248, 237, 192, 0.8);
104
+ backdrop-filter: blur(8px);
106
105
  color: black;
107
106
  text-align: justify;
108
107
  width: inherit;
@@ -112,14 +111,13 @@
112
111
  }
113
112
 
114
113
  .BRlineElement[_msttexthash] {
115
- background: #ccbfa7;
116
114
  word-spacing: unset !important;
117
115
  }
118
116
 
119
117
  .BRtranslateLayer .BRparagraphElement {
120
118
  pointer-events: auto;
121
119
  overflow-y: auto;
122
- background: rgba(248, 237, 192, 0.5);
120
+ background: rgba(248, 237, 192, 0.8);
123
121
  backdrop-filter: blur(8px);
124
122
  color:black;
125
123
  line-height: 1em;
@@ -4,6 +4,7 @@ import { SelectionObserver } from '../BookReader/utils/SelectionObserver.js';
4
4
  import { BookReaderPlugin } from '../BookReaderPlugin.js';
5
5
  import { applyVariables } from '../util/strings.js';
6
6
  import { Cache } from '../util/cache.js';
7
+ import { toISO6391 } from './tts/utils.js';
7
8
  /** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
8
9
  /** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
9
10
 
@@ -314,6 +315,10 @@ export class TextSelectionPlugin extends BookReaderPlugin {
314
315
  const ratioW = parseFloat(pageContainer.$container[0].style.width) / pageContainer.page.width;
315
316
  const ratioH = parseFloat(pageContainer.$container[0].style.height) / pageContainer.page.height;
316
317
  textLayer.style.transform = `scale(${ratioW}, ${ratioH})`;
318
+ const bookLangCode = toISO6391(this.br.options.bookLanguage);
319
+ if (bookLangCode) {
320
+ textLayer.setAttribute("lang", bookLangCode);
321
+ }
317
322
  textLayer.setAttribute("dir", this.rtl ? "rtl" : "ltr");
318
323
 
319
324
  const ocrParagraphs = $(XMLpage).find("PARAGRAPH[coords]").toArray();
@@ -353,6 +358,10 @@ export class TextSelectionPlugin extends BookReaderPlugin {
353
358
  renderParagraph(ocrParagraph) {
354
359
  const paragEl = document.createElement('p');
355
360
  paragEl.classList.add('BRparagraphElement');
361
+ if (ocrParagraph.getAttribute("x-role")) {
362
+ paragEl.classList.add('ocr-role-header-footer');
363
+ paragEl.ariaHidden = "true";
364
+ }
356
365
  const [paragLeft, paragBottom, paragRight, paragTop] = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
357
366
  const wordHeightArr = [];
358
367
  const lines = $(ocrParagraph).find("LINE[coords]").toArray();
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
  import { Cache } from '../../util/cache.js';
3
3
  import { BatchTranslator } from '@internetarchive/bergamot-translator/translator.js';
4
+ import { toISO6391 } from '../tts/utils.js';
4
5
 
5
6
  export const langs = /** @type {{[lang: string]: string}} */ {
6
7
  "bg": "Bulgarian",
@@ -68,7 +69,7 @@ export class TranslationManager {
68
69
  this._initResolve = resolve;
69
70
  this._initReject = reject;
70
71
  });
71
- const registryUrl = "https://cors.archive.org/cors/mozilla-translate-models/";
72
+ const registryUrl = "https://cors.archive.org/cors/mozilla-translate-models/firefox_models/";
72
73
  const registryJson = await fetch(registryUrl + "registry.json").then(r => r.json());
73
74
  for (const language of Object.values(registryJson)) {
74
75
  for (const file of Object.values(language)) {
@@ -97,10 +98,10 @@ export class TranslationManager {
97
98
  // List of dev models found here https://github.com/mozilla/firefox-translations-models/tree/main/models/base
98
99
  // There are also differences between the model types in the repo above here: https://github.com/mozilla/firefox-translations-models?tab=readme-ov-file#firefox-translations-models
99
100
  if (firstLang !== "en") {
100
- this.fromLanguages.push({code: firstLang, name:langs[firstLang], type: "prod"});
101
+ this.fromLanguages.push({code: firstLang, name: toISO6391(firstLang, true), type: "prod"});
101
102
  }
102
103
  if (secondLang !== "en") {
103
- this.toLanguages.push({code: secondLang, name:langs[secondLang], type: "prod"});
104
+ this.toLanguages.push({code: secondLang, name: toISO6391(secondLang, true), type: "prod"});
104
105
  }
105
106
  }
106
107
  this._initResolve([this.modelRegistry]);
@@ -160,6 +161,8 @@ export class TranslationManager {
160
161
  }).then((resp) => {
161
162
  const response = resp;
162
163
  this.currentlyTranslating[key].resolve(response.target.text);
164
+ this.alreadyTranslated.add({index: key, response: response.target.text});
165
+ delete this.currentlyTranslating[key];
163
166
  });
164
167
 
165
168
  return promise;
@@ -3,6 +3,8 @@ import { html, LitElement } from 'lit';
3
3
  import { BookReaderPlugin } from '../../BookReaderPlugin.js';
4
4
  import { customElement, property } from 'lit/decorators.js';
5
5
  import { langs, TranslationManager } from "./TranslationManager.js";
6
+ import { toISO6391 } from '../tts/utils.js';
7
+ import { sortBy } from '../../../src/BookReader/utils.js';
6
8
 
7
9
  // @ts-ignore
8
10
  const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
@@ -11,6 +13,8 @@ export class TranslatePlugin extends BookReaderPlugin {
11
13
 
12
14
  options = {
13
15
  enabled: true,
16
+
17
+ /** @type {string | import('lit').TemplateResult} */
14
18
  panelDisclaimerText: "Translations are in alpha",
15
19
  }
16
20
 
@@ -27,10 +31,10 @@ export class TranslatePlugin extends BookReaderPlugin {
27
31
  toLanguages = [];
28
32
 
29
33
  /**
30
- * Current language code that is being translated From
34
+ * Current language code that is being translated From. Defaults to EN currently
31
35
  * @type {!string}
32
36
  */
33
- langFromCode = "en";
37
+ langFromCode
34
38
 
35
39
  /**
36
40
  * Current language code that is being translated To
@@ -43,7 +47,16 @@ export class TranslatePlugin extends BookReaderPlugin {
43
47
  */
44
48
  _panel;
45
49
 
50
+ /**
51
+ * @type {boolean} userToggleTranslate - Checks if user has initiated translation
52
+ * Should synchronize with the state of TranslationManager's active state
53
+ */
54
+ userToggleTranslate;
55
+
46
56
  async init() {
57
+ const currentLanguage = toISO6391(this.br.options.bookLanguage.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, ""));
58
+ this.langFromCode = currentLanguage ?? "en";
59
+
47
60
  if (!this.options.enabled) {
48
61
  return;
49
62
  }
@@ -84,10 +97,8 @@ export class TranslatePlugin extends BookReaderPlugin {
84
97
  await this.translationManager.initWorker();
85
98
  // Note await above lets _render function properly, since it gives the browser
86
99
  // time to render the rest of bookreader, which _render depends on
87
- this._render();
88
-
89
100
  this.langToCode = this.translationManager.toLanguages[0].code;
90
-
101
+ this._render();
91
102
  }
92
103
 
93
104
  /** @param {HTMLElement} page*/
@@ -114,7 +125,8 @@ export class TranslatePlugin extends BookReaderPlugin {
114
125
 
115
126
  /** @param {HTMLElement} page */
116
127
  async translateRenderedLayer(page, priority) {
117
- if (this.br.mode == this.br.constModeThumb) {
128
+ // Do not run translation if in thumbnail mode or if user did not initiate transations
129
+ if (this.br.mode == this.br.constModeThumb || !this.userToggleTranslate || this.langFromCode == this.langToCode) {
118
130
  return;
119
131
  }
120
132
 
@@ -123,7 +135,7 @@ export class TranslatePlugin extends BookReaderPlugin {
123
135
  let pageTranslationLayer;
124
136
  if (!page.querySelector('.BRPageLayer.BRtranslateLayer')) {
125
137
  pageTranslationLayer = document.createElement('div');
126
- pageTranslationLayer.classList.add('BRPageLayer', 'BRtranslateLayer');
138
+ pageTranslationLayer.classList.add('BRPageLayer', 'BRtranslateLayer', 'BRtranslateLayerLoading');
127
139
  pageTranslationLayer.setAttribute('lang', `${this.langToCode}`);
128
140
  page.prepend(pageTranslationLayer);
129
141
  } else {
@@ -141,7 +153,7 @@ export class TranslatePlugin extends BookReaderPlugin {
141
153
  });
142
154
  const paragraphs = this.getParagraphsOnPage(page);
143
155
 
144
- paragraphs.forEach(async (paragraph, pidx) => {
156
+ const paragraphTranslationPromises = paragraphs.map(async (paragraph, pidx) => {
145
157
  let translatedParagraph = page.querySelector(`[data-translate-index='${pageIndex}-${pidx}']`);
146
158
  if (!translatedParagraph) {
147
159
  translatedParagraph = document.createElement('p');
@@ -149,6 +161,11 @@ export class TranslatePlugin extends BookReaderPlugin {
149
161
  translatedParagraph.setAttribute('data-translate-index', `${pageIndex}-${pidx}`);
150
162
  translatedParagraph.className = 'BRparagraphElement';
151
163
  const originalParagraphStyle = paragraphs[pidx];
164
+ // check text selection paragraphs for header/footer roles
165
+ if (paragraph.classList.contains('ocr-role-header-footer')) {
166
+ translatedParagraph.ariaHidden = "true";
167
+ translatedParagraph.classList.add('ocr-role-header-footer');
168
+ }
152
169
  const fontSize = `${parseInt($(originalParagraphStyle).css("font-size"))}px`;
153
170
 
154
171
  $(translatedParagraph).css({
@@ -199,6 +216,32 @@ export class TranslatePlugin extends BookReaderPlugin {
199
216
  }
200
217
  }
201
218
  });
219
+ await Promise.all(paragraphTranslationPromises);
220
+ this.br.trigger('translateLayerRendered', {
221
+ leafIndex: pageIndex,
222
+ translateLayer: pageTranslationLayer,
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Get the translation layers for a specific leaf index.
228
+ * @param {number} leafIndex
229
+ * @returns {Promise<HTMLElement[]>}
230
+ */
231
+ async getTranslateLayers(leafIndex) {
232
+ const pageContainerElements = this.br.getActivePageContainerElementsForIndex(leafIndex);
233
+ const translateLayer = $(pageContainerElements).filter(`[data-index='${leafIndex}']`).find('.BRtranslateLayer');
234
+ if (translateLayer.length) return translateLayer.toArray();
235
+
236
+ return new Promise((res, rej) => {
237
+ const handler = async (_, extraParams) => {
238
+ if (extraParams.leafIndex == leafIndex) {
239
+ this.br.off('translateLayerRendered', handler); // remember to detach translateLayer
240
+ res([extraParams.translateLayer]);
241
+ }
242
+ };
243
+ this.br.on('translateLayerRendered', handler);
244
+ });
202
245
  }
203
246
 
204
247
  clearAllTranslations() {
@@ -268,9 +311,20 @@ export class TranslatePlugin extends BookReaderPlugin {
268
311
  this.translateActivePageContainerElements();
269
312
  }
270
313
 
314
+ handleToggleTranslation = async () => {
315
+ this.userToggleTranslate = !this.userToggleTranslate;
316
+ this.translationManager.active = this.userToggleTranslate;
317
+ if (!this.userToggleTranslate) {
318
+ this.clearAllTranslations();
319
+ this.br.trigger('translationDisabled', { });
320
+ } else {
321
+ this.br.trigger('translationEnabled', { });
322
+ this.translateActivePageContainerElements();
323
+ }
324
+ }
271
325
 
272
326
  /**
273
- * Update the table of contents based on array of TOC entries.
327
+ * Update translation side menu
274
328
  */
275
329
  _render() {
276
330
  this.br.shell.menuProviders['translate'] = {
@@ -286,9 +340,13 @@ export class TranslatePlugin extends BookReaderPlugin {
286
340
  }"
287
341
  @langFromChanged="${this.handleFromLangChange}"
288
342
  @langToChanged="${this.handleToLangChange}"
343
+ @toggleTranslation="${this.handleToggleTranslation}"
289
344
  .fromLanguages="${this.translationManager.fromLanguages}"
290
345
  .toLanguages="${this.translationManager.toLanguages}"
291
346
  .disclaimerMessage="${this.options.panelDisclaimerText}"
347
+ .userTranslationActive=${false}
348
+ .detectedFromLang=${this.langFromCode}
349
+ .detectedToLang=${this.langToCode}
292
350
  class="translate-panel"
293
351
  />`,
294
352
  };
@@ -303,6 +361,9 @@ export class BrTranslatePanel extends LitElement {
303
361
  @property({ type: Array }) toLanguages = []; // List of obj {code, name}
304
362
  @property({ type: String }) prevSelectedLang = ''; // Tracks the previous selected language for the "To" dropdown
305
363
  @property({ type: String }) disclaimerMessage = '';
364
+ @property({ type: Boolean }) userTranslationActive = false;
365
+ @property({ type: String }) detectedFromLang = '';
366
+ @property({ type: String }) detectedToLang = '';
306
367
 
307
368
  /** @override */
308
369
  createRenderRoot() {
@@ -316,42 +377,56 @@ export class BrTranslatePanel extends LitElement {
316
377
  }
317
378
 
318
379
  render() {
319
- const showPrevLangButton =
320
- this.prevSelectedLang &&
321
- (this.prevSelectedLang !== this._getSelectedLang('to') || this._getSelectedLang('from') === this._getSelectedLang('to'));
322
-
323
- return html`<div class="app">
324
- <div class="panel panel--from">
380
+ return html`<div class="app" style="margin-top: 5%;padding-right: 5px;">
381
+ <div
382
+ class="disclaimer"
383
+ id="disclaimerMessage"
384
+ style="background-color: rgba(255,255,255,0.1);padding: 10px;border-radius: 8px;font-size: 12px;margin-bottom: 10px;color: rgba(255,255,255, 0.9);"
385
+ >${this.disclaimerMessage}</div>
386
+
387
+ <div class="panel panel--to" style="padding: 0 10px;">
325
388
  <label>
326
- From
327
- <select id="lang-from" name="from" class="lang-select" @change="${this._onLangFromChange}">
328
- ${this.fromLanguages.map(
329
- lang => html`<option value="${lang.code}">${lang.name}</option>`,
330
- )}
389
+ <span style="font-size: 12px;color: #ccc;">Translate To</span>
390
+ <select id="lang-to" name="to" class="lang-select" style="display:block; width:100%;" @change="${this._onLangToChange}">
391
+ ${sortBy(this.toLanguages, ((lang) => lang.name.toLowerCase()
392
+ )).map((lang) => {
393
+ return html`<option value="${lang.code}"
394
+ ?selected=${lang.code == this.detectedToLang}
395
+ > ${lang.name ? lang.name : lang.code} </option>`;
396
+ })
397
+ }
331
398
  </select>
332
399
  </label>
333
400
  </div>
334
- <div class="panel panel--to">
335
- <label>
336
- To
337
- <select id="lang-to" name="to" class="lang-select" @change="${this._onLangToChange}">
338
- ${this.toLanguages.map(
339
- lang => html`<option value="${lang.code}">${lang.name}</option>`,
340
- )}
401
+
402
+ <div class="panel panel--start" style="text-align: right;padding: 0 10px;/*! font-size: 18px; */margin-top: 10px;">
403
+ <button class="start-translation-brn" @click="${this._toggleTranslation}">
404
+ ${this.userTranslationActive ? "Stop Translating" : "Translate"}
405
+ </button>
406
+ </div>
407
+
408
+ <div class="panel panel--from" style="font-size: 12px;color: #ccc;text-align: center;padding: 8px 10px;">
409
+ <details style="display: contents">
410
+ <summary style="text-decoration: underline white; cursor:pointer; display:inline-block">
411
+ <i>
412
+ Source: ${this._getLangName(this.detectedFromLang)} ${this.prevSelectedLang ? "" : "(detected)"}
413
+ </i> Change
414
+ </summary>
415
+ <select id="lang-from" name="from" class="lang-select" value=${this.detectedFromLang} @change="${this._onLangFromChange}" style="width:65%; margin-top: 3%; margin-bottom: 3%">
416
+ ${sortBy(this.fromLanguages, ((lang) => lang.name.toLowerCase()
417
+ )).map((lang) => {
418
+ return html`<option value="${lang.code}"
419
+ ?selected=${lang.code == this.detectedFromLang}
420
+ >${lang.name ? lang.name : lang.code} </option>`;
421
+ })
422
+ }
341
423
  </select>
342
- </label>
343
- ${showPrevLangButton
344
- ? html`<button class="prev-lang-btn" @click="${this._onPrevLangClick}">
345
- ${this._getLangName(this.prevSelectedLang)}
346
- </button>`
347
- : ''}
424
+ </details>
425
+ <div class="footer" id="status" style="margin-top:5%">
426
+ ${this._statusWarning()}
348
427
  </div>
349
- <div class="footer" id="status"></div>
350
- <br/>
351
- <div class="disclaimer" id="disclaimerMessage"> ${this.disclaimerMessage} </div>
352
428
  </div>`;
353
429
  }
354
-
355
430
  _onLangFromChange(event) {
356
431
  const langFromChangedEvent = new CustomEvent('langFromChanged', {
357
432
  detail: { value: event.target.value },
@@ -364,6 +439,7 @@ export class BrTranslatePanel extends LitElement {
364
439
  if (this._getSelectedLang('to') !== this._getSelectedLang('from')) {
365
440
  this.prevSelectedLang = this._getSelectedLang('from');
366
441
  }
442
+ this.detectedFromLang = event.target.value;
367
443
  }
368
444
 
369
445
  _onLangToChange(event) {
@@ -378,30 +454,11 @@ export class BrTranslatePanel extends LitElement {
378
454
  if (this._getSelectedLang('from') !== event.target.value) {
379
455
  this.prevSelectedLang = this._getSelectedLang('from');
380
456
  }
381
- }
382
-
383
- _onPrevLangClick() {
384
- const prevLang = this.prevSelectedLang;
385
- if (prevLang == this._getSelectedLang('from')) {
386
- console.log("_onPrevLangClick: will not change since prevLang is the same as the current 'To' language code");
387
- return;
388
- }
389
- this.prevSelectedLang = this._getSelectedLang('to'); // Update prevSelectedLang to current "To" value
390
- const langToChangedEvent = new CustomEvent('langToChanged', {
391
- detail: { value: prevLang },
392
- bubbles: true,
393
- composed: true,
394
- });
395
- this.dispatchEvent(langToChangedEvent);
396
-
397
- // Update the "To" dropdown to the previous language
398
- const toDropdown = this.querySelector('#lang-to');
399
- if (toDropdown) {
400
- toDropdown.value = prevLang;
401
- }
457
+ this.detectedToLang = event.target.value;
402
458
  }
403
459
 
404
460
  _getSelectedLang(type) {
461
+ /** @type {HTMLSelectElement} */
405
462
  const dropdown = this.querySelector(`#lang-${type}`);
406
463
  return dropdown ? dropdown.value : '';
407
464
  }
@@ -410,5 +467,23 @@ export class BrTranslatePanel extends LitElement {
410
467
  const lang = [...this.fromLanguages, ...this.toLanguages].find(lang => lang.code === code);
411
468
  return lang ? lang.name : '';
412
469
  }
470
+
471
+ _toggleTranslation(event) {
472
+ const toggleTranslateEvent = new CustomEvent('toggleTranslation', {
473
+ detail: {value: event.target.value},
474
+ bubbles: true,
475
+ composed:true,
476
+ });
477
+ this.userTranslationActive = !this.userTranslationActive;
478
+ this.dispatchEvent(toggleTranslateEvent);
479
+ }
480
+
481
+ // TODO: Hardcoded warning message for now but should add more statuses
482
+ _statusWarning() {
483
+ if (this.detectedFromLang == this.detectedToLang) {
484
+ return "Translate To language is the same as the Source language";
485
+ }
486
+ return "";
487
+ }
413
488
  }
414
489
 
@@ -86,9 +86,8 @@ export default class AbstractTTSEngine {
86
86
 
87
87
  this._chunkIterator = new PageChunkIterator(numLeafs, leafIndex, {
88
88
  pageChunkUrl: this.opts.pageChunkUrl,
89
- pageBufferSize: 5,
89
+ pageBufferSize: 3,
90
90
  });
91
-
92
91
  this.step();
93
92
  this.events.trigger('start');
94
93
  }
@@ -205,8 +204,8 @@ export default class AbstractTTSEngine {
205
204
  }
206
205
 
207
206
  /** Convenience wrapper for {@see AbstractTTSEngine.getBestVoice} */
208
- getBestVoice() {
209
- return AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
207
+ getBestVoice(languageOverride) {
208
+ return AbstractTTSEngine.getBestBookVoice(this.getVoices(), languageOverride || this.opts.bookLanguage);
210
209
  }
211
210
 
212
211
  /**
@@ -23,15 +23,34 @@ export default class PageChunk {
23
23
  * @return {Promise<PageChunk[]>}
24
24
  */
25
25
  static async fetch(pageChunkUrl, leafIndex) {
26
- const chunks = await $.ajax({
27
- type: 'GET',
28
- url: applyVariables(pageChunkUrl, { pageIndex: leafIndex }),
29
- cache: true,
30
- xhrFields: {
31
- withCredentials: window.br.protected,
32
- },
33
- });
34
- return PageChunk._fromTextWrapperResponse(leafIndex, chunks);
26
+ if (window.br.plugins.translate?.translationManager.active) {
27
+ const translateLayers = await window.br.plugins.translate.getTranslateLayers(leafIndex);
28
+ const paragraphs = Array.from(translateLayers[0].childNodes);
29
+
30
+ const pageChunks = [];
31
+ for (const [idx, item] of paragraphs.entries()) {
32
+ // Should not read paragraphs w/ header or footer roles
33
+ if (!item.classList.contains('ocr-role-header-footer')) {
34
+ const translatedChunk = new PageChunk(leafIndex, idx, item.textContent, []);
35
+ pageChunks.push(translatedChunk);
36
+ }
37
+ }
38
+ if (pageChunks.length === 0) {
39
+ const placeholder = new PageChunk(leafIndex, 0, "", []);
40
+ pageChunks.push(placeholder);
41
+ }
42
+ return pageChunks;
43
+ } else {
44
+ const chunks = await $.ajax({
45
+ type: 'GET',
46
+ url: applyVariables(pageChunkUrl, { pageIndex: leafIndex }),
47
+ cache: true,
48
+ xhrFields: {
49
+ withCredentials: window.br.protected,
50
+ },
51
+ });
52
+ return PageChunk._fromTextWrapperResponse(leafIndex, chunks);
53
+ }
35
54
  }
36
55
 
37
56
  /**