@diplodoc/cli 4.13.4 → 4.13.6
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/assets/vendor.js +1 -1
- package/build/index.js +175 -287
- package/build/index.js.map +4 -4
- package/package.json +4 -4
- package/src/cmd/translate/handler.ts +311 -0
- package/src/cmd/translate/index.ts +14 -320
- package/src/cmd/xliff/compose.ts +71 -177
- package/src/cmd/xliff/extract.ts +97 -168
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"author": "Yandex Data UI Team <data-ui@yandex-team.ru>",
|
|
4
4
|
"description": "Make documentation using yfm-docs in Markdown and HTML formats",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "4.13.
|
|
6
|
+
"version": "4.13.6",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git@github.com:diplodoc-platform/cli.git"
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@aws-sdk/client-s3": "^3.369.0",
|
|
34
|
-
"@diplodoc/client": "^2.0.
|
|
35
|
-
"@diplodoc/latex-extension": "^1.0
|
|
36
|
-
"@diplodoc/markdown-translation": "^2.0.0-
|
|
34
|
+
"@diplodoc/client": "^2.0.5",
|
|
35
|
+
"@diplodoc/latex-extension": "^1.1.0",
|
|
36
|
+
"@diplodoc/markdown-translation": "^2.0.0-beta-1",
|
|
37
37
|
"@diplodoc/mermaid-extension": "^1.2.1",
|
|
38
38
|
"@diplodoc/openapi-extension": "^1.4.13",
|
|
39
39
|
"@diplodoc/transform": "^4.8.2",
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import {Arguments} from 'yargs';
|
|
2
|
+
import {ArgvService} from '../../services';
|
|
3
|
+
import {logger} from '../../utils';
|
|
4
|
+
import {ok} from 'assert';
|
|
5
|
+
import {basename, dirname, join, resolve} from 'path';
|
|
6
|
+
import glob from 'glob';
|
|
7
|
+
import {getYandexOAuthToken} from '../../packages/credentials';
|
|
8
|
+
import {asyncify, eachLimit, retry} from 'async';
|
|
9
|
+
import {Session} from '@yandex-cloud/nodejs-sdk/dist/session';
|
|
10
|
+
import {TranslationServiceClient} from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/service_clients';
|
|
11
|
+
import {
|
|
12
|
+
TranslateRequest_Format as Format,
|
|
13
|
+
TranslateRequest,
|
|
14
|
+
} from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/ai/translate/v2/translation_service';
|
|
15
|
+
import {mkdir, readFile, writeFile} from 'fs/promises';
|
|
16
|
+
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
import {compose, extract} from '@diplodoc/markdown-translation';
|
|
19
|
+
|
|
20
|
+
const REQUESTS_LIMIT = 20;
|
|
21
|
+
const BYTES_LIMIT = 10000;
|
|
22
|
+
const RETRY_LIMIT = 3;
|
|
23
|
+
|
|
24
|
+
class TranslatorError extends Error {
|
|
25
|
+
path: string;
|
|
26
|
+
|
|
27
|
+
constructor(message: string, path: string) {
|
|
28
|
+
super(message);
|
|
29
|
+
|
|
30
|
+
this.path = path;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type TranslateConfig = {
|
|
35
|
+
input: string;
|
|
36
|
+
output?: string;
|
|
37
|
+
sourceLanguage: string;
|
|
38
|
+
targetLanguage: string;
|
|
39
|
+
folderId?: string;
|
|
40
|
+
glossary?: string;
|
|
41
|
+
include?: string[];
|
|
42
|
+
exclude?: string[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function handler(args: Arguments<any>) {
|
|
46
|
+
ArgvService.init({
|
|
47
|
+
...(args.translate || {}),
|
|
48
|
+
...args,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
folderId,
|
|
53
|
+
// yandexCloudTranslateGlossaryPairs,
|
|
54
|
+
sourceLanguage,
|
|
55
|
+
targetLanguage,
|
|
56
|
+
exclude = [],
|
|
57
|
+
} = ArgvService.getConfig() as unknown as TranslateConfig;
|
|
58
|
+
|
|
59
|
+
let {input, output, include = []} = ArgvService.getConfig() as unknown as TranslateConfig;
|
|
60
|
+
|
|
61
|
+
logger.info(
|
|
62
|
+
input,
|
|
63
|
+
`translating documentation from ${sourceLanguage} to ${targetLanguage} language`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
output = output || input;
|
|
67
|
+
|
|
68
|
+
ok(input, 'Required param input is not configured');
|
|
69
|
+
ok(sourceLanguage, 'Required param sourceLanguage is not configured');
|
|
70
|
+
ok(targetLanguage, 'Required param targetLanguage is not configured');
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (input.endsWith('.md')) {
|
|
74
|
+
include = [basename(input)];
|
|
75
|
+
input = dirname(input);
|
|
76
|
+
} else if (!include.length) {
|
|
77
|
+
include.push('**/*');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const files = ([] as string[]).concat(
|
|
81
|
+
...include.map((match) =>
|
|
82
|
+
glob.sync(match, {
|
|
83
|
+
cwd: join(input, sourceLanguage),
|
|
84
|
+
ignore: exclude,
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
const found = [...new Set(files)];
|
|
89
|
+
|
|
90
|
+
const oauthToken = await getYandexOAuthToken();
|
|
91
|
+
|
|
92
|
+
const translatorParams = {
|
|
93
|
+
input,
|
|
94
|
+
output,
|
|
95
|
+
sourceLanguage,
|
|
96
|
+
targetLanguage,
|
|
97
|
+
// yandexCloudTranslateGlossaryPairs,
|
|
98
|
+
folderId,
|
|
99
|
+
oauthToken,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const translateFn = translator(translatorParams);
|
|
103
|
+
|
|
104
|
+
await eachLimit(found, REQUESTS_LIMIT, asyncify(translateFn));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof Error || err instanceof TranslatorError) {
|
|
107
|
+
const message = err.message;
|
|
108
|
+
|
|
109
|
+
const file = err instanceof TranslatorError ? err.path : '';
|
|
110
|
+
|
|
111
|
+
logger.error(file, message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
logger.info(
|
|
116
|
+
output,
|
|
117
|
+
`translated documentation from ${sourceLanguage} to ${targetLanguage} language`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type TranslatorParams = {
|
|
122
|
+
oauthToken: string;
|
|
123
|
+
folderId: string | undefined;
|
|
124
|
+
input: string;
|
|
125
|
+
output: string;
|
|
126
|
+
sourceLanguage: string;
|
|
127
|
+
targetLanguage: string;
|
|
128
|
+
// yandexCloudTranslateGlossaryPairs: YandexCloudTranslateGlossaryPair[];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function translator(params: TranslatorParams) {
|
|
132
|
+
const {
|
|
133
|
+
oauthToken,
|
|
134
|
+
folderId,
|
|
135
|
+
input,
|
|
136
|
+
output,
|
|
137
|
+
sourceLanguage,
|
|
138
|
+
targetLanguage,
|
|
139
|
+
// yandexCloudTranslateGlossaryPairs,
|
|
140
|
+
} = params;
|
|
141
|
+
|
|
142
|
+
const tmap = new Map<string, Defer>();
|
|
143
|
+
const session = new Session({oauthToken});
|
|
144
|
+
const client = session.client(TranslationServiceClient);
|
|
145
|
+
const request = (texts: string[]) => () => {
|
|
146
|
+
return client
|
|
147
|
+
.translate(
|
|
148
|
+
TranslateRequest.fromPartial({
|
|
149
|
+
texts,
|
|
150
|
+
folderId,
|
|
151
|
+
sourceLanguageCode: sourceLanguage,
|
|
152
|
+
targetLanguageCode: targetLanguage,
|
|
153
|
+
// glossaryConfig: {
|
|
154
|
+
// glossaryData: {
|
|
155
|
+
// glossaryPairs: yandexCloudTranslateGlossaryPairs,
|
|
156
|
+
// },
|
|
157
|
+
// },
|
|
158
|
+
format: Format.HTML,
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
.then((results) => {
|
|
162
|
+
return results.translations.map(({text}, index) => {
|
|
163
|
+
const defer = tmap.get(texts[index]);
|
|
164
|
+
if (defer) {
|
|
165
|
+
defer.resolve([text]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return text;
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return async (mdPath: string) => {
|
|
174
|
+
if (!mdPath.endsWith('.md')) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
logger.info(mdPath, 'translating');
|
|
180
|
+
|
|
181
|
+
const inputPath = resolve(input, sourceLanguage, mdPath);
|
|
182
|
+
const outputPath = resolve(output, targetLanguage, mdPath);
|
|
183
|
+
const md = await readFile(inputPath, {encoding: 'utf-8'});
|
|
184
|
+
|
|
185
|
+
await mkdir(dirname(outputPath), {recursive: true});
|
|
186
|
+
|
|
187
|
+
if (!md) {
|
|
188
|
+
await writeFile(outputPath, md);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const {units, skeleton} = extract({
|
|
193
|
+
source: {
|
|
194
|
+
language: sourceLanguage,
|
|
195
|
+
locale: 'RU',
|
|
196
|
+
},
|
|
197
|
+
target: {
|
|
198
|
+
language: targetLanguage,
|
|
199
|
+
locale: 'US',
|
|
200
|
+
},
|
|
201
|
+
markdown: md,
|
|
202
|
+
markdownPath: mdPath,
|
|
203
|
+
skeletonPath: '',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!units.length) {
|
|
207
|
+
await writeFile(outputPath, md);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const parts = await Promise.all(
|
|
212
|
+
(units as string[]).reduce(
|
|
213
|
+
(
|
|
214
|
+
{
|
|
215
|
+
promises,
|
|
216
|
+
buffer,
|
|
217
|
+
bufferSize,
|
|
218
|
+
}: {
|
|
219
|
+
promises: Promise<string[]>[];
|
|
220
|
+
buffer: string[];
|
|
221
|
+
bufferSize: number;
|
|
222
|
+
},
|
|
223
|
+
text,
|
|
224
|
+
index,
|
|
225
|
+
) => {
|
|
226
|
+
if (text.length >= BYTES_LIMIT) {
|
|
227
|
+
logger.warn(
|
|
228
|
+
mdPath,
|
|
229
|
+
'Skip document part for translation. Part is too big.',
|
|
230
|
+
);
|
|
231
|
+
promises.push(Promise.resolve([text]));
|
|
232
|
+
return {promises, buffer, bufferSize};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const defer = tmap.get(text);
|
|
236
|
+
if (defer) {
|
|
237
|
+
console.log('SKIPPED', text);
|
|
238
|
+
promises.push(defer.promise);
|
|
239
|
+
return {promises, buffer, bufferSize};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (bufferSize + text.length > BYTES_LIMIT) {
|
|
243
|
+
promises.push(backoff(request(buffer)));
|
|
244
|
+
buffer = [];
|
|
245
|
+
bufferSize = 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
buffer.push(text);
|
|
249
|
+
bufferSize += text.length;
|
|
250
|
+
tmap.set(text, new Defer());
|
|
251
|
+
|
|
252
|
+
if (index === units.length - 1) {
|
|
253
|
+
promises.push(backoff(request(buffer)));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {promises, buffer, bufferSize};
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
promises: [],
|
|
260
|
+
buffer: [],
|
|
261
|
+
bufferSize: 0,
|
|
262
|
+
},
|
|
263
|
+
).promises,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const translations = ([] as string[]).concat(...parts);
|
|
267
|
+
|
|
268
|
+
const composed = await compose({
|
|
269
|
+
useSource: true,
|
|
270
|
+
units: translations,
|
|
271
|
+
skeleton,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await writeFile(outputPath, composed);
|
|
275
|
+
|
|
276
|
+
logger.info(outputPath, 'finished translating');
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (err instanceof Error) {
|
|
279
|
+
throw new TranslatorError(err.toString(), mdPath);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function backoff(action: () => Promise<string[]>): Promise<string[]> {
|
|
286
|
+
return retry(
|
|
287
|
+
{
|
|
288
|
+
times: RETRY_LIMIT,
|
|
289
|
+
interval: (count: number) => {
|
|
290
|
+
// eslint-disable-next-line no-bitwise
|
|
291
|
+
return (1 << count) * 1000;
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
asyncify(action),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
class Defer {
|
|
299
|
+
resolve!: (text: string[]) => void;
|
|
300
|
+
|
|
301
|
+
reject!: (error: any) => void;
|
|
302
|
+
|
|
303
|
+
promise: Promise<string[]>;
|
|
304
|
+
|
|
305
|
+
constructor() {
|
|
306
|
+
this.promise = new Promise((resolve, reject) => {
|
|
307
|
+
this.resolve = resolve;
|
|
308
|
+
this.reject = reject;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import {extract} from '../xliff/extract';
|
|
2
|
+
import {compose} from '../xliff/compose';
|
|
3
|
+
import {handler} from './handler';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {mkdir, readFile, writeFile} from 'fs/promises';
|
|
5
|
+
import {Argv} from 'yargs';
|
|
7
6
|
|
|
8
|
-
import {Session} from '@yandex-cloud/nodejs-sdk/dist/session';
|
|
9
|
-
import {TranslationServiceClient} from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/service_clients';
|
|
10
|
-
import {
|
|
11
|
-
TranslateRequest_Format as Format,
|
|
12
|
-
TranslateRequest,
|
|
13
|
-
} from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/ai/translate/v2/translation_service';
|
|
14
|
-
|
|
15
|
-
// @ts-ignore
|
|
16
|
-
import {compose, extract} from '@diplodoc/markdown-translation';
|
|
17
|
-
|
|
18
|
-
import {ArgvService} from '../../services';
|
|
19
|
-
import {getYandexOAuthToken} from '../../packages/credentials';
|
|
20
|
-
import {logger} from '../../utils';
|
|
21
|
-
|
|
22
|
-
import {Arguments, Argv} from 'yargs';
|
|
23
|
-
|
|
24
|
-
// import {YandexCloudTranslateGlossaryPair} from '../../models';
|
|
25
7
|
import {argvValidator} from '../../validator';
|
|
26
8
|
|
|
27
9
|
const command = 'translate';
|
|
@@ -35,12 +17,10 @@ const translate = {
|
|
|
35
17
|
builder,
|
|
36
18
|
};
|
|
37
19
|
|
|
38
|
-
const REQUESTS_LIMIT = 20;
|
|
39
|
-
const BYTES_LIMIT = 10000;
|
|
40
|
-
const RETRY_LIMIT = 3;
|
|
41
|
-
|
|
42
20
|
function builder<T>(argv: Argv<T>) {
|
|
43
21
|
return argv
|
|
22
|
+
.command(extract)
|
|
23
|
+
.command(compose)
|
|
44
24
|
.option('folder-id', {
|
|
45
25
|
describe: 'folder id',
|
|
46
26
|
type: 'string',
|
|
@@ -55,301 +35,15 @@ function builder<T>(argv: Argv<T>) {
|
|
|
55
35
|
describe: 'target language code',
|
|
56
36
|
type: 'string',
|
|
57
37
|
})
|
|
38
|
+
.option('include', {
|
|
39
|
+
describe: 'relative to input globs to include in processing',
|
|
40
|
+
type: 'string',
|
|
41
|
+
})
|
|
42
|
+
.option('exclude', {
|
|
43
|
+
describe: 'relative to input globs to exclude from processing',
|
|
44
|
+
type: 'string',
|
|
45
|
+
})
|
|
58
46
|
.check(argvValidator);
|
|
59
|
-
// .demandOption(
|
|
60
|
-
// ['source-language', 'target-language'],
|
|
61
|
-
// 'command requires to specify source and target languages',
|
|
62
|
-
// );
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
class TranslatorError extends Error {
|
|
66
|
-
path: string;
|
|
67
|
-
|
|
68
|
-
constructor(message: string, path: string) {
|
|
69
|
-
super(message);
|
|
70
|
-
|
|
71
|
-
this.path = path;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
type TranslateConfig = {
|
|
76
|
-
input: string;
|
|
77
|
-
output?: string;
|
|
78
|
-
sourceLanguage: string;
|
|
79
|
-
targetLanguage: string;
|
|
80
|
-
folderId?: string;
|
|
81
|
-
glossary?: string;
|
|
82
|
-
include?: string[];
|
|
83
|
-
exclude?: string[];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
87
|
-
async function handler(args: Arguments<any>) {
|
|
88
|
-
ArgvService.init({
|
|
89
|
-
...(args.translate || {}),
|
|
90
|
-
...args,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const {
|
|
94
|
-
folderId,
|
|
95
|
-
// yandexCloudTranslateGlossaryPairs,
|
|
96
|
-
sourceLanguage,
|
|
97
|
-
targetLanguage,
|
|
98
|
-
exclude = [],
|
|
99
|
-
} = ArgvService.getConfig() as unknown as TranslateConfig;
|
|
100
|
-
|
|
101
|
-
let {input, output, include = []} = ArgvService.getConfig() as unknown as TranslateConfig;
|
|
102
|
-
|
|
103
|
-
logger.info(
|
|
104
|
-
input,
|
|
105
|
-
`translating documentation from ${sourceLanguage} to ${targetLanguage} language`,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
output = output || input;
|
|
109
|
-
|
|
110
|
-
ok(input, 'Required param input is not configured');
|
|
111
|
-
ok(sourceLanguage, 'Required param sourceLanguage is not configured');
|
|
112
|
-
ok(targetLanguage, 'Required param targetLanguage is not configured');
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
if (input.endsWith('.md')) {
|
|
116
|
-
include = [basename(input)];
|
|
117
|
-
input = dirname(input);
|
|
118
|
-
} else if (!include.length) {
|
|
119
|
-
include.push('**/*');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const files = ([] as string[]).concat(
|
|
123
|
-
...include.map((match) =>
|
|
124
|
-
glob.sync(match, {
|
|
125
|
-
cwd: join(input, sourceLanguage),
|
|
126
|
-
ignore: exclude,
|
|
127
|
-
}),
|
|
128
|
-
),
|
|
129
|
-
);
|
|
130
|
-
const found = [...new Set(files)];
|
|
131
|
-
|
|
132
|
-
const oauthToken = await getYandexOAuthToken();
|
|
133
|
-
|
|
134
|
-
const translatorParams = {
|
|
135
|
-
input,
|
|
136
|
-
output,
|
|
137
|
-
sourceLanguage,
|
|
138
|
-
targetLanguage,
|
|
139
|
-
// yandexCloudTranslateGlossaryPairs,
|
|
140
|
-
folderId,
|
|
141
|
-
oauthToken,
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const translateFn = translator(translatorParams);
|
|
145
|
-
|
|
146
|
-
await eachLimit(found, REQUESTS_LIMIT, asyncify(translateFn));
|
|
147
|
-
} catch (err) {
|
|
148
|
-
if (err instanceof Error || err instanceof TranslatorError) {
|
|
149
|
-
const message = err.message;
|
|
150
|
-
|
|
151
|
-
const file = err instanceof TranslatorError ? err.path : '';
|
|
152
|
-
|
|
153
|
-
logger.error(file, message);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
logger.info(
|
|
158
|
-
output,
|
|
159
|
-
`translated documentation from ${sourceLanguage} to ${targetLanguage} language`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export type TranslatorParams = {
|
|
164
|
-
oauthToken: string;
|
|
165
|
-
folderId: string | undefined;
|
|
166
|
-
input: string;
|
|
167
|
-
output: string;
|
|
168
|
-
sourceLanguage: string;
|
|
169
|
-
targetLanguage: string;
|
|
170
|
-
// yandexCloudTranslateGlossaryPairs: YandexCloudTranslateGlossaryPair[];
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
function translator(params: TranslatorParams) {
|
|
174
|
-
const {
|
|
175
|
-
oauthToken,
|
|
176
|
-
folderId,
|
|
177
|
-
input,
|
|
178
|
-
output,
|
|
179
|
-
sourceLanguage,
|
|
180
|
-
targetLanguage,
|
|
181
|
-
// yandexCloudTranslateGlossaryPairs,
|
|
182
|
-
} = params;
|
|
183
|
-
|
|
184
|
-
const tmap = new Map<string, Defer>();
|
|
185
|
-
const session = new Session({oauthToken});
|
|
186
|
-
const client = session.client(TranslationServiceClient);
|
|
187
|
-
const request = (texts: string[]) => () => {
|
|
188
|
-
return client
|
|
189
|
-
.translate(
|
|
190
|
-
TranslateRequest.fromPartial({
|
|
191
|
-
texts,
|
|
192
|
-
folderId,
|
|
193
|
-
sourceLanguageCode: sourceLanguage,
|
|
194
|
-
targetLanguageCode: targetLanguage,
|
|
195
|
-
// glossaryConfig: {
|
|
196
|
-
// glossaryData: {
|
|
197
|
-
// glossaryPairs: yandexCloudTranslateGlossaryPairs,
|
|
198
|
-
// },
|
|
199
|
-
// },
|
|
200
|
-
format: Format.HTML,
|
|
201
|
-
}),
|
|
202
|
-
)
|
|
203
|
-
.then((results) => {
|
|
204
|
-
return results.translations.map(({text}, index) => {
|
|
205
|
-
const defer = tmap.get(texts[index]);
|
|
206
|
-
if (defer) {
|
|
207
|
-
defer.resolve([text]);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return text;
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
return async (mdPath: string) => {
|
|
216
|
-
if (!mdPath.endsWith('.md')) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
logger.info(mdPath, 'translating');
|
|
222
|
-
|
|
223
|
-
const inputPath = resolve(input, sourceLanguage, mdPath);
|
|
224
|
-
const outputPath = resolve(output, targetLanguage, mdPath);
|
|
225
|
-
const md = await readFile(inputPath, {encoding: 'utf-8'});
|
|
226
|
-
|
|
227
|
-
await mkdir(dirname(outputPath), {recursive: true});
|
|
228
|
-
|
|
229
|
-
if (!md) {
|
|
230
|
-
await writeFile(outputPath, md);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const {units, skeleton} = extract({
|
|
235
|
-
source: {
|
|
236
|
-
language: sourceLanguage,
|
|
237
|
-
locale: 'RU',
|
|
238
|
-
},
|
|
239
|
-
target: {
|
|
240
|
-
language: targetLanguage,
|
|
241
|
-
locale: 'US',
|
|
242
|
-
},
|
|
243
|
-
markdown: md,
|
|
244
|
-
markdownPath: mdPath,
|
|
245
|
-
skeletonPath: '',
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
if (!units.length) {
|
|
249
|
-
await writeFile(outputPath, md);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const parts = await Promise.all(
|
|
254
|
-
(units as string[]).reduce(
|
|
255
|
-
(
|
|
256
|
-
{
|
|
257
|
-
promises,
|
|
258
|
-
buffer,
|
|
259
|
-
bufferSize,
|
|
260
|
-
}: {
|
|
261
|
-
promises: Promise<string[]>[];
|
|
262
|
-
buffer: string[];
|
|
263
|
-
bufferSize: number;
|
|
264
|
-
},
|
|
265
|
-
text,
|
|
266
|
-
index,
|
|
267
|
-
) => {
|
|
268
|
-
if (text.length >= BYTES_LIMIT) {
|
|
269
|
-
logger.warn(
|
|
270
|
-
mdPath,
|
|
271
|
-
'Skip document part for translation. Part is too big.',
|
|
272
|
-
);
|
|
273
|
-
promises.push(Promise.resolve([text]));
|
|
274
|
-
return {promises, buffer, bufferSize};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const defer = tmap.get(text);
|
|
278
|
-
if (defer) {
|
|
279
|
-
console.log('SKIPPED', text);
|
|
280
|
-
promises.push(defer.promise);
|
|
281
|
-
return {promises, buffer, bufferSize};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (bufferSize + text.length > BYTES_LIMIT) {
|
|
285
|
-
promises.push(backoff(request(buffer)));
|
|
286
|
-
buffer = [];
|
|
287
|
-
bufferSize = 0;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
buffer.push(text);
|
|
291
|
-
bufferSize += text.length;
|
|
292
|
-
tmap.set(text, new Defer());
|
|
293
|
-
|
|
294
|
-
if (index === units.length - 1) {
|
|
295
|
-
promises.push(backoff(request(buffer)));
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return {promises, buffer, bufferSize};
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
promises: [],
|
|
302
|
-
buffer: [],
|
|
303
|
-
bufferSize: 0,
|
|
304
|
-
},
|
|
305
|
-
).promises,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
const translations = ([] as string[]).concat(...parts);
|
|
309
|
-
|
|
310
|
-
const composed = await compose({
|
|
311
|
-
useSource: true,
|
|
312
|
-
units: translations,
|
|
313
|
-
skeleton,
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
await writeFile(outputPath, composed);
|
|
317
|
-
|
|
318
|
-
logger.info(outputPath, 'finished translating');
|
|
319
|
-
} catch (err) {
|
|
320
|
-
if (err instanceof Error) {
|
|
321
|
-
throw new TranslatorError(err.toString(), mdPath);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function backoff(action: () => Promise<string[]>): Promise<string[]> {
|
|
328
|
-
return retry(
|
|
329
|
-
{
|
|
330
|
-
times: RETRY_LIMIT,
|
|
331
|
-
interval: (count: number) => {
|
|
332
|
-
// eslint-disable-next-line no-bitwise
|
|
333
|
-
return (1 << count) * 1000;
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
asyncify(action),
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
class Defer {
|
|
341
|
-
resolve!: (text: string[]) => void;
|
|
342
|
-
|
|
343
|
-
reject!: (error: any) => void;
|
|
344
|
-
|
|
345
|
-
promise: Promise<string[]>;
|
|
346
|
-
|
|
347
|
-
constructor() {
|
|
348
|
-
this.promise = new Promise((resolve, reject) => {
|
|
349
|
-
this.resolve = resolve;
|
|
350
|
-
this.reject = reject;
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
47
|
}
|
|
354
48
|
|
|
355
49
|
export {translate};
|