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 ""