ai-l10n-core 1.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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/NOTICE +27 -0
- package/README.md +487 -0
- package/dist/consoleLogger.d.ts +26 -0
- package/dist/consoleLogger.js +62 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.js +22 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/languageUtils.d.ts +27 -0
- package/dist/languageUtils.js +79 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +2 -0
- package/dist/test/consoleLogger.test.d.ts +1 -0
- package/dist/test/consoleLogger.test.js +242 -0
- package/dist/test/translationService.test.d.ts +1 -0
- package/dist/test/translationService.test.js +454 -0
- package/dist/translationService.d.ts +94 -0
- package/dist/translationService.js +155 -0
- package/package.json +51 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const assert = __importStar(require("assert"));
|
|
37
|
+
const sinon = __importStar(require("sinon"));
|
|
38
|
+
const constants_1 = require("../constants");
|
|
39
|
+
const translationService_1 = require("../translationService");
|
|
40
|
+
// Mock fetch globally
|
|
41
|
+
const mockFetch = sinon.stub();
|
|
42
|
+
global.fetch = mockFetch;
|
|
43
|
+
// Mock URL constructor
|
|
44
|
+
global.URL = function (url) {
|
|
45
|
+
this.searchParams = {
|
|
46
|
+
append: sinon.stub(),
|
|
47
|
+
};
|
|
48
|
+
this.toString = () => url;
|
|
49
|
+
};
|
|
50
|
+
suite("L10nTranslationService Test Suite", () => {
|
|
51
|
+
let service;
|
|
52
|
+
let mockLogger;
|
|
53
|
+
// Helper function to create a proper translation request
|
|
54
|
+
const createRequest = (overrides = {}) => ({
|
|
55
|
+
sourceStrings: JSON.stringify({}),
|
|
56
|
+
targetLanguageCode: "es",
|
|
57
|
+
useContractions: false,
|
|
58
|
+
useShortening: false,
|
|
59
|
+
returnTranslationsAsString: true,
|
|
60
|
+
client: "test",
|
|
61
|
+
schema: null,
|
|
62
|
+
...overrides,
|
|
63
|
+
});
|
|
64
|
+
setup(() => {
|
|
65
|
+
// Reset all stubs
|
|
66
|
+
sinon.resetHistory();
|
|
67
|
+
// Reset fetch mock
|
|
68
|
+
mockFetch.reset();
|
|
69
|
+
// Create mock logger
|
|
70
|
+
mockLogger = {
|
|
71
|
+
logInfo: sinon.stub(),
|
|
72
|
+
logWarning: sinon.stub(),
|
|
73
|
+
logError: sinon.stub(),
|
|
74
|
+
showAndLogError: sinon.stub(),
|
|
75
|
+
};
|
|
76
|
+
// Create service instance with mocked logger only
|
|
77
|
+
service = new translationService_1.L10nTranslationService(mockLogger);
|
|
78
|
+
});
|
|
79
|
+
teardown(() => {
|
|
80
|
+
sinon.restore();
|
|
81
|
+
});
|
|
82
|
+
suite("Language Prediction", () => {
|
|
83
|
+
test("predictLanguages returns parsed results on success", async () => {
|
|
84
|
+
const mockResponse = {
|
|
85
|
+
languages: [
|
|
86
|
+
{ code: "es", name: "Spanish" },
|
|
87
|
+
{ code: "fr", name: "French" },
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
const mockFetchResponse = {
|
|
91
|
+
ok: true,
|
|
92
|
+
json: sinon.stub().resolves(mockResponse),
|
|
93
|
+
};
|
|
94
|
+
mockFetch.resolves(mockFetchResponse);
|
|
95
|
+
const result = await service.predictLanguages("spanish", 5);
|
|
96
|
+
assert.strictEqual(result.length, 2);
|
|
97
|
+
assert.deepStrictEqual(result, mockResponse.languages);
|
|
98
|
+
});
|
|
99
|
+
test("predictLanguages throws error on failed API call", async () => {
|
|
100
|
+
const mockFetchResponse = {
|
|
101
|
+
ok: false,
|
|
102
|
+
statusText: "Bad Request",
|
|
103
|
+
};
|
|
104
|
+
mockFetch.resolves(mockFetchResponse);
|
|
105
|
+
await assert.rejects(async () => await service.predictLanguages("test"), /Failed to predict languages: Bad Request/);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
suite("JSON Translation", () => {
|
|
109
|
+
test("translate returns null when no API Key is set", async () => {
|
|
110
|
+
const result = await service.translate(createRequest(), "");
|
|
111
|
+
assert.strictEqual(result, null);
|
|
112
|
+
// Verify error was logged
|
|
113
|
+
assert.ok(mockLogger.showAndLogError.called);
|
|
114
|
+
});
|
|
115
|
+
test("translate makes correct API call with valid API Key", async () => {
|
|
116
|
+
const apiKey = "valid-api-key";
|
|
117
|
+
const sourceStrings = { hello: "Hello", world: "World" };
|
|
118
|
+
const targetLanguage = "es";
|
|
119
|
+
const useContractions = false;
|
|
120
|
+
const useShortening = true;
|
|
121
|
+
const request = {
|
|
122
|
+
sourceStrings: JSON.stringify(sourceStrings),
|
|
123
|
+
targetLanguageCode: targetLanguage,
|
|
124
|
+
useContractions,
|
|
125
|
+
useShortening,
|
|
126
|
+
returnTranslationsAsString: true,
|
|
127
|
+
client: "test",
|
|
128
|
+
schema: null,
|
|
129
|
+
};
|
|
130
|
+
const mockTranslationResult = {
|
|
131
|
+
targetLanguageCode: "es",
|
|
132
|
+
translations: JSON.stringify({ hello: "Hola", world: "Mundo" }),
|
|
133
|
+
usage: { charsUsed: 10 },
|
|
134
|
+
completedChunks: 1,
|
|
135
|
+
totalChunks: 1,
|
|
136
|
+
};
|
|
137
|
+
const mockFetchResponse = {
|
|
138
|
+
ok: true,
|
|
139
|
+
json: sinon.stub().resolves(mockTranslationResult),
|
|
140
|
+
};
|
|
141
|
+
mockFetch.resolves(mockFetchResponse);
|
|
142
|
+
const result = await service.translate(request, apiKey);
|
|
143
|
+
assert.deepStrictEqual(result, mockTranslationResult);
|
|
144
|
+
// Verify fetch was called with correct parameters
|
|
145
|
+
assert.ok(mockFetch.called);
|
|
146
|
+
const fetchCall = mockFetch.getCall(0);
|
|
147
|
+
assert.strictEqual(fetchCall.args[0], `${constants_1.URLS.API_BASE}/v2/translate`);
|
|
148
|
+
const requestOptions = fetchCall.args[1];
|
|
149
|
+
assert.strictEqual(requestOptions.method, "POST");
|
|
150
|
+
assert.strictEqual(requestOptions.headers["Content-Type"], "application/json");
|
|
151
|
+
assert.strictEqual(requestOptions.headers["X-API-Key"], apiKey);
|
|
152
|
+
const requestBody = JSON.parse(requestOptions.body);
|
|
153
|
+
assert.strictEqual(requestBody.sourceStrings, JSON.stringify(sourceStrings));
|
|
154
|
+
assert.strictEqual(requestBody.targetLanguageCode, targetLanguage);
|
|
155
|
+
assert.strictEqual(requestBody.useContractions, false);
|
|
156
|
+
assert.strictEqual(requestBody.useShortening, true);
|
|
157
|
+
assert.strictEqual(requestBody.returnTranslationsAsString, true);
|
|
158
|
+
assert.strictEqual(requestBody.client, "test");
|
|
159
|
+
assert.strictEqual(requestBody.schema, null);
|
|
160
|
+
});
|
|
161
|
+
test("includes translateMetadata in API request when set to true", async () => {
|
|
162
|
+
const apiKey = "valid-api-key";
|
|
163
|
+
const request = {
|
|
164
|
+
sourceStrings: JSON.stringify({ hello: "Hello" }),
|
|
165
|
+
targetLanguageCode: "es",
|
|
166
|
+
useContractions: true,
|
|
167
|
+
useShortening: false,
|
|
168
|
+
translateMetadata: true,
|
|
169
|
+
returnTranslationsAsString: true,
|
|
170
|
+
client: "test",
|
|
171
|
+
schema: null,
|
|
172
|
+
};
|
|
173
|
+
const mockTranslationResult = {
|
|
174
|
+
targetLanguageCode: "es",
|
|
175
|
+
translations: JSON.stringify({ hello: "Hola" }),
|
|
176
|
+
usage: { charsUsed: 10 },
|
|
177
|
+
completedChunks: 1,
|
|
178
|
+
totalChunks: 1,
|
|
179
|
+
};
|
|
180
|
+
const mockFetchResponse = {
|
|
181
|
+
ok: true,
|
|
182
|
+
json: sinon.stub().resolves(mockTranslationResult),
|
|
183
|
+
};
|
|
184
|
+
mockFetch.resolves(mockFetchResponse);
|
|
185
|
+
await service.translate(request, apiKey);
|
|
186
|
+
const fetchCall = mockFetch.getCall(0);
|
|
187
|
+
const requestOptions = fetchCall.args[1];
|
|
188
|
+
const requestBody = JSON.parse(requestOptions.body);
|
|
189
|
+
assert.strictEqual(requestBody.translateMetadata, true);
|
|
190
|
+
});
|
|
191
|
+
test("includes translateMetadata in API request when set to false", async () => {
|
|
192
|
+
const apiKey = "valid-api-key";
|
|
193
|
+
const request = {
|
|
194
|
+
sourceStrings: JSON.stringify({ hello: "Hello" }),
|
|
195
|
+
targetLanguageCode: "es",
|
|
196
|
+
useContractions: true,
|
|
197
|
+
useShortening: false,
|
|
198
|
+
translateMetadata: false,
|
|
199
|
+
returnTranslationsAsString: true,
|
|
200
|
+
client: "test",
|
|
201
|
+
schema: null,
|
|
202
|
+
};
|
|
203
|
+
const mockTranslationResult = {
|
|
204
|
+
targetLanguageCode: "es",
|
|
205
|
+
translations: JSON.stringify({ hello: "Hola" }),
|
|
206
|
+
usage: { charsUsed: 10 },
|
|
207
|
+
completedChunks: 1,
|
|
208
|
+
totalChunks: 1,
|
|
209
|
+
};
|
|
210
|
+
const mockFetchResponse = {
|
|
211
|
+
ok: true,
|
|
212
|
+
json: sinon.stub().resolves(mockTranslationResult),
|
|
213
|
+
};
|
|
214
|
+
mockFetch.resolves(mockFetchResponse);
|
|
215
|
+
await service.translate(request, apiKey);
|
|
216
|
+
const fetchCall = mockFetch.getCall(0);
|
|
217
|
+
const requestOptions = fetchCall.args[1];
|
|
218
|
+
const requestBody = JSON.parse(requestOptions.body);
|
|
219
|
+
assert.strictEqual(requestBody.translateMetadata, false);
|
|
220
|
+
});
|
|
221
|
+
test("translate handles 400 Bad Request error", async () => {
|
|
222
|
+
const apiKey = "valid-api-key";
|
|
223
|
+
const mockErrorResponse = {
|
|
224
|
+
ok: false,
|
|
225
|
+
status: 400,
|
|
226
|
+
json: sinon.stub().resolves({
|
|
227
|
+
errors: ["Invalid source strings format"],
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
mockFetch.resolves(mockErrorResponse);
|
|
231
|
+
await assert.rejects(async () => await service.translate(createRequest(), apiKey), /Invalid source strings format/);
|
|
232
|
+
});
|
|
233
|
+
test("translate handles 401 Unauthorized error", async () => {
|
|
234
|
+
const apiKey = "invalid-api-key";
|
|
235
|
+
const mockErrorResponse = {
|
|
236
|
+
ok: false,
|
|
237
|
+
status: 401,
|
|
238
|
+
json: sinon.stub().resolves({}),
|
|
239
|
+
};
|
|
240
|
+
mockFetch.resolves(mockErrorResponse);
|
|
241
|
+
const result = await service.translate(createRequest(), apiKey);
|
|
242
|
+
// 401 errors now return null instead of throwing
|
|
243
|
+
assert.strictEqual(result, null);
|
|
244
|
+
// Verify error was logged
|
|
245
|
+
assert.ok(mockLogger.showAndLogError.called);
|
|
246
|
+
});
|
|
247
|
+
test("translate handles 402 Payment Required error with specific message", async () => {
|
|
248
|
+
const apiKey = "valid-api-key";
|
|
249
|
+
const mockErrorResponse = {
|
|
250
|
+
ok: false,
|
|
251
|
+
status: 402,
|
|
252
|
+
json: sinon.stub().resolves({
|
|
253
|
+
data: {
|
|
254
|
+
requiredBalance: 1000,
|
|
255
|
+
currentBalance: 500,
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
259
|
+
mockFetch.resolves(mockErrorResponse);
|
|
260
|
+
const result = await service.translate(createRequest(), apiKey);
|
|
261
|
+
// 402 errors now return null instead of throwing
|
|
262
|
+
assert.strictEqual(result, null);
|
|
263
|
+
});
|
|
264
|
+
test("translate handles 413 Request Too Large error", async () => {
|
|
265
|
+
const apiKey = "valid-api-key";
|
|
266
|
+
const mockErrorResponse = {
|
|
267
|
+
ok: false,
|
|
268
|
+
status: 413,
|
|
269
|
+
json: sinon.stub().resolves({}),
|
|
270
|
+
};
|
|
271
|
+
mockFetch.resolves(mockErrorResponse);
|
|
272
|
+
await assert.rejects(async () => await service.translate(createRequest(), apiKey), /Request too large. Maximum request size is 5 MB./);
|
|
273
|
+
});
|
|
274
|
+
test("translate handles 500 Internal Server Error", async () => {
|
|
275
|
+
const apiKey = "valid-api-key";
|
|
276
|
+
const mockErrorResponse = {
|
|
277
|
+
ok: false,
|
|
278
|
+
status: 500,
|
|
279
|
+
json: sinon.stub().resolves({
|
|
280
|
+
errorCode: "INTERNAL_ERROR_123",
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
mockFetch.resolves(mockErrorResponse);
|
|
284
|
+
await assert.rejects(async () => await service.translate(createRequest(), apiKey), /An internal server error occurred \(Error code: INTERNAL_ERROR_123\)/);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
suite("Error Handling", () => {
|
|
288
|
+
test("handles complex validation error structure", async () => {
|
|
289
|
+
const apiKey = "valid-api-key";
|
|
290
|
+
const mockErrorResponse = {
|
|
291
|
+
ok: false,
|
|
292
|
+
status: 400,
|
|
293
|
+
json: sinon.stub().resolves({
|
|
294
|
+
errors: {
|
|
295
|
+
sourceStrings: ["is required"],
|
|
296
|
+
targetLanguageCode: ["is invalid", "must be BCP-47 format"],
|
|
297
|
+
},
|
|
298
|
+
}),
|
|
299
|
+
};
|
|
300
|
+
mockFetch.resolves(mockErrorResponse);
|
|
301
|
+
await assert.rejects(async () => await service.translate(createRequest(), apiKey), /is required is invalid must be BCP-47 format/);
|
|
302
|
+
});
|
|
303
|
+
test("handles array validation error structure", async () => {
|
|
304
|
+
const apiKey = "valid-api-key";
|
|
305
|
+
const mockErrorResponse = {
|
|
306
|
+
ok: false,
|
|
307
|
+
status: 400,
|
|
308
|
+
json: sinon.stub().resolves({
|
|
309
|
+
errors: ["Field validation failed", "Invalid input format"],
|
|
310
|
+
}),
|
|
311
|
+
};
|
|
312
|
+
mockFetch.resolves(mockErrorResponse);
|
|
313
|
+
await assert.rejects(async () => await service.translate(createRequest(), apiKey), /Field validation failed Invalid input format/);
|
|
314
|
+
});
|
|
315
|
+
test("handles JSON parsing failure in error response", async () => {
|
|
316
|
+
const apiKey = "valid-api-key";
|
|
317
|
+
const mockErrorResponse = {
|
|
318
|
+
ok: false,
|
|
319
|
+
status: 500,
|
|
320
|
+
json: sinon.stub().rejects(new Error("JSON parse error")),
|
|
321
|
+
};
|
|
322
|
+
mockFetch.resolves(mockErrorResponse);
|
|
323
|
+
await assert.rejects(async () => await service.translate(createRequest(), apiKey), /An internal server error occurred \(Error code: unknown\)/);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
suite("Finish Reason Handling", () => {
|
|
327
|
+
test("handles insufficientBalance finish reason", async () => {
|
|
328
|
+
const apiKey = "valid-api-key";
|
|
329
|
+
const expectedResult = {
|
|
330
|
+
targetLanguageCode: "es",
|
|
331
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
332
|
+
usage: { charsUsed: 5 },
|
|
333
|
+
finishReason: "insufficientBalance",
|
|
334
|
+
completedChunks: 1,
|
|
335
|
+
totalChunks: 1,
|
|
336
|
+
};
|
|
337
|
+
const mockResponse = {
|
|
338
|
+
ok: true,
|
|
339
|
+
json: sinon.stub().resolves(expectedResult),
|
|
340
|
+
};
|
|
341
|
+
mockFetch.resolves(mockResponse);
|
|
342
|
+
const result = await service.translate(createRequest(), apiKey);
|
|
343
|
+
// insufficientBalance finish reason now returns result with partial translations
|
|
344
|
+
assert.deepStrictEqual(result, expectedResult);
|
|
345
|
+
// Verify error was logged
|
|
346
|
+
assert.ok(mockLogger.showAndLogError.called);
|
|
347
|
+
});
|
|
348
|
+
test("throws error for error finish reason", async () => {
|
|
349
|
+
const apiKey = "valid-api-key";
|
|
350
|
+
const mockResponse = {
|
|
351
|
+
ok: true,
|
|
352
|
+
json: sinon.stub().resolves({
|
|
353
|
+
targetLanguageCode: "es",
|
|
354
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
355
|
+
usage: { charsUsed: 5 },
|
|
356
|
+
finishReason: "error",
|
|
357
|
+
completedChunks: 1,
|
|
358
|
+
totalChunks: 1,
|
|
359
|
+
}),
|
|
360
|
+
};
|
|
361
|
+
mockFetch.resolves(mockResponse);
|
|
362
|
+
await assert.rejects(async () => await service.translate(createRequest(), apiKey), /Translation failed due to an error\./);
|
|
363
|
+
});
|
|
364
|
+
test("does not throw error for length finish reason", async () => {
|
|
365
|
+
const apiKey = "valid-api-key";
|
|
366
|
+
const expectedResult = {
|
|
367
|
+
targetLanguageCode: "es",
|
|
368
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
369
|
+
usage: { charsUsed: 5 },
|
|
370
|
+
finishReason: "length",
|
|
371
|
+
filteredStrings: '{"hello":"hola"}',
|
|
372
|
+
completedChunks: 1,
|
|
373
|
+
totalChunks: 1,
|
|
374
|
+
};
|
|
375
|
+
const mockResponse = {
|
|
376
|
+
ok: true,
|
|
377
|
+
json: sinon.stub().resolves({
|
|
378
|
+
targetLanguageCode: "es",
|
|
379
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
380
|
+
usage: { charsUsed: 5 },
|
|
381
|
+
finishReason: "length",
|
|
382
|
+
filteredStrings: '{"hello":"hola"}',
|
|
383
|
+
completedChunks: 1,
|
|
384
|
+
totalChunks: 1,
|
|
385
|
+
}),
|
|
386
|
+
};
|
|
387
|
+
mockFetch.resolves(mockResponse);
|
|
388
|
+
const result = await service.translate(createRequest(), apiKey);
|
|
389
|
+
assert.deepStrictEqual(result, expectedResult);
|
|
390
|
+
});
|
|
391
|
+
test("does not throw error for contentFilter finish reason", async () => {
|
|
392
|
+
const apiKey = "valid-api-key";
|
|
393
|
+
const expectedResult = {
|
|
394
|
+
targetLanguageCode: "es",
|
|
395
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
396
|
+
usage: { charsUsed: 5 },
|
|
397
|
+
finishReason: "contentFilter",
|
|
398
|
+
filteredStrings: '{"hello":"hola"}',
|
|
399
|
+
completedChunks: 1,
|
|
400
|
+
totalChunks: 1,
|
|
401
|
+
};
|
|
402
|
+
const mockResponse = {
|
|
403
|
+
ok: true,
|
|
404
|
+
json: sinon.stub().resolves({
|
|
405
|
+
targetLanguageCode: "es",
|
|
406
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
407
|
+
usage: { charsUsed: 5 },
|
|
408
|
+
finishReason: "contentFilter",
|
|
409
|
+
filteredStrings: '{"hello":"hola"}',
|
|
410
|
+
completedChunks: 1,
|
|
411
|
+
totalChunks: 1,
|
|
412
|
+
}),
|
|
413
|
+
};
|
|
414
|
+
mockFetch.resolves(mockResponse);
|
|
415
|
+
const result = await service.translate(createRequest(), apiKey);
|
|
416
|
+
assert.deepStrictEqual(result, expectedResult);
|
|
417
|
+
});
|
|
418
|
+
test("does not throw error for stop finish reason", async () => {
|
|
419
|
+
const apiKey = "valid-api-key";
|
|
420
|
+
const expectedResult = {
|
|
421
|
+
targetLanguageCode: "es",
|
|
422
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
423
|
+
usage: { charsUsed: 5 },
|
|
424
|
+
finishReason: "stop",
|
|
425
|
+
completedChunks: 1,
|
|
426
|
+
totalChunks: 1,
|
|
427
|
+
};
|
|
428
|
+
const mockResponse = {
|
|
429
|
+
ok: true,
|
|
430
|
+
json: sinon.stub().resolves(expectedResult),
|
|
431
|
+
};
|
|
432
|
+
mockFetch.resolves(mockResponse);
|
|
433
|
+
const result = await service.translate(createRequest(), apiKey);
|
|
434
|
+
assert.deepStrictEqual(result, expectedResult);
|
|
435
|
+
});
|
|
436
|
+
test("works normally when no finish reason is present", async () => {
|
|
437
|
+
const apiKey = "valid-api-key";
|
|
438
|
+
const expectedResult = {
|
|
439
|
+
targetLanguageCode: "es",
|
|
440
|
+
translations: JSON.stringify({ hello: "hola" }),
|
|
441
|
+
usage: { charsUsed: 5 },
|
|
442
|
+
completedChunks: 1,
|
|
443
|
+
totalChunks: 1,
|
|
444
|
+
};
|
|
445
|
+
const mockResponse = {
|
|
446
|
+
ok: true,
|
|
447
|
+
json: sinon.stub().resolves(expectedResult),
|
|
448
|
+
};
|
|
449
|
+
mockFetch.resolves(mockResponse);
|
|
450
|
+
const result = await service.translate(createRequest(), apiKey);
|
|
451
|
+
assert.deepStrictEqual(result, expectedResult);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ILogger } from "./logger";
|
|
2
|
+
/**
|
|
3
|
+
* Supported file schema formats for translation requests.
|
|
4
|
+
* Used to enable schema-specific translation handling.
|
|
5
|
+
*/
|
|
6
|
+
export declare enum FileSchema {
|
|
7
|
+
/** OpenAPI specification format */
|
|
8
|
+
OpenAPI = "openApi",
|
|
9
|
+
/** Flutter ARB (Application Resource Bundle) format */
|
|
10
|
+
ARBFlutter = "arbFlutter"
|
|
11
|
+
}
|
|
12
|
+
export interface TranslationRequest {
|
|
13
|
+
sourceStrings: string;
|
|
14
|
+
targetLanguageCode: string;
|
|
15
|
+
useContractions?: boolean;
|
|
16
|
+
useShortening?: boolean;
|
|
17
|
+
generatePluralForms?: boolean;
|
|
18
|
+
translateMetadata?: boolean;
|
|
19
|
+
returnTranslationsAsString: boolean;
|
|
20
|
+
client: string;
|
|
21
|
+
translateOnlyNewStrings?: boolean;
|
|
22
|
+
targetStrings?: string;
|
|
23
|
+
schema: FileSchema | null;
|
|
24
|
+
/**
|
|
25
|
+
* Localization file format (e.g., "json", "arb", "po", "yaml", "xml").
|
|
26
|
+
* If not specified, auto-detected from sourceStrings content.
|
|
27
|
+
* See https://l10n.dev/ws/translate-i18n-files#supported-formats for supported formats.
|
|
28
|
+
*/
|
|
29
|
+
format?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface TranslationResult {
|
|
32
|
+
targetLanguageCode: string;
|
|
33
|
+
translations?: string;
|
|
34
|
+
usage: TranslationUsage;
|
|
35
|
+
finishReason?: FinishReason;
|
|
36
|
+
completedChunks: number;
|
|
37
|
+
totalChunks: number;
|
|
38
|
+
remainingBalance?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Source strings that were filtered out due to content policy violations or length limits.
|
|
41
|
+
* Populated when the finish reason is 'contentFilter' or 'length'.
|
|
42
|
+
* Raw text in the same format as the input (JSON, YAML, PO, etc.).
|
|
43
|
+
*/
|
|
44
|
+
filteredStrings?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface TranslationUsage {
|
|
47
|
+
charsUsed?: number;
|
|
48
|
+
}
|
|
49
|
+
export declare enum FinishReason {
|
|
50
|
+
stop = "stop",
|
|
51
|
+
length = "length",
|
|
52
|
+
contentFilter = "contentFilter",
|
|
53
|
+
insufficientBalance = "insufficientBalance",
|
|
54
|
+
error = "error"
|
|
55
|
+
}
|
|
56
|
+
export interface Language {
|
|
57
|
+
code: string;
|
|
58
|
+
name: string;
|
|
59
|
+
}
|
|
60
|
+
export interface LanguagePredictionResponse {
|
|
61
|
+
languages: Language[];
|
|
62
|
+
}
|
|
63
|
+
export type LanguageProficiencyLevel = "strong" | "high" | "moderate" | "limited";
|
|
64
|
+
export interface Region {
|
|
65
|
+
code: string | null;
|
|
66
|
+
name: string | null;
|
|
67
|
+
nativeName: string | null;
|
|
68
|
+
}
|
|
69
|
+
export interface Script {
|
|
70
|
+
code: string | null;
|
|
71
|
+
name: string | null;
|
|
72
|
+
nativeName: string | null;
|
|
73
|
+
}
|
|
74
|
+
export interface SupportedLanguage {
|
|
75
|
+
code: string | null;
|
|
76
|
+
name: string | null;
|
|
77
|
+
nativeName: string | null;
|
|
78
|
+
level?: LanguageProficiencyLevel;
|
|
79
|
+
regions?: Region[] | null;
|
|
80
|
+
scripts?: Script[] | null;
|
|
81
|
+
}
|
|
82
|
+
export interface SupportedLanguagesResponse {
|
|
83
|
+
languages: SupportedLanguage[];
|
|
84
|
+
}
|
|
85
|
+
export declare class L10nTranslationService {
|
|
86
|
+
private readonly logger;
|
|
87
|
+
constructor(logger?: ILogger);
|
|
88
|
+
getLanguages(options?: {
|
|
89
|
+
codes?: string[];
|
|
90
|
+
proficiencyLevels?: LanguageProficiencyLevel[];
|
|
91
|
+
}): Promise<SupportedLanguagesResponse>;
|
|
92
|
+
predictLanguages(input: string, limit?: number): Promise<Language[]>;
|
|
93
|
+
translate(request: TranslationRequest, apiKey: string): Promise<TranslationResult | null>;
|
|
94
|
+
}
|