Skip to content

Commit

Permalink
feat: Enhanced the usability of the Google Translate engine. #158
Browse files Browse the repository at this point in the history
  • Loading branch information
bookfere committed Nov 13, 2023
1 parent 782ea61 commit c783801
Show file tree
Hide file tree
Showing 17 changed files with 484 additions and 212 deletions.
11 changes: 6 additions & 5 deletions engines/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .google import (
GoogleFreeTranslate, GoogleBasicTranslate, GoogleAdvancedTranslate)
GoogleFreeTranslate, GoogleBasicTranslate, GoogleBasicTranslateADC,
GoogleAdvancedTranslate)
from .chatgpt import ChatgptTranslate, AzureChatgptTranslate
from .deepl import DeeplTranslate, DeeplProTranslate, DeeplFreeTranslate
from .youdao import YoudaoTranslate
Expand All @@ -8,7 +9,7 @@


builtin_engines = (
GoogleFreeTranslate, GoogleBasicTranslate, GoogleAdvancedTranslate,
ChatgptTranslate, AzureChatgptTranslate, DeeplTranslate, DeeplProTranslate,
DeeplFreeTranslate, MicrosoftEdgeTranslate, YoudaoTranslate,
BaiduTranslate)
GoogleFreeTranslate, GoogleBasicTranslate, GoogleBasicTranslateADC,
GoogleAdvancedTranslate, ChatgptTranslate, AzureChatgptTranslate,
DeeplTranslate, DeeplProTranslate, DeeplFreeTranslate,
MicrosoftEdgeTranslate, YoudaoTranslate, BaiduTranslate)
14 changes: 14 additions & 0 deletions engines/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ssl
import os.path
import traceback

from mechanize import Browser, Request
Expand All @@ -21,6 +22,7 @@ class Base:
api_key_errors = ['401']
separator = '\n\n'
placeholder = ('{{{{id_{}}}}}', r'({{\s*)+id\s*_\s*{}\s*(\s*}})+')
using_tip = None

concurrency_limit = 0
request_interval = 0.0
Expand All @@ -32,6 +34,7 @@ def __init__(self):
self.source_lang = None
self.target_lang = None
self.proxy_uri = None
self.search_paths = []

self.merge_enabled = False

Expand Down Expand Up @@ -108,6 +111,17 @@ def need_change_api_key(self, error_message):
return True
return False

def set_search_paths(self, paths):
self.search_paths = paths

def get_external_program(self, name, paths=[]):
for path in paths + self.search_paths:
if not path.endswith('%s%s' % (os.path.sep, name)):
path = os.path.join(path, name)
if os.path.isfile(path):
return path
return None

def set_endpoint(self, endpoint):
self.endpoint = endpoint

Expand Down
164 changes: 120 additions & 44 deletions engines/google.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re
import os
import sys
import time
import json
import os.path
import traceback
from subprocess import Popen, PIPE

from ..lib.exception import BadApiKeyFormat
Expand Down Expand Up @@ -53,90 +55,164 @@ def _parse(self, data):


class GoogleTranslate:
def _get_credential(self, key_file_path):
"""Default lifetime of api key is 3600 seconds."""
api_key_errors = ['429']
api_key_cache = []
gcloud = None
project_id = None
using_tip = _(
'This plugin uses Application Default Credentials (ADC) in your local '
'environment to access your Google Translate service. To set up the '
'ADC, follow these steps:\n'
'1. Install the gcloud CLI by checking out its instructions {}.\n'
'2. Run the command: gcloud auth application-default login.\n'
'3. Sign in to your Google account and grant needed privileges.') \
.format('<sup><a href="https://cloud.google.com/sdk/docs/install">[^]'
'</a></sup>').replace('\n', '<br />')

def _run_command(self, command, silence=False):
message = _('Cannot run the command "{}".')
try:
startupinfo = None
# Prevent the popping console window on Windows.
if sys.platform == 'win32':
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
process = Popen(
command, stdout=PIPE, stderr=PIPE, universal_newlines=True,
startupinfo=startupinfo)
except Exception:
if silence:
return None
raise Exception(
message.format(command, '\n\n%s' % traceback.format_exc()))
if process.wait() != 0:
if silence:
return None
raise Exception(
message.format(command, '\n\n%s' % process.stderr.read()))
return process.stdout.read().strip()

def _get_gcloud_command(self):
if self.gcloud is not None:
return self.gcloud
if sys.platform == 'win32':
name = 'gcloud.cmd'
which = 'where'
base = r'google-cloud-sdk\bin\%s' % name
paths = [
r'"%s\Google\Cloud SDK\%s"'
% (os.environ.get('programfiles(x86)'), base),
r'"%s\AppData\Local\Google\Cloud SDK\%s"'
% (os.environ.get('userprofile'), base)]
else:
name = 'gcloud'
which = 'which'
paths = ['/usr/local/bin/%s' % name]
gcloud = self.get_external_program(name, paths)
if gcloud is None:
gcloud = self._run_command([which, name], silence=True)
if gcloud is not None:
gcloud = gcloud.split('\n')[0]
if gcloud is None:
raise Exception(_('Cannot find the command "{}".').format(name))
self.gcloud = gcloud
return gcloud

def _get_project_id(self):
if self.project_id is not None:
return self.project_id
self.project_id = self._run_command(
[self._get_gcloud_command(), 'config', 'get', 'project'])
return self.project_id

def _get_credential(self):
"""The default lifetime of the API key is 3600 seconds. Once an
available key is generated, it will be cached until it expired.
"""
timestamp, old_api_key = self.api_key_cache or (None, None)
if old_api_key is not None and time.time() - timestamp < 3600:
return old_api_key
os.environ.update(GOOGLE_APPLICATION_CREDENTIALS=key_file_path)
# Temporarily add existing proxies.
self.proxy_uri and os.environ.update(
http_proxy=self.proxy_uri, https_proxy=self.proxy_uri)
process = Popen(
['gcloud', 'auth', 'application-default', 'print-access-token'],
stdout=PIPE, stderr=PIPE)
if process.wait() != 0:
raise Exception(_('Can not obtain Google API key. Reason: {}')
.format(process.stderr.read().decode('utf-8')))
new_api_key = self._run_command([
self._get_gcloud_command(), 'auth', 'application-default',
'print-access-token'])
# Cleanse the proxies after use.
for proxy in ('http_proxy', 'https_proxy'):
if proxy in os.environ:
del os.environ[proxy]
new_api_key = process.stdout.read().decode('utf-8').strip()
self.api_key_cache[:] = [time.time(), new_api_key]
return new_api_key


class GoogleBasicTranslate(Base, GoogleTranslate):
name = 'Google(Basic)'
alias = 'Google (Basic)'
class GoogleBasicTranslateADC(GoogleTranslate, Base):
name = 'Google(Basic)ADC'
alias = 'Google (Basic) ADC'
lang_codes = Base.load_lang_codes(google)
endpoint = 'https://translation.googleapis.com/language/translate/v2'
api_key_hint = 'API key or KEY_PATH'
api_key_errors = ['429']
api_key_cache = []
api_key_hint = 'API key'
need_api_key = False

def get_headers(self):
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % self._get_credential(),
'x-goog-user-project': self._get_project_id(),
}

def get_data(self, data):
return json.dumps(data)

def translate(self, text):
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {
'format': 'html',
'model': 'nmt',
'target': self._get_target_code(),
'q': text
}

if self.api_key:
if os.path.sep not in self.api_key:
data.update(key=self.api_key)
else:
api_key = self._get_credential(self.api_key)
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % api_key
}
data = json.dumps(data)

if not self._is_auto_lang():
data.update(source=self._get_source_code())

return self.get_result(
self.endpoint, data, headers, method='POST', callback=self._parse)
self.endpoint, self.get_data(data), self.get_headers(),
method='POST', callback=self._parse)

def _parse(self, data):
translations = json.loads(data)['data']['translations']
return ''.join(i['translatedText'] for i in translations)


class GoogleAdvancedTranslate(Base, GoogleTranslate):
class GoogleBasicTranslate(GoogleBasicTranslateADC):
name = 'Google(Basic)'
alias = 'Google (Basic)'
need_api_key = True
using_tip = None

def get_headers(self):
return {'Content-Type': 'application/x-www-form-urlencoded'}

def get_data(self, data):
data.update(key=self.api_key)
return data


class GoogleAdvancedTranslate(GoogleTranslate, Base):
name = 'Google(Advanced)'
alias = 'Google (Advanced)'
alias = 'Google (Advanced) ADC'
lang_codes = Base.load_lang_codes(google)
endpoint = 'https://translation.googleapis.com/v3/projects/{}'
api_key_hint = 'PROJECT_NUMBER_OR_ID|KEY_PATH'
api_key_pattern = r'^[^\s\|]+?\|.+$'
api_key_errors = ['429']
api_key_cache = []
api_key_hint = 'PROJECT_ID'
need_api_key = False

def translate(self, text):
try:
project_id, key_file_path = re.split(r'\|', self.api_key)
except Exception:
raise BadApiKeyFormat(self.api_key_error_message())

project_id = self._get_project_id()
endpoint = self.endpoint.format('%s:translateText' % project_id)
api_key = self._get_credential(key_file_path)

headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % api_key
'Authorization': 'Bearer %s' % self._get_credential(),
'x-goog-user-project': project_id,
}

data = {
Expand Down
1 change: 1 addition & 0 deletions lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
'merge_enabled': False,
'merge_length': 1800,
'ebook_metadata': {},
'search_paths': [],
}


Expand Down
1 change: 1 addition & 0 deletions lib/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ def get_translator(engine_class=None):
config = get_config()
engine_class = engine_class or get_engine_class()
translator = engine_class()
translator.set_search_paths(config.get('search_paths'))
if config.get('proxy_enabled'):
translator.set_proxy(config.get('proxy_setting'))
translator.set_merge_enabled(config.get('merge_enabled'))
Expand Down
Loading

0 comments on commit c783801

Please sign in to comment.