@alaikis/translation-sdk 1.2.2 → 1.2.3

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.
@@ -1,903 +1,1016 @@
1
- (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LakerTranslation = {}));
5
- })(this, (function (exports) { 'use strict';
6
-
7
- /******************************************************************************
8
- Copyright (c) Microsoft Corporation.
9
-
10
- Permission to use, copy, modify, and/or distribute this software for any
11
- purpose with or without fee is hereby granted.
12
-
13
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
14
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
16
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
17
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
18
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
19
- PERFORMANCE OF THIS SOFTWARE.
20
- ***************************************************************************** */
21
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
22
-
23
-
24
- function __awaiter(thisArg, _arguments, P, generator) {
25
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
26
- return new (P || (P = Promise))(function (resolve, reject) {
27
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
28
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
29
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
30
- step((generator = generator.apply(thisArg, _arguments || [])).next());
31
- });
32
- }
33
-
34
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
35
- var e = new Error(message);
36
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
37
- };
38
-
39
- /**
40
- * Translation Service gRPC-Web TypeScript/JavaScript Client
41
- *
42
- * Auto-generated for api.laker.dev Translation Service
43
- * Service: TranslationService
44
- * Source: proto/translation.proto
45
- */
46
- const defaultCrossTabOptions = {
47
- enabled: false,
48
- channelName: 'laker-translation-cache',
49
- storageKeyPrefix: 'laker_translation_'
50
- };
51
- /**
52
- * Automatic template extraction from text containing numeric variables
53
- * @param text Original text that may contain numeric variables
54
- * @returns Template extraction result
55
- */
56
- function extractTemplate(text) {
57
- // Regex to find numbers in text
58
- const numberRegex = /\d+(?:\.\d+)?/g;
59
- const matches = text.match(numberRegex);
60
- if (!matches || matches.length === 0) {
61
- return {
62
- isTemplated: false,
63
- srcTemplate: text,
64
- dstTemplate: '',
65
- variables: []
66
- };
67
- }
68
- let template = text;
69
- const variables = [];
70
- matches.forEach((match, index) => {
71
- const varName = `{var${index + 1}}`;
72
- template = template.replace(match, varName);
73
- variables.push(match);
74
- });
1
+ /**
2
+ * Translation Service gRPC-Web TypeScript/JavaScript Client
3
+ *
4
+ * Auto-generated for api.laker.dev Translation Service
5
+ * Service: TranslationService
6
+ * Source: proto/translation.proto
7
+ */
8
+ const defaultCrossTabOptions = {
9
+ enabled: false,
10
+ channelName: 'laker-translation-cache',
11
+ storageKeyPrefix: 'laker_translation_'
12
+ };
13
+ /**
14
+ * Automatic template extraction from text containing numeric variables
15
+ * @param text Original text that may contain numeric variables
16
+ * @returns Template extraction result
17
+ */
18
+ export function extractTemplate(text) {
19
+ // Regex to find numbers in text
20
+ const numberRegex = /\d+(?:\.\d+)?/g;
21
+ const matches = text.match(numberRegex);
22
+ if (!matches || matches.length === 0) {
75
23
  return {
76
- isTemplated: variables.length > 0,
77
- srcTemplate: template,
24
+ isTemplated: false,
25
+ srcTemplate: text,
78
26
  dstTemplate: '',
79
- variables
27
+ variables: []
80
28
  };
81
29
  }
30
+ let template = text;
31
+ const variables = [];
32
+ matches.forEach((match, index) => {
33
+ const varName = `{var${index + 1}}`;
34
+ template = template.replace(match, varName);
35
+ variables.push(match);
36
+ });
37
+ return {
38
+ isTemplated: variables.length > 0,
39
+ srcTemplate: template,
40
+ dstTemplate: '',
41
+ variables
42
+ };
43
+ }
44
+ /**
45
+ * TranslationPool - Multi-fingerprint translation cache with automatic common preloading
46
+ *
47
+ * Architecture:
48
+ * - pools: Map of fingerprint -> Map<text, translation> (each fingerprint has independent cache)
49
+ * - currentFingerprint: current active fingerprint for special translations
50
+ * - common is always loaded and cached forever, never cleared unless full clear happens
51
+ * - Optional cross-tab synchronization via Broadcast Channel and localStorage
52
+ *
53
+ * Rules:
54
+ * - common translations are always loaded on initialization and cached forever
55
+ * - If fingerprint exists, load special translations for that fingerprint
56
+ * - Switching fingerprints doesn't clear cached data for other fingerprints
57
+ * - Lookup priority: current fingerprint first, common second
58
+ * - All translations are cached independently by fingerprint
59
+ */
60
+ class TranslationPool {
82
61
  /**
83
- * TranslationPool - Multi-fingerprint translation cache with automatic common preloading
84
- *
85
- * Architecture:
86
- * - pools: Map of fingerprint -> Map<text, translation> (each fingerprint has independent cache)
87
- * - currentFingerprint: current active fingerprint for special translations
88
- * - common is always loaded and cached forever, never cleared unless full clear happens
89
- * - Optional cross-tab synchronization via Broadcast Channel and localStorage
90
- *
91
- * Rules:
92
- * - common translations are always loaded on initialization and cached forever
93
- * - If fingerprint exists, load special translations for that fingerprint
94
- * - Switching fingerprints doesn't clear cached data for other fingerprints
95
- * - Lookup priority: current fingerprint first, common second
96
- * - All translations are cached independently by fingerprint
97
- */
98
- class TranslationPool {
99
- /**
100
- * Create a new TranslationPool for a specific sense
101
- * @param client TranslationClient instance
102
- * @param senseId The semantic sense ID
103
- * @param crossTabOptions Cross-tab synchronization options
104
- */
105
- constructor(client, senseId, crossTabOptions) {
106
- // Separate cache for each fingerprint: fingerprint -> Map<text, translation>
107
- this.pools = new Map();
108
- this.currentFingerprint = null;
109
- this.broadcastChannel = null;
110
- this.loading = false;
111
- this.loadedFingerprints = new Set();
112
- this.client = client;
113
- this.senseId = senseId;
114
- this.crossTabOptions = Object.assign(Object.assign({}, defaultCrossTabOptions), crossTabOptions);
115
- // Initialize common pool always
116
- this.pools.set('common', new Map());
117
- if (this.crossTabOptions.enabled && typeof BroadcastChannel !== 'undefined') {
118
- this.initCrossTabSync();
119
- }
120
- // Load from localStorage if cross-tab enabled and storage available
121
- this.loadFromStorage();
122
- }
123
- /**
124
- * Initialize cross-tab synchronization
125
- */
126
- initCrossTabSync() {
127
- this.broadcastChannel = new BroadcastChannel(this.crossTabOptions.channelName);
128
- this.broadcastChannel.onmessage = (event) => {
129
- const message = event.data;
130
- // Ignore messages for other senses
131
- if (message.senseId !== this.senseId) {
132
- return;
133
- }
134
- switch (message.type) {
135
- case 'cache_update':
136
- this.handleCacheUpdate(message);
137
- break;
138
- case 'cache_clear':
139
- this.handleCacheClear(message);
140
- break;
141
- case 'request_initial_sync':
142
- this.handleInitialSyncRequest();
143
- break;
144
- }
145
- };
146
- // Request other tabs to share their cache
147
- this.broadcastChannel.postMessage({
148
- type: 'request_initial_sync',
149
- senseId: this.senseId
150
- });
151
- }
152
- /**
153
- * Load cache from localStorage
154
- */
155
- loadFromStorage() {
156
- if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
157
- return;
158
- }
159
- // Always load common first
160
- this.loadFingerprintFromStorage('common');
161
- // Load current fingerprint if exists
162
- if (this.currentFingerprint) {
163
- this.loadFingerprintFromStorage(this.currentFingerprint);
164
- }
165
- }
166
- /**
167
- * Load a specific fingerprint's cache from localStorage
168
- */
169
- loadFingerprintFromStorage(fp) {
170
- const storageKey = this.getStorageKey(fp);
171
- try {
172
- const stored = localStorage.getItem(storageKey);
173
- if (stored) {
174
- const data = JSON.parse(stored);
175
- let pool = this.pools.get(fp);
176
- if (!pool) {
177
- pool = new Map();
178
- this.pools.set(fp, pool);
179
- }
180
- data.forEach(({ text, translation }) => {
181
- pool.set(text, translation);
182
- });
183
- this.loadedFingerprints.add(fp);
184
- }
185
- }
186
- catch (e) {
187
- console.warn('Failed to load translation cache from localStorage:', e);
188
- }
189
- }
190
- /**
191
- * Get storage key for a specific fingerprint
192
- */
193
- getStorageKey(fp) {
194
- return `${this.crossTabOptions.storageKeyPrefix}${this.senseId}_${fp}`;
195
- }
196
- /**
197
- * Save cache to localStorage
198
- */
199
- saveToStorage(fp) {
200
- if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
62
+ * Create a new TranslationPool for a specific sense
63
+ * @param client TranslationClient instance
64
+ * @param senseId The semantic sense ID
65
+ * @param crossTabOptions Cross-tab synchronization options
66
+ */
67
+ constructor(client, senseId, crossTabOptions) {
68
+ // Separate cache for each fingerprint: fingerprint -> Map<text, translation>
69
+ this.pools = new Map();
70
+ this.currentFingerprint = null;
71
+ this.broadcastChannel = null;
72
+ this.loading = false;
73
+ this.loadedFingerprints = new Set();
74
+ // Observer pattern for queueing translation requests during load
75
+ this.queuedRequests = [];
76
+ this.pendingResolutions = {};
77
+ this.client = client;
78
+ this.senseId = senseId;
79
+ this.crossTabOptions = Object.assign(Object.assign({}, defaultCrossTabOptions), crossTabOptions);
80
+ // Initialize common pool always
81
+ this.pools.set('common', new Map());
82
+ if (this.crossTabOptions.enabled && typeof BroadcastChannel !== 'undefined') {
83
+ this.initCrossTabSync();
84
+ }
85
+ // Load from localStorage if cross-tab enabled and storage available
86
+ this.loadFromStorage();
87
+ }
88
+ /**
89
+ * Initialize cross-tab synchronization
90
+ */
91
+ initCrossTabSync() {
92
+ this.broadcastChannel = new BroadcastChannel(this.crossTabOptions.channelName);
93
+ this.broadcastChannel.onmessage = (event) => {
94
+ const message = event.data;
95
+ // Ignore messages for other senses
96
+ if (message.senseId !== this.senseId) {
201
97
  return;
202
98
  }
203
- const storageKey = this.getStorageKey(fp);
204
- try {
205
- const pool = this.pools.get(fp);
206
- const data = [];
207
- if (pool) {
208
- pool.forEach((translation, text) => {
209
- data.push({ text, translation });
210
- });
211
- }
212
- localStorage.setItem(storageKey, JSON.stringify(data));
213
- }
214
- catch (e) {
215
- console.warn('Failed to save translation cache to localStorage:', e);
99
+ switch (message.type) {
100
+ case 'cache_update':
101
+ this.handleCacheUpdate(message);
102
+ break;
103
+ case 'cache_clear':
104
+ this.handleCacheClear(message);
105
+ break;
106
+ case 'request_initial_sync':
107
+ this.handleInitialSyncRequest();
108
+ break;
216
109
  }
110
+ };
111
+ // Request other tabs to share their cache
112
+ this.broadcastChannel.postMessage({
113
+ type: 'request_initial_sync',
114
+ senseId: this.senseId
115
+ });
116
+ }
117
+ /**
118
+ * Load cache from localStorage
119
+ */
120
+ loadFromStorage() {
121
+ if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
122
+ return;
217
123
  }
218
- /**
219
- * Broadcast cache update to all other tabs
220
- */
221
- broadcastUpdate(text, translation) {
222
- if (!this.broadcastChannel || !this.crossTabOptions.enabled) {
223
- return;
224
- }
225
- const fp = this.currentFingerprint || 'common';
226
- const message = {
227
- type: 'cache_update',
228
- senseId: this.senseId,
229
- fingerprint: this.currentFingerprint || undefined,
230
- data: Object.assign({ result: this.getAllForFingerprint(fp) }, (text && translation && { text, translation }))
231
- };
232
- this.broadcastChannel.postMessage(message);
233
- this.saveToStorage(fp);
124
+ // Always load common first
125
+ this.loadFingerprintFromStorage('common');
126
+ // Load current fingerprint if exists
127
+ if (this.currentFingerprint) {
128
+ this.loadFingerprintFromStorage(this.currentFingerprint);
234
129
  }
235
- /**
236
- * Handle incoming cache update from another tab
237
- */
238
- handleCacheUpdate(message) {
239
- var _a, _b, _c;
240
- const fp = message.fingerprint || 'common';
241
- if ((_a = message.data) === null || _a === void 0 ? void 0 : _a.result) {
242
- // Update full cache for this fingerprint
130
+ }
131
+ /**
132
+ * Load a specific fingerprint's cache from localStorage
133
+ */
134
+ loadFingerprintFromStorage(fp) {
135
+ const storageKey = this.getStorageKey(fp);
136
+ try {
137
+ const stored = localStorage.getItem(storageKey);
138
+ if (stored) {
139
+ const data = JSON.parse(stored);
243
140
  let pool = this.pools.get(fp);
244
141
  if (!pool) {
245
142
  pool = new Map();
246
143
  this.pools.set(fp, pool);
247
144
  }
248
- pool.clear();
249
- message.data.result.forEach(({ text, translation }) => {
145
+ data.forEach(({ text, translation }) => {
250
146
  pool.set(text, translation);
251
147
  });
252
148
  this.loadedFingerprints.add(fp);
253
149
  }
254
- // Update specific entry
255
- if (((_b = message.data) === null || _b === void 0 ? void 0 : _b.text) && ((_c = message.data) === null || _c === void 0 ? void 0 : _c.translation)) {
256
- const pool = this.pools.get(fp) || new Map();
257
- pool.set(message.data.text, message.data.translation);
258
- this.pools.set(fp, pool);
259
- this.saveToStorage(fp);
260
- }
261
150
  }
262
- /**
263
- * Handle incoming cache clear from another tab
264
- */
265
- handleCacheClear(message) {
266
- const fp = message.fingerprint || 'common';
267
- {
268
- this.clearFingerprint(fp);
151
+ catch (e) {
152
+ console.warn('Failed to load translation cache from localStorage:', e);
153
+ }
154
+ }
155
+ /**
156
+ * Get storage key for a specific fingerprint
157
+ */
158
+ getStorageKey(fp) {
159
+ return `${this.crossTabOptions.storageKeyPrefix}${this.senseId}_${fp}`;
160
+ }
161
+ /**
162
+ * Save cache to localStorage
163
+ */
164
+ saveToStorage(fp) {
165
+ if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
166
+ return;
167
+ }
168
+ const storageKey = this.getStorageKey(fp);
169
+ try {
170
+ const pool = this.pools.get(fp);
171
+ const data = [];
172
+ if (pool) {
173
+ pool.forEach((translation, text) => {
174
+ data.push({ text, translation });
175
+ });
269
176
  }
177
+ localStorage.setItem(storageKey, JSON.stringify(data));
270
178
  }
271
- /**
272
- * Handle initial sync request from a new tab
273
- */
274
- handleInitialSyncRequest() {
275
- // Send our current cache to the new tab
276
- this.broadcastUpdate();
179
+ catch (e) {
180
+ console.warn('Failed to save translation cache to localStorage:', e);
277
181
  }
278
- /**
279
- * Check if currently loading
280
- */
281
- isLoading() {
282
- return this.loading;
283
- }
284
- /**
285
- * Initialize the pool - always loads common first, then loads current fingerprint if set
286
- * If fingerprint is set, loads special translations; common is always preloaded
287
- */
288
- initialize() {
289
- return __awaiter(this, void 0, void 0, function* () {
290
- if (this.loading) {
291
- return;
292
- }
293
- this.loading = true;
294
- try {
295
- // Always preload common first (required)
296
- if (!this.loadedFingerprints.has('common')) {
297
- yield this.loadFingerprintTranslations('common', undefined);
298
- }
299
- // Then load current fingerprint if set and not loaded
300
- if (this.currentFingerprint && !this.loadedFingerprints.has(this.currentFingerprint)) {
301
- yield this.loadFingerprintTranslations(this.currentFingerprint, this.currentFingerprint);
302
- }
303
- // Broadcast to other tabs after full initialization
304
- this.broadcastUpdate();
305
- }
306
- finally {
307
- this.loading = false;
308
- }
182
+ }
183
+ /**
184
+ * Broadcast cache update to all other tabs
185
+ */
186
+ broadcastUpdate(text, translation) {
187
+ if (!this.broadcastChannel || !this.crossTabOptions.enabled) {
188
+ return;
189
+ }
190
+ const fp = this.currentFingerprint || 'common';
191
+ const message = {
192
+ type: 'cache_update',
193
+ senseId: this.senseId,
194
+ fingerprint: this.currentFingerprint || undefined,
195
+ data: Object.assign({ result: this.getAllForFingerprint(fp) }, (text && translation && { text, translation }))
196
+ };
197
+ this.broadcastChannel.postMessage(message);
198
+ this.saveToStorage(fp);
199
+ }
200
+ /**
201
+ * Handle incoming cache update from another tab
202
+ */
203
+ handleCacheUpdate(message) {
204
+ var _a, _b, _c;
205
+ const fp = message.fingerprint || 'common';
206
+ if ((_a = message.data) === null || _a === void 0 ? void 0 : _a.result) {
207
+ // Update full cache for this fingerprint
208
+ let pool = this.pools.get(fp);
209
+ if (!pool) {
210
+ pool = new Map();
211
+ this.pools.set(fp, pool);
212
+ }
213
+ pool.clear();
214
+ message.data.result.forEach(({ text, translation }) => {
215
+ pool.set(text, translation);
309
216
  });
217
+ this.loadedFingerprints.add(fp);
310
218
  }
311
- /**
312
- * Load translations for a specific fingerprint
313
- */
314
- loadFingerprintTranslations(fp, fingerprint) {
315
- return __awaiter(this, void 0, void 0, function* () {
316
- // Already loaded
317
- if (this.loadedFingerprints.has(fp)) {
318
- return;
319
- }
320
- // Ensure pool exists for this fingerprint
321
- let pool = this.pools.get(fp);
322
- if (!pool) {
323
- pool = new Map();
324
- this.pools.set(fp, pool);
325
- }
326
- // Use streaming for batch loading
327
- yield this.client.translateStream({
328
- senseId: this.senseId,
329
- fingerprint,
330
- batchSize: 500
331
- }, (response) => {
332
- // Add all translations from this batch to the fingerprint's pool
333
- response.translations.forEach(record => {
334
- pool.set(record.text, record.translate);
335
- });
336
- return true; // Continue streaming
337
- });
338
- // Mark as loaded
339
- this.loadedFingerprints.add(fp);
340
- });
219
+ // Update specific entry
220
+ if (((_b = message.data) === null || _b === void 0 ? void 0 : _b.text) && ((_c = message.data) === null || _c === void 0 ? void 0 : _c.translation)) {
221
+ const pool = this.pools.get(fp) || new Map();
222
+ pool.set(message.data.text, message.data.translation);
223
+ this.pools.set(fp, pool);
224
+ this.saveToStorage(fp);
341
225
  }
342
- /**
343
- * Switch to a different fingerprint, loads its translations if not already loaded
344
- * Doesn't clear existing cached translations for other fingerprints
345
- * @param fingerprint The fingerprint to switch to
346
- */
347
- switchFingerprint(fingerprint) {
348
- return __awaiter(this, void 0, void 0, function* () {
349
- this.currentFingerprint = fingerprint;
350
- // Ensure pool exists
351
- if (!this.pools.has(fingerprint)) {
352
- this.pools.set(fingerprint, new Map());
226
+ }
227
+ /**
228
+ * Handle incoming cache clear from another tab
229
+ */
230
+ handleCacheClear(message) {
231
+ const fp = message.fingerprint || 'common';
232
+ if (fp) {
233
+ this.clearFingerprint(fp);
234
+ }
235
+ else {
236
+ this.clearAll();
237
+ }
238
+ }
239
+ /**
240
+ * Handle initial sync request from a new tab
241
+ */
242
+ handleInitialSyncRequest() {
243
+ // Send our current cache to the new tab
244
+ this.broadcastUpdate();
245
+ }
246
+ /**
247
+ * Queue a translation request and return original text immediately
248
+ * This is used during pool loading when translations are not yet available
249
+ * @param request Translation request to queue
250
+ * @returns Original text as the initial response
251
+ */
252
+ queueTranslationRequest(request) {
253
+ // Return original text immediately for fast fallback
254
+ const initialResponse = {
255
+ originalText: request.text,
256
+ translatedText: request.text,
257
+ provider: 'fast_fallback',
258
+ timestamp: Date.now(),
259
+ finished: true,
260
+ cached: false,
261
+ fromLang: request.fromLang,
262
+ toLang: request.toLang
263
+ };
264
+ // Resolve immediately with original text
265
+ const promise = new Promise((resolve) => {
266
+ resolve(initialResponse);
267
+ });
268
+ // Queue the request for processing after pool loads
269
+ const queuedReq = Object.assign(Object.assign({}, request), { resolveFunction: (result) => {
270
+ // Remove from pending resolutions
271
+ delete this.pendingResolutions[`${request.text}-${request.fingerprint || 'common'}`];
272
+ // Resolve the promise for all listeners
273
+ queuedReq.resolveFunction(result);
274
+ }, rejectFunction: (error) => {
275
+ delete this.pendingResolutions[`${request.text}-${request.fingerprint || 'common'}`];
276
+ queuedReq.rejectFunction(error);
277
+ } });
278
+ this.queuedRequests.push(queuedReq);
279
+ // Store reference to allow later resolution
280
+ const key = `${request.text}-${request.fingerprint || 'common'}`;
281
+ this.pendingResolutions[key] = {
282
+ resolve: (result) => queuedReq.resolveFunction(result),
283
+ reject: (error) => queuedReq.rejectFunction(error)
284
+ };
285
+ // Return original text immediately
286
+ return promise;
287
+ }
288
+ /**
289
+ * Process all queued translation requests after pool is loaded
290
+ * This should be called when the pool is fully initialized
291
+ * Includes automatic retry mechanism for failed requests
292
+ */
293
+ async processQueuedRequests(maxRetries = 3, retryDelayMs = 1000) {
294
+ if (this.queuedRequests.length === 0) {
295
+ return;
296
+ }
297
+ console.log(`[TranslationPool] Processing ${this.queuedRequests.length} queued translation requests...`);
298
+ // Copy queued requests and clear the queue
299
+ const requestsToProcess = [...this.queuedRequests];
300
+ this.queuedRequests = [];
301
+ // Process each request with retry mechanism
302
+ const processWithRetry = async (req, attempt = 0) => {
303
+ try {
304
+ // Check if translation is now available in pool
305
+ const lookup = this.lookup(req.text, req.fingerprint);
306
+ if (lookup.found) {
307
+ // Translation is now available in pool, use it
308
+ const translation = await this.client.translate(req.text, req.toLang, req.fromLang, req.fingerprint);
309
+ return { text: req.text, translation, success: true };
353
310
  }
354
- // Load from localStorage first
355
- this.loadFingerprintFromStorage(fingerprint);
356
- // Check if we need to load from backend
357
- if (!this.loadedFingerprints.has(fingerprint)) {
358
- yield this.loadFingerprintTranslations(fingerprint, fingerprint);
311
+ else {
312
+ // Not in pool, request from backend
313
+ const response = await this.client.translateWithDetails(req.text, req.toLang, req.fromLang, req.fingerprint);
314
+ return { text: req.text, translation: response.translatedText, success: true };
359
315
  }
360
- });
316
+ }
317
+ catch (error) {
318
+ console.warn(`[TranslationPool] Request failed (attempt ${attempt + 1}/${maxRetries}):`, error.message);
319
+ if (attempt < maxRetries - 1) {
320
+ // Wait before retry
321
+ await new Promise(resolve => setTimeout(resolve, retryDelayMs * (attempt + 1)));
322
+ return processWithRetry(req, attempt + 1);
323
+ }
324
+ // All retries failed, return original text as fallback
325
+ console.error(`[TranslationPool] All retries failed for: "${req.text}", using original text`);
326
+ return { text: req.text, translation: req.text, success: false };
327
+ }
328
+ };
329
+ // Process all requests in parallel
330
+ const promises = requestsToProcess.map(req => processWithRetry(req));
331
+ const results = await Promise.all(promises);
332
+ // Add successful translations to pool
333
+ const successCount = results.filter(r => r.success).length;
334
+ const failCount = results.length - successCount;
335
+ results.forEach(({ text, translation }) => {
336
+ this.addTranslation(text, translation);
337
+ });
338
+ // Broadcast updates to other tabs
339
+ this.broadcastUpdate();
340
+ console.log(`[TranslationPool] Completed processing ${results.length} queued requests (${successCount} success, ${failCount} failed)`);
341
+ // If there were failures, log them
342
+ if (failCount > 0) {
343
+ console.warn(`[TranslationPool] ${failCount} requests failed after ${maxRetries} retries`);
361
344
  }
362
- /**
363
- * Clear cached translations for a specific fingerprint to free memory
364
- * Doesn't affect other fingerprints or common
365
- * @param fingerprint The fingerprint to clear
366
- */
367
- clearFingerprint(fingerprint) {
368
- this.pools.delete(fingerprint);
369
- this.loadedFingerprints.delete(fingerprint);
370
- // Clear localStorage
371
- if (typeof localStorage !== 'undefined' && this.crossTabOptions.enabled) {
372
- const storageKey = this.getStorageKey(fingerprint);
373
- localStorage.removeItem(storageKey);
345
+ }
346
+ /**
347
+ * Check if there are any pending queued requests
348
+ */
349
+ hasQueuedRequests() {
350
+ return this.queuedRequests.length > 0;
351
+ }
352
+ /**
353
+ * Clear all queued requests (should be called when pool is being cleared)
354
+ */
355
+ clearQueuedRequests() {
356
+ console.log(`Clearing ${this.queuedRequests.length} queued requests`);
357
+ this.queuedRequests = [];
358
+ this.pendingResolutions = {};
359
+ }
360
+ /**
361
+ * Check if currently loading
362
+ */
363
+ isLoading() {
364
+ return this.loading;
365
+ }
366
+ /**
367
+ * Initialize the pool - always loads common first, then loads current fingerprint if set
368
+ * If fingerprint is set, loads special translations; common is always preloaded
369
+ * After loading, processes any queued translation requests
370
+ */
371
+ async initialize() {
372
+ if (this.loading) {
373
+ return;
374
+ }
375
+ this.loading = true;
376
+ try {
377
+ console.log('[TranslationPool] Starting pool initialization...');
378
+ // Always preload common first (required)
379
+ if (!this.loadedFingerprints.has('common')) {
380
+ await this.loadFingerprintTranslations('common', undefined);
381
+ console.log('[TranslationPool] Common translations loaded');
374
382
  }
383
+ // Then load current fingerprint if set and not loaded
384
+ if (this.currentFingerprint && !this.loadedFingerprints.has(this.currentFingerprint)) {
385
+ await this.loadFingerprintTranslations(this.currentFingerprint, this.currentFingerprint);
386
+ console.log(`[TranslationPool] ${this.currentFingerprint} translations loaded`);
387
+ }
388
+ // Broadcast to other tabs after full initialization
389
+ this.broadcastUpdate();
390
+ console.log('[TranslationPool] Pool initialization completed');
391
+ // Process queued requests after pool is loaded
392
+ await this.processQueuedRequests();
375
393
  }
376
- /**
377
- * Check if a fingerprint has been loaded
378
- * @param fingerprint Fingerprint to check
379
- */
380
- isLoaded(fingerprint) {
381
- return this.loadedFingerprints.has(fingerprint);
382
- }
383
- /**
384
- * Load a fingerprint if not already loaded
385
- * @param fingerprint Fingerprint to load
386
- */
387
- loadFingerprintIfNotLoaded(fingerprint) {
388
- return __awaiter(this, void 0, void 0, function* () {
389
- if (this.loadedFingerprints.has(fingerprint)) {
390
- return;
391
- }
392
- // Load from localStorage first
393
- this.loadFingerprintFromStorage(fingerprint);
394
- // Check if still not loaded after localStorage
395
- if (!this.loadedFingerprints.has(fingerprint)) {
396
- yield this.loadFingerprintTranslations(fingerprint, fingerprint);
397
- }
394
+ finally {
395
+ this.loading = false;
396
+ }
397
+ }
398
+ /**
399
+ * Load translations for a specific fingerprint
400
+ */
401
+ async loadFingerprintTranslations(fp, fingerprint) {
402
+ // Already loaded
403
+ if (this.loadedFingerprints.has(fp)) {
404
+ return;
405
+ }
406
+ // Ensure pool exists for this fingerprint
407
+ let pool = this.pools.get(fp);
408
+ if (!pool) {
409
+ pool = new Map();
410
+ this.pools.set(fp, pool);
411
+ }
412
+ // Use streaming for batch loading
413
+ await this.client.translateStream({
414
+ senseId: this.senseId,
415
+ fingerprint,
416
+ batchSize: 500
417
+ }, (response) => {
418
+ // Add all translations from this batch to the fingerprint's pool
419
+ // response.translation is a Record<string, string> (key-value map)
420
+ Object.entries(response.translation).forEach(([text, translate]) => {
421
+ pool.set(text, translate);
398
422
  });
423
+ return true; // Continue streaming
424
+ });
425
+ // Mark as loaded
426
+ this.loadedFingerprints.add(fp);
427
+ }
428
+ /**
429
+ * Switch to a different fingerprint, loads its translations if not already loaded
430
+ * Doesn't clear existing cached translations for other fingerprints
431
+ * @param fingerprint The fingerprint to switch to
432
+ */
433
+ async switchFingerprint(fingerprint) {
434
+ this.currentFingerprint = fingerprint;
435
+ // Ensure pool exists
436
+ if (!this.pools.has(fingerprint)) {
437
+ this.pools.set(fingerprint, new Map());
438
+ }
439
+ // Load from localStorage first
440
+ this.loadFingerprintFromStorage(fingerprint);
441
+ // Check if we need to load from backend
442
+ if (!this.loadedFingerprints.has(fingerprint)) {
443
+ await this.loadFingerprintTranslations(fingerprint, fingerprint);
399
444
  }
400
- /**
401
- * Clear the current fingerprint to free memory (switch back to common)
402
- * Current fingerprint becomes null, only common remains active
403
- */
404
- clearCurrentFingerprint() {
405
- if (this.currentFingerprint) {
406
- this.clearFingerprint(this.currentFingerprint);
407
- this.currentFingerprint = null;
408
- }
445
+ }
446
+ /**
447
+ * Clear cached translations for a specific fingerprint to free memory
448
+ * Doesn't affect other fingerprints or common
449
+ * @param fingerprint The fingerprint to clear
450
+ */
451
+ clearFingerprint(fingerprint) {
452
+ this.pools.delete(fingerprint);
453
+ this.loadedFingerprints.delete(fingerprint);
454
+ // Clear localStorage
455
+ if (typeof localStorage !== 'undefined' && this.crossTabOptions.enabled) {
456
+ const storageKey = this.getStorageKey(fingerprint);
457
+ localStorage.removeItem(storageKey);
409
458
  }
410
- /**
411
- * Lookup translation
412
- * Priority: provided fingerprint (if any) → current fingerprint (if set) → common → not found
413
- * @param text Original text to lookup
414
- * @param fingerprint Optional specific fingerprint to lookup (overrides current fingerprint)
415
- * @returns Lookup result
416
- */
417
- lookup(text, fingerprint) {
418
- // Check provided fingerprint first if given
419
- if (fingerprint) {
420
- const targetPool = this.pools.get(fingerprint);
421
- if (targetPool && targetPool.has(text)) {
422
- return {
423
- found: true,
424
- translation: targetPool.get(text),
425
- source: 'special'
426
- };
427
- }
428
- }
429
- // Check current fingerprint next if we have one and no specific fingerprint provided
430
- if (!fingerprint && this.currentFingerprint) {
431
- const currentPool = this.pools.get(this.currentFingerprint);
432
- if (currentPool && currentPool.has(text)) {
433
- return {
434
- found: true,
435
- translation: currentPool.get(text),
436
- source: 'special'
437
- };
438
- }
459
+ }
460
+ /**
461
+ * Check if a fingerprint has been loaded
462
+ * @param fingerprint Fingerprint to check
463
+ */
464
+ isLoaded(fingerprint) {
465
+ return this.loadedFingerprints.has(fingerprint);
466
+ }
467
+ /**
468
+ * Load a fingerprint if not already loaded
469
+ * @param fingerprint Fingerprint to load
470
+ */
471
+ async loadFingerprintIfNotLoaded(fingerprint) {
472
+ if (this.loadedFingerprints.has(fingerprint)) {
473
+ return;
474
+ }
475
+ // Load from localStorage first
476
+ this.loadFingerprintFromStorage(fingerprint);
477
+ // Check if still not loaded after localStorage
478
+ if (!this.loadedFingerprints.has(fingerprint)) {
479
+ await this.loadFingerprintTranslations(fingerprint, fingerprint);
480
+ }
481
+ }
482
+ /**
483
+ * Clear the current fingerprint to free memory (switch back to common)
484
+ * Current fingerprint becomes null, only common remains active
485
+ */
486
+ clearCurrentFingerprint() {
487
+ if (this.currentFingerprint) {
488
+ this.clearFingerprint(this.currentFingerprint);
489
+ this.currentFingerprint = null;
490
+ }
491
+ }
492
+ /**
493
+ * Lookup translation
494
+ * Priority: provided fingerprint (if any) → current fingerprint (if set) → common → not found
495
+ * @param text Original text to lookup
496
+ * @param fingerprint Optional specific fingerprint to lookup (overrides current fingerprint)
497
+ * @returns Lookup result
498
+ */
499
+ lookup(text, fingerprint) {
500
+ // Check provided fingerprint first if given
501
+ if (fingerprint) {
502
+ const targetPool = this.pools.get(fingerprint);
503
+ if (targetPool && targetPool.has(text)) {
504
+ return {
505
+ found: true,
506
+ translation: targetPool.get(text),
507
+ source: 'special'
508
+ };
439
509
  }
440
- // Fallback to common
441
- const commonPool = this.pools.get('common');
442
- if (commonPool && commonPool.has(text)) {
510
+ }
511
+ // Check current fingerprint next if we have one and no specific fingerprint provided
512
+ if (!fingerprint && this.currentFingerprint) {
513
+ const currentPool = this.pools.get(this.currentFingerprint);
514
+ if (currentPool && currentPool.has(text)) {
443
515
  return {
444
516
  found: true,
445
- translation: commonPool.get(text),
446
- source: 'common'
517
+ translation: currentPool.get(text),
518
+ source: 'special'
447
519
  };
448
520
  }
449
- // Not found
521
+ }
522
+ // Fallback to common
523
+ const commonPool = this.pools.get('common');
524
+ if (commonPool && commonPool.has(text)) {
450
525
  return {
451
- found: false,
452
- translation: '',
453
- source: null
526
+ found: true,
527
+ translation: commonPool.get(text),
528
+ source: 'common'
454
529
  };
455
530
  }
456
- /**
457
- * Add a translation to the pool
458
- * Adds to current fingerprint pool (or common if no fingerprint set)
459
- * @param text Original text
460
- * @param translation Translated text
461
- * @param fingerprint Optional specific fingerprint to add to (overrides current fingerprint)
462
- */
463
- addTranslation(text, translation, fingerprint) {
464
- const fp = fingerprint || this.currentFingerprint || 'common';
465
- let pool = this.pools.get(fp);
466
- if (!pool) {
467
- pool = new Map();
468
- this.pools.set(fp, pool);
469
- }
470
- pool.set(text, translation);
471
- this.broadcastUpdate(text, translation);
472
- }
473
- /**
474
- * Request translation from backend, automatically adds to pool if found
475
- *
476
- * @param text Text to translate
477
- * @param fromLang Source language
478
- * @param toLang Target language
479
- * @returns Translation response
480
- */
481
- requestTranslation(text, fromLang, toLang) {
482
- return __awaiter(this, void 0, void 0, function* () {
483
- const response = yield this.client.llmTranslate({
484
- text,
485
- fromLang,
486
- toLang,
487
- senseId: this.senseId
488
- });
489
- // Add to pool automatically
490
- if (response.translatedText) {
491
- this.addTranslation(text, response.translatedText);
531
+ // Not found
532
+ return {
533
+ found: false,
534
+ translation: '',
535
+ source: null
536
+ };
537
+ }
538
+ /**
539
+ * Add a translation to the pool
540
+ * Adds to current fingerprint pool (or common if no fingerprint set)
541
+ * @param text Original text
542
+ * @param translation Translated text
543
+ * @param fingerprint Optional specific fingerprint to add to (overrides current fingerprint)
544
+ */
545
+ addTranslation(text, translation, fingerprint) {
546
+ const fp = fingerprint || this.currentFingerprint || 'common';
547
+ let pool = this.pools.get(fp);
548
+ if (!pool) {
549
+ pool = new Map();
550
+ this.pools.set(fp, pool);
551
+ }
552
+ pool.set(text, translation);
553
+ this.broadcastUpdate(text, translation);
554
+ }
555
+ /**
556
+ * Request translation from backend, automatically adds to pool if found
557
+ *
558
+ * @param text Text to translate
559
+ * @param fromLang Source language
560
+ * @param toLang Target language
561
+ * @returns Translation response
562
+ */
563
+ async requestTranslation(text, fromLang, toLang) {
564
+ // Use translateStream API - backend will automatically call LLM if translation not found in database
565
+ return new Promise((resolve, reject) => {
566
+ this.client.translateStream({
567
+ senseId: this.senseId,
568
+ text,
569
+ from_lang: fromLang,
570
+ to_lang: toLang
571
+ }, (response) => {
572
+ // Check if we got a translation
573
+ if (response.translation && response.translation[text]) {
574
+ const translatedText = response.translation[text];
575
+ // Add to pool automatically
576
+ this.addTranslation(text, translatedText);
577
+ resolve({
578
+ originalText: text,
579
+ translatedText: translatedText,
580
+ provider: 'translate-stream',
581
+ timestamp: response.timestamp,
582
+ finished: response.finished,
583
+ cached: false,
584
+ fromLang,
585
+ toLang
586
+ });
587
+ return false; // Stop streaming
492
588
  }
493
- return response;
589
+ // Check for error
590
+ if (response.translation && response.translation['error']) {
591
+ reject(new Error(response.translation['error']));
592
+ return false;
593
+ }
594
+ // Continue if not finished
595
+ return !response.finished;
596
+ }).catch(reject);
597
+ });
598
+ }
599
+ /**
600
+ * Get all translations for current fingerprint (includes common if needed)
601
+ * @returns Array of {text, translation}
602
+ */
603
+ getAll() {
604
+ const fp = this.currentFingerprint || 'common';
605
+ return this.getAllForFingerprint(fp);
606
+ }
607
+ /**
608
+ * Get all translations for a specific fingerprint
609
+ * @param fp Fingerprint name
610
+ * @returns Array of {text, translation}
611
+ */
612
+ getAllForFingerprint(fp) {
613
+ const result = [];
614
+ const pool = this.pools.get(fp);
615
+ if (pool) {
616
+ pool.forEach((translation, text) => {
617
+ result.push({ text, translation });
494
618
  });
495
619
  }
496
- /**
497
- * Get all translations for current fingerprint (includes common if needed)
498
- * @returns Array of {text, translation}
499
- */
500
- getAll() {
501
- const fp = this.currentFingerprint || 'common';
502
- return this.getAllForFingerprint(fp);
503
- }
504
- /**
505
- * Get all translations for a specific fingerprint
506
- * @param fp Fingerprint name
507
- * @returns Array of {text, translation}
508
- */
509
- getAllForFingerprint(fp) {
510
- const result = [];
511
- const pool = this.pools.get(fp);
512
- if (pool) {
513
- pool.forEach((translation, text) => {
514
- result.push({ text, translation });
515
- });
516
- }
517
- return result;
518
- }
519
- /**
520
- * Clear all cached data to free memory
521
- */
522
- clearAll() {
523
- // Clear all fingerprints from memory
524
- this.pools.clear();
525
- this.loadedFingerprints.clear();
526
- this.currentFingerprint = null;
527
- // Re-initialize common pool
528
- this.pools.set('common', new Map());
529
- // Clear all localStorage for this sense
530
- if (typeof localStorage !== 'undefined' && this.crossTabOptions.enabled) {
531
- // We can't easily iterate all fingerprints, but at least clear common
532
- const storageKey = this.getStorageKey('common');
533
- localStorage.removeItem(storageKey);
534
- }
535
- // Broadcast clear to other tabs
536
- if (this.broadcastChannel && this.crossTabOptions.enabled) {
537
- this.broadcastChannel.postMessage({
538
- type: 'cache_clear',
539
- senseId: this.senseId,
540
- fingerprint: undefined
541
- });
542
- }
620
+ return result;
621
+ }
622
+ /**
623
+ * Clear all cached data to free memory
624
+ */
625
+ clearAll() {
626
+ // Clear all fingerprints from memory
627
+ this.pools.clear();
628
+ this.loadedFingerprints.clear();
629
+ this.currentFingerprint = null;
630
+ // Re-initialize common pool
631
+ this.pools.set('common', new Map());
632
+ // Clear all localStorage for this sense
633
+ if (typeof localStorage !== 'undefined' && this.crossTabOptions.enabled) {
634
+ // We can't easily iterate all fingerprints, but at least clear common
635
+ const storageKey = this.getStorageKey('common');
636
+ localStorage.removeItem(storageKey);
637
+ }
638
+ // Broadcast clear to other tabs
639
+ if (this.broadcastChannel && this.crossTabOptions.enabled) {
640
+ this.broadcastChannel.postMessage({
641
+ type: 'cache_clear',
642
+ senseId: this.senseId,
643
+ fingerprint: undefined
644
+ });
543
645
  }
544
- /**
545
- * Close the broadcast channel to free resources
546
- * Should be called when the pool is no longer needed
547
- */
548
- destroy() {
549
- if (this.broadcastChannel) {
550
- this.broadcastChannel.close();
551
- this.broadcastChannel = null;
552
- }
646
+ }
647
+ /**
648
+ * Close the broadcast channel to free resources
649
+ * Should be called when the pool is no longer needed
650
+ */
651
+ destroy() {
652
+ if (this.broadcastChannel) {
653
+ this.broadcastChannel.close();
654
+ this.broadcastChannel = null;
553
655
  }
554
- /**
555
- * Check if cross-tab synchronization is enabled
556
- */
557
- isCrossTabEnabled() {
558
- return this.crossTabOptions.enabled;
559
- }
560
- }
561
- /**
562
- * TranslationService - Main entry point for translation operations
563
- * Provides a clean API with automatic caching and cross-tab synchronization
564
- *
565
- * Features:
566
- * - Lazy initialization: automatically initializes on first translate() call
567
- * - Streaming batch load: uses streaming API for efficient initialization
568
- * - On-demand loading: automatically loads when fingerprint changes
569
- * - Cross-tab sync: optional Broadcast Channel + localStorage synchronization
570
- *
571
- * Usage:
572
- * - Simple: Just create and call translate() - initialization is automatic
573
- * - Advanced: Call initialize() explicitly to preload all translations upfront
574
- */
575
- /**
576
- * Simple LRU Cache implementation for TranslationClient
577
- * Uses Map with access order for O(1) get/set operations
578
- */
579
- class LRUCache {
580
- constructor(capacity = 1000) {
581
- this.capacity = capacity;
582
- this.cache = new Map();
583
- }
584
- /**
585
- * Get value from cache, moves to end (most recently used)
586
- */
587
- get(key) {
588
- if (!this.cache.has(key)) {
589
- return undefined;
590
- }
591
- // Move to end (most recently used)
592
- const value = this.cache.get(key);
656
+ }
657
+ /**
658
+ * Check if cross-tab synchronization is enabled
659
+ */
660
+ isCrossTabEnabled() {
661
+ return this.crossTabOptions.enabled;
662
+ }
663
+ }
664
+ /**
665
+ * TranslationService - Main entry point for translation operations
666
+ * Provides a clean API with automatic caching and cross-tab synchronization
667
+ *
668
+ * Features:
669
+ * - Lazy initialization: automatically initializes on first translate() call
670
+ * - Streaming batch load: uses streaming API for efficient initialization
671
+ * - On-demand loading: automatically loads when fingerprint changes
672
+ * - Cross-tab sync: optional Broadcast Channel + localStorage synchronization
673
+ *
674
+ * Usage:
675
+ * - Simple: Just create and call translate() - initialization is automatic
676
+ * - Advanced: Call initialize() explicitly to preload all translations upfront
677
+ */
678
+ /**
679
+ * Simple LRU Cache implementation for TranslationClient
680
+ * Uses Map with access order for O(1) get/set operations
681
+ */
682
+ class LRUCache {
683
+ constructor(capacity = 1000) {
684
+ this.capacity = capacity;
685
+ this.cache = new Map();
686
+ }
687
+ /**
688
+ * Get value from cache, moves to end (most recently used)
689
+ */
690
+ get(key) {
691
+ if (!this.cache.has(key)) {
692
+ return undefined;
693
+ }
694
+ // Move to end (most recently used)
695
+ const value = this.cache.get(key);
696
+ this.cache.delete(key);
697
+ this.cache.set(key, value);
698
+ return value;
699
+ }
700
+ /**
701
+ * Set value in cache, evicts oldest if over capacity
702
+ */
703
+ set(key, value) {
704
+ // Delete if exists (to move to end)
705
+ if (this.cache.has(key)) {
593
706
  this.cache.delete(key);
594
- this.cache.set(key, value);
595
- return value;
596
- }
597
- /**
598
- * Set value in cache, evicts oldest if over capacity
599
- */
600
- set(key, value) {
601
- // Delete if exists (to move to end)
602
- if (this.cache.has(key)) {
603
- this.cache.delete(key);
604
- }
605
- // Evict oldest if at capacity
606
- else if (this.cache.size >= this.capacity) {
607
- // First key is the oldest (least recently used)
608
- const oldestKey = this.cache.keys().next().value;
609
- this.cache.delete(oldestKey);
610
- }
611
- this.cache.set(key, value);
612
- }
613
- /**
614
- * Check if key exists in cache
615
- */
616
- has(key) {
617
- return this.cache.has(key);
618
- }
619
- /**
620
- * Delete key from cache
621
- */
622
- delete(key) {
623
- return this.cache.delete(key);
624
- }
625
- /**
626
- * Clear all entries
627
- */
628
- clear() {
629
- this.cache.clear();
630
- }
631
- /**
632
- * Get current cache size
633
- */
634
- get size() {
635
- return this.cache.size;
636
- }
637
- }
638
- /**
639
- * Cache key generator for translation requests
640
- */
641
- function generateCacheKey(request) {
642
- const parts = [
643
- request.text,
644
- request.toLang || '',
645
- request.fromLang || '',
646
- request.senseId || '',
647
- request.provider || ''
648
- ];
649
- return parts.join('|');
650
- }
651
- const defaultClientCrossTabOptions = {
652
- enabled: false,
653
- channelName: 'laker-translation-client-cache',
654
- storageKey: 'laker_translation_client_cache'
655
- };
707
+ }
708
+ // Evict oldest if at capacity
709
+ else if (this.cache.size >= this.capacity) {
710
+ // First key is the oldest (least recently used)
711
+ const oldestKey = this.cache.keys().next().value;
712
+ this.cache.delete(oldestKey);
713
+ }
714
+ this.cache.set(key, value);
715
+ }
656
716
  /**
657
- * TranslationClient - Main entry point for Laker Translation SDK
658
- *
659
- * Features:
660
- * - Automatic cache lookup: preloaded translations → LRU cache → backend request
661
- * - Fingerprint-based personalized translations support
662
- * - Optional cross-browser-tab cache synchronization
663
- * - Lazy initialization: automatically preloads on first use
664
- * - Simple single-level API, no complex layering
665
- */
666
- class TranslationClient {
667
- /**
668
- * Create a new TranslationClient - the only entry point you need
669
- * @param config Client configuration
670
- */
671
- constructor(config) {
672
- this.currentFingerprint = null;
673
- this.initialized = false;
674
- this.initPromise = null;
675
- // Cross-tab for LLM cache
676
- this.broadcastChannel = null;
677
- this.config = config;
678
- this.token = config.token;
679
- this.baseUrl = (config.baseUrl || 'https://api.hottol.com/laker/').endsWith('/')
680
- ? (config.baseUrl || 'https://api.hottol.com/laker/').slice(0, -1)
681
- : (config.baseUrl || 'https://api.hottol.com/laker/');
682
- this.timeout = config.timeout || 30000;
683
- // Configure LRU cache for LLM translations
684
- this.llmCacheEnabled = config.useCache !== false;
685
- const llmCacheSize = this.llmCacheEnabled ? (config.cacheSize || 1000) : 0;
686
- this.llmCache = new LRUCache(llmCacheSize);
687
- // Configure cross-tab options for translation pool
688
- const crossTabOptions = {
689
- enabled: config.crossTab === true,
690
- };
691
- if (config.crossTabChannelName) {
692
- crossTabOptions.channelName = config.crossTabChannelName;
693
- }
694
- if (config.crossTabStorageKeyPrefix) {
695
- crossTabOptions.storageKeyPrefix = config.crossTabStorageKeyPrefix;
696
- }
697
- // Create translation pool for pre-loaded translations
698
- this.pool = new TranslationPool(this, config.senseId, crossTabOptions);
699
- // Set initial fingerprint if provided
700
- if (config.fingerprint) {
701
- this.currentFingerprint = config.fingerprint;
702
- this.pool['currentFingerprint'] = config.fingerprint;
703
- }
704
- // Cross-tab for LLM cache
705
- this.crossTabOptions = Object.assign({}, defaultClientCrossTabOptions);
706
- this.storageKey = this.crossTabOptions.storageKey;
707
- // Initialize cross-tab synchronization if enabled
708
- if (config.crossTab && typeof BroadcastChannel !== 'undefined') {
709
- this.initCrossTabSync();
710
- }
711
- // Load from localStorage if cross-tab enabled
712
- this.loadFromStorage();
713
- }
714
- /**
715
- * Initialize cross-tab synchronization via Broadcast Channel
716
- */
717
- initCrossTabSync() {
718
- this.broadcastChannel = new BroadcastChannel(this.crossTabOptions.channelName);
719
- this.broadcastChannel.onmessage = (event) => {
720
- const message = event.data;
721
- switch (message.type) {
722
- case 'cache_update':
723
- if (message.key && message.data) {
724
- // Update local cache from another tab's update
725
- this.llmCache.set(message.key, message.data);
726
- }
727
- break;
728
- case 'cache_clear':
729
- this.llmCache.clear();
730
- break;
731
- case 'request_sync':
732
- // Another tab is requesting our cache, send our data
733
- this.broadcastFullCache();
734
- break;
735
- }
736
- };
737
- // Request other tabs to share their cache
738
- this.broadcastChannel.postMessage({ type: 'request_sync' });
739
- }
740
- /**
741
- * Load cache from localStorage
742
- */
743
- loadFromStorage() {
744
- if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
745
- return;
746
- }
747
- try {
748
- const stored = localStorage.getItem(this.storageKey);
749
- if (stored) {
750
- const data = JSON.parse(stored);
751
- data.forEach(({ key, value }) => {
752
- this.llmCache.set(key, value);
753
- });
754
- }
717
+ * Check if key exists in cache
718
+ */
719
+ has(key) {
720
+ return this.cache.has(key);
721
+ }
722
+ /**
723
+ * Delete key from cache
724
+ */
725
+ delete(key) {
726
+ return this.cache.delete(key);
727
+ }
728
+ /**
729
+ * Clear all entries
730
+ */
731
+ clear() {
732
+ this.cache.clear();
733
+ }
734
+ /**
735
+ * Get current cache size
736
+ */
737
+ get size() {
738
+ return this.cache.size;
739
+ }
740
+ }
741
+ /**
742
+ * Cache key generator for translation requests
743
+ */
744
+ function generateCacheKey(request) {
745
+ const parts = [
746
+ request.text,
747
+ request.toLang || '',
748
+ request.fromLang || '',
749
+ request.senseId || '',
750
+ request.provider || ''
751
+ ];
752
+ return parts.join('|');
753
+ }
754
+ const defaultClientCrossTabOptions = {
755
+ enabled: false,
756
+ channelName: 'laker-translation-client-cache',
757
+ storageKey: 'laker_translation_client_cache'
758
+ };
759
+ /**
760
+ * TranslationClient - Main entry point for Laker Translation SDK
761
+ *
762
+ * Features:
763
+ * - Automatic cache lookup: preloaded translations → LRU cache → backend request
764
+ * - Fingerprint-based personalized translations support
765
+ * - Optional cross-browser-tab cache synchronization
766
+ * - Lazy initialization: automatically preloads on first use
767
+ * - Simple single-level API, no complex layering
768
+ */
769
+ export class TranslationClient {
770
+ /**
771
+ * Create a new TranslationClient - the only entry point you need
772
+ * @param config Client configuration
773
+ */
774
+ constructor(config) {
775
+ this.currentFingerprint = null;
776
+ this.initialized = false;
777
+ this.initPromise = null;
778
+ // Cross-tab for LLM cache
779
+ this.broadcastChannel = null;
780
+ this.config = config;
781
+ this.token = config.token;
782
+ // Default baseUrl includes the API path prefix /api/v1/translate
783
+ this.baseUrl = (config.baseUrl || 'https://api.hottol.com/laker/api/v1/translate').endsWith('/')
784
+ ? (config.baseUrl || 'https://api.hottol.com/laker/api/v1/translate').slice(0, -1)
785
+ : (config.baseUrl || 'https://api.hottol.com/laker/api/v1/translate');
786
+ this.timeout = config.timeout || 30000;
787
+ // Configure LRU cache for LLM translations
788
+ this.llmCacheEnabled = config.useCache !== false;
789
+ const llmCacheSize = this.llmCacheEnabled ? (config.cacheSize || 1000) : 0;
790
+ this.llmCache = new LRUCache(llmCacheSize);
791
+ // Configure cross-tab options for translation pool
792
+ const crossTabOptions = {
793
+ enabled: config.crossTab === true,
794
+ };
795
+ if (config.crossTabChannelName) {
796
+ crossTabOptions.channelName = config.crossTabChannelName;
797
+ }
798
+ if (config.crossTabStorageKeyPrefix) {
799
+ crossTabOptions.storageKeyPrefix = config.crossTabStorageKeyPrefix;
800
+ }
801
+ // Create translation pool for pre-loaded translations
802
+ this.pool = new TranslationPool(this, config.senseId, crossTabOptions);
803
+ // Set initial fingerprint if provided
804
+ if (config.fingerprint) {
805
+ this.currentFingerprint = config.fingerprint;
806
+ this.pool['currentFingerprint'] = config.fingerprint;
807
+ }
808
+ // Cross-tab for LLM cache
809
+ this.crossTabOptions = Object.assign({}, defaultClientCrossTabOptions);
810
+ this.storageKey = this.crossTabOptions.storageKey;
811
+ // Initialize cross-tab synchronization if enabled
812
+ if (config.crossTab && typeof BroadcastChannel !== 'undefined') {
813
+ this.initCrossTabSync();
814
+ }
815
+ // Load from localStorage if cross-tab enabled
816
+ this.loadFromStorage();
817
+ }
818
+ /**
819
+ * Initialize cross-tab synchronization via Broadcast Channel
820
+ */
821
+ initCrossTabSync() {
822
+ this.broadcastChannel = new BroadcastChannel(this.crossTabOptions.channelName);
823
+ this.broadcastChannel.onmessage = (event) => {
824
+ const message = event.data;
825
+ switch (message.type) {
826
+ case 'cache_update':
827
+ if (message.key && message.data) {
828
+ // Update local cache from another tab's update
829
+ this.llmCache.set(message.key, message.data);
830
+ }
831
+ break;
832
+ case 'cache_clear':
833
+ this.llmCache.clear();
834
+ break;
835
+ case 'request_sync':
836
+ // Another tab is requesting our cache, send our data
837
+ this.broadcastFullCache();
838
+ break;
755
839
  }
756
- catch (e) {
757
- console.warn('Failed to load translation cache from localStorage:', e);
840
+ };
841
+ // Request other tabs to share their cache
842
+ this.broadcastChannel.postMessage({ type: 'request_sync' });
843
+ }
844
+ /**
845
+ * Load cache from localStorage
846
+ */
847
+ loadFromStorage() {
848
+ if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
849
+ return;
850
+ }
851
+ try {
852
+ const stored = localStorage.getItem(this.storageKey);
853
+ if (stored) {
854
+ const data = JSON.parse(stored);
855
+ data.forEach(({ key, value }) => {
856
+ this.llmCache.set(key, value);
857
+ });
758
858
  }
759
859
  }
760
- /**
761
- * Save cache to localStorage
762
- */
763
- saveToStorage() {
764
- if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
765
- return;
766
- }
767
- try {
768
- const data = this.getAllCacheEntries();
769
- localStorage.setItem(this.storageKey, JSON.stringify(data));
770
- }
771
- catch (e) {
772
- console.warn('Failed to save translation cache to localStorage:', e);
773
- }
860
+ catch (e) {
861
+ console.warn('Failed to load translation cache from localStorage:', e);
774
862
  }
775
- /**
776
- * Get all cache entries for storage/broadcast
777
- */
778
- getAllCacheEntries() {
779
- const result = [];
780
- // Access internal cache map for iteration
781
- const cacheMap = this.llmCache.cache;
782
- cacheMap.forEach((value, key) => {
783
- result.push({ key, value });
784
- });
785
- return result;
863
+ }
864
+ /**
865
+ * Save cache to localStorage
866
+ */
867
+ saveToStorage() {
868
+ if (typeof localStorage === 'undefined' || !this.crossTabOptions.enabled) {
869
+ return;
786
870
  }
787
- /**
788
- * Broadcast full cache to other tabs
789
- */
790
- broadcastFullCache() {
791
- if (!this.broadcastChannel || !this.crossTabOptions.enabled) {
792
- return;
793
- }
794
- const entries = this.getAllCacheEntries();
795
- entries.forEach(({ key, value }) => {
796
- this.broadcastChannel.postMessage({
797
- type: 'cache_update',
798
- key,
799
- data: value
800
- });
801
- });
871
+ try {
872
+ const data = this.getAllCacheEntries();
873
+ localStorage.setItem(this.storageKey, JSON.stringify(data));
802
874
  }
803
- /**
804
- * Broadcast a single cache update to other tabs
805
- */
806
- broadcastCacheUpdate(key, value) {
807
- if (!this.broadcastChannel || !this.crossTabOptions.enabled) {
808
- return;
809
- }
875
+ catch (e) {
876
+ console.warn('Failed to save translation cache to localStorage:', e);
877
+ }
878
+ }
879
+ /**
880
+ * Get all cache entries for storage/broadcast
881
+ */
882
+ getAllCacheEntries() {
883
+ const result = [];
884
+ // Access internal cache map for iteration
885
+ const cacheMap = this.llmCache.cache;
886
+ cacheMap.forEach((value, key) => {
887
+ result.push({ key, value });
888
+ });
889
+ return result;
890
+ }
891
+ /**
892
+ * Broadcast full cache to other tabs
893
+ */
894
+ broadcastFullCache() {
895
+ if (!this.broadcastChannel || !this.crossTabOptions.enabled) {
896
+ return;
897
+ }
898
+ const entries = this.getAllCacheEntries();
899
+ entries.forEach(({ key, value }) => {
810
900
  this.broadcastChannel.postMessage({
811
901
  type: 'cache_update',
812
902
  key,
813
903
  data: value
814
904
  });
815
- this.saveToStorage();
816
- }
817
- /**
818
- * Set or update the JWT authentication token
819
- * @param token JWT token
820
- */
821
- setToken(token) {
822
- this.token = token;
823
- }
824
- /**
825
- * Enable or disable LLM cache
826
- * @param enabled Whether to enable cache
827
- */
828
- setCacheEnabled(enabled) {
829
- this.llmCacheEnabled = enabled;
830
- }
831
- /**
832
- * Clear the LLM translation cache (also clears localStorage and broadcasts to other tabs)
833
- */
834
- clearCache() {
835
- this.llmCache.clear();
836
- // Clear localStorage
837
- if (typeof localStorage !== 'undefined' && this.crossTabOptions.enabled) {
838
- localStorage.removeItem(this.storageKey);
839
- }
840
- // Broadcast clear to other tabs
841
- if (this.broadcastChannel && this.crossTabOptions.enabled) {
842
- this.broadcastChannel.postMessage({ type: 'cache_clear' });
843
- }
905
+ });
906
+ }
907
+ /**
908
+ * Broadcast a single cache update to other tabs
909
+ */
910
+ broadcastCacheUpdate(key, value) {
911
+ if (!this.broadcastChannel || !this.crossTabOptions.enabled) {
912
+ return;
913
+ }
914
+ this.broadcastChannel.postMessage({
915
+ type: 'cache_update',
916
+ key,
917
+ data: value
918
+ });
919
+ this.saveToStorage();
920
+ }
921
+ /**
922
+ * Set or update the JWT authentication token
923
+ * @param token JWT token
924
+ */
925
+ setToken(token) {
926
+ this.token = token;
927
+ }
928
+ /**
929
+ * Enable or disable LLM cache
930
+ * @param enabled Whether to enable cache
931
+ */
932
+ setCacheEnabled(enabled) {
933
+ this.llmCacheEnabled = enabled;
934
+ }
935
+ /**
936
+ * Clear the LLM translation cache (also clears localStorage and broadcasts to other tabs)
937
+ */
938
+ clearCache() {
939
+ this.llmCache.clear();
940
+ // Clear localStorage
941
+ if (typeof localStorage !== 'undefined' && this.crossTabOptions.enabled) {
942
+ localStorage.removeItem(this.storageKey);
844
943
  }
845
- /**
846
- * Get current LLM cache size
847
- */
848
- getCacheSize() {
849
- return this.llmCache.size;
850
- }
851
- /**
852
- * Check if cross-tab synchronization is enabled
853
- */
854
- isCrossTabEnabled() {
855
- return this.crossTabOptions.enabled;
856
- }
857
- /**
858
- 凤 * GetSenseTranslate - One-shot unary request with pagination
859
- * @param request Request parameters
860
- */
861
- getSenseTranslate(request) {
862
- return __awaiter(this, void 0, void 0, function* () {
863
- const url = `${this.baseUrl}/TranslationService/GetSenseTranslate`;
864
- const response = yield this.fetchJson(url, request);
865
- return response;
866
- });
944
+ // Broadcast clear to other tabs
945
+ if (this.broadcastChannel && this.crossTabOptions.enabled) {
946
+ this.broadcastChannel.postMessage({ type: 'cache_clear' });
867
947
  }
868
- /**
869
- * TranslateStream - Server streaming, receives multiple batches progressively
870
- * @param request Request parameters
871
- * @param onBatch Callback for each batch received. Return false to stop streaming early.
872
- */
873
- translateStream(request, onBatch) {
874
- return __awaiter(this, void 0, void 0, function* () {
875
- const url = `${this.baseUrl}/TranslationService/TranslateStream`;
876
- // For gRPC-Web streaming over HTTP, we use POST with streaming response
877
- const response = yield this.fetchWithTimeout(url, {
878
- method: 'POST',
879
- body: JSON.stringify(request),
880
- headers: this.getHeaders()
881
- });
882
- if (!response.body) {
883
- throw new Error('No response body for streaming request');
884
- }
885
- const reader = response.body.getReader();
886
- const decoder = new TextDecoder();
887
- while (true) {
888
- const { done, value } = yield reader.read();
889
- if (done) {
890
- break;
948
+ }
949
+ /**
950
+ * Get current LLM cache size
951
+ */
952
+ getCacheSize() {
953
+ return this.llmCache.size;
954
+ }
955
+ /**
956
+ * Check if cross-tab synchronization is enabled
957
+ */
958
+ isCrossTabEnabled() {
959
+ return this.crossTabOptions.enabled;
960
+ }
961
+ /**
962
+ 凤 * GetSenseTranslate - One-shot unary request with pagination
963
+ * @param request Request parameters
964
+ */
965
+ async getSenseTranslate(request) {
966
+ const url = `${this.baseUrl}/api/v1/translate/TranslationService/GetSenseTranslate`;
967
+ const response = await this.fetchJson(url, request);
968
+ return response;
969
+ }
970
+ /**
971
+ * TranslateStream - Server streaming, receives multiple batches progressively
972
+ * @param request Request parameters
973
+ * @param onBatch Callback for each batch received. Return false to stop streaming early.
974
+ */
975
+ async translateStream(request, onBatch) {
976
+ const url = `${this.baseUrl}/api/v1/translate/TranslationService/TranslateStream`;
977
+ // For gRPC-Web streaming over HTTP, we use POST with streaming response
978
+ const response = await this.fetchWithTimeout(url, {
979
+ method: 'POST',
980
+ body: JSON.stringify(request),
981
+ headers: this.getHeaders()
982
+ });
983
+ if (!response.ok) {
984
+ const text = await response.text();
985
+ throw new Error(`HTTP ${response.status}: ${text}`);
986
+ }
987
+ if (!response.body) {
988
+ throw new Error('No response body for streaming request');
989
+ }
990
+ const decoder = new TextDecoder();
991
+ // Handle both browser ReadableStream and Node.js stream from node-fetch
992
+ // Check for Node.js stream first (node-fetch v2 uses Node.js streams)
993
+ if ('on' in response.body && typeof response.body.on === 'function') {
994
+ // Node.js Stream (for testing)
995
+ await new Promise((resolve, reject) => {
996
+ let buffer = '';
997
+ response.body.on('data', (chunk) => {
998
+ buffer += chunk.toString();
999
+ const lines = buffer.split('\n').filter(line => line.trim().length > 0);
1000
+ // Keep incomplete line in buffer
1001
+ if (!buffer.endsWith('\n')) {
1002
+ buffer = lines.pop() || '';
1003
+ }
1004
+ else {
1005
+ buffer = '';
891
1006
  }
892
- const chunk = decoder.decode(value);
893
- // Parse each line as a JSON message
894
- const lines = chunk.split('\n').filter(line => line.trim().length > 0);
895
1007
  for (const line of lines) {
896
1008
  try {
897
1009
  const data = JSON.parse(line);
898
1010
  const shouldContinue = onBatch(data);
899
1011
  if (shouldContinue === false) {
900
- reader.cancel();
1012
+ response.body.destroy();
1013
+ resolve();
901
1014
  return;
902
1015
  }
903
1016
  }
@@ -905,305 +1018,396 @@
905
1018
  console.warn('Failed to parse streaming chunk:', line, e);
906
1019
  }
907
1020
  }
908
- }
909
- });
910
- }
911
- /**
912
- * Collect all streaming responses into an array
913
- * @param request Request parameters
914
- */
915
- translateStreamCollect(request) {
916
- return __awaiter(this, void 0, void 0, function* () {
917
- const result = [];
918
- yield this.translateStream(request, (response) => {
919
- result.push(response);
920
- return true;
921
1021
  });
922
- return result;
1022
+ response.body.on('end', () => {
1023
+ // Process any remaining data
1024
+ if (buffer.trim().length > 0) {
1025
+ try {
1026
+ const data = JSON.parse(buffer.trim());
1027
+ onBatch(data);
1028
+ }
1029
+ catch (e) {
1030
+ console.warn('Failed to parse final chunk:', buffer, e);
1031
+ }
1032
+ }
1033
+ resolve();
1034
+ });
1035
+ response.body.on('error', (err) => {
1036
+ reject(err);
1037
+ });
923
1038
  });
924
1039
  }
925
- /**
926
- * LLMTranslate - One-shot large language model translation
927
- * Uses LRU cache to avoid repeated requests for the same text
928
- * With cross-tab enabled, automatically syncs cache across browser tabs
929
- * @param request Translation request
930
- * @param skipCache If true, bypass cache and always request from backend
931
- */
932
- llmTranslate(request_1) {
933
- return __awaiter(this, arguments, void 0, function* (request, skipCache = false) {
934
- const cacheKey = generateCacheKey(request);
935
- // Check cache first
936
- if (this.llmCacheEnabled && !skipCache) {
937
- const cached = this.llmCache.get(cacheKey);
938
- if (cached) {
939
- // Return cached response with cached flag set
940
- return Object.assign(Object.assign({}, cached), { cached: true });
941
- }
1040
+ else if ('getReader' in response.body && typeof response.body.getReader === 'function') {
1041
+ // Browser ReadableStream (whatwg streams)
1042
+ const reader = response.body.getReader();
1043
+ while (true) {
1044
+ const { done, value } = await reader.read();
1045
+ if (done) {
1046
+ break;
942
1047
  }
943
- // Request from backend
944
- const url = `${this.baseUrl}/TranslationService/LLMTranslate`;
945
- const response = yield this.fetchJson(url, request);
946
- // Cache the response
947
- if (this.llmCacheEnabled && response.translatedText) {
948
- const cachedResponse = Object.assign(Object.assign({}, response), { cached: true });
949
- this.llmCache.set(cacheKey, cachedResponse);
950
- // Broadcast to other tabs and save to localStorage
951
- this.broadcastCacheUpdate(cacheKey, cachedResponse);
1048
+ const chunk = decoder.decode(value);
1049
+ const lines = chunk.split('\n').filter(line => line.trim().length > 0);
1050
+ for (const line of lines) {
1051
+ try {
1052
+ const data = JSON.parse(line);
1053
+ const shouldContinue = onBatch(data);
1054
+ if (shouldContinue === false) {
1055
+ reader.cancel();
1056
+ return;
1057
+ }
1058
+ }
1059
+ catch (e) {
1060
+ console.warn('Failed to parse streaming chunk:', line, e);
1061
+ }
952
1062
  }
953
- return Object.assign(Object.assign({}, response), { cached: false });
954
- });
1063
+ }
955
1064
  }
956
- // ========== High-level translation API ==========
957
- /**
958
- * Translate text - this is the main method you need
959
- *
960
- * Workflow:
961
- * 1. Check pre-loaded translation pool (provided fingerprint → current fingerprint → common)
962
- * 2. If found, return immediately from cache
963
- * 3. If not found, request LLM translation from backend
964
- * 4. Auto-initialize on first call
965
- *
966
- * @param text Text to translate
967
- * @param toLang Target language
968
- * @param fromLang Source language (optional, auto-detected if not provided)
969
- * @param fingerprint Optional specific fingerprint for this translation (overrides client-level fingerprint)
970
- * @returns Translated text
971
- */
972
- translate(text, toLang, fromLang, fingerprint) {
973
- return __awaiter(this, void 0, void 0, function* () {
974
- const response = yield this.translateWithDetails(text, toLang, fromLang, fingerprint);
975
- return response.translatedText;
976
- });
1065
+ else {
1066
+ throw new Error('Unsupported response body stream type');
977
1067
  }
978
- /**
979
- * Translate text with full response details
980
- * @param text Text to translate
981
- * @param toLang Target language
982
- * @param fromLang Source language (optional)
983
- * @param fingerprint Optional specific fingerprint for this translation (overrides client-level fingerprint)
984
- * @returns Full translation response
985
- */
986
- translateWithDetails(text, toLang, fromLang, fingerprint) {
987
- return __awaiter(this, void 0, void 0, function* () {
988
- // Auto-initialize if not initialized yet
989
- if (!this.initialized && !this.initPromise) {
990
- this.initPromise = this.initialize();
991
- }
992
- // Wait for initialization to complete
993
- if (this.initPromise) {
994
- yield this.initPromise;
995
- this.initPromise = null;
996
- this.initialized = true;
997
- }
998
- // If specific fingerprint provided and not loaded yet, load it first
999
- if (fingerprint && !this.pool.isLoaded(fingerprint)) {
1000
- yield this.pool.loadFingerprintIfNotLoaded(fingerprint);
1001
- }
1002
- // Check pre-loaded translation pool first
1003
- const lookup = this.pool.lookup(text, fingerprint);
1004
- if (lookup.found) {
1005
- return {
1068
+ }
1069
+ /**
1070
+ * Collect all streaming responses into an array
1071
+ * @param request Request parameters
1072
+ */
1073
+ async translateStreamCollect(request) {
1074
+ const result = [];
1075
+ await this.translateStream(request, (response) => {
1076
+ result.push(response);
1077
+ return true;
1078
+ });
1079
+ return result;
1080
+ }
1081
+ /**
1082
+ * LLMTranslate - One-shot large language model translation
1083
+ * Uses LRU cache to avoid repeated requests for the same text
1084
+ * With cross-tab enabled, automatically syncs cache across browser tabs
1085
+ * @param request Translation request
1086
+ * @param skipCache If true, bypass cache and always request from backend
1087
+ */
1088
+ async llmTranslate(request, skipCache = false) {
1089
+ const cacheKey = generateCacheKey(request);
1090
+ // Check cache first
1091
+ if (this.llmCacheEnabled && !skipCache) {
1092
+ const cached = this.llmCache.get(cacheKey);
1093
+ if (cached) {
1094
+ // Return cached response with cached flag set
1095
+ return Object.assign(Object.assign({}, cached), { cached: true });
1096
+ }
1097
+ }
1098
+ // Request from backend using gRPC-web streaming LLMTranslateStream
1099
+ let finalResponse = null;
1100
+ await this.llmTranslateStream(request, (response) => {
1101
+ finalResponse = response;
1102
+ // Continue until finished
1103
+ return !response.finished;
1104
+ });
1105
+ if (!finalResponse) {
1106
+ throw new Error('No response received from streaming translation');
1107
+ }
1108
+ // Cache the response
1109
+ if (this.llmCacheEnabled && finalResponse.translatedText) {
1110
+ const cachedResponse = Object.assign(Object.assign({}, finalResponse), { cached: true });
1111
+ this.llmCache.set(cacheKey, cachedResponse);
1112
+ // Broadcast to other tabs and save to localStorage
1113
+ this.broadcastCacheUpdate(cacheKey, cachedResponse);
1114
+ }
1115
+ return Object.assign(Object.assign({}, finalResponse), { cached: false });
1116
+ }
1117
+ // ========== High-level translation API ==========
1118
+ /**
1119
+ * Translate text - this is the main method you need
1120
+ *
1121
+ * Workflow:
1122
+ * 1. Check pre-loaded translation pool (provided fingerprint → current fingerprint → common)
1123
+ * 2. If found, return immediately from cache
1124
+ * 3. If not found, request LLM translation from backend
1125
+ * 4. Auto-initialize on first call
1126
+ *
1127
+ * @param text Text to translate
1128
+ * @param toLang Target language
1129
+ * @param fromLang Source language (optional, auto-detected if not provided)
1130
+ * @param fingerprint Optional specific fingerprint for this translation (overrides client-level fingerprint)
1131
+ * @returns Translated text
1132
+ */
1133
+ async translate(text, toLang, fromLang, fingerprint) {
1134
+ const response = await this.translateWithDetails(text, toLang, fromLang, fingerprint);
1135
+ return response.translatedText;
1136
+ }
1137
+ /**
1138
+ * Translate text with full response details
1139
+ * @param text Text to translate
1140
+ * @param toLang Target language
1141
+ * @param fromLang Source language (optional)
1142
+ * @param fingerprint Optional specific fingerprint for this translation (overrides client-level fingerprint)
1143
+ * @returns Full translation response
1144
+ */
1145
+ async translateWithDetails(text, toLang, fromLang, fingerprint) {
1146
+ // Auto-initialize if not initialized yet
1147
+ if (!this.initialized && !this.initPromise) {
1148
+ this.initPromise = this.initialize();
1149
+ }
1150
+ // Check if pool is currently loading
1151
+ const isPoolLoading = this.pool.isLoading();
1152
+ // If pool is loading, use observer pattern to queue request and return original immediately
1153
+ if (isPoolLoading) {
1154
+ console.log(`[TranslationClient] Pool is loading, using fast fallback for: "${text}"`);
1155
+ return this.pool.queueTranslationRequest({ text, toLang, fromLang, fingerprint });
1156
+ }
1157
+ // Wait for initialization to complete if still in progress
1158
+ if (this.initPromise) {
1159
+ await this.initPromise;
1160
+ this.initPromise = null;
1161
+ this.initialized = true;
1162
+ }
1163
+ // If specific fingerprint provided and not loaded yet, load it first
1164
+ if (fingerprint && !this.pool.isLoaded(fingerprint)) {
1165
+ await this.pool.loadFingerprintIfNotLoaded(fingerprint);
1166
+ }
1167
+ // Check pre-loaded translation pool first
1168
+ const lookup = this.pool.lookup(text, fingerprint);
1169
+ if (lookup.found) {
1170
+ return {
1171
+ originalText: text,
1172
+ translatedText: lookup.translation,
1173
+ provider: 'preloaded',
1174
+ timestamp: Date.now(),
1175
+ finished: true,
1176
+ cached: true,
1177
+ fromLang,
1178
+ toLang
1179
+ };
1180
+ }
1181
+ // Not found in pool - request from backend via TranslateStream
1182
+ // This will automatically call LLM if translation doesn't exist in database
1183
+ return new Promise((resolve, reject) => {
1184
+ this.translateStream({
1185
+ senseId: this.config.senseId,
1186
+ fingerprint,
1187
+ text,
1188
+ from_lang: fromLang,
1189
+ to_lang: toLang
1190
+ }, (response) => {
1191
+ // Check if we got a translation
1192
+ if (response.translation && response.translation[text]) {
1193
+ resolve({
1006
1194
  originalText: text,
1007
- translatedText: lookup.translation,
1008
- provider: 'preloaded',
1009
- timestamp: Date.now(),
1010
- finished: true,
1011
- cached: true,
1195
+ translatedText: response.translation[text],
1196
+ provider: 'translate-stream',
1197
+ timestamp: response.timestamp,
1198
+ finished: response.finished,
1199
+ cached: false,
1012
1200
  fromLang,
1013
1201
  toLang
1014
- };
1202
+ });
1203
+ return false; // Stop streaming
1015
1204
  }
1016
- // Not found in pool, request LLM translation
1017
- return this.llmTranslate({
1018
- text,
1019
- fromLang,
1020
- toLang,
1021
- senseId: this.config.senseId,
1022
- fingerprint
1023
- });
1024
- });
1025
- }
1026
- /**
1027
- * Translate without using cache (always request from backend)
1028
- * @param text Text to translate
1029
- * @param toLang Target language
1030
- * @param fromLang Source language (optional)
1031
- * @param fingerprint Optional specific fingerprint for this translation
1032
- * @returns Translated text
1033
- */
1034
- translateNoCache(text, toLang, fromLang, fingerprint) {
1035
- return __awaiter(this, void 0, void 0, function* () {
1036
- const response = yield this.llmTranslate({
1037
- text,
1038
- fromLang,
1039
- toLang,
1040
- senseId: this.config.senseId,
1041
- fingerprint
1042
- }, true);
1043
- return response.translatedText;
1044
- });
1045
- }
1046
- /**
1047
- * Batch translate multiple texts
1048
- * @param texts Array of texts to translate
1049
- * @param toLang Target language
1050
- * @param fromLang Source language (optional)
1051
- * @param fingerprint Optional specific fingerprint for all translations in this batch
1052
- * @returns Array of translated texts in same order
1053
- */
1054
- translateBatch(texts, toLang, fromLang, fingerprint) {
1055
- return __awaiter(this, void 0, void 0, function* () {
1056
- const results = yield Promise.all(texts.map(text => this.translate(text, toLang, fromLang, fingerprint)));
1057
- return results;
1058
- });
1059
- }
1060
- /**
1061
- * Initialize and preload all translations
1062
- * Call this to warm up cache before translating
1063
- */
1064
- preload() {
1065
- return __awaiter(this, void 0, void 0, function* () {
1066
- if (!this.initialized && !this.initPromise) {
1067
- this.initPromise = this.initialize();
1068
- yield this.initPromise;
1069
- this.initPromise = null;
1070
- this.initialized = true;
1205
+ // Check for error
1206
+ if (response.translation && response.translation['error']) {
1207
+ reject(new Error(response.translation['error']));
1208
+ return false;
1071
1209
  }
1072
- });
1073
- }
1074
- /**
1075
- * Internal initialization - preloads translations via streaming
1076
- */
1077
- initialize() {
1078
- return __awaiter(this, void 0, void 0, function* () {
1079
- if (this.llmCacheEnabled) {
1080
- yield this.pool.initialize();
1210
+ // Continue if not finished
1211
+ return !response.finished;
1212
+ }).catch(reject);
1213
+ });
1214
+ }
1215
+ /**
1216
+ * Translate without using cache (always request from backend)
1217
+ * @param text Text to translate
1218
+ * @param toLang Target language
1219
+ * @param fromLang Source language (optional)
1220
+ * @param fingerprint Optional specific fingerprint for this translation
1221
+ * @returns Translated text
1222
+ */
1223
+ async translateNoCache(text, toLang, fromLang, fingerprint) {
1224
+ // Use TranslateStream which will automatically call LLM if needed
1225
+ return new Promise((resolve, reject) => {
1226
+ this.translateStream({
1227
+ senseId: this.config.senseId,
1228
+ fingerprint,
1229
+ text,
1230
+ from_lang: fromLang,
1231
+ to_lang: toLang
1232
+ }, (response) => {
1233
+ if (response.translation && response.translation[text]) {
1234
+ resolve(response.translation[text]);
1235
+ return false;
1081
1236
  }
1082
- });
1083
- }
1084
- /**
1085
- * Check if service is initialized
1086
- */
1087
- isInitialized() {
1088
- return this.initialized;
1089
- }
1090
- // ========== Fingerprint management ==========
1091
- /**
1092
- * Set or change the current fingerprint
1093
- * Automatically loads special translations for this fingerprint
1094
- * @param fingerprint The fingerprint to use
1095
- */
1096
- setFingerprint(fingerprint) {
1097
- return __awaiter(this, void 0, void 0, function* () {
1098
- if (this.currentFingerprint === fingerprint) {
1099
- return;
1237
+ if (response.translation && response.translation['error']) {
1238
+ reject(new Error(response.translation['error']));
1239
+ return false;
1100
1240
  }
1101
- this.currentFingerprint = fingerprint;
1102
- yield this.pool.switchFingerprint(fingerprint);
1103
- });
1241
+ return !response.finished;
1242
+ }).catch(reject);
1243
+ });
1244
+ }
1245
+ /**
1246
+ * Batch translate multiple texts
1247
+ * @param texts Array of texts to translate
1248
+ * @param toLang Target language
1249
+ * @param fromLang Source language (optional)
1250
+ * @param fingerprint Optional specific fingerprint for all translations in this batch
1251
+ * @returns Array of translated texts in same order
1252
+ */
1253
+ async translateBatch(texts, toLang, fromLang, fingerprint) {
1254
+ const results = await Promise.all(texts.map(text => this.translate(text, toLang, fromLang, fingerprint)));
1255
+ return results;
1256
+ }
1257
+ /**
1258
+ * Initialize and preload all translations
1259
+ * Call this to warm up cache before translating
1260
+ */
1261
+ async preload() {
1262
+ if (!this.initialized && !this.initPromise) {
1263
+ this.initPromise = this.initialize();
1264
+ await this.initPromise;
1265
+ this.initPromise = null;
1266
+ this.initialized = true;
1104
1267
  }
1105
- /**
1106
- * Clear the current fingerprint
1107
- * Falls back to common translations
1108
- */
1109
- clearFingerprint() {
1110
- this.currentFingerprint = null;
1111
- this.pool.clearCurrentFingerprint();
1112
- }
1113
- /**
1114
- * Get the current fingerprint
1115
- */
1116
- getFingerprint() {
1117
- return this.currentFingerprint;
1118
- }
1119
- // ========== Cache management ==========
1120
- /**
1121
- * Check if cache is enabled
1122
- * @returns true if cache is enabled
1123
- */
1124
- isCacheEnabled() {
1125
- return this.llmCacheEnabled;
1126
- }
1127
- /**
1128
- * Check if a translation exists in pre-loaded pool
1129
- * @param text Text to check
1130
- * @param fingerprint Optional specific fingerprint to check
1131
- * @returns true if translation exists in cache
1132
- */
1133
- hasTranslation(text, fingerprint) {
1134
- return this.pool.lookup(text, fingerprint).found;
1135
- }
1136
- /**
1137
- * Get translation from pre-loaded cache without requesting from backend
1138
- * @param text Text to look up
1139
- * @param fingerprint Optional specific fingerprint to look up
1140
- * @returns Translation if found, null otherwise
1141
- */
1142
- getCached(text, fingerprint) {
1143
- const result = this.pool.lookup(text, fingerprint);
1144
- return result.found ? result.translation : null;
1145
- }
1146
- /**
1147
- * Add a custom translation to the pre-loaded pool
1148
- * @param text Original text
1149
- * @param translation Translated text
1150
- * @param fingerprint Optional specific fingerprint to add to
1151
- */
1152
- addTranslation(text, translation, fingerprint) {
1153
- if (this.llmCacheEnabled) {
1154
- this.pool.addTranslation(text, translation, fingerprint);
1155
- }
1268
+ }
1269
+ /**
1270
+ * Internal initialization - preloads translations via streaming
1271
+ */
1272
+ async initialize() {
1273
+ if (this.llmCacheEnabled) {
1274
+ await this.pool.initialize();
1156
1275
  }
1157
- /**
1158
- * Clear all cached translations
1159
- */
1160
- clearAllCache() {
1161
- this.pool.clearAll();
1162
- this.clearCache(); // Clear LLM cache too
1163
- }
1164
- /**
1165
- * Destroy the instance and free resources
1166
- * Call this when the instance is no longer needed
1167
- */
1168
- destroy() {
1169
- this.pool.destroy();
1170
- if (this.broadcastChannel) {
1171
- this.broadcastChannel.close();
1172
- this.broadcastChannel = null;
1173
- }
1276
+ }
1277
+ /**
1278
+ * Check if service is initialized
1279
+ */
1280
+ isInitialized() {
1281
+ return this.initialized;
1282
+ }
1283
+ // ========== Fingerprint management ==========
1284
+ /**
1285
+ * Set or change the current fingerprint
1286
+ * Automatically loads special translations for this fingerprint
1287
+ * @param fingerprint The fingerprint to use
1288
+ */
1289
+ async setFingerprint(fingerprint) {
1290
+ if (this.currentFingerprint === fingerprint) {
1291
+ return;
1174
1292
  }
1175
- /**
1176
- * LLMTranslateStream - Streaming large language model translation
1177
- * Note: Streaming requests are not cached
1178
- * @param request Translation request
1179
- * @param onResponse Callback for each response chunk
1180
- */
1181
- llmTranslateStream(request, onResponse) {
1182
- return __awaiter(this, void 0, void 0, function* () {
1183
- const url = `${this.baseUrl}/TranslationService/LLMTranslateStream`;
1184
- const response = yield this.fetchWithTimeout(url, {
1185
- method: 'POST',
1186
- body: JSON.stringify(request),
1187
- headers: this.getHeaders()
1188
- });
1189
- if (!response.body) {
1190
- throw new Error('No response body for streaming request');
1191
- }
1192
- const reader = response.body.getReader();
1193
- const decoder = new TextDecoder();
1194
- while (true) {
1195
- const { done, value } = yield reader.read();
1196
- if (done) {
1197
- break;
1293
+ this.currentFingerprint = fingerprint;
1294
+ await this.pool.switchFingerprint(fingerprint);
1295
+ }
1296
+ /**
1297
+ * Clear the current fingerprint
1298
+ * Falls back to common translations
1299
+ */
1300
+ clearFingerprint() {
1301
+ this.currentFingerprint = null;
1302
+ this.pool.clearCurrentFingerprint();
1303
+ }
1304
+ /**
1305
+ * Get the current fingerprint
1306
+ */
1307
+ getFingerprint() {
1308
+ return this.currentFingerprint;
1309
+ }
1310
+ // ========== Cache management ==========
1311
+ /**
1312
+ * Check if cache is enabled
1313
+ * @returns true if cache is enabled
1314
+ */
1315
+ isCacheEnabled() {
1316
+ return this.llmCacheEnabled;
1317
+ }
1318
+ /**
1319
+ * Check if a translation exists in pre-loaded pool
1320
+ * @param text Text to check
1321
+ * @param fingerprint Optional specific fingerprint to check
1322
+ * @returns true if translation exists in cache
1323
+ */
1324
+ hasTranslation(text, fingerprint) {
1325
+ return this.pool.lookup(text, fingerprint).found;
1326
+ }
1327
+ /**
1328
+ * Get translation from pre-loaded cache without requesting from backend
1329
+ * @param text Text to look up
1330
+ * @param fingerprint Optional specific fingerprint to look up
1331
+ * @returns Translation if found, null otherwise
1332
+ */
1333
+ getCached(text, fingerprint) {
1334
+ const result = this.pool.lookup(text, fingerprint);
1335
+ return result.found ? result.translation : null;
1336
+ }
1337
+ /**
1338
+ * Add a custom translation to the pre-loaded pool
1339
+ * @param text Original text
1340
+ * @param translation Translated text
1341
+ * @param fingerprint Optional specific fingerprint to add to
1342
+ */
1343
+ addTranslation(text, translation, fingerprint) {
1344
+ if (this.llmCacheEnabled) {
1345
+ this.pool.addTranslation(text, translation, fingerprint);
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Clear all cached translations
1350
+ */
1351
+ clearAllCache() {
1352
+ this.pool.clearAll();
1353
+ this.pool.clearQueuedRequests(); // Clear waiting translation requests too
1354
+ this.clearCache(); // Clear LLM cache too
1355
+ }
1356
+ /**
1357
+ * Destroy the instance and free resources
1358
+ * Call this when the instance is no longer needed
1359
+ */
1360
+ destroy() {
1361
+ this.pool.destroy();
1362
+ if (this.broadcastChannel) {
1363
+ this.broadcastChannel.close();
1364
+ this.broadcastChannel = null;
1365
+ }
1366
+ }
1367
+ /**
1368
+ * LLMTranslateStream - Streaming large language model translation
1369
+ * Note: Streaming requests are not cached
1370
+ * @param request Translation request
1371
+ * @param onResponse Callback for each response chunk
1372
+ */
1373
+ async llmTranslateStream(request, onResponse) {
1374
+ const url = `${this.baseUrl}/api/v1/translate/TranslationService/LLMTranslateStream`;
1375
+ const response = await this.fetchWithTimeout(url, {
1376
+ method: 'POST',
1377
+ body: JSON.stringify(request),
1378
+ headers: this.getHeaders()
1379
+ });
1380
+ if (!response.ok) {
1381
+ const text = await response.text();
1382
+ throw new Error(`HTTP ${response.status}: ${text}`);
1383
+ }
1384
+ if (!response.body) {
1385
+ throw new Error('No response body for streaming request');
1386
+ }
1387
+ const decoder = new TextDecoder();
1388
+ // Handle both browser ReadableStream and Node.js stream from node-fetch
1389
+ // Check for Node.js stream first (node-fetch v2 uses Node.js streams)
1390
+ if ('on' in response.body && typeof response.body.on === 'function') {
1391
+ // Node.js Stream (for testing)
1392
+ await new Promise((resolve, reject) => {
1393
+ let buffer = '';
1394
+ response.body.on('data', (chunk) => {
1395
+ buffer += chunk.toString();
1396
+ const lines = buffer.split('\n').filter(line => line.trim().length > 0);
1397
+ // Keep incomplete line in buffer
1398
+ if (!buffer.endsWith('\n')) {
1399
+ buffer = lines.pop() || '';
1400
+ }
1401
+ else {
1402
+ buffer = '';
1198
1403
  }
1199
- const chunk = decoder.decode(value);
1200
- const lines = chunk.split('\n').filter(line => line.trim().length > 0);
1201
1404
  for (const line of lines) {
1202
1405
  try {
1203
1406
  const data = JSON.parse(line);
1204
1407
  const shouldContinue = onResponse(data);
1205
1408
  if (shouldContinue === false) {
1206
- reader.cancel();
1409
+ response.body.destroy();
1410
+ resolve();
1207
1411
  return;
1208
1412
  }
1209
1413
  }
@@ -1211,67 +1415,124 @@
1211
1415
  console.warn('Failed to parse streaming chunk:', line, e);
1212
1416
  }
1213
1417
  }
1214
- }
1418
+ });
1419
+ response.body.on('end', () => {
1420
+ // Process any remaining data
1421
+ if (buffer.trim().length > 0) {
1422
+ try {
1423
+ const data = JSON.parse(buffer.trim());
1424
+ onResponse(data);
1425
+ }
1426
+ catch (e) {
1427
+ console.warn('Failed to parse final chunk:', buffer, e);
1428
+ }
1429
+ }
1430
+ resolve();
1431
+ });
1432
+ response.body.on('error', (err) => {
1433
+ reject(err);
1434
+ });
1215
1435
  });
1216
1436
  }
1217
- getHeaders() {
1218
- const headers = {
1219
- 'Content-Type': 'application/grpc-web+json',
1220
- 'X-Grpc-Web': '1'
1221
- };
1222
- if (this.token) {
1223
- headers['Authorization'] = `Bearer ${this.token}`;
1224
- }
1225
- return headers;
1226
- }
1227
- fetchJson(url, body) {
1228
- return __awaiter(this, void 0, void 0, function* () {
1229
- const response = yield this.fetchWithTimeout(url, {
1230
- method: 'POST',
1231
- body: JSON.stringify(body),
1232
- headers: this.getHeaders()
1233
- });
1234
- if (!response.ok) {
1235
- throw new Error(`HTTP error! status: ${response.status}`);
1437
+ else if ('getReader' in response.body && typeof response.body.getReader === 'function') {
1438
+ // Browser ReadableStream (whatwg streams)
1439
+ const reader = response.body.getReader();
1440
+ while (true) {
1441
+ const { done, value } = await reader.read();
1442
+ if (done) {
1443
+ break;
1236
1444
  }
1237
- return yield response.json();
1238
- });
1445
+ const chunk = decoder.decode(value);
1446
+ const lines = chunk.split('\n').filter(line => line.trim().length > 0);
1447
+ for (const line of lines) {
1448
+ try {
1449
+ const data = JSON.parse(line);
1450
+ const shouldContinue = onResponse(data);
1451
+ if (shouldContinue === false) {
1452
+ reader.cancel();
1453
+ return;
1454
+ }
1455
+ }
1456
+ catch (e) {
1457
+ console.warn('Failed to parse streaming chunk:', line, e);
1458
+ }
1459
+ }
1460
+ }
1239
1461
  }
1240
- fetchWithTimeout(url, options) {
1241
- return __awaiter(this, void 0, void 0, function* () {
1242
- const controller = new AbortController();
1243
- const id = setTimeout(() => controller.abort(), this.timeout);
1244
- const response = yield fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal }));
1245
- clearTimeout(id);
1246
- return response;
1247
- });
1462
+ else {
1463
+ throw new Error('Unsupported response body type');
1248
1464
  }
1249
1465
  }
1466
+ getHeaders() {
1467
+ const headers = {
1468
+ 'Content-Type': 'application/grpc-web+json',
1469
+ 'X-Grpc-Web': '1'
1470
+ };
1471
+ if (this.token) {
1472
+ // Use api-key-token header for API key authentication
1473
+ // (not Bearer token which requires valid JWT)
1474
+ headers['api-key-token'] = this.token;
1475
+ }
1476
+ return headers;
1477
+ }
1478
+ async fetchJson(url, body) {
1479
+ const response = await this.fetchWithTimeout(url, {
1480
+ method: 'POST',
1481
+ body: JSON.stringify(body),
1482
+ headers: this.getHeaders()
1483
+ });
1484
+ if (!response.ok) {
1485
+ throw new Error(`HTTP error! status: ${response.status}`);
1486
+ }
1487
+ return await response.json();
1488
+ }
1489
+ async fetchWithTimeout(url, options) {
1490
+ const controller = new AbortController();
1491
+ const id = setTimeout(() => controller.abort(), this.timeout);
1492
+ const response = await fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal }));
1493
+ clearTimeout(id);
1494
+ return response;
1495
+ }
1250
1496
  /**
1251
- * Create a TranslationClient instance with simplified configuration
1252
- * @param token JWT authentication token
1253
- * @param senseId Translation sense ID
1254
- * @param options Additional options
1255
- * @returns TranslationClient instance
1497
+ * Get cache statistics for both pre-loaded translations and LLM translations
1498
+ * @returns Human-readable cache statistics string
1256
1499
  */
1257
- function createTranslation(token, senseId, options) {
1258
- return new TranslationClient(Object.assign({ token,
1259
- senseId }, options));
1260
- }
1261
- // Auto-export to global for browser usage
1262
- if (typeof window !== 'undefined') {
1263
- window.LakerTranslation = {
1264
- TranslationClient,
1265
- createTranslation,
1266
- default: createTranslation
1267
- };
1500
+ getStats() {
1501
+ let preloadedCount = 0;
1502
+ const fingerprintCount = [];
1503
+ // Use type assertion to access private pools property
1504
+ const pools = this.pool.pools;
1505
+ pools.forEach((cache, fp) => {
1506
+ const count = cache.size;
1507
+ preloadedCount += count;
1508
+ fingerprintCount.push(`${fp}: ${count}`);
1509
+ });
1510
+ const llmCount = this.llmCache.size;
1511
+ const llmEnabled = this.llmCacheEnabled;
1512
+ return [
1513
+ `Pre-loaded translations: ${preloadedCount} total`,
1514
+ ...(fingerprintCount.length > 0 ? [` Breakdown by fingerprint: ${fingerprintCount.join(', ')}`] : []),
1515
+ `LLM translation cache: ${llmEnabled ? `${llmCount} entries` : 'disabled'}`
1516
+ ].join('\n');
1268
1517
  }
1269
-
1270
- exports.TranslationClient = TranslationClient;
1271
- exports.createTranslation = createTranslation;
1272
- exports.default = createTranslation;
1273
- exports.extractTemplate = extractTemplate;
1274
-
1275
- Object.defineProperty(exports, '__esModule', { value: true });
1276
-
1277
- }));
1518
+ }
1519
+ /**
1520
+ * Create a TranslationClient instance with simplified configuration
1521
+ * @param token JWT authentication token
1522
+ * @param senseId Translation sense ID
1523
+ * @param options Additional options
1524
+ * @returns TranslationClient instance
1525
+ */
1526
+ export function createTranslation(token, senseId, options) {
1527
+ return new TranslationClient(Object.assign({ token,
1528
+ senseId }, options));
1529
+ }
1530
+ export default createTranslation;
1531
+ // Auto-export to global for browser usage
1532
+ if (typeof window !== 'undefined') {
1533
+ window.LakerTranslation = {
1534
+ TranslationClient,
1535
+ createTranslation,
1536
+ default: createTranslation
1537
+ };
1538
+ }