diff --git a/engines/__init__.py b/engines/__init__.py
index ae44aef..a48457e 100644
--- a/engines/__init__.py
+++ b/engines/__init__.py
@@ -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
@@ -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)
diff --git a/engines/base.py b/engines/base.py
index ca22578..a31e6cd 100644
--- a/engines/base.py
+++ b/engines/base.py
@@ -1,4 +1,5 @@
import ssl
+import os.path
import traceback
from mechanize import Browser, Request
@@ -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
@@ -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
@@ -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
diff --git a/engines/google.py b/engines/google.py
index 1074736..870118e 100644
--- a/engines/google.py
+++ b/engines/google.py
@@ -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
@@ -53,39 +55,117 @@ 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('[^]'
+ '').replace('\n', '
')
+
+ 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',
@@ -93,50 +173,46 @@ def translate(self, text):
'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 = {
diff --git a/lib/config.py b/lib/config.py
index 9cd111b..7c6faa5 100644
--- a/lib/config.py
+++ b/lib/config.py
@@ -28,6 +28,7 @@
'merge_enabled': False,
'merge_length': 1800,
'ebook_metadata': {},
+ 'search_paths': [],
}
diff --git a/lib/translation.py b/lib/translation.py
index 8f770ef..fa39753 100644
--- a/lib/translation.py
+++ b/lib/translation.py
@@ -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'))
diff --git a/setting.py b/setting.py
index 52653f5..16d0540 100644
--- a/setting.py
+++ b/setting.py
@@ -291,6 +291,20 @@ def change_input_format(format):
log_translation.toggled.connect(
lambda checked: self.config.update(log_translation=checked))
+ # Search path
+ path_group = QGroupBox(_('Search Paths'))
+ path_layout = QVBoxLayout(path_group)
+ path_desc = QLabel(
+ _('The plugin will search for external programs via these paths.'))
+ self.path_list = QPlainTextEdit()
+ self.path_list.setMinimumHeight(100)
+ path_layout.addWidget(path_desc)
+ path_layout.addWidget(self.path_list)
+
+ self.path_list.setPlainText('\n'.join(self.config.get('search_paths')))
+
+ layout.addWidget(path_group)
+
layout.addStretch(1)
return widget
@@ -311,6 +325,16 @@ def layout_engine(self):
engine_layout.addWidget(manage_engine)
layout.addWidget(engine_group)
+ # Using Tip
+ self.tip_group = QGroupBox(_('Using Tip'))
+ tip_layout = QVBoxLayout(self.tip_group)
+ self.using_tip = QLabel()
+ self.using_tip.setTextFormat(Qt.RichText)
+ self.using_tip.setWordWrap(True)
+ self.using_tip.setOpenExternalLinks(True)
+ tip_layout.addWidget(self.using_tip)
+ layout.addWidget(self.tip_group)
+
# API Keys
self.keys_group = QGroupBox(_('API Keys'))
keys_layout = QVBoxLayout(self.keys_group)
@@ -467,7 +491,7 @@ def choose_default_engine(index):
engine_name = engine_list.itemData(index)
self.config.update(translate_engine=engine_name)
self.current_engine = get_engine_class(engine_name)
- # refresh preferred language
+ # Refresh preferred language
source_lang = self.current_engine.config.get('source_lang')
self.source_lang.refresh.emit(
self.current_engine.lang_codes.get('source'), source_lang,
@@ -475,7 +499,12 @@ def choose_default_engine(index):
target_lang = self.current_engine.config.get('target_lang')
self.target_lang.refresh.emit(
self.current_engine.lang_codes.get('target'), target_lang)
- self.set_api_keys() # show api key setting
+ # show use notice
+ show_tip = self.current_engine.using_tip is not None
+ self.tip_group.setVisible(show_tip)
+ show_tip and self.using_tip.setText(self.current_engine.using_tip)
+ # show api key setting
+ self.set_api_keys()
# Request setting
value = self.current_engine.config.get('concurrency_limit')
if value is None:
@@ -538,6 +567,7 @@ def make_test_translator():
if config is not None:
self.current_engine.set_config(config)
translator = self.current_engine()
+ translator.set_search_paths(self.get_search_paths())
self.proxy_enabled.isChecked() and translator.set_proxy(
[self.proxy_host.text(), self.proxy_port.text()])
EngineTester(self, translator)
@@ -551,10 +581,9 @@ def set_api_keys(self):
need_api_key = self.current_engine.need_api_key
self.keys_group.setVisible(need_api_key)
if need_api_key:
- self.api_keys.clear()
- self.api_keys.setPlaceholderText(
- self.current_engine.api_key_hint)
+ self.api_keys.setPlaceholderText(self.current_engine.api_key_hint)
api_keys = self.current_engine.config.get('api_keys', [])
+ self.api_keys.clear()
for api_key in api_keys:
self.api_keys.appendPlainText(api_key)
@@ -787,6 +816,10 @@ def is_valid_data(self, validator, value):
return state == 2 # Compatible with PyQt5
return state.value == 2
+ def get_search_paths(self):
+ path_list = self.path_list.toPlainText()
+ return [p for p in path_list.split('\n') if os.path.exists(p)]
+
def update_general_config(self):
# Output path
if not self.config.get('to_library'):
@@ -813,6 +846,12 @@ def update_general_config(self):
proxy_setting.append(int(port))
self.config.update(proxy_setting=proxy_setting)
len(proxy_setting) < 1 and self.config.delete('proxy_setting')
+
+ # Search paths
+ search_paths = self.get_search_paths()
+ self.config.update(search_paths=search_paths)
+ self.path_list.setPlainText('\n'.join(search_paths))
+
return True
def get_engine_config(self):
@@ -823,7 +862,7 @@ def get_engine_config(self):
api_key_validator = QRegularExpressionValidator(
QRegularExpression(self.current_engine.api_key_pattern))
key_str = re.sub('\n+', '\n', self.api_keys.toPlainText()).strip()
- for key in [key.strip() for key in key_str.split('\n')]:
+ for key in [k.strip() for k in key_str.split('\n')]:
if not self.is_valid_data(api_key_validator, key):
self.alert.pop(
self.current_engine.api_key_error_message(), 'warning')
@@ -897,7 +936,7 @@ def update_content_config(self):
# Filter rules
rule_content = self.filter_rules.toPlainText()
- filter_rules = [r for r in rule_content.split('\n') if r]
+ filter_rules = [r for r in rule_content.split('\n') if r.strip()]
if self.config.get('rule_mode') == 'regex':
for rule in filter_rules:
if not self.is_valid_regex(rule):
@@ -910,7 +949,7 @@ def update_content_config(self):
# Element rules
rule_content = self.element_rules.toPlainText()
- element_rules = [r for r in rule_content.split('\n') if r]
+ element_rules = [r for r in rule_content.split('\n') if r.strip()]
for rule in element_rules:
if css(rule) is None:
self.alert.pop(
diff --git a/tests/test_config.py b/tests/test_config.py
index c6706eb..04a9e14 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -33,6 +33,7 @@ def test_default(self):
'merge_enabled': False,
'merge_length': 1800,
'ebook_metadata': {},
+ 'search_paths': [],
}
self.assertEqual(defaults, self.config.preferences.defaults)
diff --git a/tests/test_engine.py b/tests/test_engine.py
index e40bed0..809687f 100644
--- a/tests/test_engine.py
+++ b/tests/test_engine.py
@@ -16,7 +16,7 @@
class TestBase(unittest.TestCase):
def setUp(self):
- self.mark, self.pattern = Base.placeholder
+ self.translator = Base()
def test_placeholder(self):
marks = [
@@ -24,7 +24,30 @@ def test_placeholder(self):
for mark in marks:
with self.subTest(mark=mark):
self.assertIsNotNone(
- re.search(self.pattern.format(1), 'xxx %s xxx' % mark))
+ re.search(
+ Base.placeholder[1].format(1),
+ 'xxx %s xxx' % mark))
+
+ @patch('calibre_plugins.ebook_translator.engines.base.os.path.isfile')
+ def test_get_external_program(self, mock_os_path_isfile):
+ mock_os_path_isfile.side_effect = lambda p: p in [
+ '/path/to/real', '/path/to/folder/real', '/path/to/specify/real']
+
+ self.translator.search_paths = ['/path/to/real']
+ self.assertEqual(
+ '/path/to/real',
+ self.translator.get_external_program('real'))
+
+ self.translator.search_paths = ['/path/to/folder']
+ self.assertEqual(
+ '/path/to/folder/real',
+ self.translator.get_external_program('real'))
+ self.assertEqual(
+ '/path/to/specify/real',
+ self.translator.get_external_program('real', ['/path/to/specify']))
+
+ self.assertIsNone(
+ self.translator.get_external_program('/path/to/fake'))
@patch('calibre_plugins.ebook_translator.engines.base.Browser')
diff --git a/translations/es.mo b/translations/es.mo
index 10b76bb..7350abd 100644
Binary files a/translations/es.mo and b/translations/es.mo differ
diff --git a/translations/es.po b/translations/es.po
index 461145d..354c6dd 100644
--- a/translations/es.po
+++ b/translations/es.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Ebook Translator Calibre Plugin\n"
"Report-Msgid-Bugs-To: bookfere@gmail.com\n"
-"POT-Creation-Date: 2023-09-29 21:24+0800\n"
+"POT-Creation-Date: 2023-11-14 02:37+0800\n"
"PO-Revision-Date: 2023-04-17 14:17+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -333,8 +333,20 @@ msgstr "La respuesta fue analizada incorrectamente."
msgid "{} total, {} used, {} left"
msgstr "{} en total, {} utilizado(s), {} restante(s)"
-msgid "Can not obtain Google API key. Reason: {}"
-msgstr "No se puede obtener la clave de API de Google. Razón: {}"
+msgid ""
+"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."
+msgstr ""
+
+msgid "Cannot run the command \"{}\"."
+msgstr ""
+
+msgid "Cannot find the command \"{}\"."
+msgstr ""
msgid "Failed get APP key due to an invalid Token."
msgstr ""
@@ -468,9 +480,18 @@ msgstr ""
msgid "Show translation"
msgstr "Mostrar traducción"
+msgid "Search Paths"
+msgstr ""
+
+msgid "The plugin will search for external programs via these paths."
+msgstr ""
+
msgid "Custom"
msgstr "Personalizado"
+msgid "Using Tip"
+msgstr ""
+
msgid "Tip:"
msgstr ""
diff --git a/translations/fr.mo b/translations/fr.mo
index ebde37c..7846700 100644
Binary files a/translations/fr.mo and b/translations/fr.mo differ
diff --git a/translations/fr.po b/translations/fr.po
index e8a2a7c..63596a3 100644
--- a/translations/fr.po
+++ b/translations/fr.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Ebook Translator Calibre Plugin\n"
"Report-Msgid-Bugs-To: bookfere@gmail.com\n"
-"POT-Creation-Date: 2023-09-29 21:24+0800\n"
+"POT-Creation-Date: 2023-11-14 02:37+0800\n"
"PO-Revision-Date: 2023-10-01 15:35-0400\n"
"Last-Translator: PoP\n"
@@ -341,8 +341,20 @@ msgstr "La réponse a été analysée incorrectement."
msgid "{} total, {} used, {} left"
msgstr "{} total, {} utilisé, {} restant"
-msgid "Can not obtain Google API key. Reason: {}"
-msgstr "Ne peut obtenir la clée d'API de Google. Raison: {}"
+msgid ""
+"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."
+msgstr ""
+
+msgid "Cannot run the command \"{}\"."
+msgstr ""
+
+msgid "Cannot find the command \"{}\"."
+msgstr ""
msgid "Failed get APP key due to an invalid Token."
msgstr "Échoua l'obtention d'une clé APP à cause d'un Token non-valide."
@@ -477,9 +489,18 @@ msgstr "Registre de travail"
msgid "Show translation"
msgstr "Montrer la traduction"
+msgid "Search Paths"
+msgstr ""
+
+msgid "The plugin will search for external programs via these paths."
+msgstr ""
+
msgid "Custom"
msgstr "Personnalisé"
+msgid "Using Tip"
+msgstr ""
+
msgid "Tip:"
msgstr "Indice:"
diff --git a/translations/message.pot b/translations/message.pot
index 35c2d8b..32f752a 100644
--- a/translations/message.pot
+++ b/translations/message.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Ebook Translator Calibre Plugin\n"
"Report-Msgid-Bugs-To: bookfere@gmail.com\n"
-"POT-Creation-Date: 2023-09-29 21:24+0800\n"
+"POT-Creation-Date: 2023-11-14 02:37+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -21,145 +21,145 @@ msgstr ""
msgid "Ebook Translator"
msgstr ""
-#: __init__.py:25
+#: __init__.py:26
msgid ""
"A Calibre plugin to translate ebook into a specified language (optionally "
"keeping the original content)."
msgstr ""
-#: advanced.py:89
+#: advanced.py:90
msgid "Extracting ebook content..."
msgstr ""
-#: advanced.py:98
+#: advanced.py:99
msgid "Filtering ebook content..."
msgstr ""
-#: advanced.py:107
+#: advanced.py:108
msgid "Preparing user interface..."
msgstr ""
-#: advanced.py:184
+#: advanced.py:185
msgid "Start"
msgstr ""
-#: advanced.py:198 batch.py:54 setting.py:176
+#: advanced.py:199 batch.py:54 setting.py:176
msgid "Input Format"
msgstr ""
-#: advanced.py:209 advanced.py:517 batch.py:55 setting.py:335
+#: advanced.py:210 advanced.py:516 batch.py:55 setting.py:359
msgid "Target Language"
msgstr ""
-#: advanced.py:305
+#: advanced.py:304
msgid "Failed to translate {} paragraph(s), Would you like to retry?"
msgstr ""
-#: advanced.py:311 lib/translation.py:250
+#: advanced.py:310 lib/translation.py:252
msgid "Translation completed."
msgstr ""
-#: advanced.py:326
+#: advanced.py:325
msgid "There is no content that needs to be translated."
msgstr ""
-#: advanced.py:363
+#: advanced.py:362
msgid "Loading ebook data, please wait..."
msgstr ""
-#: advanced.py:386
+#: advanced.py:385
msgid "Review"
msgstr ""
-#: advanced.py:387
+#: advanced.py:386
msgid "Log"
msgstr ""
-#: advanced.py:388
+#: advanced.py:387
msgid "Errors"
msgstr ""
-#: advanced.py:444
+#: advanced.py:443
msgid "Translate All"
msgstr ""
-#: advanced.py:445
+#: advanced.py:444
msgid "Translate Selected"
msgstr ""
-#: advanced.py:446 cache.py:92 components/engine.py:200 components/table.py:81
+#: advanced.py:445 cache.py:92 components/engine.py:200 components/table.py:81
msgid "Delete"
msgstr ""
-#: advanced.py:468 advanced.py:479
+#: advanced.py:467 advanced.py:478
msgid "Stop"
msgstr ""
-#: advanced.py:474
+#: advanced.py:473
msgid "Stopping..."
msgstr ""
-#: advanced.py:505 setting.py:304
+#: advanced.py:504 setting.py:318
msgid "Translation Engine"
msgstr ""
-#: advanced.py:511 batch.py:55 setting.py:334
+#: advanced.py:510 batch.py:55 setting.py:358
msgid "Source Language"
msgstr ""
-#: advanced.py:523
+#: advanced.py:522
msgid "Cache Status"
msgstr ""
-#: advanced.py:526
+#: advanced.py:525
msgid "Disabled"
msgstr ""
-#: advanced.py:526
+#: advanced.py:525
msgid "Enabled"
msgstr ""
-#: advanced.py:533
+#: advanced.py:532
msgid "Output Ebook"
msgstr ""
-#: advanced.py:535
+#: advanced.py:534
msgid "Output"
msgstr ""
-#: advanced.py:541 batch.py:54 cache.py:195
+#: advanced.py:540 batch.py:54 cache.py:195
msgid "Title"
msgstr ""
-#: advanced.py:585 components/table.py:70
+#: advanced.py:584 components/table.py:70
msgid "Translated"
msgstr ""
-#: advanced.py:618
+#: advanced.py:617
msgid "No translation yet"
msgstr ""
-#: advanced.py:665 components/engine.py:206 setting.py:88
+#: advanced.py:664 components/engine.py:206 setting.py:88
msgid "Save"
msgstr ""
-#: advanced.py:722
+#: advanced.py:723
msgid "Your changes have been saved."
msgstr ""
-#: advanced.py:735
+#: advanced.py:736
msgid "Translation log"
msgstr ""
-#: advanced.py:746
+#: advanced.py:747
msgid "Error log"
msgstr ""
-#: advanced.py:766
+#: advanced.py:767
msgid "Are you sure you want to translate all {:n} paragraphs?"
msgstr ""
-#: advanced.py:792
+#: advanced.py:793
msgid "Are you sure you want to stop the translation progress?"
msgstr ""
@@ -175,7 +175,7 @@ msgstr ""
msgid "Unknown"
msgstr ""
-#: batch.py:137 cache.py:130 setting.py:796
+#: batch.py:137 cache.py:130 setting.py:829
msgid "The specified path does not exist."
msgstr ""
@@ -184,7 +184,7 @@ msgid "Choose a path to store cache files."
msgstr ""
#: cache.py:73 components/mode.py:41 components/mode.py:51 setting.py:148
-#: setting.py:606 setting.py:640
+#: setting.py:635 setting.py:669
msgid "Choose"
msgstr ""
@@ -237,7 +237,7 @@ msgstr ""
msgid "Engine"
msgstr ""
-#: cache.py:195 components/table.py:36 setting.py:761
+#: cache.py:195 components/table.py:36 setting.py:790
msgid "Language"
msgstr ""
@@ -253,7 +253,8 @@ msgstr ""
msgid "Size (MB)"
msgstr ""
-#: components/engine.py:64 lib/translation.py:167
+#: components/engine.py:64 lib/translation.py:167 tests/test_translation.py:109
+#: tests/test_translation.py:129 tests/test_translation.py:142
msgid "Translating..."
msgstr ""
@@ -309,7 +310,7 @@ msgstr ""
msgid "Feedback"
msgstr ""
-#: components/lang.py:34 engines/base.py:65 setting.py:855
+#: components/lang.py:34 engines/base.py:70 setting.py:896
msgid "Auto detect"
msgstr ""
@@ -317,7 +318,7 @@ msgstr ""
msgid "Choose a translation mode for clicking the icon button."
msgstr ""
-#: components/mode.py:33 setting.py:105 ui.py:47 ui.py:75
+#: components/mode.py:33 setting.py:105 ui.py:54 ui.py:82
msgid "Advanced Mode"
msgstr ""
@@ -327,7 +328,7 @@ msgid ""
"for more control and customization."
msgstr ""
-#: components/mode.py:43 setting.py:106 ui.py:48 ui.py:102
+#: components/mode.py:43 setting.py:106 ui.py:55 ui.py:109
msgid "Batch Mode"
msgstr ""
@@ -365,76 +366,90 @@ msgstr ""
msgid "Baidu"
msgstr ""
-#: engines/base.py:18 setting.py:315
+#: engines/base.py:20 setting.py:339
msgid "API Keys"
msgstr ""
-#: engines/base.py:82
+#: engines/base.py:87
msgid "A correct key format \"{}\" is required."
msgstr ""
-#: engines/base.py:195 engines/chatgpt.py:104 tests/test_engine.py:63
+#: engines/base.py:214 engines/chatgpt.py:108 tests/test_engine.py:90
msgid "Can not parse returned response. Raw data: {}"
msgstr ""
-#: engines/custom.py:45 tests/test_custom.py:43
+#: engines/custom.py:45 tests/test_engine.py:236
msgid "Engine data must be in valid JSON format."
msgstr ""
-#: engines/custom.py:48 tests/test_custom.py:46
+#: engines/custom.py:48 tests/test_engine.py:239
msgid "Invalid engine data."
msgstr ""
-#: engines/custom.py:52 tests/test_custom.py:49
+#: engines/custom.py:52 tests/test_engine.py:242
msgid "Engine name is required."
msgstr ""
-#: engines/custom.py:55 tests/test_custom.py:53
+#: engines/custom.py:55 tests/test_engine.py:246
msgid "Engine name must be different from builtin engine name."
msgstr ""
-#: engines/custom.py:59 tests/test_custom.py:56 tests/test_custom.py:59
+#: engines/custom.py:59 tests/test_engine.py:249 tests/test_engine.py:252
msgid "Language codes are required."
msgstr ""
-#: engines/custom.py:63 tests/test_custom.py:62 tests/test_custom.py:65
+#: engines/custom.py:63 tests/test_engine.py:255 tests/test_engine.py:258
msgid "Source and target must be added in pair."
msgstr ""
-#: engines/custom.py:67 tests/test_custom.py:68
+#: engines/custom.py:67 tests/test_engine.py:261
msgid "Request information is required."
msgstr ""
-#: engines/custom.py:69 tests/test_custom.py:72
+#: engines/custom.py:69 tests/test_engine.py:265
msgid "API URL is required."
msgstr ""
-#: engines/custom.py:73 tests/test_custom.py:77
+#: engines/custom.py:73 tests/test_engine.py:270
msgid "Placeholder is required."
msgstr ""
-#: engines/custom.py:77 tests/test_custom.py:82
+#: engines/custom.py:77 tests/test_engine.py:275
msgid "Request headers must be an JSON object."
msgstr ""
-#: engines/custom.py:80 tests/test_custom.py:88
+#: engines/custom.py:80 tests/test_engine.py:281
msgid "A appropriate Content-Type in headers is required."
msgstr ""
-#: engines/custom.py:84 tests/test_custom.py:93
+#: engines/custom.py:84 tests/test_engine.py:286
msgid "Expression to parse response is required."
msgstr ""
-#: engines/custom.py:133
+#: engines/custom.py:134
msgid "Response was parsed incorrectly."
msgstr ""
-#: engines/deepl.py:31 tests/test_engine.py:44
+#: engines/deepl.py:36 tests/test_engine.py:72
msgid "{} total, {} used, {} left"
msgstr ""
#: engines/google.py:63
-msgid "Can not obtain Google API key. Reason: {}"
+msgid ""
+"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."
+msgstr ""
+
+#: engines/google.py:73
+msgid "Cannot run the command \"{}\"."
+msgstr ""
+
+#: engines/google.py:118
+msgid "Cannot find the command \"{}\"."
msgstr ""
#: engines/microsoft.py:38
@@ -449,31 +464,31 @@ msgstr ""
msgid "Youdao"
msgstr ""
-#: lib/conversion.py:107
+#: lib/conversion.py:120
msgid "Start to convert ebook format:"
msgstr ""
-#: lib/conversion.py:143
+#: lib/conversion.py:156
msgid "[{} > {}] Translating \"{}\""
msgstr ""
-#: lib/conversion.py:152
+#: lib/conversion.py:165
msgid "Translation job failed"
msgstr ""
-#: lib/conversion.py:180
+#: lib/conversion.py:193
msgid "completed"
msgstr ""
-#: lib/conversion.py:188
+#: lib/conversion.py:201
msgid "Ebook Translation Log"
msgstr ""
-#: lib/conversion.py:189
+#: lib/conversion.py:202
msgid "Translation Completed"
msgstr ""
-#: lib/conversion.py:190
+#: lib/conversion.py:203
msgid "The translation of \"{}\" was completed. Do you want to open the book?"
msgstr ""
@@ -497,39 +512,39 @@ msgstr ""
msgid "Translating: {}/{}"
msgstr ""
-#: lib/translation.py:201 lib/translation.py:209
+#: lib/translation.py:203 lib/translation.py:211
msgid "Original: {}"
msgstr ""
-#: lib/translation.py:203
+#: lib/translation.py:205
msgid "Translation: {}"
msgstr ""
-#: lib/translation.py:205
+#: lib/translation.py:207
msgid "Translation (Cached): {}"
msgstr ""
-#: lib/translation.py:211
+#: lib/translation.py:213
msgid "Error: {}"
msgstr ""
-#: lib/translation.py:222
+#: lib/translation.py:224
msgid "Start to translate ebook content"
msgstr ""
-#: lib/translation.py:224
+#: lib/translation.py:226
msgid "Total items: {}"
msgstr ""
-#: lib/translation.py:225
+#: lib/translation.py:227
msgid "Character count: {}"
msgstr ""
-#: lib/translation.py:227
+#: lib/translation.py:229
msgid "There is no content need to translate."
msgstr ""
-#: lib/translation.py:247
+#: lib/translation.py:249
msgid "Translation failed."
msgstr ""
@@ -573,7 +588,7 @@ msgstr ""
msgid "Merge to Translate"
msgstr ""
-#: setting.py:198 setting.py:219 setting.py:263 setting.py:637
+#: setting.py:198 setting.py:219 setting.py:263 setting.py:666
msgid "Enable"
msgstr ""
@@ -593,11 +608,11 @@ msgstr ""
msgid "Port"
msgstr ""
-#: setting.py:245 setting.py:307
+#: setting.py:245 setting.py:321
msgid "Test"
msgstr ""
-#: setting.py:261 ui.py:50
+#: setting.py:261 ui.py:57
msgid "Cache"
msgstr ""
@@ -613,246 +628,258 @@ msgstr ""
msgid "Show translation"
msgstr ""
-#: setting.py:308
+#: setting.py:295
+msgid "Search Paths"
+msgstr ""
+
+#: setting.py:298
+msgid "The plugin will search for external programs via these paths."
+msgstr ""
+
+#: setting.py:322
msgid "Custom"
msgstr ""
-#: setting.py:319
+#: setting.py:329
+msgid "Using Tip"
+msgstr ""
+
+#: setting.py:343
msgid "Tip:"
msgstr ""
-#: setting.py:320
+#: setting.py:344
msgid "API keys will auto-switch if the previous one is unavailable."
msgstr ""
-#: setting.py:330
+#: setting.py:354
msgid "Preferred Language"
msgstr ""
-#: setting.py:341
+#: setting.py:365
msgid "HTTP Request"
msgstr ""
-#: setting.py:355
+#: setting.py:379
msgid "Concurrency limit"
msgstr ""
-#: setting.py:356
+#: setting.py:380
msgid "Interval (seconds)"
msgstr ""
-#: setting.py:357
+#: setting.py:381
msgid "Attempt times"
msgstr ""
-#: setting.py:358
+#: setting.py:382
msgid "Timeout (seconds)"
msgstr ""
-#: setting.py:360
+#: setting.py:384
msgid "Error count to stop translation"
msgstr ""
-#: setting.py:370
+#: setting.py:394
msgid "Tune ChatGPT"
msgstr ""
-#: setting.py:378
+#: setting.py:402
msgid "Prompt"
msgstr ""
-#: setting.py:380
+#: setting.py:404
msgid "Endpoint"
msgstr ""
-#: setting.py:383
+#: setting.py:407
msgid "Model"
msgstr ""
-#: setting.py:406
+#: setting.py:430
msgid "Sampling"
msgstr ""
-#: setting.py:411
+#: setting.py:435
msgid "Enable streaming text like in ChatGPT"
msgstr ""
-#: setting.py:412
+#: setting.py:436
msgid "Stream"
msgstr ""
-#: setting.py:567
+#: setting.py:596
msgid "Translation Position"
msgstr ""
-#: setting.py:569
+#: setting.py:598
msgid "Add after original"
msgstr ""
-#: setting.py:571
+#: setting.py:600
msgid "Add before original"
msgstr ""
-#: setting.py:572
+#: setting.py:601
msgid "Add without original"
msgstr ""
-#: setting.py:595
+#: setting.py:624
msgid "Translation Color"
msgstr ""
-#: setting.py:599
+#: setting.py:628
msgid "CSS color value, e.g., #666666, grey, rgb(80, 80, 80)"
msgstr ""
-#: setting.py:635
+#: setting.py:664
msgid "Translation Glossary"
msgstr ""
-#: setting.py:639
+#: setting.py:668
msgid "Choose a glossary file"
msgstr ""
-#: setting.py:658
+#: setting.py:687
msgid "Ignore Paragraph"
msgstr ""
-#: setting.py:664
+#: setting.py:693
msgid "Scope"
msgstr ""
-#: setting.py:665
+#: setting.py:694
msgid "Text only"
msgstr ""
-#: setting.py:667
+#: setting.py:696
msgid "HTML element"
msgstr ""
-#: setting.py:674
+#: setting.py:703
msgid "Mode"
msgstr ""
-#: setting.py:675
+#: setting.py:704
msgid "Normal"
msgstr ""
-#: setting.py:677
+#: setting.py:706
msgid "Normal (case-sensitive)"
msgstr ""
-#: setting.py:678
+#: setting.py:707
msgid "Regular Expression"
msgstr ""
-#: setting.py:720
+#: setting.py:749
msgid "Exclude paragraph by keyword. One keyword per line:"
msgstr ""
-#: setting.py:721
+#: setting.py:750
msgid "Exclude paragraph by case-sensitive keyword. One keyword per line:"
msgstr ""
-#: setting.py:723
+#: setting.py:752
msgid "Exclude paragraph by regular expression pattern. One pattern per line:"
msgstr ""
-#: setting.py:739
+#: setting.py:768
msgid "Ignore Element"
msgstr ""
-#: setting.py:747
+#: setting.py:776
msgid "CSS selectors to exclude elements. One rule per line:"
msgstr ""
-#: setting.py:750
+#: setting.py:779
msgid "e.g."
msgstr ""
-#: setting.py:754
+#: setting.py:783
msgid "Ebook Metadata"
msgstr ""
-#: setting.py:757
+#: setting.py:786
msgid "Set \"Target Language\" to metadata"
msgstr ""
-#: setting.py:760
+#: setting.py:789
msgid "Subjects of ebook (one subject per line)"
msgstr ""
-#: setting.py:762
+#: setting.py:791
msgid "Subject"
msgstr ""
-#: setting.py:779 setting.py:810
+#: setting.py:808 setting.py:843
msgid "Proxy host or port is incorrect."
msgstr ""
-#: setting.py:781
+#: setting.py:810
msgid "The proxy is available."
msgstr ""
-#: setting.py:782
+#: setting.py:811
msgid "The proxy is not available."
msgstr ""
-#: setting.py:840
+#: setting.py:879
msgid "the prompt must include {}."
msgstr ""
-#: setting.py:882
+#: setting.py:923
msgid "Invalid color value."
msgstr ""
-#: setting.py:891
+#: setting.py:932
msgid "The specified glossary file does not exist."
msgstr ""
-#: setting.py:903
+#: setting.py:944
msgid "{} is not a valid regular expression."
msgstr ""
-#: setting.py:915
+#: setting.py:956
msgid "{} is not a valid CSS seletor."
msgstr ""
-#: ui.py:32
+#: ui.py:38
msgid "Translate Book"
msgstr ""
-#: ui.py:32
+#: ui.py:38
msgid "Translate Ebook Content"
msgstr ""
-#: ui.py:51 ui.py:118
+#: ui.py:58 ui.py:125
msgid "Setting"
msgstr ""
-#: ui.py:52 ui.py:146
+#: ui.py:59 ui.py:153
msgid "About"
msgstr ""
-#: ui.py:83
+#: ui.py:90
msgid "Please choose one single book."
msgstr ""
-#: ui.py:96
+#: ui.py:103
msgid "Please choose at least one book."
msgstr ""
-#: ui.py:110
+#: ui.py:117
msgid "Cannot change setting while book(s) are under translation."
msgstr ""
-#: ui.py:126
+#: ui.py:133
msgid "Cannot manage cache while book(s) are under translation."
msgstr ""
-#: ui.py:135
+#: ui.py:142
msgid "Cache Manager"
msgstr ""
-#: ui.py:164
+#: ui.py:171
msgid "Choose Translation Mode"
msgstr ""
diff --git a/translations/zh_CN.mo b/translations/zh_CN.mo
index cfc2ca3..1fa930d 100644
Binary files a/translations/zh_CN.mo and b/translations/zh_CN.mo differ
diff --git a/translations/zh_CN.po b/translations/zh_CN.po
index 90397ad..4c92569 100644
--- a/translations/zh_CN.po
+++ b/translations/zh_CN.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Ebook Translator Calibre Plugin\n"
"Report-Msgid-Bugs-To: bookfere@gmail.com\n"
-"POT-Creation-Date: 2023-09-29 21:24+0800\n"
+"POT-Creation-Date: 2023-11-14 02:37+0800\n"
"PO-Revision-Date: 2023-04-17 14:17+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -330,8 +330,25 @@ msgstr "响应没有正确解析。"
msgid "{} total, {} used, {} left"
msgstr "总计 {},已用 {},剩余 {}"
-msgid "Can not obtain Google API key. Reason: {}"
-msgstr "无法获取 Google API 密钥。原因:{}"
+msgid ""
+"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."
+msgstr ""
+"本插件使用你本机环境中的 Application Default Credentials (ADC) 来访问你的 "
+"Google Translate 服务。请参照以下步骤设置 ADC:\n"
+"1、参考 gcloud CLI 安装指南安装 gcloud {}。\n"
+"2、运行命令:gcloud auth application-default login。\n"
+"3、登录你的 Google 账号并准许必要的权限。"
+
+msgid "Cannot run the command \"{}\"."
+msgstr "无法运行命令 \"{}\"。"
+
+msgid "Cannot find the command \"{}\"."
+msgstr "找不到命令 \"{}\"。"
msgid "Failed get APP key due to an invalid Token."
msgstr "因错误的令牌导致 APP 密钥获取失败。"
@@ -465,9 +482,18 @@ msgstr "任务日志"
msgid "Show translation"
msgstr "显示译文"
+msgid "Search Paths"
+msgstr "搜索路径"
+
+msgid "The plugin will search for external programs via these paths."
+msgstr "本插件将会通过以下路径搜索外部程序。"
+
msgid "Custom"
msgstr "自定义"
+msgid "Using Tip"
+msgstr "使用提示"
+
msgid "Tip:"
msgstr "提示:"
diff --git a/translations/zh_TW.mo b/translations/zh_TW.mo
index ec90f10..42833f0 100644
Binary files a/translations/zh_TW.mo and b/translations/zh_TW.mo differ
diff --git a/translations/zh_TW.po b/translations/zh_TW.po
index 9ecdafa..dbd2b5a 100644
--- a/translations/zh_TW.po
+++ b/translations/zh_TW.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Ebook Translator Calibre Plugin\n"
"Report-Msgid-Bugs-To: bookfere@gmail.com\n"
-"POT-Creation-Date: 2023-09-29 21:24+0800\n"
+"POT-Creation-Date: 2023-11-14 02:37+0800\n"
"PO-Revision-Date: 2023-04-25 15:36+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -331,8 +331,20 @@ msgstr "回應剖析不正確。"
msgid "{} total, {} used, {} left"
msgstr "總計 {},已用 {},剩餘 {}"
-msgid "Can not obtain Google API key. Reason: {}"
-msgstr "無法取得 Google API 金鑰。原因:{}"
+msgid ""
+"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."
+msgstr ""
+
+msgid "Cannot run the command \"{}\"."
+msgstr ""
+
+msgid "Cannot find the command \"{}\"."
+msgstr ""
msgid "Failed get APP key due to an invalid Token."
msgstr ""
@@ -466,9 +478,18 @@ msgstr "工作記錄"
msgid "Show translation"
msgstr "顯示譯文"
+msgid "Search Paths"
+msgstr ""
+
+msgid "The plugin will search for external programs via these paths."
+msgstr ""
+
msgid "Custom"
msgstr "自訂"
+msgid "Using Tip"
+msgstr ""
+
msgid "Tip:"
msgstr ""