-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
363 lines (223 loc) · 8.75 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/*
Author: Aleksai Losey
Version: 5/19/2020
Email: [email protected]
License: MIT
*/
const { Translate } = require('@google-cloud/translate').v2,
Html5Entities = require('html-entities').Html5Entities,
entities = new Html5Entities(),
fs = require('fs');
class MarkupTranslator {
#SUPPORTED_LANGUAGES = require('./supported.js');
#API_KEY;
#TRANSLATOR;
#EXCLUDE_DELIMITERS;
#INCLUDE_ATTRIBUTES;
#PLACEHOLDER_BASE = 'MARKUPTRANSLATORPLACEHOLDER';
#PLACEHOLDER_INDEX = 0;
#INVALID_DELIMITERS = [`'`, '"', "'"];
/*
@param API_KEY: string
@param options: object
*/
constructor (API_KEY, options) {
if (typeof API_KEY === 'undefined') {
throw new Error('Please provide a Google Cloud API key.');
}
if (typeof API_KEY !== 'string') {
throw new Error('The Google Cloud API key must be a string.');
}
if (API_KEY.trim() === '') {
throw new Error('The Google Cloud API key may not be an empty string.');
}
this.#API_KEY = API_KEY;
this.#TRANSLATOR = new Translate({ key: this.#API_KEY });
this.#EXCLUDE_DELIMITERS = options && options.excludeDelimiters && Array.isArray(options.excludeDelimiters) ? options.excludeDelimiters : [];
this.#INCLUDE_ATTRIBUTES = options && options.includeAttributes && Array.isArray(options.includeAttributes) ? options.includeAttributes : [];
for (var delimiter of this.#EXCLUDE_DELIMITERS) {
if (typeof delimiter !== 'object' || !delimiter.start || !delimiter.end || typeof delimiter.start !== 'string' || typeof delimiter.end !== 'string') {
throw new Error(`Invalid delimiter (${JSON.stringify(delimiter)}) provided in the excludeDelimiters field. Delimiter objects must have the form { start: string, end: string }.`);
}
}
// now check for validity of delimiters
for (var delimiter of this.#EXCLUDE_DELIMITERS) {
for (var invalidDelimiter of this.#INVALID_DELIMITERS) {
if (delimiter.start.indexOf(invalidDelimiter) !== -1 || delimiter.end.indexOf(invalidDelimiter) !== -1) {
throw new Error(`Invalid character (${invalidDelimiter}) is present in delimiter. Delimiters may not contain the following characters: ', ", or \`.`);
}
}
}
for (var attribute of this.#INCLUDE_ATTRIBUTES) {
if (typeof attribute !== 'string') {
throw new Error(`Invalid attribute (${attribute}) provided in the includeAttributes field. Attributes must be non-empty strings.`);
}
}
}
/*
@param inputFilePath: string
@param outputFilePath: string
@param targetLanguage: string
*/
async translateFromFile (inputFilePath, outputFilePath, targetLanguage) {
if (typeof inputFilePath === 'undefined') {
throw new Error('Please provide an input file path.');
}
if (typeof inputFilePath.trim() === '') {
throw new Error('Input file path must be a non-empty string.');
}
if (typeof outputFilePath === 'undefined') {
throw new Error('Please provide an output file path.');
}
if (typeof outputFilePath.trim() === '') {
throw new Error('Output file path must be a non-empty string.');
}
if (typeof targetLanguage === 'undefined') {
throw new Error('Please provide a target language.');
}
if (Object.values(this.#SUPPORTED_LANGUAGES).indexOf(targetLanguage) === -1) {
throw new Error(`The provided target language (${targetLanguage}) is not supported: The following languages are supported: ${JSON.stringify(this.#SUPPORTED_LANGUAGES)}.`);
}
if (!fs.existsSync(inputFilePath)) {
throw new Error(`Input file path ${inputFilePath} does not exist.`)
}
const fileContents = fs.readFileSync(inputFilePath.trim(), { encoding: 'utf8' }),
translatedContents = await this.translateFromText(fileContents, targetLanguage);
fs.writeFileSync(outputFilePath, translatedContents);
return true;
}
/*
@param text: string
@param targetLanguage: string
*/
async translateFromText (text, targetLanguage) {
targetLanguage = targetLanguage.trim();
if (typeof text === 'undefined') {
throw new Error('Please provide text to translate.')
}
if (typeof text !== 'string') {
throw new Error(`The text value provided (${text}) must be a string.`);
}
if (typeof targetLanguage === 'undefined') {
throw new Error('Please provide a target language.');
}
if (Object.values(this.#SUPPORTED_LANGUAGES).indexOf(targetLanguage) === -1) {
throw new Error(`The provided target language (${targetLanguage}) is not supported: The following languages are supported: ${JSON.stringify(this.#SUPPORTED_LANGUAGES)}.`);
}
try {
return await this.#translate(text, targetLanguage);
} catch (error) {
if (error.code === 403) {
throw new Error(`The provided Google Cloud API key (${this.#API_KEY}) is invalid.`);
} else if (error.message) {
throw new Error(error.message);
} else {
throw new Error('An unexpected error has occurred');
}
}
}
/*
Translates markup while considering excluded delimiters and included attributes
@param text: string
@param targetLanguage: string
*/
#translate = async function (text, targetLanguage) {
const { restoreMap, garbled } = this.#garble(text),
translatedAttributes = await this.#translateAttributes(garbled, targetLanguage),
[translatedEncoded] = await this.#TRANSLATOR.translate(translatedAttributes, { to: targetLanguage, format: 'html' }),
translatedDecoded = this.#decode(translatedEncoded),
translated = this.#ungarble(translatedDecoded, restoreMap);
return translated;
}
/*
Decodes HTML 5 Entities
@params text: string
*/
#decode = function (text) {
return entities.decode(text);
}
#translateAttributes = async function (text, targetLanguage) {
var translated = text,
attributes = [];
if (this.#INCLUDE_ATTRIBUTES.length) {
for (var attribute of this.#INCLUDE_ATTRIBUTES) {
// note: greedy inner capture
var regex = new RegExp(`${attribute}\\s*=\\s*('|")(.*)\\1`, 'g'),
matches = [...translated.matchAll(regex)];
for (var match of matches) {
var item = {
fullCapture: match[0],
value: match[2]
};
var exists = false;
for (var attribute of attributes) {
if (attribute.fullCapture === item.fullCapture) {
exists = true;
break;
}
}
if (!exists) {
attributes.push(item);
}
}
}
}
for (var attribute of attributes) {
var [translatedAttributeEncoded] = await this.#TRANSLATOR.translate(attribute.value, { to: targetLanguage, format: 'text' }),
translatedAttributeDecoded = this.#decode(translatedAttributeEncoded),
fullCaptureReplaced = attribute.fullCapture.replace(attribute.value, translatedAttributeDecoded);
translated = translated.replace(new RegExp(`${attribute.fullCapture}`, 'g'), fullCaptureReplaced);
}
return translated;
}
/*
Replaces instances of placeholder in restoreMap with their respective values
@param text: string
@param items: array
*/
#ungarble = function (text, restoreMap) {
var ungarbled = text;
for (var placeholder in restoreMap) {
var n = 0,
ungarbled = ungarbled.replace(new RegExp(placeholder, 'g'), function (_) {
return restoreMap[placeholder]['items'][n++];
});
}
return ungarbled;
}
/*
Returns garbled text object containing map of placeholders to their corresponding text elements.
@param text: string
@return {
restoreMap: Array
garbled: string
}
*/
#garble = function (text) {
var garbled = text,
restoreMap = [];
if (this.#EXCLUDE_DELIMITERS.length) {
for (var delimiter of this.#EXCLUDE_DELIMITERS) {
var regex = new RegExp(`${delimiter.start}(.*?)${delimiter.end}`, 'g'),
matches = [...garbled.matchAll(regex)],
items = [];
for (var match of matches) {
items.push(match[0]);
}
if (items.length) {
var placeholder = this.#PLACEHOLDER_BASE + this.#PLACEHOLDER_INDEX++;
restoreMap[placeholder] = { delimiter: delimiter, items: items };
garbled = garbled.replace(regex, placeholder);
}
}
}
return { garbled, restoreMap };
}
/*
Outputs supported language map
*/
printSupportedLanguages() {
console.log(this.#SUPPORTED_LANGUAGES);
}
}
module.exports = MarkupTranslator;