Blame Raw
· · · 409 lines (12.3 KB)
0 contributors
1import fetch from 'node-fetch';
2import express from 'express';
3import { translate as bingTranslate } from 'bing-translate-api';
4import urlJoin from 'url-join';
5import { Translator } from 'google-translate-api-x';
6
7import { readSecret, SECRET_KEYS } from './secrets.js';
8import { getConfigValue, uuidv4 } from '../util.js';
9
10const DEEPLX_URL_DEFAULT = 'http://127.0.0.1:1188/translate';
11const ONERING_URL_DEFAULT = 'http://127.0.0.1:4990/translate';
12const LINGVA_DEFAULT = 'https://lingva.ml/api/v1';
13
14export const router = express.Router();
15
16router.post('/libre', async (request, response) => {
17 try {
18 const key = readSecret(request.user.directories, SECRET_KEYS.LIBRE);
19 const url = readSecret(request.user.directories, SECRET_KEYS.LIBRE_URL);
20
21 if (!url) {
22 console.warn('LibreTranslate URL is not configured.');
23 return response.sendStatus(400);
24 }
25
26 if (request.body.lang === 'zh-CN') {
27 request.body.lang = 'zh';
28 }
29
30 if (request.body.lang === 'zh-TW') {
31 request.body.lang = 'zt';
32 }
33
34 if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') {
35 request.body.lang = 'pt';
36 }
37
38 const text = request.body.text;
39 const lang = request.body.lang;
40
41 if (!text || !lang) {
42 return response.sendStatus(400);
43 }
44
45 console.debug('Input text: ' + text);
46
47 const result = await fetch(url, {
48 method: 'POST',
49 body: JSON.stringify({
50 q: text,
51 source: 'auto',
52 target: lang,
53 format: 'text',
54 api_key: key,
55 }),
56 headers: { 'Content-Type': 'application/json' },
57 });
58
59 if (!result.ok) {
60 const error = await result.text();
61 console.warn('LibreTranslate error: ', result.statusText, error);
62 return response.sendStatus(500);
63 }
64
65 /** @type {any} */
66 const json = await result.json();
67 console.debug('Translated text: ' + json.translatedText);
68
69 return response.send(json.translatedText);
70 } catch (error) {
71 console.error('Translation error: ' + error.message);
72 return response.sendStatus(500);
73 }
74});
75
76router.post('/google', async (request, response) => {
77 try {
78 if (request.body.lang === 'pt-BR') {
79 request.body.lang = 'pt';
80 }
81
82 const text = String(request.body.text ?? '');
83 const lang = String(request.body.lang ?? '');
84
85 if (!text || !lang) {
86 return response.sendStatus(400);
87 }
88
89 console.debug('Input text: ' + text);
90
91 const translator = new Translator({ to: lang, requestFunction: fetch });
92 const translatedText = await translator.translate(text).then(result => result.text);
93
94 response.setHeader('Content-Type', 'text/plain; charset=utf-8');
95 console.debug('Translated text: ' + translatedText);
96 return response.send(translatedText);
97 } catch (error) {
98 console.error('Translation error', error);
99 return response.sendStatus(500);
100 }
101});
102
103router.post('/yandex', async (request, response) => {
104 try {
105 if (request.body.lang === 'pt-PT') {
106 request.body.lang = 'pt';
107 }
108
109 if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
110 request.body.lang = 'zh';
111 }
112
113 const chunks = request.body.chunks;
114 const lang = request.body.lang;
115
116 if (!chunks || !lang) {
117 return response.sendStatus(400);
118 }
119
120 // reconstruct original text to log
121 let inputText = '';
122
123 const params = new URLSearchParams();
124 for (const chunk of chunks) {
125 params.append('text', chunk);
126 inputText += chunk;
127 }
128 params.append('lang', lang);
129 const ucid = uuidv4().replaceAll('-', '');
130
131 console.debug('Input text: ' + inputText);
132
133 const result = await fetch(`https://translate.yandex.net/api/v1/tr.json/translate?ucid=${ucid}&srv=android&format=text`, {
134 method: 'POST',
135 body: params,
136 headers: {
137 'Content-Type': 'application/x-www-form-urlencoded',
138 },
139 });
140
141 if (!result.ok) {
142 const error = await result.text();
143 console.warn('Yandex error: ', result.statusText, error);
144 return response.sendStatus(500);
145 }
146
147 /** @type {any} */
148 const json = await result.json();
149 const translated = json.text.join();
150 console.debug('Translated text: ' + translated);
151
152 return response.send(translated);
153 } catch (error) {
154 console.error('Translation error: ' + error.message);
155 return response.sendStatus(500);
156 }
157});
158
159router.post('/lingva', async (request, response) => {
160 try {
161 const secretUrl = readSecret(request.user.directories, SECRET_KEYS.LINGVA_URL);
162 const baseUrl = secretUrl || LINGVA_DEFAULT;
163
164 if (!secretUrl && baseUrl === LINGVA_DEFAULT) {
165 console.warn('Lingva URL is using default value.', LINGVA_DEFAULT);
166 }
167
168 if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
169 request.body.lang = 'zh';
170 }
171
172 if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') {
173 request.body.lang = 'pt';
174 }
175
176 const text = request.body.text;
177 const lang = request.body.lang;
178
179 if (!text || !lang) {
180 return response.sendStatus(400);
181 }
182
183 console.debug('Input text: ' + text);
184
185 const url = urlJoin(baseUrl, 'auto', lang, encodeURIComponent(text));
186 const result = await fetch(url);
187
188 if (!result.ok) {
189 const error = await result.text();
190 console.warn('Lingva error: ', result.statusText, error);
191 }
192
193 /** @type {any} */
194 const data = await result.json();
195 console.debug('Translated text: ' + data.translation);
196 return response.send(data.translation);
197 } catch (error) {
198 console.error('Translation error', error);
199 return response.sendStatus(500);
200 }
201});
202
203router.post('/deepl', async (request, response) => {
204 try {
205 const key = readSecret(request.user.directories, SECRET_KEYS.DEEPL);
206
207 if (!key) {
208 console.warn('DeepL key is not configured.');
209 return response.sendStatus(400);
210 }
211
212 if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
213 request.body.lang = 'ZH';
214 }
215
216 const text = request.body.text;
217 const lang = request.body.lang;
218 const formality = getConfigValue('deepl.formality', 'default');
219
220 if (!text || !lang) {
221 return response.sendStatus(400);
222 }
223
224 console.debug('Input text: ' + text);
225
226 const params = new URLSearchParams();
227 params.append('text', text);
228 params.append('target_lang', lang);
229
230 if (['de', 'fr', 'it', 'es', 'nl', 'ja', 'ru', 'pt-BR', 'pt-PT'].includes(lang)) {
231 params.append('formality', formality);
232 }
233
234 const endpoint = request.body.endpoint === 'pro'
235 ? 'https://api.deepl.com/v2/translate'
236 : 'https://api-free.deepl.com/v2/translate';
237
238 const result = await fetch(endpoint, {
239 method: 'POST',
240 body: params,
241 headers: {
242 'Accept': 'application/json',
243 'Authorization': `DeepL-Auth-Key ${key}`,
244 'Content-Type': 'application/x-www-form-urlencoded',
245 },
246 });
247
248 if (!result.ok) {
249 const error = await result.text();
250 console.warn('DeepL error: ', result.statusText, error);
251 return response.sendStatus(500);
252 }
253
254 /** @type {any} */
255 const json = await result.json();
256 console.debug('Translated text: ' + json.translations[0].text);
257
258 return response.send(json.translations[0].text);
259 } catch (error) {
260 console.error('Translation error: ' + error.message);
261 return response.sendStatus(500);
262 }
263});
264
265router.post('/onering', async (request, response) => {
266 try {
267 const secretUrl = readSecret(request.user.directories, SECRET_KEYS.ONERING_URL);
268 const url = secretUrl || ONERING_URL_DEFAULT;
269
270 if (!url) {
271 console.warn('OneRing URL is not configured.');
272 return response.sendStatus(400);
273 }
274
275 if (!secretUrl && url === ONERING_URL_DEFAULT) {
276 console.info('OneRing URL is using default value.', ONERING_URL_DEFAULT);
277 }
278
279 if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') {
280 request.body.lang = 'pt';
281 }
282
283 const text = request.body.text;
284 const from_lang = request.body.from_lang;
285 const to_lang = request.body.to_lang;
286
287 if (!text || !from_lang || !to_lang) {
288 return response.sendStatus(400);
289 }
290
291 const params = new URLSearchParams();
292 params.append('text', text);
293 params.append('from_lang', from_lang);
294 params.append('to_lang', to_lang);
295
296 console.debug('Input text: ' + text);
297
298 const fetchUrl = new URL(url);
299 fetchUrl.search = params.toString();
300
301 const result = await fetch(fetchUrl, {
302 method: 'GET',
303 });
304
305 if (!result.ok) {
306 const error = await result.text();
307 console.warn('OneRing error: ', result.statusText, error);
308 return response.sendStatus(500);
309 }
310
311 /** @type {any} */
312 const data = await result.json();
313 console.debug('Translated text: ' + data.result);
314
315 return response.send(data.result);
316 } catch (error) {
317 console.error('Translation error: ' + error.message);
318 return response.sendStatus(500);
319 }
320});
321
322router.post('/deeplx', async (request, response) => {
323 try {
324 const secretUrl = readSecret(request.user.directories, SECRET_KEYS.DEEPLX_URL);
325 const url = secretUrl || DEEPLX_URL_DEFAULT;
326
327 if (!url) {
328 console.warn('DeepLX URL is not configured.');
329 return response.sendStatus(400);
330 }
331
332 if (!secretUrl && url === DEEPLX_URL_DEFAULT) {
333 console.info('DeepLX URL is using default value.', DEEPLX_URL_DEFAULT);
334 }
335
336 const text = request.body.text;
337 let lang = request.body.lang;
338 if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
339 lang = 'ZH';
340 }
341
342 if (!text || !lang) {
343 return response.sendStatus(400);
344 }
345
346 console.debug('Input text: ' + text);
347
348 const result = await fetch(url, {
349 method: 'POST',
350 body: JSON.stringify({
351 text: text,
352 source_lang: 'auto',
353 target_lang: lang,
354 }),
355 headers: {
356 'Accept': 'application/json',
357 'Content-Type': 'application/json',
358 },
359 });
360
361 if (!result.ok) {
362 const error = await result.text();
363 console.warn('DeepLX error: ', result.statusText, error);
364 return response.sendStatus(500);
365 }
366
367 /** @type {any} */
368 const json = await result.json();
369 console.debug('Translated text: ' + json.data);
370
371 return response.send(json.data);
372 } catch (error) {
373 console.error('DeepLX translation error: ' + error.message);
374 return response.sendStatus(500);
375 }
376});
377
378router.post('/bing', async (request, response) => {
379 try {
380 const text = request.body.text;
381 let lang = request.body.lang;
382
383 if (request.body.lang === 'zh-CN') {
384 lang = 'zh-Hans';
385 }
386
387 if (request.body.lang === 'zh-TW') {
388 lang = 'zh-Hant';
389 }
390
391 if (request.body.lang === 'pt-BR') {
392 lang = 'pt';
393 }
394
395 if (!text || !lang) {
396 return response.sendStatus(400);
397 }
398
399 console.debug('Input text: ' + text);
400
401 const result = await bingTranslate(text, null, lang);
402 const translatedText = result?.translation;
403 console.debug('Translated text: ' + translatedText);
404 return response.send(translatedText);
405 } catch (error) {
406 console.error('Translation error', error);
407 return response.sendStatus(500);
408 }
409});