@internetarchive/bookreader 5.0.0-96 → 5.0.0-97

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 (40) hide show
  1. package/BookReader/474.js +2 -0
  2. package/BookReader/474.js.map +1 -0
  3. package/BookReader/BookReader.css +32 -26
  4. package/BookReader/BookReader.js +1 -1
  5. package/BookReader/BookReader.js.map +1 -1
  6. package/BookReader/bergamot-translator-worker.js +2966 -0
  7. package/BookReader/bergamot-translator-worker.wasm +0 -0
  8. package/BookReader/ia-bookreader-bundle.js +1 -1
  9. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  10. package/BookReader/images/icon_experiment.svg +1 -0
  11. package/BookReader/images/translate.svg +1 -0
  12. package/BookReader/plugins/plugin.experiments.js +1 -1
  13. package/BookReader/plugins/plugin.experiments.js.map +1 -1
  14. package/BookReader/plugins/plugin.text_selection.js +1 -1
  15. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  16. package/BookReader/plugins/plugin.translate.js +3 -0
  17. package/BookReader/plugins/plugin.translate.js.LICENSE.txt +1 -0
  18. package/BookReader/plugins/plugin.translate.js.map +1 -0
  19. package/BookReader/plugins/plugin.tts.js +1 -1
  20. package/BookReader/plugins/plugin.tts.js.map +1 -1
  21. package/BookReader/plugins/translator-worker.js +2 -0
  22. package/BookReader/plugins/translator-worker.js.map +1 -0
  23. package/BookReader/translator-worker.js +475 -0
  24. package/package.json +6 -3
  25. package/src/BookNavigator/book-navigator.js +1 -0
  26. package/src/BookReader/Mode1UpLit.js +6 -1
  27. package/src/BookReader/Mode2UpLit.js +11 -1
  28. package/src/BookReader/Navbar/Navbar.js +61 -0
  29. package/src/BookReader.js +22 -139
  30. package/src/assets/images/icon_experiment.svg +1 -0
  31. package/src/assets/images/translate.svg +1 -0
  32. package/src/css/_BRnav.scss +0 -24
  33. package/src/css/_BRsearch.scss +0 -5
  34. package/src/css/_TextSelection.scss +32 -1
  35. package/src/plugins/plugin.experiments.js +34 -9
  36. package/src/plugins/plugin.text_selection.js +8 -20
  37. package/src/plugins/translate/TranslationManager.js +167 -0
  38. package/src/plugins/translate/plugin.translate.js +414 -0
  39. package/src/plugins/tts/utils.js +9 -20
  40. package/src/util/cache.js +20 -0
@@ -0,0 +1,167 @@
1
+ // @ts-check
2
+ import { Cache } from '../../util/cache.js';
3
+ import { BatchTranslator } from '@internetarchive/bergamot-translator/translator.js';
4
+
5
+ export const langs = /** @type {{[lang: string]: string}} */ {
6
+ "bg": "Bulgarian",
7
+ "ca": "Catalan",
8
+ "cs": "Czech",
9
+ "nl": "Dutch",
10
+ "en": "English",
11
+ "et": "Estonian",
12
+ "de": "German",
13
+ "fr": "French",
14
+ "is": "Icelandic",
15
+ "it": "Italian",
16
+ "nb": "Norwegian Bokmål",
17
+ "nn": "Norwegian Nynorsk",
18
+ "fa": "Persian",
19
+ "pl": "Polish",
20
+ "pt": "Portuguese",
21
+ "ru": "Russian",
22
+ "es": "Spanish",
23
+ "uk": "Ukrainian",
24
+ };
25
+
26
+ export class TranslationManager {
27
+ /** @type {Cache<{index: string, response: string}>} */
28
+ alreadyTranslated = new Cache(100);
29
+
30
+ /**
31
+ * @typedef {Object} genericModelInfo
32
+ * @property {string} name
33
+ * @property {number} size
34
+ * @property {number} estimatedCompressedSize
35
+ * @property {any} [qualityModel]
36
+ * @property {string} [expectedSha256Hash]
37
+ * @property {string} [modelType]
38
+ */
39
+ /**
40
+ * @type { {[langPair: string] : {model: genericModelInfo, lex: genericModelInfo, vocab: genericModelInfo, quality?: genericModelInfo}} }
41
+ */
42
+ modelRegistry = {};
43
+
44
+ /** @type {Record<key, {promise: Promise<string>, resolve: function, reject: function}>} */
45
+ currentlyTranslating = {}
46
+
47
+ /** @type {Record<string, string>[]} */
48
+ fromLanguages = [];
49
+ /** @type {Record<string, string>[]} */
50
+ toLanguages = [];
51
+
52
+ /** @type {boolean} */
53
+ active = false;
54
+
55
+ publicPath = '';
56
+
57
+ constructor() {
58
+ //TODO Should default to the book language as the first element
59
+ const enModel = {code: "en", name: "English", type: "prod"};
60
+ this.fromLanguages.push(enModel);
61
+ this.toLanguages.push(enModel);
62
+ }
63
+
64
+
65
+ async initWorker() {
66
+ if (this.initPromise) return this.initPromise;
67
+ this.initPromise = new Promise((resolve, reject) => {
68
+ this._initResolve = resolve;
69
+ this._initReject = reject;
70
+ });
71
+ const registryUrl = "https://cors.archive.org/cors/mozilla-translate-models/";
72
+ const registryJson = await fetch(registryUrl + "registry.json").then(r => r.json());
73
+ for (const language of Object.values(registryJson)) {
74
+ for (const file of Object.values(language)) {
75
+ file.name = registryUrl + file.name;
76
+ }
77
+ }
78
+
79
+ /** @type {BatchTranslator} */
80
+ // Arbitrary setting for number of workers, 1 is already quite fast
81
+ // batchSize from 8 -> 4 for improved performance
82
+ this.translator = new BatchTranslator({
83
+ registryUrl: `data:application/json,${encodeURIComponent(JSON.stringify(registryJson))}`,
84
+ workers: 2,
85
+ batchSize: 4,
86
+ workerUrl: this.publicPath + '/translator-worker.js',
87
+ });
88
+
89
+ const modelType = await this.translator.backing.registry;
90
+ const arr = {}; // unsure if we need to keep track of the files
91
+ for (const obj of Object.values(modelType)) {
92
+ const firstLang = obj['from'];
93
+ const secondLang = obj['to'];
94
+ const fromModelType = obj['files'];
95
+ arr[`${firstLang}${secondLang}`] = fromModelType;
96
+ // Assuming that all of the languages loaded from the registryUrl inside @internetarchive/bergamot-translator/translator.js are prod
97
+ // List of dev models found here https://github.com/mozilla/firefox-translations-models/tree/main/models/base
98
+ // 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
+ if (firstLang !== "en") {
100
+ this.fromLanguages.push({code: firstLang, name:langs[firstLang], type: "prod"});
101
+ }
102
+ if (secondLang !== "en") {
103
+ this.toLanguages.push({code: secondLang, name:langs[secondLang], type: "prod"});
104
+ }
105
+ }
106
+ this._initResolve([this.modelRegistry]);
107
+ return this.initPromise;
108
+ }
109
+
110
+ /**
111
+ * Targets the page and paragraph of a text layer to create a translation from the "fromLang" to the "toLang". Tries to force order in translation by using the pageIndex (+1000 if the current page is not visible) and paragraphIndex
112
+ * @param {string} fromLang
113
+ * @param {string} toLang
114
+ * @param {string} pageIndex
115
+ * @param {number} paragraphIndex
116
+ * @param {string} text
117
+ * @param {number} priority
118
+ * @return {Promise<string>} translated text
119
+ */
120
+
121
+ getTranslation = async (fromLang, toLang, pageIndex, paragraphIndex, text, priority) => {
122
+ this.active = true;
123
+ if (fromLang == toLang || !fromLang || !toLang) {
124
+ return;
125
+ }
126
+ const key = `${fromLang}${toLang}-${pageIndex}:${paragraphIndex}`;
127
+ const cachedEntry = this.alreadyTranslated.entries.find(x => x.index == key);
128
+
129
+ if (cachedEntry) {
130
+ return cachedEntry.response;
131
+ }
132
+
133
+ if (key in this.currentlyTranslating) {
134
+ return this.currentlyTranslating[key].promise;
135
+ }
136
+
137
+ let _resolve = null;
138
+ let _reject = null;
139
+ const promise = new Promise((res, rej) => {
140
+ _resolve = res;
141
+ _reject = rej;
142
+ });
143
+
144
+ this.currentlyTranslating[key] = {
145
+ promise,
146
+ resolve: _resolve,
147
+ reject: _reject,
148
+ };
149
+
150
+ if (!text) {
151
+ this.currentlyTranslating[key].reject("No text was provided");
152
+ return promise;
153
+ }
154
+ this.translator.translate({
155
+ to: toLang,
156
+ from: fromLang,
157
+ text: text,
158
+ html: false,
159
+ priority: priority,
160
+ }).then((resp) => {
161
+ const response = resp;
162
+ this.currentlyTranslating[key].resolve(response.target.text);
163
+ });
164
+
165
+ return promise;
166
+ }
167
+ }
@@ -0,0 +1,414 @@
1
+ // @ts-check
2
+ import { html, LitElement } from 'lit';
3
+ import { BookReaderPlugin } from '../../BookReaderPlugin.js';
4
+ import { customElement, property } from 'lit/decorators.js';
5
+ import { langs, TranslationManager } from "./TranslationManager.js";
6
+
7
+ // @ts-ignore
8
+ const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
9
+
10
+ export class TranslatePlugin extends BookReaderPlugin {
11
+
12
+ options = {
13
+ enabled: true,
14
+ panelDisclaimerText: "Translations are in alpha",
15
+ }
16
+
17
+ /** @type {TranslationManager} */
18
+ translationManager = new TranslationManager();
19
+
20
+ /** @type {Worker}*/
21
+ worker;
22
+
23
+ /**
24
+ * Contains the list of languages available to translate to
25
+ * @type {string[]}
26
+ */
27
+ toLanguages = [];
28
+
29
+ /**
30
+ * Current language code that is being translated From
31
+ * @type {!string}
32
+ */
33
+ langFromCode = "en";
34
+
35
+ /**
36
+ * Current language code that is being translated To
37
+ * @type {!string}
38
+ */
39
+ langToCode;
40
+ /**
41
+ * @type {BrTranslatePanel} _panel - Represents a panel used in the plugin.
42
+ * The specific type and purpose of this panel should be defined based on its usage.
43
+ */
44
+ _panel;
45
+
46
+ async init() {
47
+ if (!this.options.enabled) {
48
+ return;
49
+ }
50
+
51
+ this.translationManager.publicPath = this.br.options.imagesBaseURL.replace(/\/+$/, '') + '/..';
52
+
53
+ /**
54
+ * @param {*} ev
55
+ * @param {object} eventProps
56
+ */
57
+ this.br.on('textLayerRendered', async (_, {pageIndex, pageContainer}) => {
58
+ // Stops invalid models from running, also prevents translation on page load
59
+ // TODO check if model has finished loading or if it exists
60
+ if (!this.translationManager) {
61
+ return;
62
+ }
63
+ if (this.translationManager.active) {
64
+ const pageElement = pageContainer.$container[0];
65
+ this.translateRenderedLayer(pageElement);
66
+ }
67
+ });
68
+
69
+ /**
70
+ * @param {*} ev
71
+ * @param {object} eventProps
72
+ */
73
+ this.br.on('pageVisible', (_, {pageContainerEl}) => {
74
+ if (!this.translationManager.active) {
75
+ return;
76
+ }
77
+ for (const paragraphEl of pageContainerEl.querySelectorAll('.BRtranslateLayer > .BRparagraphElement')) {
78
+ if (paragraphEl.textContent) {
79
+ this.fitVisiblePage(paragraphEl);
80
+ }
81
+ }
82
+ });
83
+
84
+ await this.translationManager.initWorker();
85
+ // Note await above lets _render function properly, since it gives the browser
86
+ // time to render the rest of bookreader, which _render depends on
87
+ this._render();
88
+
89
+ this.langToCode = this.translationManager.toLanguages[0].code;
90
+
91
+ }
92
+
93
+ /** @param {HTMLElement} page*/
94
+ getParagraphsOnPage = (page) => {
95
+ return page ? Array.from(page.querySelectorAll(".BRtextLayer > .BRparagraphElement")) : [];
96
+ }
97
+
98
+ translateActivePageContainerElements() {
99
+ const currentlyActiveContainers = this.br.getActivePageContainerElements();
100
+ const visiblePageContainers = currentlyActiveContainers.filter((element) => {
101
+ return element.classList.contains('BRpage-visible');
102
+ });
103
+ const hiddenPageContainers = currentlyActiveContainers.filter((element) => {
104
+ return !element.classList.contains('BRpage-visible');
105
+ });
106
+
107
+ for (const page of visiblePageContainers) {
108
+ this.translateRenderedLayer(page, 0);
109
+ }
110
+ for (const loadingPage of hiddenPageContainers) {
111
+ this.translateRenderedLayer(loadingPage, 1000);
112
+ }
113
+ }
114
+
115
+ /** @param {HTMLElement} page */
116
+ async translateRenderedLayer(page, priority) {
117
+ if (this.br.mode == this.br.constModeThumb) {
118
+ return;
119
+ }
120
+
121
+ const pageIndex = page.dataset.index;
122
+
123
+ let pageTranslationLayer;
124
+ if (!page.querySelector('.BRPageLayer.BRtranslateLayer')) {
125
+ pageTranslationLayer = document.createElement('div');
126
+ pageTranslationLayer.classList.add('BRPageLayer', 'BRtranslateLayer');
127
+ pageTranslationLayer.setAttribute('lang', `${this.langToCode}`);
128
+ page.prepend(pageTranslationLayer);
129
+ } else {
130
+ pageTranslationLayer = page.querySelector('.BRPageLayer.BRtranslateLayer');
131
+ }
132
+
133
+ const textLayerElement = page.querySelector('.BRtextLayer');
134
+ textLayerElement.classList.add('showingTranslation');
135
+ $(pageTranslationLayer).css({
136
+ "width": $(textLayerElement).css("width"),
137
+ "height": $(textLayerElement).css("height"),
138
+ "transform": $(textLayerElement).css("transform"),
139
+ "pointer-events": $(textLayerElement).css("pointer-events"),
140
+ "z-index": 3,
141
+ });
142
+ const paragraphs = this.getParagraphsOnPage(page);
143
+
144
+ paragraphs.forEach(async (paragraph, pidx) => {
145
+ let translatedParagraph = page.querySelector(`[data-translate-index='${pageIndex}-${pidx}']`);
146
+ if (!translatedParagraph) {
147
+ translatedParagraph = document.createElement('p');
148
+ // set data-translate-index on the placeholder
149
+ translatedParagraph.setAttribute('data-translate-index', `${pageIndex}-${pidx}`);
150
+ translatedParagraph.className = 'BRparagraphElement';
151
+ const originalParagraphStyle = paragraphs[pidx];
152
+ const fontSize = `${parseInt($(originalParagraphStyle).css("font-size"))}px`;
153
+
154
+ $(translatedParagraph).css({
155
+ "margin-left": $(originalParagraphStyle).css("margin-left"),
156
+ "margin-top": $(originalParagraphStyle).css("margin-top"),
157
+ "top": $(originalParagraphStyle).css("top"),
158
+ "height": $(originalParagraphStyle).css("height"),
159
+ "width": $(originalParagraphStyle).css("width"),
160
+ "font-size": fontSize,
161
+ });
162
+
163
+ // Note: We'll likely want to switch to using the same logic as
164
+ // TextSelectionPlugin's selection, which allows for e.g. click-to-flip
165
+ // to work simultaneously with text selection.
166
+ translatedParagraph.addEventListener('mousedown', (e) => {
167
+ e.stopPropagation();
168
+ e.stopImmediatePropagation();
169
+ });
170
+
171
+ translatedParagraph.addEventListener('mouseup', (e) => {
172
+ e.stopPropagation();
173
+ e.stopImmediatePropagation();
174
+ });
175
+
176
+ translatedParagraph.addEventListener('dragstart', (e) =>{
177
+ e.preventDefault();
178
+ });
179
+ pageTranslationLayer.append(translatedParagraph);
180
+ }
181
+
182
+ if (paragraph.textContent.length !== 0) {
183
+ const pagePriority = parseFloat(pageIndex) + priority + pidx;
184
+ const translatedText = await this.translationManager.getTranslation(this.langFromCode, this.langToCode, pageIndex, pidx, paragraph.textContent, pagePriority);
185
+ // prevent duplicate spans from appearing if exists
186
+ translatedParagraph.firstElementChild?.remove();
187
+
188
+ const firstWordSpacing = paragraphs[pidx]?.firstChild?.firstChild;
189
+ const createSpan = document.createElement('span');
190
+ createSpan.className = 'BRlineElement';
191
+ createSpan.textContent = translatedText;
192
+ translatedParagraph.appendChild(createSpan);
193
+
194
+ $(createSpan).css({
195
+ "text-indent": $(firstWordSpacing).css('padding-left'),
196
+ });
197
+ if (page.classList.contains('BRpage-visible')) {
198
+ this.fitVisiblePage(translatedParagraph);
199
+ }
200
+ }
201
+ });
202
+ }
203
+
204
+ clearAllTranslations() {
205
+ document.querySelectorAll('.BRtranslateLayer').forEach(el => el.remove());
206
+ }
207
+
208
+ /**
209
+ * @param {Element} paragEl
210
+ */
211
+ fitVisiblePage(paragEl) {
212
+ // For some reason, Chrome does not detect the transform property for the translation + text layers
213
+ // Could not get it to fetch the transform value using $().css method
214
+ // Oddly enough the value is retrieved if using .style.transform instead?
215
+ const translateLayerEl = paragEl.parentElement;
216
+ if ($(translateLayerEl).css('transform') == 'none') {
217
+ const pageNumber = paragEl.getAttribute('data-translate-index').split('-')[0];
218
+ /** @type {HTMLElement} selectionTransform */
219
+ const textLayerEl = document.querySelector(`[data-index='${pageNumber}'] .BRtextLayer`);
220
+ $(translateLayerEl).css({'transform': textLayerEl.style.transform});
221
+ }
222
+
223
+ const originalFontSize = parseInt($(paragEl).css("font-size"));
224
+ let adjustedFontSize = originalFontSize;
225
+ while (paragEl.clientHeight < paragEl.scrollHeight && adjustedFontSize > 0) {
226
+ adjustedFontSize--;
227
+ $(paragEl).css({ "font-size": `${adjustedFontSize}px` });
228
+ }
229
+
230
+ const textHeight = paragEl.firstElementChild.clientHeight;
231
+ const scrollHeight = paragEl.scrollHeight;
232
+ const fits = textHeight < scrollHeight;
233
+ if (fits) {
234
+ const lines = textHeight / adjustedFontSize;
235
+ // Line heights for smaller paragraphs occasionally need a minor adjustment
236
+ const newLineHeight = scrollHeight / lines;
237
+ $(paragEl).css({
238
+ "line-height" : `${newLineHeight}px`,
239
+ "overflow": "visible",
240
+ });
241
+ }
242
+ }
243
+
244
+ handleFromLangChange = async (e) => {
245
+ this.clearAllTranslations();
246
+ const selectedLangFrom = e.detail.value;
247
+
248
+ // Update the from language
249
+ this.langFromCode = selectedLangFrom;
250
+
251
+ // Add 'From' language to 'To' list if not already present
252
+ if (!this.translationManager.toLanguages.some(lang => lang.code === selectedLangFrom)) {
253
+ this.translationManager.toLanguages.push({ code: selectedLangFrom, name: langs[selectedLangFrom] });
254
+ }
255
+
256
+ // Update the 'To' languages list and set the correct 'To' language
257
+ this._panel.toLanguages = this.translationManager.toLanguages;
258
+
259
+ console.log(this.langFromCode, this.langToCode);
260
+ if (this.langFromCode !== this.langToCode) {
261
+ this.translateActivePageContainerElements();
262
+ }
263
+ }
264
+
265
+ handleToLangChange = async (e) => {
266
+ this.clearAllTranslations();
267
+ this.langToCode = e.detail.value;
268
+ this.translateActivePageContainerElements();
269
+ }
270
+
271
+
272
+ /**
273
+ * Update the table of contents based on array of TOC entries.
274
+ */
275
+ _render() {
276
+ this.br.shell.menuProviders['translate'] = {
277
+ id: 'translate',
278
+ icon: html`<img src='${this.br.options.imagesBaseURL}/translate.svg' width="26"/>`,
279
+ label: 'Translate',
280
+ component: html`<br-translate-panel
281
+ @connected="${e => {
282
+ this._panel = e.target;
283
+ this._panel.fromLanguages = this.translationManager.fromLanguages;
284
+ this._panel.toLanguages = this.translationManager.toLanguages;
285
+ }
286
+ }"
287
+ @langFromChanged="${this.handleFromLangChange}"
288
+ @langToChanged="${this.handleToLangChange}"
289
+ .fromLanguages="${this.translationManager.fromLanguages}"
290
+ .toLanguages="${this.translationManager.toLanguages}"
291
+ .disclaimerMessage="${this.options.panelDisclaimerText}"
292
+ class="translate-panel"
293
+ />`,
294
+ };
295
+ this.br.shell.updateMenuContents();
296
+ }
297
+ }
298
+ BookReader?.registerPlugin('translate', TranslatePlugin);
299
+
300
+ @customElement('br-translate-panel')
301
+ export class BrTranslatePanel extends LitElement {
302
+ @property({ type: Array }) fromLanguages = []; // List of obj {code, name}
303
+ @property({ type: Array }) toLanguages = []; // List of obj {code, name}
304
+ @property({ type: String }) prevSelectedLang = ''; // Tracks the previous selected language for the "To" dropdown
305
+ @property({ type: String }) disclaimerMessage = '';
306
+
307
+ /** @override */
308
+ createRenderRoot() {
309
+ // Disable shadow DOM; that would require a huge rejiggering of CSS
310
+ return this;
311
+ }
312
+
313
+ connectedCallback() {
314
+ super.connectedCallback();
315
+ this.dispatchEvent(new CustomEvent('connected'));
316
+ }
317
+
318
+ 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">
325
+ <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
+ )}
331
+ </select>
332
+ </label>
333
+ </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
+ )}
341
+ </select>
342
+ </label>
343
+ ${showPrevLangButton
344
+ ? html`<button class="prev-lang-btn" @click="${this._onPrevLangClick}">
345
+ ${this._getLangName(this.prevSelectedLang)}
346
+ </button>`
347
+ : ''}
348
+ </div>
349
+ <div class="footer" id="status"></div>
350
+ <br/>
351
+ <div class="disclaimer" id="disclaimerMessage"> ${this.disclaimerMessage} </div>
352
+ </div>`;
353
+ }
354
+
355
+ _onLangFromChange(event) {
356
+ const langFromChangedEvent = new CustomEvent('langFromChanged', {
357
+ detail: { value: event.target.value },
358
+ bubbles: true,
359
+ composed: true,
360
+ });
361
+ this.dispatchEvent(langFromChangedEvent);
362
+
363
+ // Update the prevSelectedLang if "To" is different from "From"
364
+ if (this._getSelectedLang('to') !== this._getSelectedLang('from')) {
365
+ this.prevSelectedLang = this._getSelectedLang('from');
366
+ }
367
+ }
368
+
369
+ _onLangToChange(event) {
370
+ const langToChangedEvent = new CustomEvent('langToChanged', {
371
+ detail: { value: event.target.value },
372
+ bubbles: true,
373
+ composed: true,
374
+ });
375
+ this.dispatchEvent(langToChangedEvent);
376
+
377
+ // Update the prevSelectedLang if "To" is different from "From"
378
+ if (this._getSelectedLang('from') !== event.target.value) {
379
+ this.prevSelectedLang = this._getSelectedLang('from');
380
+ }
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
+ }
402
+ }
403
+
404
+ _getSelectedLang(type) {
405
+ const dropdown = this.querySelector(`#lang-${type}`);
406
+ return dropdown ? dropdown.value : '';
407
+ }
408
+
409
+ _getLangName(code) {
410
+ const lang = [...this.fromLanguages, ...this.toLanguages].find(lang => lang.code === code);
411
+ return lang ? lang.name : '';
412
+ }
413
+ }
414
+
@@ -1,4 +1,4 @@
1
- import langs from 'iso-language-codes/js/data.js';
1
+ import langs from 'iso-language-codes';
2
2
 
3
3
  /**
4
4
  * Use regex to approximate word count in a string
@@ -24,15 +24,6 @@ export function isAndroid(userAgent = navigator.userAgent) {
24
24
  * Language code in ISO 639-1 format. e.g. en, fr, zh
25
25
  **/
26
26
 
27
- /** Each lang is an array, with each index mapping to a different property */
28
- const COLUMN_TO_LANG_INDEX = {
29
- 'Name': 0,
30
- 'Endonym': 1,
31
- 'ISO 639-1': 2,
32
- 'ISO 639-2/T': 3,
33
- 'ISO 639-2/B': 4,
34
- };
35
-
36
27
  /**
37
28
  * @param {string} language in some format
38
29
  * @return {ISO6391?}
@@ -41,24 +32,22 @@ export function toISO6391(language) {
41
32
  if (!language) return null;
42
33
  language = language.toLowerCase();
43
34
 
44
- return searchForISO6391(language, ['ISO 639-1']) ||
45
- searchForISO6391(language, ['ISO 639-2/B']) ||
46
- searchForISO6391(language, ['ISO 639-2/T', 'Endonym', 'Name']);
35
+ return searchForISO6391(language, ['iso639_1']) ||
36
+ searchForISO6391(language, ['iso639_2T']) ||
37
+ searchForISO6391(language, ['iso639_2B', 'nativeName', 'name']);
47
38
  }
48
39
 
49
40
  /**
50
41
  * Searches for the given long in the given columns.
51
42
  * @param {string} language
52
- * @param {Array<keyof COLUMN_TO_LANG_INDEX>} columnsToSearch
43
+ * @param {Array<keyof import('iso-language-codes').Code>} columnsToSearch
53
44
  * @return {ISO6391?}
54
45
  */
55
46
  function searchForISO6391(language, columnsToSearch) {
56
- for (let i = 0; i < langs.length; i++) {
57
- for (let colI = 0; colI < columnsToSearch.length; colI++) {
58
- const column = columnsToSearch[colI];
59
- const columnValue = langs[i][COLUMN_TO_LANG_INDEX[column]];
60
- if (columnValue.split(', ').map(x => x.toLowerCase()).indexOf(language) != -1) {
61
- return langs[i][COLUMN_TO_LANG_INDEX['ISO 639-1']];
47
+ for (const lang of langs) {
48
+ for (const colName of columnsToSearch) {
49
+ if (lang[colName].split(', ').map(x => x.toLowerCase()).indexOf(language) != -1) {
50
+ return lang.iso639_1;
62
51
  }
63
52
  }
64
53
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @template T
3
+ */
4
+ export class Cache {
5
+ constructor(maxSize = 10) {
6
+ this.maxSize = maxSize;
7
+ /** @type {T[]} */
8
+ this.entries = [];
9
+ }
10
+
11
+ /**
12
+ * @param {T} entry
13
+ */
14
+ add(entry) {
15
+ if (this.entries.length >= this.maxSize) {
16
+ this.entries.shift();
17
+ }
18
+ this.entries.push(entry);
19
+ }
20
+ }