@l10nmonster/helpers-translated 1.0.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/index.js +2 -0
- package/modernMT.js +234 -0
- package/package.json +17 -0
- package/translationOS.js +270 -0
package/index.js
ADDED
package/modernMT.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
|
17
|
+
}
|
package/translationOS.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
const { utils } = require('@l10nmonster/helpers');
|
|
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
|
+
}
|
|
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
|
+
}
|
|
50
|
+
|
|
51
|
+
async function tosCombineTranslationChunksOp(args, committedGuids) {
|
|
52
|
+
return committedGuids.flat(1);
|
|
53
|
+
}
|
|
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
|
+
}
|
|
95
|
+
|
|
96
|
+
async function tosCombineFetchedTusOp(args, tuChunks) {
|
|
97
|
+
return tuChunks.flat(1).filter(tu => Boolean(tu));
|
|
98
|
+
}
|
|
99
|
+
|
|
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
|
+
}
|
|
125
|
+
|
|
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
|
+
});
|
|
160
|
+
|
|
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
|
+
}
|
|
200
|
+
|
|
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
|
+
}
|
|
244
|
+
|
|
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
|
+
}
|
|
262
|
+
|
|
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
|
+
}
|