hybridtm 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/hybridtm.d.ts +1 -0
- package/dist/hybridtm.js +21 -1
- package/dist/hybridtm.js.map +1 -1
- package/dist/package.json +35 -0
- package/dist/tmxhandler.d.ts +1 -0
- package/dist/tmxhandler.js +10 -0
- package/dist/tmxhandler.js.map +1 -1
- package/dist/ts/batchImporter.d.ts +22 -0
- package/dist/ts/batchImporter.js +203 -0
- package/dist/ts/batchImporter.js.map +1 -0
- package/dist/ts/hybridtm.d.ts +67 -0
- package/dist/ts/hybridtm.js +932 -0
- package/dist/ts/hybridtm.js.map +1 -0
- package/dist/ts/hybridtmFactory.d.ts +43 -0
- package/dist/ts/hybridtmFactory.js +165 -0
- package/dist/ts/hybridtmFactory.js.map +1 -0
- package/dist/ts/importOptions.d.ts +21 -0
- package/dist/ts/importOptions.js +24 -0
- package/dist/ts/importOptions.js.map +1 -0
- package/dist/ts/index.d.ts +25 -0
- package/dist/ts/index.js +22 -0
- package/dist/ts/index.js.map +1 -0
- package/dist/ts/langEntry.d.ts +64 -0
- package/dist/ts/langEntry.js +13 -0
- package/dist/ts/langEntry.js.map +1 -0
- package/dist/ts/match.d.ts +22 -0
- package/dist/ts/match.js +38 -0
- package/dist/ts/match.js.map +1 -0
- package/dist/ts/matchQuality.d.ts +16 -0
- package/dist/ts/matchQuality.js +83 -0
- package/dist/ts/matchQuality.js.map +1 -0
- package/dist/ts/pendingEntry.d.ts +24 -0
- package/dist/ts/pendingEntry.js +13 -0
- package/dist/ts/pendingEntry.js.map +1 -0
- package/dist/ts/searchFilters.d.ts +24 -0
- package/dist/ts/searchFilters.js +13 -0
- package/dist/ts/searchFilters.js.map +1 -0
- package/dist/ts/tmxHandler.d.ts +61 -0
- package/dist/ts/tmxHandler.js +347 -0
- package/dist/ts/tmxHandler.js.map +1 -0
- package/dist/ts/tmxReader.d.ts +25 -0
- package/dist/ts/tmxReader.js +51 -0
- package/dist/ts/tmxReader.js.map +1 -0
- package/dist/ts/tmxtest.d.ts +16 -0
- package/dist/ts/tmxtest.js +316 -0
- package/dist/ts/tmxtest.js.map +1 -0
- package/dist/ts/utils.d.ts +17 -0
- package/dist/ts/utils.js +49 -0
- package/dist/ts/utils.js.map +1 -0
- package/dist/ts/xliffHandler.d.ts +67 -0
- package/dist/ts/xliffHandler.js +522 -0
- package/dist/ts/xliffHandler.js.map +1 -0
- package/dist/ts/xliffReader.d.ts +25 -0
- package/dist/ts/xliffReader.js +50 -0
- package/dist/ts/xliffReader.js.map +1 -0
- package/dist/ts/xlifftest.d.ts +54 -0
- package/dist/ts/xlifftest.js +245 -0
- package/dist/ts/xlifftest.js.map +1 -0
- package/dist/xliffhandler.d.ts +1 -0
- package/dist/xliffhandler.js +10 -0
- package/dist/xliffhandler.js.map +1 -1
- package/package.json +13 -8
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Copyright (c) 2025-2026 Maxprograms.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials
|
|
5
|
+
* are made available under the terms of the Eclipse License 1.0
|
|
6
|
+
* which accompanies this distribution, and is available at
|
|
7
|
+
* https://www.eclipse.org/org/documents/epl-v10.html
|
|
8
|
+
*
|
|
9
|
+
* Contributors:
|
|
10
|
+
* Maxprograms - initial API and implementation
|
|
11
|
+
*******************************************************************************/
|
|
12
|
+
import { connect } from '@lancedb/lancedb';
|
|
13
|
+
import { pipeline } from '@xenova/transformers';
|
|
14
|
+
import { Field, FixedSizeList, Float32, Int32, Schema, Utf8 } from 'apache-arrow';
|
|
15
|
+
import { unlinkSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { TMReader } from 'sdltm';
|
|
19
|
+
import { BatchImporter } from './batchImporter.js';
|
|
20
|
+
import { resolveImportOptions } from './importOptions.js';
|
|
21
|
+
import { Match } from './match.js';
|
|
22
|
+
import { MatchQuality } from './matchQuality.js';
|
|
23
|
+
import { TMXReader } from './tmxReader.js';
|
|
24
|
+
import { Utils } from './utils.js';
|
|
25
|
+
import { XLIFFReader } from './xliffReader.js';
|
|
26
|
+
export class HybridTM {
|
|
27
|
+
// OPTIMIZED MODELS
|
|
28
|
+
static SPEED_MODEL = 'Xenova/bge-small-en-v1.5'; // 384-dim, optimized for real-time
|
|
29
|
+
static QUALITY_MODEL = 'Xenova/LaBSE'; // 768-dim, optimized for accuracy
|
|
30
|
+
static RESOURCE_MODEL = 'Xenova/multilingual-e5-small'; // 384-dim, optimized for modest hardware
|
|
31
|
+
name;
|
|
32
|
+
db = null;
|
|
33
|
+
table = null;
|
|
34
|
+
dbPath = '';
|
|
35
|
+
embedder = null;
|
|
36
|
+
modelName = '';
|
|
37
|
+
initialized = false;
|
|
38
|
+
initializationPromise = null;
|
|
39
|
+
constructor(name, filePath, modelName = HybridTM.QUALITY_MODEL) {
|
|
40
|
+
this.name = name;
|
|
41
|
+
this.dbPath = filePath;
|
|
42
|
+
this.modelName = modelName;
|
|
43
|
+
}
|
|
44
|
+
// ============================
|
|
45
|
+
// INITIALIZATION METHODS
|
|
46
|
+
// ============================
|
|
47
|
+
async detectModelDimensions() {
|
|
48
|
+
try {
|
|
49
|
+
// Ensure embedder is initialized
|
|
50
|
+
if (!this.embedder) {
|
|
51
|
+
await this.initializeEmbedder();
|
|
52
|
+
}
|
|
53
|
+
if (!this.embedder) {
|
|
54
|
+
throw new Error('Failed to initialize embedder for dimension detection');
|
|
55
|
+
}
|
|
56
|
+
// Generate a small test embedding
|
|
57
|
+
const testResult = await this.embedder('test', {
|
|
58
|
+
pooling: 'mean',
|
|
59
|
+
normalize: true
|
|
60
|
+
});
|
|
61
|
+
return Array.from(testResult.data).length;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error('Error detecting model dimensions:', err);
|
|
65
|
+
throw new Error('Unable to detect model dimensions for ' + this.modelName + ': ' + (err instanceof Error ? err.message : String(err)));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async initialize() {
|
|
69
|
+
try {
|
|
70
|
+
// Initialize database and embedder in parallel
|
|
71
|
+
await Promise.all([
|
|
72
|
+
this.initializeDatabase(),
|
|
73
|
+
this.initializeEmbedder()
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error('Failed to initialize HybridTM:', err);
|
|
78
|
+
throw new Error('HybridTM initialization failed: ' + (err instanceof Error ? err.message : String(err)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async initializeDatabase() {
|
|
82
|
+
try {
|
|
83
|
+
// Connect to LanceDB
|
|
84
|
+
this.db = await connect(this.dbPath);
|
|
85
|
+
// Check if table exists, if not create it
|
|
86
|
+
const tableNames = await this.db.tableNames();
|
|
87
|
+
if (!tableNames.includes('langEntry')) {
|
|
88
|
+
// Detect model dimensions first
|
|
89
|
+
const dimensions = await this.detectModelDimensions();
|
|
90
|
+
// Create Arrow schema for the langEntry table
|
|
91
|
+
const schema = new Schema([
|
|
92
|
+
Field.new('id', new Utf8(), false),
|
|
93
|
+
Field.new('language', new Utf8(), false),
|
|
94
|
+
Field.new('pureText', new Utf8(), false),
|
|
95
|
+
Field.new('element', new Utf8(), false),
|
|
96
|
+
Field.new('fileId', new Utf8(), false),
|
|
97
|
+
Field.new('original', new Utf8(), false),
|
|
98
|
+
Field.new('unitId', new Utf8(), false),
|
|
99
|
+
Field.new('segmentIndex', new Int32(), false),
|
|
100
|
+
Field.new('segmentCount', new Int32(), false),
|
|
101
|
+
Field.new('metadataState', new Utf8(), true),
|
|
102
|
+
Field.new('metadataSubState', new Utf8(), true),
|
|
103
|
+
Field.new('metadataQuality', new Int32(), true),
|
|
104
|
+
Field.new('metadataCreationDate', new Utf8(), true),
|
|
105
|
+
Field.new('metadataCreationId', new Utf8(), true),
|
|
106
|
+
Field.new('metadataChangeDate', new Utf8(), true),
|
|
107
|
+
Field.new('metadataChangeId', new Utf8(), true),
|
|
108
|
+
Field.new('metadataCreationTool', new Utf8(), true),
|
|
109
|
+
Field.new('metadataCreationToolVersion', new Utf8(), true),
|
|
110
|
+
Field.new('metadataContext', new Utf8(), true),
|
|
111
|
+
Field.new('metadataNotes', new Utf8(), true),
|
|
112
|
+
Field.new('metadataUsageCount', new Int32(), true),
|
|
113
|
+
Field.new('metadataLastUsageDate', new Utf8(), true),
|
|
114
|
+
Field.new('metadataProperties', new Utf8(), true),
|
|
115
|
+
Field.new('metadataSegment', new Utf8(), true),
|
|
116
|
+
Field.new('vector', new FixedSizeList(dimensions, Field.new('item', new Float32(), false)), false),
|
|
117
|
+
]);
|
|
118
|
+
// Create table with the schema
|
|
119
|
+
this.table = await this.db.createEmptyTable('langEntry', schema);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.table = await this.db.openTable('langEntry');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.error('Error initializing LanceDB:', err);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async initializeEmbedder() {
|
|
131
|
+
try {
|
|
132
|
+
this.embedder = await pipeline('feature-extraction', this.modelName);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.error('Error initializing embedder:', err);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async generateEmbedding(text) {
|
|
140
|
+
try {
|
|
141
|
+
// Ensure embedder is initialized
|
|
142
|
+
if (!this.embedder) {
|
|
143
|
+
await this.initializeEmbedder();
|
|
144
|
+
}
|
|
145
|
+
if (!this.embedder) {
|
|
146
|
+
throw new Error('Failed to initialize embedder');
|
|
147
|
+
}
|
|
148
|
+
// Generate embeddings using the transformer model
|
|
149
|
+
const result = await this.embedder(text, {
|
|
150
|
+
pooling: 'mean',
|
|
151
|
+
normalize: true
|
|
152
|
+
});
|
|
153
|
+
// Convert tensor to array
|
|
154
|
+
const embedding = Array.from(result.data);
|
|
155
|
+
return embedding;
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.error('Error generating embedding:', err);
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async ensureInitialized() {
|
|
163
|
+
if (this.initialized) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this.initializationPromise) {
|
|
167
|
+
await this.initializationPromise;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.initializationPromise = this.initialize();
|
|
171
|
+
await this.initializationPromise;
|
|
172
|
+
this.initialized = true;
|
|
173
|
+
}
|
|
174
|
+
async ensureTable() {
|
|
175
|
+
await this.ensureInitialized();
|
|
176
|
+
if (!this.table) {
|
|
177
|
+
throw new Error('Database table is not available');
|
|
178
|
+
}
|
|
179
|
+
return this.table;
|
|
180
|
+
}
|
|
181
|
+
hydrateEntries(rows) {
|
|
182
|
+
if (!Array.isArray(rows)) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
return rows.map((row) => this.hydrateEntry((row ?? {})));
|
|
186
|
+
}
|
|
187
|
+
hydrateEntry(row) {
|
|
188
|
+
const metadata = this.unflattenMetadata(row);
|
|
189
|
+
const vectorData = row.vector;
|
|
190
|
+
const vector = Array.isArray(vectorData)
|
|
191
|
+
? vectorData
|
|
192
|
+
: ArrayBuffer.isView(vectorData)
|
|
193
|
+
? Array.from(vectorData)
|
|
194
|
+
: typeof vectorData === 'object' && vectorData !== null && typeof vectorData.toArray === 'function'
|
|
195
|
+
? Array.from(vectorData.toArray())
|
|
196
|
+
: [];
|
|
197
|
+
const hydrated = { ...row };
|
|
198
|
+
hydrated.id = typeof row.id === 'string' ? row.id : '';
|
|
199
|
+
hydrated.language = typeof row.language === 'string' ? row.language : '';
|
|
200
|
+
hydrated.pureText = typeof row.pureText === 'string' ? row.pureText : '';
|
|
201
|
+
hydrated.element = typeof row.element === 'string' ? row.element : '';
|
|
202
|
+
hydrated.fileId = typeof row.fileId === 'string' ? row.fileId : '';
|
|
203
|
+
hydrated.original = typeof row.original === 'string' ? row.original : '';
|
|
204
|
+
hydrated.unitId = typeof row.unitId === 'string' ? row.unitId : '';
|
|
205
|
+
hydrated.vector = vector;
|
|
206
|
+
hydrated.segmentIndex = this.parseNumber(row.segmentIndex, 0);
|
|
207
|
+
hydrated.segmentCount = this.parseNumber(row.segmentCount, 1);
|
|
208
|
+
hydrated.metadata = metadata;
|
|
209
|
+
return hydrated;
|
|
210
|
+
}
|
|
211
|
+
flattenEntry(entry) {
|
|
212
|
+
const metadataFields = this.flattenMetadata(entry.metadata);
|
|
213
|
+
return {
|
|
214
|
+
id: entry.id,
|
|
215
|
+
language: entry.language,
|
|
216
|
+
pureText: entry.pureText,
|
|
217
|
+
element: entry.element,
|
|
218
|
+
fileId: entry.fileId,
|
|
219
|
+
original: entry.original,
|
|
220
|
+
unitId: entry.unitId,
|
|
221
|
+
segmentIndex: entry.segmentIndex,
|
|
222
|
+
segmentCount: entry.segmentCount,
|
|
223
|
+
vector: entry.vector,
|
|
224
|
+
...metadataFields,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
mapToSearchResult(entry) {
|
|
228
|
+
if (!entry.metadata) {
|
|
229
|
+
entry.metadata = {};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
id: entry.id,
|
|
233
|
+
language: entry.language,
|
|
234
|
+
pureText: entry.pureText,
|
|
235
|
+
element: entry.element,
|
|
236
|
+
fileId: entry.fileId,
|
|
237
|
+
original: entry.original,
|
|
238
|
+
unitId: entry.unitId,
|
|
239
|
+
segmentIndex: entry.segmentIndex,
|
|
240
|
+
segmentCount: entry.segmentCount,
|
|
241
|
+
metadata: entry.metadata,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
buildEntryId(fileId, unitId, segmentIndex, lang) {
|
|
245
|
+
return fileId + ':' + unitId + ':' + segmentIndex + ':' + lang;
|
|
246
|
+
}
|
|
247
|
+
flattenMetadata(metadata) {
|
|
248
|
+
const safeMetadata = metadata ? metadata : {};
|
|
249
|
+
return {
|
|
250
|
+
metadataState: safeMetadata.state ?? null,
|
|
251
|
+
metadataSubState: safeMetadata.subState ?? null,
|
|
252
|
+
metadataQuality: typeof safeMetadata.quality === 'number' ? safeMetadata.quality : null,
|
|
253
|
+
metadataCreationDate: safeMetadata.creationDate ?? null,
|
|
254
|
+
metadataCreationId: safeMetadata.creationId ?? null,
|
|
255
|
+
metadataChangeDate: safeMetadata.changeDate ?? null,
|
|
256
|
+
metadataChangeId: safeMetadata.changeId ?? null,
|
|
257
|
+
metadataCreationTool: safeMetadata.creationTool ?? null,
|
|
258
|
+
metadataCreationToolVersion: safeMetadata.creationToolVersion ?? null,
|
|
259
|
+
metadataContext: safeMetadata.context ?? null,
|
|
260
|
+
metadataNotes: safeMetadata.notes ? JSON.stringify(safeMetadata.notes) : null,
|
|
261
|
+
metadataUsageCount: typeof safeMetadata.usageCount === 'number' ? safeMetadata.usageCount : null,
|
|
262
|
+
metadataLastUsageDate: safeMetadata.lastUsageDate ?? null,
|
|
263
|
+
metadataProperties: safeMetadata.properties ? JSON.stringify(safeMetadata.properties) : null,
|
|
264
|
+
metadataSegment: safeMetadata.segment ? JSON.stringify(safeMetadata.segment) : null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
unflattenMetadata(row) {
|
|
268
|
+
const metadata = {};
|
|
269
|
+
const str = (value) => {
|
|
270
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
};
|
|
275
|
+
const num = (value) => {
|
|
276
|
+
if (typeof value === 'number' && !Number.isNaN(value)) {
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
280
|
+
const parsed = Number(value);
|
|
281
|
+
if (!Number.isNaN(parsed)) {
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
};
|
|
287
|
+
const stateValue = str(row.metadataState);
|
|
288
|
+
const validStatuses = ['initial', 'translated', 'reviewed', 'final'];
|
|
289
|
+
if (stateValue && validStatuses.includes(stateValue)) {
|
|
290
|
+
metadata.state = stateValue;
|
|
291
|
+
}
|
|
292
|
+
const subStateValue = str(row.metadataSubState);
|
|
293
|
+
if (subStateValue) {
|
|
294
|
+
metadata.subState = subStateValue;
|
|
295
|
+
}
|
|
296
|
+
const qualityValue = num(row.metadataQuality);
|
|
297
|
+
if (qualityValue !== undefined) {
|
|
298
|
+
metadata.quality = qualityValue;
|
|
299
|
+
}
|
|
300
|
+
const creationDateValue = str(row.metadataCreationDate);
|
|
301
|
+
if (creationDateValue) {
|
|
302
|
+
metadata.creationDate = creationDateValue;
|
|
303
|
+
}
|
|
304
|
+
const creationIdValue = str(row.metadataCreationId);
|
|
305
|
+
if (creationIdValue) {
|
|
306
|
+
metadata.creationId = creationIdValue;
|
|
307
|
+
}
|
|
308
|
+
const changeDateValue = str(row.metadataChangeDate);
|
|
309
|
+
if (changeDateValue) {
|
|
310
|
+
metadata.changeDate = changeDateValue;
|
|
311
|
+
}
|
|
312
|
+
const changeIdValue = str(row.metadataChangeId);
|
|
313
|
+
if (changeIdValue) {
|
|
314
|
+
metadata.changeId = changeIdValue;
|
|
315
|
+
}
|
|
316
|
+
const creationToolValue = str(row.metadataCreationTool);
|
|
317
|
+
if (creationToolValue) {
|
|
318
|
+
metadata.creationTool = creationToolValue;
|
|
319
|
+
}
|
|
320
|
+
const creationToolVersionValue = str(row.metadataCreationToolVersion);
|
|
321
|
+
if (creationToolVersionValue) {
|
|
322
|
+
metadata.creationToolVersion = creationToolVersionValue;
|
|
323
|
+
}
|
|
324
|
+
const contextValue = str(row.metadataContext);
|
|
325
|
+
if (contextValue) {
|
|
326
|
+
metadata.context = contextValue;
|
|
327
|
+
}
|
|
328
|
+
const notesValue = this.parseNotes(row.metadataNotes);
|
|
329
|
+
if (notesValue && notesValue.length > 0) {
|
|
330
|
+
metadata.notes = notesValue;
|
|
331
|
+
}
|
|
332
|
+
const usageCountValue = num(row.metadataUsageCount);
|
|
333
|
+
if (usageCountValue !== undefined) {
|
|
334
|
+
metadata.usageCount = usageCountValue;
|
|
335
|
+
}
|
|
336
|
+
const lastUsageDateValue = str(row.metadataLastUsageDate);
|
|
337
|
+
if (lastUsageDateValue) {
|
|
338
|
+
metadata.lastUsageDate = lastUsageDateValue;
|
|
339
|
+
}
|
|
340
|
+
const properties = this.parseProperties(row.metadataProperties);
|
|
341
|
+
if (properties && Object.keys(properties).length > 0) {
|
|
342
|
+
metadata.properties = properties;
|
|
343
|
+
}
|
|
344
|
+
const segment = this.parseSegment(row.metadataSegment);
|
|
345
|
+
if (segment) {
|
|
346
|
+
metadata.segment = segment;
|
|
347
|
+
}
|
|
348
|
+
return metadata;
|
|
349
|
+
}
|
|
350
|
+
parseNotes(value) {
|
|
351
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(value);
|
|
356
|
+
if (Array.isArray(parsed)) {
|
|
357
|
+
return parsed.filter((item) => typeof item === 'string');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
parseSegment(value) {
|
|
366
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const parsed = JSON.parse(value);
|
|
371
|
+
if (parsed && typeof parsed === 'object') {
|
|
372
|
+
return parsed;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
console.warn('Failed to parse metadata.segment:', err);
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
parseProperties(value) {
|
|
381
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(value);
|
|
386
|
+
if (parsed && typeof parsed === 'object') {
|
|
387
|
+
const result = {};
|
|
388
|
+
Object.entries(parsed).forEach(([key, val]) => {
|
|
389
|
+
if (typeof val === 'string') {
|
|
390
|
+
result[key] = val;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
parseNumber(value, fallback) {
|
|
402
|
+
const parsed = typeof value === 'number' && !Number.isNaN(value)
|
|
403
|
+
? value
|
|
404
|
+
: typeof value === 'string' && value.length > 0 && !Number.isNaN(Number(value))
|
|
405
|
+
? Number(value)
|
|
406
|
+
: undefined;
|
|
407
|
+
return typeof parsed === 'number' ? parsed : fallback;
|
|
408
|
+
}
|
|
409
|
+
async close() {
|
|
410
|
+
try {
|
|
411
|
+
if (this.db) {
|
|
412
|
+
// LanceDB connections are automatically managed
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
console.error('Error closing database:', err);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// ============================
|
|
420
|
+
// SEARCH METHODS
|
|
421
|
+
// ============================
|
|
422
|
+
async concordanceSearch(textFragment, language, limit = 100, filters) {
|
|
423
|
+
// Enhanced concordance search: finds text fragments and returns all language variants for matching units
|
|
424
|
+
try {
|
|
425
|
+
const table = await this.ensureTable();
|
|
426
|
+
const escapeLiteral = (value) => value.replaceAll("'", "''");
|
|
427
|
+
const escapedFragment = escapeLiteral(textFragment);
|
|
428
|
+
const whereFragment = 'language = ' + '\'' + language + '\'' + ' AND contains(pureText, ' + '\'' + escapedFragment + '\')';
|
|
429
|
+
const fragmentEntries = this.hydrateEntries(await table
|
|
430
|
+
.query()
|
|
431
|
+
.where(whereFragment)
|
|
432
|
+
.limit(limit)
|
|
433
|
+
.toArray());
|
|
434
|
+
const filteredEntries = filters
|
|
435
|
+
? fragmentEntries.filter((entry) => this.metadataMatches(entry.metadata, filters))
|
|
436
|
+
: fragmentEntries;
|
|
437
|
+
if (filteredEntries.length === 0) {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
const segmentDescriptors = new Map();
|
|
441
|
+
filteredEntries.forEach((entry) => {
|
|
442
|
+
const segmentIndex = typeof entry.segmentIndex === 'number' ? entry.segmentIndex : 0;
|
|
443
|
+
const descriptorKey = entry.fileId + '|' + entry.unitId + '|' + segmentIndex;
|
|
444
|
+
if (!segmentDescriptors.has(descriptorKey)) {
|
|
445
|
+
segmentDescriptors.set(descriptorKey, {
|
|
446
|
+
fileId: entry.fileId,
|
|
447
|
+
unitId: entry.unitId,
|
|
448
|
+
segmentIndex
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
const result = [];
|
|
453
|
+
for (const descriptor of segmentDescriptors.values()) {
|
|
454
|
+
const segmentPrefix = descriptor.fileId + ':' + descriptor.unitId + ':' + descriptor.segmentIndex + ':';
|
|
455
|
+
const sanitizedPrefix = escapeLiteral(segmentPrefix);
|
|
456
|
+
const segmentVariants = this.hydrateEntries(await table
|
|
457
|
+
.query()
|
|
458
|
+
.where('starts_with(id, ' + '\'' + sanitizedPrefix + '\')')
|
|
459
|
+
.toArray());
|
|
460
|
+
if (segmentVariants.length === 0) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const languageMap = new Map();
|
|
464
|
+
for (const variant of segmentVariants) {
|
|
465
|
+
try {
|
|
466
|
+
const xmlElement = Utils.buildXMLElement(variant.element);
|
|
467
|
+
languageMap.set(variant.language, xmlElement);
|
|
468
|
+
}
|
|
469
|
+
catch (parseErr) {
|
|
470
|
+
const errorMessage = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
471
|
+
throw new Error('Failed to create XML element for ' + variant.id + ': ' + errorMessage);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (languageMap.size > 0) {
|
|
475
|
+
result.push(languageMap);
|
|
476
|
+
if (result.length >= limit) {
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
console.error('Error performing concordance search:', err);
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async semanticSearch(queryText, language, limit = 10, filters) {
|
|
489
|
+
try {
|
|
490
|
+
const table = await this.ensureTable();
|
|
491
|
+
const queryEmbedding = await this.generateEmbedding(queryText);
|
|
492
|
+
const results = this.hydrateEntries(await table
|
|
493
|
+
.vectorSearch(queryEmbedding)
|
|
494
|
+
.where('language = ' + '\'' + language + '\'')
|
|
495
|
+
.limit(limit)
|
|
496
|
+
.toArray());
|
|
497
|
+
const filtered = filters
|
|
498
|
+
? results.filter((entry) => this.metadataMatches(entry.metadata, filters))
|
|
499
|
+
: results;
|
|
500
|
+
return filtered.map((entry) => this.mapToSearchResult(entry));
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
console.error('Error performing semantic search:', err);
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async semanticTranslationSearch(searchStr, srcLang, tgtLang, similarity, limit = 100, filters) {
|
|
508
|
+
try {
|
|
509
|
+
const table = await this.ensureTable();
|
|
510
|
+
// Generate embedding for the search string
|
|
511
|
+
const queryEmbedding = await this.generateEmbedding(searchStr);
|
|
512
|
+
// Get all entries for the source language
|
|
513
|
+
const sourceEntries = this.hydrateEntries(await table
|
|
514
|
+
.vectorSearch(queryEmbedding)
|
|
515
|
+
.where('language = ' + '\'' + srcLang + '\'')
|
|
516
|
+
.toArray());
|
|
517
|
+
const rankedMatches = [];
|
|
518
|
+
const sourceCriteria = filters?.source;
|
|
519
|
+
const targetCriteria = filters?.target ?? filters?.source;
|
|
520
|
+
for (const sourceEntry of sourceEntries) {
|
|
521
|
+
if (!sourceEntry.pureText || !sourceEntry.vector) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (sourceCriteria && !this.metadataMatches(sourceEntry.metadata, sourceCriteria)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
// Linear mapping (more intuitive)
|
|
528
|
+
const rawDistance = sourceEntry._distance;
|
|
529
|
+
const l2Distance = typeof rawDistance === 'number'
|
|
530
|
+
? rawDistance
|
|
531
|
+
: typeof rawDistance === 'string' && rawDistance.length > 0 && !Number.isNaN(Number(rawDistance))
|
|
532
|
+
? Number(rawDistance)
|
|
533
|
+
: 0;
|
|
534
|
+
const semanticScore = Math.max(0, Math.round((2 - l2Distance) / 2 * 100));
|
|
535
|
+
const fuzzyScore = MatchQuality.similarity(searchStr, sourceEntry.pureText);
|
|
536
|
+
const hybridScore = Math.round((semanticScore + fuzzyScore) / 2);
|
|
537
|
+
// Only include matches that meet the minimum similarity threshold
|
|
538
|
+
if (hybridScore >= similarity) {
|
|
539
|
+
const targetEntry = await this.findTargetEntry(table, sourceEntry, tgtLang, targetCriteria);
|
|
540
|
+
if (!targetEntry) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (targetCriteria && !this.metadataMatches(targetEntry.metadata, targetCriteria)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const sourceElement = Utils.buildXMLElement(sourceEntry.element);
|
|
548
|
+
const targetElement = Utils.buildXMLElement(targetEntry.element);
|
|
549
|
+
const match = new Match(sourceElement, targetElement, this.name, semanticScore, fuzzyScore);
|
|
550
|
+
const rankingScore = this.computeRankingScore(match.hybridScore(), sourceEntry, targetEntry);
|
|
551
|
+
rankedMatches.push({ match, score: rankingScore });
|
|
552
|
+
}
|
|
553
|
+
catch (parseErr) {
|
|
554
|
+
console.error('Error creating Match for semantic translation search: ' + (parseErr instanceof Error ? parseErr.message : String(parseErr)));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
rankedMatches.sort((a, b) => b.score - a.score);
|
|
559
|
+
return rankedMatches.slice(0, limit).map((item) => item.match);
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
console.error('Error performing semantic search with quality:', err);
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async findTargetEntry(table, sourceEntry, tgtLang, filters) {
|
|
567
|
+
if (!sourceEntry.fileId || !sourceEntry.unitId) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
const exactId = this.buildEntryId(sourceEntry.fileId, sourceEntry.unitId, sourceEntry.segmentIndex, tgtLang);
|
|
571
|
+
const exactMatches = this.hydrateEntries(await table
|
|
572
|
+
.query()
|
|
573
|
+
.where('id = ' + '\'' + Utils.replaceQuotes(exactId) + '\'')
|
|
574
|
+
.toArray());
|
|
575
|
+
const exactMatch = this.selectPreferredCandidate(exactMatches, sourceEntry.segmentIndex, filters);
|
|
576
|
+
if (exactMatch) {
|
|
577
|
+
return exactMatch;
|
|
578
|
+
}
|
|
579
|
+
const unitPrefix = sourceEntry.fileId + ':' + sourceEntry.unitId + ':';
|
|
580
|
+
const sanitizedPrefix = Utils.replaceQuotes(unitPrefix);
|
|
581
|
+
const sanitizedLang = Utils.replaceQuotes(tgtLang);
|
|
582
|
+
const unitMatches = this.hydrateEntries(await table
|
|
583
|
+
.query()
|
|
584
|
+
.where('starts_with(id, ' + '\'' + sanitizedPrefix + '\') AND language = ' + '\'' + sanitizedLang + '\'')
|
|
585
|
+
.limit(50)
|
|
586
|
+
.toArray());
|
|
587
|
+
if (unitMatches.length === 0) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
const preferred = this.selectPreferredCandidate(unitMatches, sourceEntry.segmentIndex, filters);
|
|
591
|
+
return preferred ?? null;
|
|
592
|
+
}
|
|
593
|
+
computeRankingScore(baseScore, sourceEntry, targetEntry) {
|
|
594
|
+
let score = baseScore;
|
|
595
|
+
if (sourceEntry.segmentIndex > 0 && targetEntry.segmentIndex > 0) {
|
|
596
|
+
score += sourceEntry.segmentIndex === targetEntry.segmentIndex ? 10 : 5;
|
|
597
|
+
}
|
|
598
|
+
const quality = targetEntry.metadata && typeof targetEntry.metadata.quality === 'number'
|
|
599
|
+
? targetEntry.metadata.quality
|
|
600
|
+
: undefined;
|
|
601
|
+
if (quality !== undefined && !Number.isNaN(quality)) {
|
|
602
|
+
score += Math.min(Math.max(quality, 0), 100) / 20;
|
|
603
|
+
}
|
|
604
|
+
const recencyTimestamp = this.parseMetadataTimestamp(targetEntry.metadata?.changeDate || targetEntry.metadata?.creationDate);
|
|
605
|
+
if (recencyTimestamp !== undefined) {
|
|
606
|
+
const ageDays = (Date.now() - recencyTimestamp) / 86400000;
|
|
607
|
+
if (Number.isFinite(ageDays) && ageDays >= 0) {
|
|
608
|
+
const normalized = Math.max(0, 1 - Math.min(ageDays, 365) / 365);
|
|
609
|
+
score += normalized * 5;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (targetEntry.metadata?.state === 'final') {
|
|
613
|
+
score += 3;
|
|
614
|
+
}
|
|
615
|
+
else if (targetEntry.metadata?.state === 'reviewed') {
|
|
616
|
+
score += 2;
|
|
617
|
+
}
|
|
618
|
+
else if (targetEntry.metadata?.state === 'translated') {
|
|
619
|
+
score += 1;
|
|
620
|
+
}
|
|
621
|
+
return score;
|
|
622
|
+
}
|
|
623
|
+
parseMetadataTimestamp(value) {
|
|
624
|
+
if (!value) {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
const parsed = Date.parse(value);
|
|
628
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
629
|
+
}
|
|
630
|
+
selectPreferredCandidate(entries, desiredSegmentIndex, filters) {
|
|
631
|
+
if (!entries || entries.length === 0) {
|
|
632
|
+
return undefined;
|
|
633
|
+
}
|
|
634
|
+
const filtered = filters
|
|
635
|
+
? entries.filter((entry) => this.metadataMatches(entry.metadata, filters))
|
|
636
|
+
: entries.slice();
|
|
637
|
+
if (filtered.length === 0) {
|
|
638
|
+
return undefined;
|
|
639
|
+
}
|
|
640
|
+
if (desiredSegmentIndex > 0) {
|
|
641
|
+
const exact = filtered.find((entry) => entry.segmentIndex === desiredSegmentIndex);
|
|
642
|
+
if (exact) {
|
|
643
|
+
return exact;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const segmentEntry = filtered.find((entry) => entry.segmentIndex > 0);
|
|
647
|
+
if (segmentEntry) {
|
|
648
|
+
return segmentEntry;
|
|
649
|
+
}
|
|
650
|
+
return filtered[0];
|
|
651
|
+
}
|
|
652
|
+
metadataMatches(metadata, filter) {
|
|
653
|
+
if (!filter) {
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
if (!metadata) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
if (filter.states && filter.states.length > 0) {
|
|
660
|
+
if (!metadata.state || !filter.states.includes(metadata.state)) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (filter.minState) {
|
|
665
|
+
const metadataRank = this.getStateRank(metadata.state);
|
|
666
|
+
if (metadataRank === 0 || metadataRank < this.getStateRank(filter.minState)) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (typeof filter.minQuality === 'number') {
|
|
671
|
+
if (typeof metadata.quality !== 'number' || metadata.quality < filter.minQuality) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (filter.contextIncludes && filter.contextIncludes.length > 0) {
|
|
676
|
+
const contextValue = (metadata.context || '').toLowerCase();
|
|
677
|
+
const matchesAll = filter.contextIncludes.every((needle) => contextValue.includes(needle.toLowerCase()));
|
|
678
|
+
if (!matchesAll) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (filter.requiredProperties) {
|
|
683
|
+
if (!metadata.properties) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
for (const [key, expected] of Object.entries(filter.requiredProperties)) {
|
|
687
|
+
if (metadata.properties[key] !== expected) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (filter.provider) {
|
|
693
|
+
const provider = metadata.segment?.provider;
|
|
694
|
+
if (!provider || provider !== filter.provider) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
getStateRank(value) {
|
|
701
|
+
if (!value) {
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
switch (value) {
|
|
705
|
+
case 'initial':
|
|
706
|
+
return 1;
|
|
707
|
+
case 'translated':
|
|
708
|
+
return 2;
|
|
709
|
+
case 'reviewed':
|
|
710
|
+
return 3;
|
|
711
|
+
case 'final':
|
|
712
|
+
return 4;
|
|
713
|
+
default:
|
|
714
|
+
return 0;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// ============================
|
|
718
|
+
// DATA MANAGEMENT METHODS
|
|
719
|
+
// ============================
|
|
720
|
+
async storeLangEntry(fileId, original, unitId, lang, pureText, element, embeddings, segmentIndex = 0, segmentCount = 1, metadata) {
|
|
721
|
+
try {
|
|
722
|
+
const table = await this.ensureTable();
|
|
723
|
+
const sanitizedFileId = Utils.replaceQuotes(fileId);
|
|
724
|
+
const sanitizedUnitId = Utils.replaceQuotes(unitId);
|
|
725
|
+
const safeSegmentIndex = typeof segmentIndex === 'number' ? segmentIndex : 0;
|
|
726
|
+
const safeSegmentCount = typeof segmentCount === 'number' && segmentCount > 0 ? segmentCount : 1;
|
|
727
|
+
const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, safeSegmentIndex, lang);
|
|
728
|
+
const existingEntries = this.hydrateEntries(await table
|
|
729
|
+
.query()
|
|
730
|
+
.where('id = ' + '\'' + entryId + '\'')
|
|
731
|
+
.toArray());
|
|
732
|
+
if (existingEntries.length > 0) {
|
|
733
|
+
const existingEntry = existingEntries[0];
|
|
734
|
+
const contentChanged = (existingEntry.pureText !== pureText ||
|
|
735
|
+
existingEntry.element !== element.toString() ||
|
|
736
|
+
existingEntry.original !== original);
|
|
737
|
+
if (!contentChanged) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Generate embeddings if not provided
|
|
742
|
+
let vectorEmbeddings;
|
|
743
|
+
if (embeddings && embeddings.length > 0) {
|
|
744
|
+
vectorEmbeddings = embeddings;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
vectorEmbeddings = await this.generateEmbedding(pureText);
|
|
748
|
+
}
|
|
749
|
+
const entry = {
|
|
750
|
+
id: entryId,
|
|
751
|
+
language: lang,
|
|
752
|
+
pureText: pureText,
|
|
753
|
+
element: element.toString(),
|
|
754
|
+
fileId: sanitizedFileId,
|
|
755
|
+
original: original,
|
|
756
|
+
unitId: sanitizedUnitId,
|
|
757
|
+
vector: vectorEmbeddings,
|
|
758
|
+
segmentIndex: safeSegmentIndex,
|
|
759
|
+
segmentCount: safeSegmentCount,
|
|
760
|
+
metadata: metadata ?? {}
|
|
761
|
+
};
|
|
762
|
+
// Delete existing entry first (LanceDB upsert approach)
|
|
763
|
+
try {
|
|
764
|
+
await table.delete('id = ' + '\'' + entryId + '\'');
|
|
765
|
+
}
|
|
766
|
+
catch (deleteErr) {
|
|
767
|
+
// Entry might not exist, which is fine
|
|
768
|
+
}
|
|
769
|
+
await table.add([this.flattenEntry(entry)]);
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
console.error('Error storing language entry:', err);
|
|
773
|
+
throw err;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async storeBatchEntries(entries) {
|
|
777
|
+
try {
|
|
778
|
+
const table = await this.ensureTable();
|
|
779
|
+
const langEntries = [];
|
|
780
|
+
// Collect all entry IDs for bulk deletion
|
|
781
|
+
const entryIds = [];
|
|
782
|
+
// Generate embeddings one by one and build LangEntry objects
|
|
783
|
+
for (const entry of entries) {
|
|
784
|
+
const sanitizedFileId = Utils.replaceQuotes(entry.fileId);
|
|
785
|
+
const sanitizedUnitId = Utils.replaceQuotes(entry.unitId);
|
|
786
|
+
const vectorEmbeddings = await this.generateEmbedding(entry.pureText);
|
|
787
|
+
const safeSegmentIndex = typeof entry.segmentIndex === 'number' ? entry.segmentIndex : 0;
|
|
788
|
+
const safeSegmentCount = typeof entry.segmentCount === 'number' && entry.segmentCount > 0 ? entry.segmentCount : 1;
|
|
789
|
+
const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, safeSegmentIndex, entry.language);
|
|
790
|
+
const langEntry = {
|
|
791
|
+
id: entryId,
|
|
792
|
+
language: entry.language,
|
|
793
|
+
pureText: entry.pureText,
|
|
794
|
+
element: entry.element.toString(),
|
|
795
|
+
fileId: sanitizedFileId,
|
|
796
|
+
original: entry.original,
|
|
797
|
+
unitId: sanitizedUnitId,
|
|
798
|
+
vector: vectorEmbeddings,
|
|
799
|
+
segmentIndex: safeSegmentIndex,
|
|
800
|
+
segmentCount: safeSegmentCount,
|
|
801
|
+
metadata: entry.metadata ?? {}
|
|
802
|
+
};
|
|
803
|
+
langEntries.push(langEntry);
|
|
804
|
+
entryIds.push(entryId);
|
|
805
|
+
}
|
|
806
|
+
// Delete any existing entries with these IDs to prevent duplicates
|
|
807
|
+
// This ensures that if the same file is imported twice, entries are overwritten
|
|
808
|
+
if (entryIds.length > 0) {
|
|
809
|
+
const idsFilter = entryIds.map(id => '\'' + id + '\'').join(',');
|
|
810
|
+
try {
|
|
811
|
+
await table.delete('id IN (' + idsFilter + ')');
|
|
812
|
+
}
|
|
813
|
+
catch (deleteErr) {
|
|
814
|
+
// Entries might not exist, which is fine for first import
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// Bulk insert all entries at once
|
|
818
|
+
const flattenedEntries = langEntries.map((item) => this.flattenEntry(item));
|
|
819
|
+
await table.add(flattenedEntries);
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
console.error('Error storing batch entries:', err);
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async entryExists(fileId, unitId, lang, segmentIndex = 0) {
|
|
827
|
+
try {
|
|
828
|
+
if (!this.table) {
|
|
829
|
+
await this.initializeDatabase();
|
|
830
|
+
}
|
|
831
|
+
if (!this.table) {
|
|
832
|
+
throw new Error('Failed to initialize database table');
|
|
833
|
+
}
|
|
834
|
+
const sanitizedFileId = Utils.replaceQuotes(fileId);
|
|
835
|
+
const sanitizedUnitId = Utils.replaceQuotes(unitId);
|
|
836
|
+
const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, segmentIndex, lang);
|
|
837
|
+
const existingEntries = this.hydrateEntries(await this.table
|
|
838
|
+
.query()
|
|
839
|
+
.where('id = ' + '\'' + entryId + '\'')
|
|
840
|
+
.toArray());
|
|
841
|
+
return existingEntries.length > 0;
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
console.error('Error checking entry existence:', err);
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async getLangEntry(fileId, unitId, lang, segmentIndex = 0) {
|
|
849
|
+
try {
|
|
850
|
+
if (!this.table) {
|
|
851
|
+
await this.initializeDatabase();
|
|
852
|
+
}
|
|
853
|
+
if (!this.table) {
|
|
854
|
+
throw new Error('Failed to initialize database table');
|
|
855
|
+
}
|
|
856
|
+
const sanitizedFileId = Utils.replaceQuotes(fileId);
|
|
857
|
+
const sanitizedUnitId = Utils.replaceQuotes(unitId);
|
|
858
|
+
const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, segmentIndex, lang);
|
|
859
|
+
const existingEntries = this.hydrateEntries(await this.table
|
|
860
|
+
.query()
|
|
861
|
+
.where('id = ' + '\'' + entryId + '\'')
|
|
862
|
+
.toArray());
|
|
863
|
+
return existingEntries.length > 0 ? existingEntries[0] : null;
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
console.error('Error retrieving language entry:', err);
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async deleteLangEntry(fileId, unitId, lang, segmentIndex = 0) {
|
|
871
|
+
try {
|
|
872
|
+
if (!this.table) {
|
|
873
|
+
await this.initializeDatabase();
|
|
874
|
+
}
|
|
875
|
+
if (!this.table) {
|
|
876
|
+
throw new Error('Failed to initialize database table');
|
|
877
|
+
}
|
|
878
|
+
const sanitizedFileId = Utils.replaceQuotes(fileId);
|
|
879
|
+
const sanitizedUnitId = Utils.replaceQuotes(unitId);
|
|
880
|
+
const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, segmentIndex, lang);
|
|
881
|
+
// Check if entry exists first
|
|
882
|
+
const exists = await this.entryExists(fileId, unitId, lang, segmentIndex);
|
|
883
|
+
if (!exists) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
// Delete the entry
|
|
887
|
+
await this.table.delete('id = \'' + entryId + '\'');
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
catch (err) {
|
|
891
|
+
console.error('Error deleting language entry:', err);
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// ============================
|
|
896
|
+
// DATA IMPORT METHODS
|
|
897
|
+
// ============================
|
|
898
|
+
async importXLIFF(filePath, options) {
|
|
899
|
+
// Phase 1: Parse XLIFF and write to temporary JSONL file
|
|
900
|
+
const resolvedOptions = resolveImportOptions(options);
|
|
901
|
+
const reader = new XLIFFReader(filePath, resolvedOptions);
|
|
902
|
+
await reader.parse();
|
|
903
|
+
// Phase 2: Batch import from JSONL file (asynchronous)
|
|
904
|
+
const importer = new BatchImporter(this, reader.getTempFilePath(), reader.getEntryCount());
|
|
905
|
+
await importer.import();
|
|
906
|
+
}
|
|
907
|
+
async importTMX(filePath, options) {
|
|
908
|
+
// Phase 1: Parse TMX and write to temporary JSONL file
|
|
909
|
+
const resolvedOptions = resolveImportOptions(options);
|
|
910
|
+
const reader = new TMXReader(filePath, resolvedOptions);
|
|
911
|
+
await reader.parse();
|
|
912
|
+
// Phase 2: Batch import from JSONL file (asynchronous)
|
|
913
|
+
const importer = new BatchImporter(this, reader.getTempFilePath(), reader.getEntryCount());
|
|
914
|
+
await importer.import();
|
|
915
|
+
}
|
|
916
|
+
async importSDLTM(filePath, options) {
|
|
917
|
+
const tempDir = tmpdir();
|
|
918
|
+
const tempFileName = 'tmx_' + Date.now() + '_' + Math.random().toString(36).substring(7) + '.tmx';
|
|
919
|
+
const tempFilePath = join(tempDir, tempFileName);
|
|
920
|
+
const packageJson = await import('../package.json', { assert: { type: 'json' } });
|
|
921
|
+
const productName = packageJson.default.productName;
|
|
922
|
+
const version = packageJson.default.version;
|
|
923
|
+
const tmReader = new TMReader({ 'productName': productName, 'version': version });
|
|
924
|
+
const data = await tmReader.convert(filePath, tempFilePath);
|
|
925
|
+
if (data.status !== 'Success') {
|
|
926
|
+
throw new Error('SDL TM conversion failed for file ' + filePath);
|
|
927
|
+
}
|
|
928
|
+
await this.importTMX(tempFilePath, options);
|
|
929
|
+
unlinkSync(tempFilePath);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
//# sourceMappingURL=hybridtm.js.map
|