@l10nmonster/helpers-translated 1.0.0 → 3.0.0-alpha.2
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 +61 -0
- package/index.js +3 -2
- package/laraProvider.js +92 -0
- package/mmtProvider.js +112 -0
- package/package.json +17 -15
- package/translationOS.js +254 -259
- package/modernMT.js +0 -234
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @l10nmonster/helpers-translated
|
|
2
|
+
|
|
3
|
+
L10n Monster helpers for integrating with Translated.com services including Modern MT and Lara.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @l10nmonster/helpers-translated
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Providers
|
|
12
|
+
|
|
13
|
+
### MMTProvider
|
|
14
|
+
|
|
15
|
+
Machine translation provider using Modern MT API with both real-time and batch translation support.
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { MMTProvider } from '@l10nmonster/helpers-translated';
|
|
19
|
+
|
|
20
|
+
const mmtProvider = new MMTProvider({
|
|
21
|
+
apiKey: 'your-mmt-api-key',
|
|
22
|
+
// Additional configuration options
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### LaraProvider
|
|
27
|
+
|
|
28
|
+
Human translation provider using Lara API for professional translation services.
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
import { LaraProvider } from '@l10nmonster/helpers-translated';
|
|
32
|
+
|
|
33
|
+
const laraProvider = new LaraProvider({
|
|
34
|
+
apiKey: 'your-lara-api-key',
|
|
35
|
+
// Additional configuration options
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Modern MT Integration**: High-quality machine translation with customizable models
|
|
42
|
+
- **Lara Integration**: Professional human translation services
|
|
43
|
+
- **Batch Processing**: Efficient handling of large translation jobs
|
|
44
|
+
- **Real-time Translation**: Immediate translation for smaller content
|
|
45
|
+
- **Cost Tracking**: Built-in billing and usage monitoring
|
|
46
|
+
|
|
47
|
+
## Dependencies
|
|
48
|
+
|
|
49
|
+
- `@translated/lara`: Official Lara API client
|
|
50
|
+
- `modernmt`: Modern MT API client
|
|
51
|
+
- `@l10nmonster/core`: Core L10n Monster functionality (peer dependency)
|
|
52
|
+
|
|
53
|
+
## Testing
|
|
54
|
+
|
|
55
|
+
The package includes comprehensive tests for Modern MT functionality:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm test
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Test artifacts are included for validation of API responses and job processing.
|
package/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// export { TranslationOS } from './translationOS.js';
|
|
2
|
+
export { MMTProvider } from './mmtProvider.js';
|
|
3
|
+
export { LaraProvider } from './laraProvider.js';
|
package/laraProvider.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { logWarn, providers, styleString } from '@l10nmonster/core';
|
|
2
|
+
import { Credentials, Translator } from '@translated/lara';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} LaraProviderOptions
|
|
6
|
+
* @extends ChunkedRemoteTranslationProviderOptions
|
|
7
|
+
* @property {string} keyId - The Lara API key id. This is required.
|
|
8
|
+
* @property {string} [keySecret] - The Lara API key secret. Optional, but often needed for authentication.
|
|
9
|
+
* @property {string|Array<string>} [adaptTo] - An optional single translation memory ID or an array of IDs to adapt translations to.
|
|
10
|
+
* @property {number} [maxChunkSize=60] - Maximum number of text segments (strings) allowed in a single API request chunk. Defaults to 60 if not provided.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Provider for Translated Lara MT.
|
|
15
|
+
*/
|
|
16
|
+
export class LaraProvider extends providers.ChunkedRemoteTranslationProvider {
|
|
17
|
+
#keyId;
|
|
18
|
+
#keySecret;
|
|
19
|
+
#adaptTo;
|
|
20
|
+
#lara;
|
|
21
|
+
#translateOptions;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initializes a new instance of the LaraProvider class.
|
|
25
|
+
* @param {LaraProviderOptions} options - Configuration options for the provider.
|
|
26
|
+
*/
|
|
27
|
+
constructor({ keyId, keySecret, adaptTo, ...options }) {
|
|
28
|
+
super({ maxChunkSize: 60, ...options }); // maximum number of strings sent to Lara is 128 including notes
|
|
29
|
+
this.#keyId = keyId;
|
|
30
|
+
this.#keySecret = keySecret;
|
|
31
|
+
this.#adaptTo = adaptTo && (Array.isArray(adaptTo) ? adaptTo : adaptTo.split(','));
|
|
32
|
+
const credentials = new Credentials(this.#keyId, this.#keySecret);
|
|
33
|
+
this.#lara = new Translator(credentials);
|
|
34
|
+
this.#translateOptions = {
|
|
35
|
+
contentType: 'text/plain',
|
|
36
|
+
instructions: [],
|
|
37
|
+
};
|
|
38
|
+
this.#adaptTo && (this.#translateOptions.adaptTo = this.#adaptTo);
|
|
39
|
+
this.defaultInstructions && this.#translateOptions.instructions.push(this.defaultInstructions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
prepareTranslateChunkArgs({ sourceLang, targetLang, xmlTus, instructions }) {
|
|
43
|
+
const payload = xmlTus.map(xmlTu => {
|
|
44
|
+
const textBlock = [];
|
|
45
|
+
textBlock.push({ text: `bundle: ${xmlTu.bundle} key: ${xmlTu.key} notes: ${xmlTu.notes ?? ''}`, translatable: false });
|
|
46
|
+
textBlock.push({ text: xmlTu.source, translatable: true });
|
|
47
|
+
return textBlock;
|
|
48
|
+
}).flat(1);
|
|
49
|
+
const translateOptions = instructions ? { ...this.#translateOptions, instructions: [...this.#translateOptions.instructions, instructions] } : this.#translateOptions;
|
|
50
|
+
return { payload, sourceLang, targetLang, translateOptions };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async startTranslateChunk(args) {
|
|
54
|
+
const { payload, sourceLang, targetLang, translateOptions } = args;
|
|
55
|
+
try {
|
|
56
|
+
return await this.#lara.translate(payload, sourceLang, targetLang, translateOptions);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
throw new Error(`Lara API error ${e.statusCode}: ${e.type}: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
convertTranslationResponse(chunk) {
|
|
63
|
+
return chunk.translation.filter(textBlock => textBlock.translatable).map(textBlock => ({
|
|
64
|
+
tgt: textBlock.text,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async info() {
|
|
69
|
+
const info = await super.info();
|
|
70
|
+
if (!this.#keyId || !this.#keySecret) {
|
|
71
|
+
info.description.push(styleString`Lara API key is missing. Please add the keyId and keySecret to the provider configuration.`);
|
|
72
|
+
return info;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const credentials = new Credentials(this.#keyId, this.#keySecret);
|
|
76
|
+
const lara = new Translator(credentials);
|
|
77
|
+
const languages = (await lara.getLanguages()).sort();
|
|
78
|
+
info.description.push(styleString`Vendor-supported languages: ${languages?.join(', ') ?? 'unknown'}`);
|
|
79
|
+
const memories = await lara.memories.list();
|
|
80
|
+
if (memories.length > 0) {
|
|
81
|
+
memories.forEach(m =>
|
|
82
|
+
info.description.push(styleString`Vendor TM "${m.name}": id: ${m.id} owner: ${m.ownerId} collaborators: ${m.collaboratorsCount} created: ${m.createdAt} updated: ${m.updatedAt}`)
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
info.description.push(styleString`No TMs configured.`);
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
info.description.push(styleString`Unable to connect to Lara server: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
return info;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/mmtProvider.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { logWarn, providers, styleString } from '@l10nmonster/core';
|
|
2
|
+
import { ModernMT as MMTClient } from 'modernmt';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} MMTProviderOptions
|
|
6
|
+
* @extends ChunkedRemoteTranslationProviderOptions
|
|
7
|
+
* @property {string} [id] - Global identifier (by default 'MMTBatch' or 'MMTRealtime')
|
|
8
|
+
* @property {string} apiKey - The ModernMT API key.
|
|
9
|
+
* @property {string} [webhook] - The webhook URL for batch translation.
|
|
10
|
+
* @property {function(any): any} [chunkFetcher] - The chunk fetcher operation name.
|
|
11
|
+
* @property {(string | number)[]} [hints] - Hints to include in the MMT request.
|
|
12
|
+
* @property {boolean} [multiline] - Whether to use multiline mode.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provider for Translated Modern MT.
|
|
17
|
+
*/
|
|
18
|
+
export class MMTProvider extends providers.ChunkedRemoteTranslationProvider {
|
|
19
|
+
/**
|
|
20
|
+
* Initializes a new instance of the MMTProvider class.
|
|
21
|
+
* @param {MMTProviderOptions} options - Configuration options for the provider.
|
|
22
|
+
*/
|
|
23
|
+
constructor({ id, apiKey, webhook, chunkFetcher, hints, multiline = true, ...options }) {
|
|
24
|
+
id ??= webhook ? 'MMTBatch' : 'MMTRealtime';
|
|
25
|
+
super({ id, ...options });
|
|
26
|
+
if (webhook) {
|
|
27
|
+
if (chunkFetcher) {
|
|
28
|
+
this.chunkFetcher = chunkFetcher;
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error('If you specify a webhook you must also specify a chunkFetcher');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.baseRequest = {
|
|
34
|
+
mmtConstructor: [ apiKey, 'l10n.monster/MMT', '3.0' ],
|
|
35
|
+
hints,
|
|
36
|
+
options: {
|
|
37
|
+
multiline,
|
|
38
|
+
format: 'text/xml',
|
|
39
|
+
},
|
|
40
|
+
webhook,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
start(job) {
|
|
45
|
+
if (!this.baseRequest.mmtConstructor[0]) {
|
|
46
|
+
throw new Error('You must have an apiKey to start an MMT job');
|
|
47
|
+
}
|
|
48
|
+
return super.start(job);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
prepareTranslateChunkArgs({ sourceLang, targetLang, xmlTus, jobGuid, chunkNumber }) {
|
|
52
|
+
return {
|
|
53
|
+
sourceLang,
|
|
54
|
+
targetLang,
|
|
55
|
+
q: xmlTus.map(xmlTu => xmlTu.source),
|
|
56
|
+
hints:this.baseRequest.hints,
|
|
57
|
+
contextVector: undefined,
|
|
58
|
+
options: this.baseRequest.options,
|
|
59
|
+
webhook: this.baseRequest.webhook,
|
|
60
|
+
batchOptions: {
|
|
61
|
+
...this.baseRequest.options,
|
|
62
|
+
idempotencyKey: `jobGuid:${jobGuid} chunk:${chunkNumber}`,
|
|
63
|
+
metadata: { jobGuid, chunk: chunkNumber },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async startTranslateChunk(args) {
|
|
69
|
+
const { sourceLang, targetLang, q, hints, contextVector, options, webhook, batchOptions } = args;
|
|
70
|
+
const [ apiKey, platform, platformVersion ] = this.baseRequest.mmtConstructor;
|
|
71
|
+
try {
|
|
72
|
+
const mmt = new MMTClient(apiKey, platform, platformVersion);
|
|
73
|
+
if (webhook) {
|
|
74
|
+
const response = await mmt.batchTranslate(webhook, sourceLang, targetLang, q, hints, contextVector, batchOptions);
|
|
75
|
+
return { enqueued: response };
|
|
76
|
+
}
|
|
77
|
+
return await mmt.translate(sourceLang, targetLang, q, hints, contextVector, options);
|
|
78
|
+
} catch(error) {
|
|
79
|
+
throw new Error(`${error.toString()}: ${error.response?.body}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
convertTranslationResponse(chunk) {
|
|
84
|
+
if (chunk.enqueued) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return chunk.map(mmtTx => ({
|
|
88
|
+
tgt: mmtTx.translation,
|
|
89
|
+
cost: [ mmtTx.billedCharacters, mmtTx.billed, mmtTx.characters ],
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async continueTranslateChunk(op) {
|
|
94
|
+
return await this.chunkFetcher(op.args);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async info() {
|
|
98
|
+
const info = await super.info();
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch('https://api.modernmt.com/translate/languages');
|
|
101
|
+
if (response.ok) {
|
|
102
|
+
const supportedProviderLanguages = (await response.json()).data.sort();
|
|
103
|
+
info.description.push(styleString`Vendor-supported languages: ${supportedProviderLanguages?.join(', ') ?? 'unknown'}`);
|
|
104
|
+
} else {
|
|
105
|
+
logWarn`HTTP error: status ${response.status} ${response.statusText}`
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
info.description.push(styleString`Unable to connect to MMT server: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
return info;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
2
|
+
"name": "@l10nmonster/helpers-translated",
|
|
3
|
+
"version": "3.0.0-alpha.2",
|
|
4
|
+
"description": "Helpers to integrate with Translated.com",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test"
|
|
9
|
+
},
|
|
10
|
+
"author": "Diego Lagunas",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@l10nmonster/core": "^3.0.0-alpha.0"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@translated/lara": "^1.4.0",
|
|
17
|
+
"modernmt": "^1.2.3"
|
|
18
|
+
}
|
|
17
19
|
}
|
package/translationOS.js
CHANGED
|
@@ -1,270 +1,265 @@
|
|
|
1
|
-
/* eslint-disable camelcase */
|
|
2
|
-
|
|
1
|
+
// /* eslint-disable camelcase */
|
|
2
|
+
// import { L10nContext, utils } from '@l10nmonster/core';
|
|
3
3
|
|
|
4
|
-
function createTUFromTOSTranslation({ tosUnit, content, tuMeta, quality, refreshMode }) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
4
|
+
// function createTUFromTOSTranslation({ tosUnit, content, tuMeta, quality, refreshMode }) {
|
|
5
|
+
// const guid = tosUnit.id_content;
|
|
6
|
+
// !content && (content = tosUnit.translated_content);
|
|
7
|
+
// const tu = {
|
|
8
|
+
// guid,
|
|
9
|
+
// ts: new Date().getTime(), // actual_delivery_date is garbage as it doesn't change after a bugfix, so it's better to use the retrieval time
|
|
10
|
+
// q: quality,
|
|
11
|
+
// th: tosUnit.translated_content_hash, // this is vendor-specific but it's ok to generalize
|
|
12
|
+
// };
|
|
13
|
+
// !refreshMode && (tu.cost = [ tosUnit.total, tosUnit.currency, tosUnit.wc_raw, tosUnit.wc_weighted ]);
|
|
14
|
+
// if (tosUnit.revised_words) {
|
|
15
|
+
// tu.rev = [ tosUnit.revised_words, tosUnit.error_points ?? 0];
|
|
16
|
+
// }
|
|
17
|
+
// if (tuMeta[guid]) {
|
|
18
|
+
// tuMeta[guid].src && (tu.src = tuMeta[guid].src); // TODO: remove this
|
|
19
|
+
// tuMeta[guid].nsrc && (tu.nsrc = tuMeta[guid].nsrc);
|
|
20
|
+
// tu.ntgt = utils.extractNormalizedPartsV1(content, tuMeta[guid].phMap);
|
|
21
|
+
// if (tu.ntgt.filter(e => e === undefined).length > 0) {
|
|
22
|
+
// L10nContext.logger.warn(`Unable to extract normalized parts of TU: ${JSON.stringify(tu)}`);
|
|
23
|
+
// return null;
|
|
24
|
+
// }
|
|
25
|
+
// } else {
|
|
26
|
+
// // simple content doesn't have meta
|
|
27
|
+
// tu.ntgt = [ content ];
|
|
28
|
+
// }
|
|
29
|
+
// return tu;
|
|
30
|
+
// }
|
|
31
31
|
|
|
32
|
-
async function tosRequestTranslationOfChunkOp({ request }) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
32
|
+
// async function tosRequestTranslationOfChunkOp({ request }) {
|
|
33
|
+
// const { url, json, ...options } = request;
|
|
34
|
+
// options.body = JSON.stringify(json);
|
|
35
|
+
// const rawResponse = await fetch(url, options);
|
|
36
|
+
// if (rawResponse.ok) {
|
|
37
|
+
// const response = await (rawResponse.json());
|
|
38
|
+
// const submittedGuids = json.map(tu => tu.id_content);
|
|
39
|
+
// const committedGuids = response.map(contentStatus => contentStatus.id_content);
|
|
40
|
+
// const missingTus = submittedGuids.filter(submittedGuid => !committedGuids.includes(submittedGuid));
|
|
41
|
+
// if (submittedGuids.length !== committedGuids.length || missingTus.length > 0) {
|
|
42
|
+
// L10nContext.logger.error(`sent ${submittedGuids.length} got ${committedGuids.length} missing tus: ${missingTus.map(tu => tu.id_content).join(', ')}`);
|
|
43
|
+
// throw `TOS: inconsistent behavior. submitted ${submittedGuids.length}, committed ${committedGuids.length}, missing ${missingTus.length}`;
|
|
44
|
+
// }
|
|
45
|
+
// return committedGuids;
|
|
46
|
+
// } else {
|
|
47
|
+
// throw `${rawResponse.status} ${rawResponse.statusText}: ${await rawResponse.text()}`;
|
|
48
|
+
// }
|
|
49
|
+
// }
|
|
50
50
|
|
|
51
|
-
async function tosCombineTranslationChunksOp(args, committedGuids) {
|
|
52
|
-
|
|
53
|
-
}
|
|
51
|
+
// async function tosCombineTranslationChunksOp(args, committedGuids) {
|
|
52
|
+
// return committedGuids.flat(1);
|
|
53
|
+
// }
|
|
54
54
|
|
|
55
|
-
async function tosFetchContentByGuidOp({ refreshMode, tuMap, tuMeta, request, quality, parallelism }) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} catch(error) {
|
|
92
|
-
throw `${error.toString()}: ${error.response?.body ?? error.stack}`;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
55
|
+
// async function tosFetchContentByGuidOp({ refreshMode, tuMap, tuMeta, request, quality, parallelism }) {
|
|
56
|
+
// const { url, json, ...options } = request;
|
|
57
|
+
// options.body = JSON.stringify(json);
|
|
58
|
+
// try {
|
|
59
|
+
// let tosContent = await (await fetch(url, options)).json();
|
|
60
|
+
// tosContent = tosContent.filter(tosUnit => tosUnit.translated_content_url);
|
|
61
|
+
// L10nContext.logger.info(`Retrieved ${tosContent.length} translations from TOS`);
|
|
62
|
+
// refreshMode && (tosContent = tosContent.filter(tosUnit => !(tuMap[tosUnit.id_content].th === tosUnit.translated_content_hash))); // need to consider th being undefined/null for some entries
|
|
63
|
+
// // sanitize bad responses
|
|
64
|
+
// const fetchedTus = [];
|
|
65
|
+
// const seenGuids = {};
|
|
66
|
+
// while (tosContent.length > 0) {
|
|
67
|
+
// const chunk = tosContent.splice(0, parallelism);
|
|
68
|
+
// const fetchedRaw = (await Promise.all(chunk.map(tosUnit => fetch(tosUnit.translated_content_url)))).map(async r => await r.text());
|
|
69
|
+
// const fetchedContent = await Promise.all(fetchedRaw);
|
|
70
|
+
// (await Promise.all(chunk.map(tosUnit => fetch(tosUnit.translated_content_url)))).map(async r => await r.text());
|
|
71
|
+
// L10nContext.logger.info(`Fetched ${chunk.length} pieces of content from AWS`);
|
|
72
|
+
// chunk.forEach((tosUnit, idx) => {
|
|
73
|
+
// if (seenGuids[tosUnit.id_content]) {
|
|
74
|
+
// throw `TOS: Duplicate translations found for guid: ${tosUnit.id_content}`;
|
|
75
|
+
// } else {
|
|
76
|
+
// seenGuids[tosUnit.id_content] = true;
|
|
77
|
+
// }
|
|
78
|
+
// if (fetchedContent[idx] !== null && fetchedContent[idx].indexOf('|||UNTRANSLATED_CONTENT_START|||') === -1) {
|
|
79
|
+
// const newTU = createTUFromTOSTranslation({ tosUnit, content: fetchedContent[idx], tuMeta, quality, refreshMode });
|
|
80
|
+
// fetchedTus.push(newTU);
|
|
81
|
+
// } else {
|
|
82
|
+
// L10nContext.logger.info(`TOS: for guid ${tosUnit.id_content} retrieved untranslated content ${fetchedContent[idx]}`);
|
|
83
|
+
// }
|
|
84
|
+
// });
|
|
85
|
+
// }
|
|
86
|
+
// return fetchedTus;
|
|
87
|
+
// } catch(error) {
|
|
88
|
+
// throw `${error.toString()}: ${error.response?.body ?? error.message}`;
|
|
89
|
+
// }
|
|
90
|
+
// }
|
|
95
91
|
|
|
96
|
-
async function tosCombineFetchedTusOp(args, tuChunks) {
|
|
97
|
-
|
|
98
|
-
}
|
|
92
|
+
// async function tosCombineFetchedTusOp(args, tuChunks) {
|
|
93
|
+
// return tuChunks.flat(1).filter(tu => Boolean(tu));
|
|
94
|
+
// }
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
96
|
+
// export class TranslationOS {
|
|
97
|
+
// constructor({ baseURL, apiKey, costAttributionLabel, serviceType, quality, tuDecorator, maxTranslationRequestSize, maxFetchSize, parallelism, requestOnly }) {
|
|
98
|
+
// if ((apiKey && quality) === undefined) {
|
|
99
|
+
// throw 'You must specify apiKey, quality for TranslationOS';
|
|
100
|
+
// } else {
|
|
101
|
+
// this.baseURL = baseURL ?? 'https://api.translated.com/v2';
|
|
102
|
+
// this.stdHeaders = {
|
|
103
|
+
// 'x-api-key': apiKey,
|
|
104
|
+
// 'user-agent': 'l10n.monster/TOS v0.1',
|
|
105
|
+
// 'Content-Type': 'application/json',
|
|
106
|
+
// }
|
|
107
|
+
// this.serviceType = serviceType ?? 'premium';
|
|
108
|
+
// this.costAttributionLabel = costAttributionLabel;
|
|
109
|
+
// this.quality = quality;
|
|
110
|
+
// this.tuDecorator = tuDecorator;
|
|
111
|
+
// this.maxTranslationRequestSize = maxTranslationRequestSize || 100;
|
|
112
|
+
// this.maxFetchSize = maxFetchSize || 512;
|
|
113
|
+
// this.parallelism = parallelism || 128;
|
|
114
|
+
// this.requestOnly = requestOnly;
|
|
115
|
+
// L10nContext.opsMgr.registerOp(tosRequestTranslationOfChunkOp, { idempotent: false });
|
|
116
|
+
// L10nContext.opsMgr.registerOp(tosCombineTranslationChunksOp, { idempotent: true });
|
|
117
|
+
// L10nContext.opsMgr.registerOp(tosFetchContentByGuidOp, { idempotent: true });
|
|
118
|
+
// L10nContext.opsMgr.registerOp(tosCombineFetchedTusOp, { idempotent: true });
|
|
119
|
+
// }
|
|
120
|
+
// }
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
});
|
|
122
|
+
// async requestTranslations(jobRequest) {
|
|
123
|
+
// const { tus, ...jobResponse } = jobRequest;
|
|
124
|
+
// const { contentMap, phNotes } = utils.getTUMaps(tus);
|
|
125
|
+
// const tosPayload = tus.map(tu => {
|
|
126
|
+
// const notes = typeof tu.notes === 'string' ? utils.extractStructuredNotes(tu.notes) : tu.notes;
|
|
127
|
+
// let tosTU = {
|
|
128
|
+
// 'id_order': jobRequest.jobGuid,
|
|
129
|
+
// 'id_content': tu.guid,
|
|
130
|
+
// content: contentMap[tu.guid],
|
|
131
|
+
// metadata: 'mf=v1',
|
|
132
|
+
// context: {
|
|
133
|
+
// notes: `${notes?.maxWidth ? `▶▶▶MAXIMUM WIDTH ${notes.maxWidth} chars◀◀◀\n` : ''}${notes?.desc ?? ''}${phNotes[tu.guid] ?? ''}\n rid: ${tu.rid}\n sid: ${tu.sid}\n ${tu.seq ? `seq: id_${utils.integerToLabel(tu.seq)}` : ''}`
|
|
134
|
+
// },
|
|
135
|
+
// 'source_language': jobRequest.sourceLang,
|
|
136
|
+
// 'target_languages': [ jobRequest.targetLang ],
|
|
137
|
+
// // 'content_type': 'text/html',
|
|
138
|
+
// 'service_type': this.serviceType,
|
|
139
|
+
// 'cost_attribution_label': this.costAttributionLabel,
|
|
140
|
+
// 'dashboard_query_labels': [],
|
|
141
|
+
// };
|
|
142
|
+
// notes?.screenshot && (tosTU.context.screenshot = notes.screenshot);
|
|
143
|
+
// jobRequest.instructions && (tosTU.context.instructions = jobRequest.instructions);
|
|
144
|
+
// tu.seq && tosTU.dashboard_query_labels.push(`id_${utils.integerToLabel(tu.seq)}`);
|
|
145
|
+
// tu.rid && tosTU.dashboard_query_labels.push(tu.rid.slice(-50));
|
|
146
|
+
// tosTU.dashboard_query_labels.push(tu.sid.replaceAll('\n', '').slice(-50));
|
|
147
|
+
// if (tu.prj !== undefined) {
|
|
148
|
+
// tosTU.id_order_group = tu.prj;
|
|
149
|
+
// }
|
|
150
|
+
// if (typeof this.tuDecorator === 'function') {
|
|
151
|
+
// tosTU = this.tuDecorator(tosTU, tu, jobResponse);
|
|
152
|
+
// }
|
|
153
|
+
// return tosTU;
|
|
154
|
+
// });
|
|
160
155
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
156
|
+
// const requestTranslationsTask = L10nContext.opsMgr.createTask();
|
|
157
|
+
// try {
|
|
158
|
+
// let chunkNumber = 0;
|
|
159
|
+
// const chunkOps = [];
|
|
160
|
+
// while (tosPayload.length > 0) {
|
|
161
|
+
// const json = tosPayload.splice(0, this.maxTranslationRequestSize);
|
|
162
|
+
// chunkNumber++;
|
|
163
|
+
// L10nContext.logger.info(`Enqueueing TOS translation job ${jobResponse.jobGuid} chunk size: ${json.length}`);
|
|
164
|
+
// chunkOps.push(requestTranslationsTask.enqueue(tosRequestTranslationOfChunkOp, {
|
|
165
|
+
// request: {
|
|
166
|
+
// url: `${this.baseURL}/translate`,
|
|
167
|
+
// method: 'POST',
|
|
168
|
+
// json,
|
|
169
|
+
// headers: {
|
|
170
|
+
// ...this.stdHeaders,
|
|
171
|
+
// 'x-idempotency-id': `jobGuid:${jobRequest.jobGuid} chunk:${chunkNumber}`,
|
|
172
|
+
// },
|
|
173
|
+
// },
|
|
174
|
+
// }));
|
|
175
|
+
// }
|
|
176
|
+
// requestTranslationsTask.commit(tosCombineTranslationChunksOp, null, chunkOps);
|
|
177
|
+
// jobResponse.taskName = requestTranslationsTask.taskName;
|
|
178
|
+
// const committedGuids = await requestTranslationsTask.execute();
|
|
179
|
+
// if (this.requestOnly) {
|
|
180
|
+
// return {
|
|
181
|
+
// ...jobResponse,
|
|
182
|
+
// tus: [],
|
|
183
|
+
// status: 'done'
|
|
184
|
+
// };
|
|
185
|
+
// }
|
|
186
|
+
// return {
|
|
187
|
+
// ...jobResponse,
|
|
188
|
+
// inflight: committedGuids,
|
|
189
|
+
// status: 'pending'
|
|
190
|
+
// };
|
|
191
|
+
// } catch (error) {
|
|
192
|
+
// throw `TOS call failed - ${error}`;
|
|
193
|
+
// }
|
|
194
|
+
// }
|
|
200
195
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
196
|
+
// async #fetchTranslatedTus({ jobGuid, targetLang, reqTus, refreshMode }) {
|
|
197
|
+
// const guids = reqTus.filter(tu => tu.src ?? tu.nsrc).map(tu => tu.guid); // TODO: remove .src
|
|
198
|
+
// const refreshTranslationsTask = L10nContext.opsMgr.createTask();
|
|
199
|
+
// let chunkNumber = 0;
|
|
200
|
+
// const refreshOps = [];
|
|
201
|
+
// while (guids.length > 0) {
|
|
202
|
+
// chunkNumber++;
|
|
203
|
+
// const guidsInChunk = guids.splice(0, this.maxFetchSize);
|
|
204
|
+
// const tusInChunk = reqTus.filter(tu => guidsInChunk.includes(tu.guid));
|
|
205
|
+
// const tuMap = tusInChunk.reduce((p,c) => (p[c.guid] = c, p), {});
|
|
206
|
+
// const { tuMeta } = utils.getTUMaps(tusInChunk);
|
|
207
|
+
// L10nContext.logger.verbose(`Enqueueing refresh of TOS chunk ${chunkNumber} (${guidsInChunk.length} units)...`);
|
|
208
|
+
// const json = {
|
|
209
|
+
// id_content: guidsInChunk,
|
|
210
|
+
// target_language: targetLang,
|
|
211
|
+
// fetch_content: false,
|
|
212
|
+
// limit: this.maxFetchSize,
|
|
213
|
+
// };
|
|
214
|
+
// if (refreshMode) {
|
|
215
|
+
// json.status = ['delivered', 'invoiced'];
|
|
216
|
+
// json.last_delivered_only = true;
|
|
217
|
+
// } else {
|
|
218
|
+
// json.id_order = jobGuid;
|
|
219
|
+
// }
|
|
220
|
+
// refreshOps.push(refreshTranslationsTask.enqueue(tosFetchContentByGuidOp, {
|
|
221
|
+
// refreshMode,
|
|
222
|
+
// tuMap,
|
|
223
|
+
// tuMeta,
|
|
224
|
+
// request: {
|
|
225
|
+
// url: `${this.baseURL}/status`,
|
|
226
|
+
// method: 'POST',
|
|
227
|
+
// json,
|
|
228
|
+
// headers: this.stdHeaders,
|
|
229
|
+
// },
|
|
230
|
+
// quality: this.quality,
|
|
231
|
+
// parallelism: this.parallelism,
|
|
232
|
+
// }));
|
|
233
|
+
// }
|
|
234
|
+
// refreshTranslationsTask.commit(tosCombineFetchedTusOp, null, refreshOps);
|
|
235
|
+
// const jobResponse = await refreshTranslationsTask.execute();
|
|
236
|
+
// jobResponse.taskName = refreshTranslationsTask.taskName;
|
|
237
|
+
// return jobResponse;
|
|
238
|
+
// }
|
|
244
239
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
240
|
+
// async fetchTranslations(pendingJob) {
|
|
241
|
+
// const { inflight, ...jobResponse } = pendingJob;
|
|
242
|
+
// const reqTus = pendingJob.tus.filter(tu => inflight.includes(tu.guid));
|
|
243
|
+
// const tus = await this.#fetchTranslatedTus({ jobGuid: pendingJob.originalJobGuid ?? pendingJob.originalJobGuid ?? pendingJob.jobGuid, targetLang: pendingJob.targetLang, reqTus, refreshMode: false });
|
|
244
|
+
// const tuMap = tus.reduce((p,c) => (p[c.guid] = c, p), {});
|
|
245
|
+
// const nowInflight = inflight.filter(guid => !tuMap[guid]);
|
|
246
|
+
// if (tus.length > 0) {
|
|
247
|
+
// const response = {
|
|
248
|
+
// ...jobResponse,
|
|
249
|
+
// tus,
|
|
250
|
+
// status: nowInflight.length === 0 ? 'done' : 'pending',
|
|
251
|
+
// };
|
|
252
|
+
// nowInflight.length > 0 && (response.inflight = nowInflight);
|
|
253
|
+
// return response;
|
|
254
|
+
// }
|
|
255
|
+
// return null;
|
|
256
|
+
// }
|
|
262
257
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
258
|
+
// async refreshTranslations(jobRequest) {
|
|
259
|
+
// return {
|
|
260
|
+
// ...jobRequest,
|
|
261
|
+
// tus: await this.#fetchTranslatedTus({ jobGuid: jobRequest.originalJobGuid, targetLang: jobRequest.originalJobGuid ?? jobRequest.targetLang, reqTus: jobRequest.tus, refreshMode: true }),
|
|
262
|
+
// status: 'done',
|
|
263
|
+
// };
|
|
264
|
+
// }
|
|
265
|
+
// }
|
package/modernMT.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-invalid-this */
|
|
2
|
-
const { utils, normalizers } = require('@l10nmonster/helpers');
|
|
3
|
-
const { ModernMT } = require('modernmt');
|
|
4
|
-
|
|
5
|
-
const MAX_CHAR_LENGTH = 9900;
|
|
6
|
-
const MAX_CHUNK_SIZE = 125;
|
|
7
|
-
|
|
8
|
-
async function mmtTranslateChunkOp({ q, batchOptions }) {
|
|
9
|
-
const baseRequest = this.context.baseRequest;
|
|
10
|
-
try {
|
|
11
|
-
const mmt = new ModernMT(...baseRequest.mmtConstructor);
|
|
12
|
-
if (batchOptions) {
|
|
13
|
-
const response = await mmt.batchTranslate(
|
|
14
|
-
baseRequest.webhook,
|
|
15
|
-
baseRequest.sourceLang,
|
|
16
|
-
baseRequest.targetLang,
|
|
17
|
-
q,
|
|
18
|
-
baseRequest.hints,
|
|
19
|
-
undefined,
|
|
20
|
-
{
|
|
21
|
-
...baseRequest.options,
|
|
22
|
-
...batchOptions
|
|
23
|
-
}
|
|
24
|
-
);
|
|
25
|
-
return {
|
|
26
|
-
enqueued: response
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
const response = await mmt.translate(
|
|
30
|
-
baseRequest.sourceLang,
|
|
31
|
-
baseRequest.targetLang,
|
|
32
|
-
q,
|
|
33
|
-
baseRequest.hints,
|
|
34
|
-
undefined,
|
|
35
|
-
baseRequest.options
|
|
36
|
-
);
|
|
37
|
-
return response;
|
|
38
|
-
}
|
|
39
|
-
} catch(error) {
|
|
40
|
-
throw `${error.toString()}: ${error.response?.body}`;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function mmtMergeSubmittedChunksOp(args, chunks) {
|
|
45
|
-
chunks.forEach((response, idx) => l10nmonster.logger.verbose(`MMT Chunk ${idx} enqueued=${response.enqueued}`));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function mmtMergeTranslatedChunksOp({ jobRequest, tuMeta, quality, ts, chunkSizes }, chunks) {
|
|
49
|
-
chunks.forEach((chunk, idx) => {
|
|
50
|
-
if (chunk.length !== chunkSizes[idx]) {
|
|
51
|
-
throw `MMT: Expected chunk ${idx} to have ${chunkSizes[idx]} translations but got ${chunk.length}`;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
const { tus, ...jobResponse } = jobRequest;
|
|
55
|
-
const translations = chunks.flat(1);
|
|
56
|
-
jobResponse.tus = tus.map((tu, idx) => {
|
|
57
|
-
const translation = { guid: tu.guid, ts };
|
|
58
|
-
const mmtTx = translations[idx] || {};
|
|
59
|
-
translation.ntgt = utils.extractNormalizedPartsFromXmlV1(mmtTx.translation, tuMeta[idx] || {});
|
|
60
|
-
translation.q = quality;
|
|
61
|
-
translation.cost = [ mmtTx.billedCharacters, mmtTx.billed, mmtTx.characters ];
|
|
62
|
-
return translation;
|
|
63
|
-
});
|
|
64
|
-
jobResponse.status = 'done';
|
|
65
|
-
return jobResponse;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function applyGlossary(glossaryEncoder, jobResponse) {
|
|
69
|
-
if (glossaryEncoder) {
|
|
70
|
-
const flags = { targetLang: jobResponse.targetLang };
|
|
71
|
-
for (const tu of jobResponse.tus) {
|
|
72
|
-
tu.ntgt.forEach((part, idx) => {
|
|
73
|
-
// not very solid, but if placeholder follows glossary conventions, then convert it back to a string
|
|
74
|
-
if (typeof part === 'object' && part.v.indexOf('glossary:') === 0) {
|
|
75
|
-
tu.ntgt[idx] = glossaryEncoder(part.v, flags);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
tu.ntgt = utils.consolidateDecodedParts(tu.ntgt, flags, true);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = class ModernMT {
|
|
84
|
-
constructor({ baseURL, apiKey, webhook, chunkFetcher, hints, multiline, quality, maxCharLength, languageMapper, glossary }) {
|
|
85
|
-
if ((apiKey && quality) === undefined) {
|
|
86
|
-
throw 'You must specify apiKey, quality for ModernMT';
|
|
87
|
-
} else {
|
|
88
|
-
if (webhook && !chunkFetcher) {
|
|
89
|
-
throw 'If you specify a webhook you must also specify a chunkFetcher';
|
|
90
|
-
}
|
|
91
|
-
this.baseURL = baseURL ?? 'https://api.modernmt.com';
|
|
92
|
-
this.mmtConstructor = [ apiKey, 'l10n.monster/MMT', '1.0' ];
|
|
93
|
-
this.webhook = webhook;
|
|
94
|
-
this.chunkFetcher = chunkFetcher;
|
|
95
|
-
chunkFetcher && l10nmonster.opsMgr.registerOp(chunkFetcher, { idempotent: true });
|
|
96
|
-
this.hints = hints;
|
|
97
|
-
this.multiline = multiline ?? true;
|
|
98
|
-
this.quality = quality;
|
|
99
|
-
this.maxCharLength = maxCharLength ?? MAX_CHAR_LENGTH;
|
|
100
|
-
this.languageMapper = languageMapper;
|
|
101
|
-
if (glossary) {
|
|
102
|
-
const [ glossaryDecoder, glossaryEncoder ] = normalizers.keywordTranslatorMaker('glossary', glossary);
|
|
103
|
-
this.glossaryDecoder = [ glossaryDecoder ];
|
|
104
|
-
this.glossaryEncoder = glossaryEncoder;
|
|
105
|
-
}
|
|
106
|
-
l10nmonster.opsMgr.registerOp(mmtTranslateChunkOp, { idempotent: false });
|
|
107
|
-
l10nmonster.opsMgr.registerOp(mmtMergeSubmittedChunksOp, { idempotent: true });
|
|
108
|
-
l10nmonster.opsMgr.registerOp(mmtMergeTranslatedChunksOp, { idempotent: true });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async requestTranslations(jobRequest) {
|
|
113
|
-
const sourceLang = (this.languageMapper && this.languageMapper(jobRequest.sourceLang)) ?? jobRequest.sourceLang;
|
|
114
|
-
const targetLang = (this.languageMapper && this.languageMapper(jobRequest.targetLang)) ?? jobRequest.targetLang;
|
|
115
|
-
const tuMeta = {};
|
|
116
|
-
const mmtPayload = jobRequest.tus.map((tu, idx) => {
|
|
117
|
-
const nsrc = utils.decodeNormalizedString(tu.nsrc, this.glossaryDecoder);
|
|
118
|
-
const [xmlSrc, phMap ] = utils.flattenNormalizedSourceToXmlV1(nsrc);
|
|
119
|
-
if (Object.keys(phMap).length > 0) {
|
|
120
|
-
tuMeta[idx] = phMap;
|
|
121
|
-
}
|
|
122
|
-
return xmlSrc;
|
|
123
|
-
});
|
|
124
|
-
const context = {
|
|
125
|
-
baseRequest: {
|
|
126
|
-
mmtConstructor: this.mmtConstructor,
|
|
127
|
-
sourceLang,
|
|
128
|
-
targetLang,
|
|
129
|
-
hints: this.hints,
|
|
130
|
-
options: {
|
|
131
|
-
multiline: this.multiline,
|
|
132
|
-
format: 'text/xml',
|
|
133
|
-
},
|
|
134
|
-
webhook: this.webhook,
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
const requestTranslationsTask = l10nmonster.opsMgr.createTask();
|
|
138
|
-
requestTranslationsTask.setContext(context);
|
|
139
|
-
const chunkOps = [];
|
|
140
|
-
const chunkSizes = [];
|
|
141
|
-
for (let currentIdx = 0; currentIdx < mmtPayload.length;) {
|
|
142
|
-
const q = [];
|
|
143
|
-
let currentTotalLength = 0;
|
|
144
|
-
while (currentIdx < mmtPayload.length && q.length < MAX_CHUNK_SIZE && mmtPayload[currentIdx].length + currentTotalLength < this.maxCharLength) {
|
|
145
|
-
currentTotalLength += mmtPayload[currentIdx].length;
|
|
146
|
-
q.push(mmtPayload[currentIdx]);
|
|
147
|
-
currentIdx++;
|
|
148
|
-
}
|
|
149
|
-
if (q.length === 0) {
|
|
150
|
-
throw `String at index ${currentIdx} exceeds ${this.maxCharLength} max char length`;
|
|
151
|
-
}
|
|
152
|
-
l10nmonster.logger.info(`Preparing MMT translate: chunk strings: ${q.length} chunk char length: ${currentTotalLength}`);
|
|
153
|
-
const req = { q };
|
|
154
|
-
if (this.webhook) {
|
|
155
|
-
req.batchOptions = {
|
|
156
|
-
idempotencyKey: `jobGuid:${jobRequest.jobGuid} chunk:${chunkOps.length}`,
|
|
157
|
-
metadata: {
|
|
158
|
-
jobGuid: jobRequest.jobGuid,
|
|
159
|
-
chunk: chunkOps.length
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
chunkSizes.push(q.length);
|
|
164
|
-
chunkOps.push(requestTranslationsTask.enqueue(mmtTranslateChunkOp, req));
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
if (this.webhook) {
|
|
168
|
-
requestTranslationsTask.commit(mmtMergeSubmittedChunksOp, null, chunkOps);
|
|
169
|
-
await requestTranslationsTask.execute();
|
|
170
|
-
const { tus, ...jobResponse } = jobRequest;
|
|
171
|
-
jobResponse.inflight = tus.map(tu => tu.guid);
|
|
172
|
-
jobResponse.envelope = { chunkSizes, tuMeta };
|
|
173
|
-
jobResponse.status = 'pending';
|
|
174
|
-
jobResponse.taskName = l10nmonster.regression ? 'x' : requestTranslationsTask.taskName;
|
|
175
|
-
return jobResponse;
|
|
176
|
-
} else {
|
|
177
|
-
requestTranslationsTask.commit(mmtMergeTranslatedChunksOp, {
|
|
178
|
-
jobRequest,
|
|
179
|
-
tuMeta,
|
|
180
|
-
quality: this.quality,
|
|
181
|
-
ts: l10nmonster.regression ? 1 : new Date().getTime(),
|
|
182
|
-
chunkSizes,
|
|
183
|
-
}, chunkOps);
|
|
184
|
-
const jobResponse = await requestTranslationsTask.execute();
|
|
185
|
-
jobResponse.taskName = l10nmonster.regression ? 'x' : requestTranslationsTask.taskName;
|
|
186
|
-
applyGlossary(this.glossaryEncoder, jobResponse);
|
|
187
|
-
return jobResponse;
|
|
188
|
-
}
|
|
189
|
-
} catch (error) {
|
|
190
|
-
throw `MMT call failed - ${error}`;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async fetchTranslations(pendingJob, jobRequest) {
|
|
195
|
-
try {
|
|
196
|
-
// eslint-disable-next-line no-unused-vars
|
|
197
|
-
const requestTranslationsTask = l10nmonster.opsMgr.createTask();
|
|
198
|
-
const chunkOps = [];
|
|
199
|
-
pendingJob.envelope.chunkSizes.forEach(async (chunkSize, chunk) => {
|
|
200
|
-
l10nmonster.logger.info(`Enqueue chunk fetcher for job: ${jobRequest.jobGuid} chunk:${chunk} chunkSize:${chunkSize}`);
|
|
201
|
-
chunkOps.push(requestTranslationsTask.enqueue(this.chunkFetcher, {
|
|
202
|
-
jobGuid: jobRequest.jobGuid,
|
|
203
|
-
chunk,
|
|
204
|
-
chunkSize,
|
|
205
|
-
}));
|
|
206
|
-
});
|
|
207
|
-
requestTranslationsTask.commit(mmtMergeTranslatedChunksOp, {
|
|
208
|
-
jobRequest,
|
|
209
|
-
tuMeta: pendingJob.envelope.tuMeta,
|
|
210
|
-
quality: this.quality,
|
|
211
|
-
ts: l10nmonster.regression ? 1 : new Date().getTime(),
|
|
212
|
-
chunkSizes: pendingJob.envelope.chunkSizes,
|
|
213
|
-
}, chunkOps);
|
|
214
|
-
const jobResponse = await requestTranslationsTask.execute();
|
|
215
|
-
jobResponse.taskName = l10nmonster.regression ? 'x' : requestTranslationsTask.taskName;
|
|
216
|
-
applyGlossary(this.glossaryEncoder, jobResponse);
|
|
217
|
-
return jobResponse;
|
|
218
|
-
} catch (error) {
|
|
219
|
-
return null; // getting errors is expected, just leave the job pending
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async refreshTranslations(jobRequest) {
|
|
224
|
-
if (this.webhook) {
|
|
225
|
-
throw 'Refreshing MMT translations not supported in batch mode';
|
|
226
|
-
}
|
|
227
|
-
const fullResponse = await this.requestTranslations(jobRequest);
|
|
228
|
-
const reqTuMap = jobRequest.tus.reduce((p,c) => (p[c.guid] = c, p), {});
|
|
229
|
-
return {
|
|
230
|
-
...fullResponse,
|
|
231
|
-
tus: fullResponse.tus.filter(tu => !utils.normalizedStringsAreEqual(reqTuMap[tu.guid].ntgt, tu.ntgt)),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|