@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 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
- exports.TranslationOS = require('./translationOS');
2
- exports.ModernMT = require('./modernMT');
1
+ // export { TranslationOS } from './translationOS.js';
2
+ export { MMTProvider } from './mmtProvider.js';
3
+ export { LaraProvider } from './laraProvider.js';
@@ -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
- "name": "@l10nmonster/helpers-translated",
3
- "version": "1.0.0",
4
- "description": "Translators integrating with Translated.com",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "author": "Diego Lagunas",
10
- "license": "MIT",
11
- "peerDependencies": {
12
- "@l10nmonster/helpers": "^1"
13
- },
14
- "dependencies": {
15
- "modernmt": "^1.2.3"
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
- const { utils } = require('@l10nmonster/helpers');
1
+ // /* eslint-disable camelcase */
2
+ // import { L10nContext, utils } from '@l10nmonster/core';
3
3
 
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
- l10nmonster.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
- }
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
- 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
- l10nmonster.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
- }
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
- return committedGuids.flat(1);
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
- 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
- // eslint-disable-next-line no-invalid-this
62
- l10nmonster.logger.info(`Retrieved ${tosContent.length} translations from TOS`);
63
- refreshMode && (tosContent = tosContent.filter(tosUnit => !(tuMap[tosUnit.id_content].th === tosUnit.translated_content_hash))); // need to consider th being undefined/null for some entries
64
- // sanitize bad responses
65
- const fetchedTus = [];
66
- const seenGuids = {};
67
- while (tosContent.length > 0) {
68
- const chunk = tosContent.splice(0, parallelism);
69
- const fetchedRaw = (await Promise.all(chunk.map(tosUnit => fetch(tosUnit.translated_content_url)))).map(async r => await r.text());
70
- const fetchedContent = await Promise.all(fetchedRaw);
71
- (await Promise.all(chunk.map(tosUnit => fetch(tosUnit.translated_content_url)))).map(async r => await r.text());
72
- // eslint-disable-next-line no-invalid-this
73
- l10nmonster.logger.info(`Fetched ${chunk.length} pieces of content from AWS`);
74
- chunk.forEach((tosUnit, idx) => {
75
- if (seenGuids[tosUnit.id_content]) {
76
- throw `TOS: Duplicate translations found for guid: ${tosUnit.id_content}`;
77
- } else {
78
- seenGuids[tosUnit.id_content] = true;
79
- }
80
- if (fetchedContent[idx] !== null && fetchedContent[idx].indexOf('|||UNTRANSLATED_CONTENT_START|||') === -1) {
81
- // eslint-disable-next-line no-invalid-this
82
- const newTU = createTUFromTOSTranslation({ tosUnit, content: fetchedContent[idx], tuMeta, quality, refreshMode });
83
- fetchedTus.push(newTU);
84
- } else {
85
- // eslint-disable-next-line no-invalid-this
86
- l10nmonster.logger.info(`TOS: for guid ${tosUnit.id_content} retrieved untranslated content ${fetchedContent[idx]}`);
87
- }
88
- });
89
- }
90
- return fetchedTus;
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
- return tuChunks.flat(1).filter(tu => Boolean(tu));
98
- }
92
+ // async function tosCombineFetchedTusOp(args, tuChunks) {
93
+ // return tuChunks.flat(1).filter(tu => Boolean(tu));
94
+ // }
99
95
 
100
- module.exports = class TranslationOS {
101
- constructor({ baseURL, apiKey, costAttributionLabel, serviceType, quality, tuDecorator, maxTranslationRequestSize, maxFetchSize, parallelism, requestOnly }) {
102
- if ((apiKey && quality) === undefined) {
103
- throw 'You must specify apiKey, quality for TranslationOS';
104
- } else {
105
- this.baseURL = baseURL ?? 'https://api.translated.com/v2';
106
- this.stdHeaders = {
107
- 'x-api-key': apiKey,
108
- 'user-agent': 'l10n.monster/TOS v0.1',
109
- 'Content-Type': 'application/json',
110
- }
111
- this.serviceType = serviceType ?? 'premium';
112
- this.costAttributionLabel = costAttributionLabel;
113
- this.quality = quality;
114
- this.tuDecorator = tuDecorator;
115
- this.maxTranslationRequestSize = maxTranslationRequestSize || 100;
116
- this.maxFetchSize = maxFetchSize || 512;
117
- this.parallelism = parallelism || 128;
118
- this.requestOnly = requestOnly;
119
- l10nmonster.opsMgr.registerOp(tosRequestTranslationOfChunkOp, { idempotent: false });
120
- l10nmonster.opsMgr.registerOp(tosCombineTranslationChunksOp, { idempotent: true });
121
- l10nmonster.opsMgr.registerOp(tosFetchContentByGuidOp, { idempotent: true });
122
- l10nmonster.opsMgr.registerOp(tosCombineFetchedTusOp, { idempotent: true });
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
- async requestTranslations(jobRequest) {
127
- const { tus, ...jobResponse } = jobRequest;
128
- const { contentMap, phNotes } = utils.getTUMaps(tus);
129
- const tosPayload = tus.map(tu => {
130
- const notes = typeof tu.notes === 'string' ? utils.extractStructuredNotes(tu.notes) : tu.notes;
131
- let tosTU = {
132
- 'id_order': jobRequest.jobGuid,
133
- 'id_content': tu.guid,
134
- content: contentMap[tu.guid],
135
- metadata: 'mf=v1',
136
- context: {
137
- 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)}` : ''}`
138
- },
139
- 'source_language': jobRequest.sourceLang,
140
- 'target_languages': [ jobRequest.targetLang ],
141
- // 'content_type': 'text/html',
142
- 'service_type': this.serviceType,
143
- 'cost_attribution_label': this.costAttributionLabel,
144
- 'dashboard_query_labels': [],
145
- };
146
- notes?.screenshot && (tosTU.context.screenshot = notes.screenshot);
147
- jobRequest.instructions && (tosTU.context.instructions = jobRequest.instructions);
148
- tu.seq && tosTU.dashboard_query_labels.push(`id_${utils.integerToLabel(tu.seq)}`);
149
- tu.rid && tosTU.dashboard_query_labels.push(tu.rid.slice(-50));
150
- tosTU.dashboard_query_labels.push(tu.sid.replaceAll('\n', '').slice(-50));
151
- if (tu.prj !== undefined) {
152
- // eslint-disable-next-line camelcase
153
- tosTU.id_order_group = tu.prj;
154
- }
155
- if (typeof this.tuDecorator === 'function') {
156
- tosTU = this.tuDecorator(tosTU, tu, jobResponse);
157
- }
158
- return tosTU;
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
- const requestTranslationsTask = l10nmonster.opsMgr.createTask();
162
- try {
163
- let chunkNumber = 0;
164
- const chunkOps = [];
165
- while (tosPayload.length > 0) {
166
- const json = tosPayload.splice(0, this.maxTranslationRequestSize);
167
- chunkNumber++;
168
- l10nmonster.logger.info(`Enqueueing TOS translation job ${jobResponse.jobGuid} chunk size: ${json.length}`);
169
- chunkOps.push(requestTranslationsTask.enqueue(tosRequestTranslationOfChunkOp, {
170
- request: {
171
- url: `${this.baseURL}/translate`,
172
- method: 'POST',
173
- json,
174
- headers: {
175
- ...this.stdHeaders,
176
- 'x-idempotency-id': `jobGuid:${jobRequest.jobGuid} chunk:${chunkNumber}`,
177
- },
178
- },
179
- }));
180
- }
181
- requestTranslationsTask.commit(tosCombineTranslationChunksOp, null, chunkOps);
182
- jobResponse.taskName = requestTranslationsTask.taskName;
183
- const committedGuids = await requestTranslationsTask.execute();
184
- if (this.requestOnly) {
185
- return {
186
- ...jobResponse,
187
- tus: [],
188
- status: 'done'
189
- };
190
- }
191
- return {
192
- ...jobResponse,
193
- inflight: committedGuids,
194
- status: 'pending'
195
- };
196
- } catch (error) {
197
- throw `TOS call failed - ${error}`;
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
- async #fetchTranslatedTus({ jobGuid, targetLang, reqTus, refreshMode }) {
202
- const guids = reqTus.filter(tu => tu.src ?? tu.nsrc).map(tu => tu.guid); // TODO: remove .src
203
- const refreshTranslationsTask = l10nmonster.opsMgr.createTask();
204
- let chunkNumber = 0;
205
- const refreshOps = [];
206
- while (guids.length > 0) {
207
- chunkNumber++;
208
- const guidsInChunk = guids.splice(0, this.maxFetchSize);
209
- const tusInChunk = reqTus.filter(tu => guidsInChunk.includes(tu.guid));
210
- const tuMap = tusInChunk.reduce((p,c) => (p[c.guid] = c, p), {});
211
- const { tuMeta } = utils.getTUMaps(tusInChunk);
212
- l10nmonster.logger.verbose(`Enqueueing refresh of TOS chunk ${chunkNumber} (${guidsInChunk.length} units)...`);
213
- const json = {
214
- id_content: guidsInChunk,
215
- target_language: targetLang,
216
- fetch_content: false,
217
- limit: this.maxFetchSize,
218
- };
219
- if (refreshMode) {
220
- json.status = ['delivered', 'invoiced'];
221
- json.last_delivered_only = true;
222
- } else {
223
- json.id_order = jobGuid;
224
- }
225
- refreshOps.push(refreshTranslationsTask.enqueue(tosFetchContentByGuidOp, {
226
- refreshMode,
227
- tuMap,
228
- tuMeta,
229
- request: {
230
- url: `${this.baseURL}/status`,
231
- method: 'POST',
232
- json,
233
- headers: this.stdHeaders,
234
- },
235
- quality: this.quality,
236
- parallelism: this.parallelism,
237
- }));
238
- }
239
- refreshTranslationsTask.commit(tosCombineFetchedTusOp, null, refreshOps);
240
- const jobResponse = await refreshTranslationsTask.execute();
241
- jobResponse.taskName = refreshTranslationsTask.taskName;
242
- return jobResponse;
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
- async fetchTranslations(pendingJob, jobRequest) {
246
- const { inflight, ...jobResponse } = pendingJob;
247
- const reqTus = jobRequest.tus.filter(tu => inflight.includes(tu.guid));
248
- const tus = await this.#fetchTranslatedTus({ jobGuid: pendingJob.originalJobGuid ?? jobRequest.originalJobGuid ?? jobRequest.jobGuid, targetLang: jobRequest.targetLang, reqTus, refreshMode: false });
249
- const tuMap = tus.reduce((p,c) => (p[c.guid] = c, p), {});
250
- const nowInflight = inflight.filter(guid => !tuMap[guid]);
251
- if (tus.length > 0) {
252
- const response = {
253
- ...jobResponse,
254
- tus,
255
- status: nowInflight.length === 0 ? 'done' : 'pending',
256
- };
257
- nowInflight.length > 0 && (response.inflight = nowInflight);
258
- return response;
259
- }
260
- return null;
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
- async refreshTranslations(jobRequest) {
264
- return {
265
- ...jobRequest,
266
- tus: await this.#fetchTranslatedTus({ targetLang: jobRequest.originalJobGuid ?? jobRequest.targetLang, reqTus: jobRequest.tus, refreshMode: true }),
267
- status: 'done',
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
- }