├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── deepl ├── __init__.py ├── __main__.py └── translator.py ├── doc ├── request.json ├── request_min.json └── response.json ├── setup.cfg ├── setup.py └── test ├── __init__.py └── translator.py /.gitignore: -------------------------------------------------------------------------------- 1 | #IDE 2 | .idea/ 3 | 4 | #Python 5 | __pycache__/ 6 | *.pyc 7 | 8 | #setuptools 9 | build/ 10 | dist/ 11 | *.egg-info/ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | - "3.4" 5 | - "3.5" 6 | - "3.5-dev" # 3.5 development branch 7 | - "3.6" 8 | - "3.6-dev" # 3.6 development branch 9 | - "3.7-dev" # 3.7 development branch 10 | - "nightly" 11 | install: 12 | - pip install setuptools 13 | script: 14 | - python setup.py test sdist bdist_wheel -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adrian Freund & Jonathan Böttcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This project is discontinued and broken. Please use the official DeepL API python client: https://github.com/DeepLcom/deepl-python 2 | ================================================================================================================================== 3 | 4 | DeepL commandline client 5 | ======================== 6 | 7 | A simple python commandline client for deepl.com/translate. 8 | 9 | This is *NOT* an official API and might break at any moment. 10 | 11 | .. code-block:: 12 | 13 | Usage: deepl.py [-h] [-s lang] [-t lang] [-v] [text [text ...]] 14 | 15 | Translate text to other languages using deepl.com 16 | 17 | positional arguments: 18 | text 19 | 20 | optional arguments: 21 | -h, --help show this help message and exit 22 | -s lang, --source lang Source language 23 | -t lang, --target lang Target language 24 | -v, --verbose Print additional information 25 | 26 | 27 | This can also be used as a library: 28 | 29 | .. code-block:: python 30 | 31 | import deepl 32 | 33 | translation, extra_data = deepl.translate("This is a test", target="DE") 34 | 35 | This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with DeepL GmbH, 36 | or any of its subsidiaries or its affiliates. 37 | -------------------------------------------------------------------------------- /deepl/__init__.py: -------------------------------------------------------------------------------- 1 | from .translator import translate 2 | 3 | __all__ = ['translate'] -------------------------------------------------------------------------------- /deepl/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import locale 3 | import sys 4 | 5 | from deepl import translator 6 | 7 | 8 | def print_results(result, extra_data, verbose=False): 9 | if verbose: 10 | print("Translated from {} to {}".format(extra_data["source"], extra_data["target"])) 11 | print(result) 12 | 13 | 14 | def main(): 15 | parser = argparse.ArgumentParser(description="Translate text to other languages using deepl.com") 16 | parser.add_argument("-s", "--source", help="Source language", metavar="lang") 17 | parser.add_argument("-t", "--target", help="Target language", metavar="lang") 18 | parser.add_argument("-i", "--interactive", help="Force interactive mode", action="store_true") 19 | parser.add_argument("-v", "--verbose", help="Print additional information", action="store_true") 20 | parser.add_argument("text", nargs='*') 21 | 22 | args = parser.parse_args() 23 | 24 | locale_ = locale.getdefaultlocale() 25 | preferred_langs = [locale_[0].split("_")[0].upper()] 26 | 27 | if not args.source is None: 28 | source = args.source.upper() 29 | else: 30 | source = 'auto' 31 | if not args.target is None: 32 | target = args.target.upper() 33 | else: 34 | target = None 35 | 36 | if len(args.text) == 0 or args.interactive: 37 | if sys.stdin.isatty() or args.interactive: 38 | print("Please input text to translate") 39 | while True: 40 | text = input("> ") 41 | result, extra_data = translator.translate(text, source, target, preferred_langs) 42 | print_results(result, extra_data, args.verbose) 43 | 44 | if extra_data["source"] not in preferred_langs: 45 | preferred_langs.append(extra_data["source"]) 46 | if extra_data["target"] not in preferred_langs: 47 | preferred_langs.append(extra_data["target"]) 48 | else: 49 | text = sys.stdin.read() 50 | result, extra_data = translator.translate(text, source, target, preferred_langs) 51 | print_results(result, extra_data, args.verbose) 52 | 53 | else: 54 | text = " ".join(args.text) 55 | result, extra_data = translator.translate(text, source, target, preferred_langs) 56 | print_results(result, extra_data, args.verbose) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() -------------------------------------------------------------------------------- /deepl/translator.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import sys 4 | import re 5 | 6 | request_id = 0 7 | 8 | 9 | def translate(text, source="auto", target=None, preferred_langs=[]): 10 | paragraphs = _split_paragraphs(text) 11 | sentences = _request_split_sentences(paragraphs, source, preferred_langs) 12 | result = _request_translate(sentences, source, target, preferred_langs) 13 | translation = _insert_translation(result["translations"], sentences, text) 14 | 15 | return translation, { 16 | "source": result["source"], 17 | "target": result["target"] 18 | } 19 | 20 | 21 | def _split_paragraphs(text): 22 | cleaned_paragraphs = [] 23 | 24 | # Split into paragraphs 25 | parts = re.split(r'(?:\s*\n)+\s*', text) 26 | for part in parts: 27 | part = part.lstrip().rstrip() 28 | if len(part) > 0: 29 | cleaned_paragraphs.append(part) 30 | 31 | return cleaned_paragraphs 32 | 33 | 34 | def _request_split_sentences(paragraphs, source, preferred_langs): 35 | request_paragraphs = [] 36 | request_paragraph_ids = [] 37 | 38 | splitted_paragraphs = [] 39 | 40 | for i, paragraph in enumerate(paragraphs): 41 | # Check if the paragraph contains more than one sentence. 42 | if re.search(r'[.!?\":].*\S.*$', paragraph, re.M): 43 | request_paragraphs.append(paragraph) 44 | request_paragraph_ids.append(i) 45 | splitted_paragraphs.append([]) 46 | else: 47 | splitted_paragraphs.append([paragraph]) 48 | 49 | global request_id 50 | request_id += 1 51 | 52 | current_id = request_id 53 | 54 | url = "https://www.deepl.com/jsonrpc" 55 | headers = {} # {'content-type': 'application/json'} 56 | 57 | payload = { 58 | "method": "LMT_split_into_sentences", 59 | "params": { 60 | "texts": [p for p in request_paragraphs], 61 | "lang": { 62 | "lang_user_selected": source, 63 | "user_preferred_langs": json.dumps(preferred_langs), 64 | }, 65 | }, 66 | "jsonrpc": "2.0", 67 | "id": current_id, 68 | } 69 | 70 | response = requests.post( 71 | url, data=json.dumps(payload), headers=headers).json() 72 | 73 | assert response["jsonrpc"] 74 | assert response["id"] == current_id 75 | 76 | for i, paragraph in enumerate(response["result"]["splitted_texts"]): 77 | splitted_paragraphs[request_paragraph_ids[i]] = paragraph 78 | 79 | sentences = [s for paragraph in splitted_paragraphs for s in paragraph] 80 | 81 | return sentences 82 | 83 | 84 | def _insert_translation(translated_sentences, original_sentences, original_text): 85 | # We are going to modify those arrays, so copy them beforehand. 86 | translated_sentences = translated_sentences[:] 87 | original_sentences = original_sentences[:] 88 | for i, orig_sentence in enumerate(original_sentences): 89 | if translated_sentences[i] is None: 90 | translated_sentences[i] = orig_sentence # Sentence couldn't be translated 91 | 92 | whitespace = re.findall(r'^\s*', original_text)[0] 93 | translated_sentences[i] = whitespace+translated_sentences[i] 94 | original_text = original_text[len(whitespace):] 95 | if original_text.startswith(orig_sentence): 96 | original_text = original_text[len(orig_sentence):] 97 | else: 98 | print("\n\nSomething went wrong. Please report this to the maintainers.", file=sys.stderr) 99 | 100 | return "".join(translated_sentences) 101 | 102 | 103 | def _request_translate(sentences, source, target, preferred_langs): 104 | global request_id 105 | request_id += 1 106 | 107 | current_id = request_id 108 | 109 | url = "https://www.deepl.com/jsonrpc" 110 | headers = {} # {'content-type': 'application/json'} 111 | 112 | payload = { 113 | "method": "LMT_handle_jobs", 114 | "params": { 115 | "jobs": [ 116 | { 117 | "raw_en_sentence": sentence, 118 | "kind": "default" 119 | } for sentence in sentences 120 | ], 121 | "lang": { 122 | "user_preferred_langs": preferred_langs, 123 | }, 124 | }, 125 | "jsonrpc": "2.0", 126 | "id": current_id, 127 | } 128 | 129 | if not source is None: 130 | payload["params"]["lang"]["source_lang_user_selected"] = source 131 | 132 | if not target is None: 133 | payload["params"]["lang"]["target_lang"] = target 134 | 135 | response = requests.post( 136 | url, data=json.dumps(payload), headers=headers).json() 137 | 138 | assert response["jsonrpc"] 139 | assert response["id"] == current_id 140 | 141 | return_ = { 142 | "translations": [ 143 | # FIXME: Not very readable 144 | response["result"]["translations"][i]["beams"][0]["postprocessed_sentence"] 145 | if len(response["result"]["translations"][i]["beams"]) else None 146 | for i in range(len(response["result"]["translations"])) 147 | ], 148 | "source": response["result"]["source_lang"], 149 | "target": response["result"]["target_lang"] 150 | } 151 | 152 | return return_ 153 | -------------------------------------------------------------------------------- /doc/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": 2.0, 3 | "method": "LMT_handle_jobs", 4 | "params": { 5 | "jobs": [ 6 | { 7 | "kind": "default", 8 | "raw_en_sentence": "Dies ist ein Test" 9 | } 10 | ], 11 | "lang": { 12 | "user_preferred_langs": ["EN", "DE"], 13 | "source_lang_user_selected": "auto", 14 | "target_lang": "DE" 15 | }, 16 | "priority": -1 17 | }, 18 | "id": 1 19 | } 20 | -------------------------------------------------------------------------------- /doc/request_min.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "LMT_handle_jobs", 3 | "params": { 4 | "jobs": [ 5 | { 6 | "raw_en_sentence": "Dies ist ein Test" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /doc/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 0, 3 | "jsonrpc": "2.0", 4 | "result": { 5 | "source_lang": "DE", 6 | "source_lang_is_confident": 1, 7 | "target_lang": "EN", 8 | "translations": [ 9 | { 10 | "beams": [ 11 | { 12 | "num_symbols": 5, 13 | "postprocessed_sentence": "This is a test", 14 | "score": -5000.35, 15 | "totalLogProb": -0.922911 16 | } 17 | ], 18 | "timeAfterPreprocessing": 0, 19 | "timeReceivedFromEndpoint": 194, 20 | "timeSentToEndpoint": 12, 21 | "total_time_endpoint": 1 22 | } 23 | ] 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freundTech/deepl-cli/ac50469923b71ceeb44f64de827a1cedf5e427de/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | from codecs import open 4 | from os import path 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | name='deepl', 13 | version='0.3', 14 | description='Library and CLI for DeepL translator', 15 | long_description=long_description, 16 | author='Adrian Freund', 17 | author_email='mail@freundtech.com', 18 | url='https://github.com/freundTech/deepl-cli', 19 | license='MIT', 20 | classifiers=[ 21 | "Development Status :: 3 - Alpha", 22 | "Environment :: Console", 23 | "License :: OSI Approved :: MIT License", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Programming Language :: Python :: 3.6", 26 | ], 27 | keywords='translation deepl cli', 28 | packages=find_packages(include=['deepl']), 29 | install_requires=["requests"], 30 | python_requires='>=3', 31 | entry_points={ 32 | "console_scripts": ["deepl=deepl.__main__:main"] 33 | }, 34 | test_suite="test" 35 | ) 36 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freundTech/deepl-cli/ac50469923b71ceeb44f64de827a1cedf5e427de/test/__init__.py -------------------------------------------------------------------------------- /test/translator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import requests 3 | import deepl 4 | 5 | paragraph_text = """This is a text with multiple paragraphs. This is still the first one. 6 | This is the second one. 7 | 8 | This is the third paragraph.""" 9 | 10 | paragraph_list = [ 11 | 'This is a text with multiple paragraphs. This is still the first one.', 12 | 'This is the second one.', 13 | 'This is the third paragraph.' 14 | ] 15 | 16 | sentence_list = [ 17 | 'This is a text with multiple paragraphs.', 18 | 'This is still the first one.', 19 | 'This is the second one.', 20 | 'This is the third paragraph.' 21 | ] 22 | 23 | 24 | class TestOfflineMethods(unittest.TestCase): 25 | def test_split_paragraphs(self): 26 | self.assertListEqual(deepl.translator._split_paragraphs(paragraph_text), paragraph_list) 27 | 28 | @unittest.skip("Not yet implemented") 29 | def test_insert_translation(self): 30 | pass 31 | 32 | 33 | class TestOnlineMethods(unittest.TestCase): 34 | def setUp(self): 35 | try: 36 | requests.get("https://www.deepl.com/jsonrpc") 37 | except ConnectionError: 38 | self.skipTest("Can't contact deepl API. Skipping online tests") 39 | 40 | def test_split_sentences(self): 41 | self.assertListEqual(deepl.translator._request_split_sentences(paragraph_list, "EN", ["EN"]), 42 | sentence_list) 43 | 44 | def test_translate(self): 45 | self.assertListEqual( 46 | deepl.translator._request_translate(["This is a test"], "EN", "DE", ["EN", "DE"])["translations"], 47 | ["Das ist ein Test"]) 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | --------------------------------------------------------------------------------