Skip to content

Commit

Permalink
refactor: support auto for source language
Browse files Browse the repository at this point in the history
  • Loading branch information
JounQin committed May 30, 2022
1 parent 0882e36 commit b65e4d7
Show file tree
Hide file tree
Showing 31 changed files with 996 additions and 301 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-jobs-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'deepl-translate': minor
---

refactor: support `auto` for source language
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
coverage
lib
CHANGELOG.md
!/.*.js
/api/_deepl.js
/public/index.html
!/.*.cjs
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ jobs:
- 16
- 18
os:
- macos-latest
- windows-latest
- ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -40,18 +38,19 @@ jobs:
run: |
pnpm build
pnpm lint
pnpm test
env:
EFF_NO_LINK_RULES: true
PARSER_NO_WATCH: true

- name: Test
run: pnpm test
continue-on-error: true

- name: Codecov
uses: codecov/codecov-action@v3

- name: Codacy Coverage
if: matrix.os != 'windows-latest'
run: |
bash <(curl -Ls https://coverage.codacy.com/get.sh) -- -r coverage/*.json
run: bash <(curl -Ls https://coverage.codacy.com/get.sh) -- -r coverage/*.json
env:
CODACY_ACCOUNT_TOKEN: ${{ secrets.CODACY_ACCOUNT_TOKEN }}
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.*cache
.type-coverage
.vercel
coverage
lib
node_modules
/public/index.html
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Options:
This will translate a Spanish (`ES`) text into Russian (`RU`):

```sh
deepl -sl spanish -tl russian -t "¡Buenos días!"
deepl -tl russian -t "¡Buenos días!"
```

```plain
Expand All @@ -107,15 +107,15 @@ deepl -sl spanish -tl russian -t "¡Buenos días!"
This will translate the file (`test.txt`) text from Italian (`IT`) into Portuguese (`PT`):

```sh
deepl -sl IT -tl PT -f test.txt
deepl -tl PT -f test.txt
```

#### Example 3

This will translate a Spanish (`ES`) text into Russian (`RU`) in _formal_ tone:

```sh
deepl -sl ES -tl RU --text "¿Cómo te llamas?" --formal
deepl -tl RU --text "¿Cómo te llamas?" --formal
```

```plain
Expand All @@ -129,7 +129,7 @@ Note: _informal_ would be "_Как **тебя** зовут?_"
This will translate a Japanese (`JP`) text into German (`DE`) in _informal_ tone:

```sh
deepl -sl JP -tl DE --text "お元気ですか?" --formal false
deepl -tl DE --text "お元気ですか?" --formal false
```

```plain
Expand All @@ -147,7 +147,7 @@ This will translate a Chinese (`ZH`) text into Dutch (`NL`):
```js
import { translate } from 'deepl-translate'

translate('ZH', 'NL', '你好')
translate('你好', 'NL', 'ZH')
```

```log
Expand All @@ -161,7 +161,7 @@ This will translate a `danish` text into `german` in informal tone:
```js
import { translate } from 'deepl-translate'

translate('danish', 'german', 'Ring til mig!', undefined, undefined, false)
translate('Ring til mig!', 'german', 'danish', undefined, undefined, false)
```

```log
Expand Down
1 change: 1 addition & 0 deletions api/_deepl.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from 'deepl-translate'
230 changes: 230 additions & 0 deletions api/_deepl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var _got = require('got');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

var _got__default = /*#__PURE__*/_interopDefaultLegacy(_got);

function extractTranslatedSentences(response) {
return response.result.translations.reduce((sentences, translation) => {
sentences.push(translation.beams[0].postprocessed_sentence);
return sentences;
}, []);
}
function extractSplitSentences(response) {
return response.result.splitted_texts[0];
}

function calculateValidTimestamp(timestamp, iCount) {
return iCount ? timestamp + (iCount - timestamp % iCount) : timestamp;
}
function count(sentence, part) {
return sentence.split(part).length - 1;
}
function generateTimestamp(sentences) {
const now = Date.now();
let iCount = 1;
for (const sentence of sentences) {
iCount += count(sentence, "i");
}
return calculateValidTimestamp(now, iCount);
}
function randRange(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateId() {
const MIN = 1e6;
const MAX = 1e8;
return randRange(MIN, MAX);
}

const API_URL = "https://www2.deepl.com/jsonrpc";
const AUTO = "auto";
const SUPPORTED_LANGUAGES = [
{ code: "BG", language: "Bulgarian" },
{ code: "ZH", language: "Chinese" },
{ code: "CS", language: "Czech" },
{ code: "DA", language: "Danish" },
{ code: "NL", language: "Dutch" },
{ code: "EN", language: "English" },
{ code: "ET", language: "Estonian" },
{ code: "FI", language: "Finnish" },
{ code: "FR", language: "French" },
{ code: "DE", language: "German" },
{ code: "EL", language: "Greek" },
{ code: "HU", language: "Hungarian" },
{ code: "IT", language: "Italian" },
{ code: "JA", language: "Japanese" },
{ code: "LV", language: "Latvian" },
{ code: "LT", language: "Lithuanian" },
{ code: "PL", language: "Polish" },
{ code: "PT", language: "Portuguese" },
{ code: "RO", language: "Romanian" },
{ code: "RU", language: "Russian" },
{ code: "SK", language: "Slovak" },
{ code: "SL", language: "Slovenian" },
{ code: "ES", language: "Spanish" },
{ code: "SV", language: "Swedish" }
];
const SUPPORTED_FORMALITY_TONES = ["formal", "informal"];

function generateSplitSentencesRequestData(text, sourceLanguage = AUTO, identifier = generateId()) {
return {
jsonrpc: "2.0",
method: "LMT_split_into_sentences",
params: {
lang: {
lang_user_selected: sourceLanguage,
user_preferred_langs: []
},
texts: [text]
},
id: identifier
};
}
function generateJobs(sentences, beams = 1) {
return sentences.reduce((jobs, sentence, idx) => {
jobs.push({
kind: "default",
raw_en_sentence: sentence,
raw_en_context_before: sentences.slice(0, idx),
raw_en_context_after: idx + 1 < sentences.length ? [sentences[idx + 1]] : [],
preferred_num_beams: beams
});
return jobs;
}, []);
}
function generateCommonJobParams(formality) {
if (!formality) {
return {};
}
if (!SUPPORTED_FORMALITY_TONES.includes(formality)) {
throw new Error("Formality tone '{formality_tone}' not supported.");
}
return { formality };
}
function generateTranslationRequestData(sourceLanguage, targetLanguage, sentences, identifier = generateId(), alternatives = 1, formality) {
return {
jsonrpc: "2.0",
method: "LMT_handle_jobs",
params: {
jobs: generateJobs(sentences, alternatives),
lang: {
user_preferred_langs: [targetLanguage, sourceLanguage],
source_lang_computed: sourceLanguage,
target_lang: targetLanguage
},
priority: 1,
commonJobParams: generateCommonJobParams(formality),
timestamp: generateTimestamp(sentences)
},
id: identifier
};
}

function createAbbreviationsDictionary(languages = SUPPORTED_LANGUAGES) {
return languages.reduce((acc, lang) => {
acc[lang.code.toLowerCase()] = lang.code;
acc[lang.language.toLowerCase()] = lang.code;
return acc;
}, {});
}
function abbreviateLanguage(language) {
return createAbbreviationsDictionary()[language.toLowerCase()];
}

var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
const got = _got__default["default"].extend({
headers: {
accept: "*/*",
"accept-language": "en-US;q=0.8,en;q=0.7",
authority: "www2.deepl.com",
"content-type": "application/json",
origin: "https://www.deepl.com",
referer: "https://www.deepl.com/translator",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Mobile Safari/537.36"
},
stringifyJson(object) {
return JSON.stringify(object).replace('"method":"', () => {
const self = object;
if ((self.id + 3) % 13 === 0 || (self.id + 5) % 29 === 0) {
return '"method" : "';
}
return '"method": "';
});
}
});
function splitSentences(text, sourceLanguage, identifier) {
return __async(this, null, function* () {
const data = generateSplitSentencesRequestData(text, sourceLanguage, identifier);
return yield got.post(API_URL, {
json: data
}).json();
});
}
function splitIntoSentences(text, sourceLanguage, identifier) {
return __async(this, null, function* () {
return extractSplitSentences(yield splitSentences(text, sourceLanguage, identifier));
});
}
function requestTranslation(text, targetLanguage, sourceLanguage, identifier, alternatives, formalityTone) {
return __async(this, null, function* () {
const res = yield splitSentences(text, sourceLanguage, identifier);
const data = generateTranslationRequestData(sourceLanguage === "auto" ? res.result.lang : sourceLanguage, targetLanguage, extractSplitSentences(res), identifier, alternatives, formalityTone);
return yield got.post(API_URL, {
json: data
}).json();
});
}
function translate(_0, _1) {
return __async(this, arguments, function* (text, targetLanguage, sourceLanguage = AUTO, identifier, alternatives, formalityTone) {
var _a;
return extractTranslatedSentences(yield requestTranslation(text, abbreviateLanguage(targetLanguage), (_a = abbreviateLanguage(sourceLanguage)) != null ? _a : "auto", identifier, alternatives, formalityTone)).join(" ");
});
}

exports.API_URL = API_URL;
exports.AUTO = AUTO;
exports.SUPPORTED_FORMALITY_TONES = SUPPORTED_FORMALITY_TONES;
exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
exports.abbreviateLanguage = abbreviateLanguage;
exports.calculateValidTimestamp = calculateValidTimestamp;
exports.count = count;
exports.createAbbreviationsDictionary = createAbbreviationsDictionary;
exports.extractSplitSentences = extractSplitSentences;
exports.extractTranslatedSentences = extractTranslatedSentences;
exports.generateId = generateId;
exports.generateJobs = generateJobs;
exports.generateSplitSentencesRequestData = generateSplitSentencesRequestData;
exports.generateTimestamp = generateTimestamp;
exports.generateTranslationRequestData = generateTranslationRequestData;
exports.randRange = randRange;
exports.requestTranslation = requestTranslation;
exports.splitIntoSentences = splitIntoSentences;
exports.splitSentences = splitSentences;
exports.translate = translate;
7 changes: 7 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "deepl-api",
"type": "commonjs",
"dependencies": {
"got": "^11.8.5"
}
}
46 changes: 46 additions & 0 deletions api/translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { VercelRequest, VercelResponse } from '@vercel/node'
import type { SourceLanguage, TargetLanguage } from 'deepl-translate'

// Workaround for Vercel `Cannot find module 'deepl-translate'`
import { translate } from './_deepl'

export interface RequestParams {
text: string
source_lang?: SourceLanguage
target_lang: TargetLanguage
}

const NO_CONTENT = 204
const NOT_ALLOWED = 405

export default async (
req: VercelRequest,
res: VercelResponse,
): Promise<void> => {
if (req.method !== 'POST') {
res.status(NOT_ALLOWED)
res.end()
return
}

const {
text,
source_lang: sourceLang,
target_lang: targetLang,
// type-coverage:ignore-next-line
} = req.body as RequestParams

if (!text) {
res.status(NO_CONTENT)
res.end()
return
}

try {
const translated = await translate(text, targetLang, sourceLang)
res.end(translated)
} catch (err) {
res.status(500)
res.end(String(err))
}
}
Loading

0 comments on commit b65e4d7

Please sign in to comment.