From bcf812aac5f8d951bb24e80fc21f225140a790d7 Mon Sep 17 00:00:00 2001 From: bookfere Date: Mon, 8 Apr 2024 23:12:39 +0800 Subject: [PATCH] feat: Supports reserving elements using CSS selectors. resolve #114, fix #238, resolve #271 --- advanced.py | 3 +- components/table.py | 2 +- lib/config.py | 1 + lib/element.py | 90 +++++++++++++++-------- lib/utils.py | 13 ++++ setting.py | 27 ++++++- tests/test_config.py | 1 + tests/test_element.py | 154 +++++++++++++++++++++++++++++++++++---- translations/es.po | 8 +- translations/fr.po | 8 +- translations/message.pot | 110 +++++++++++++++------------- translations/pt.po | 8 +- translations/tr.po | 8 +- translations/zh_CN.mo | Bin 17877 -> 18052 bytes translations/zh_CN.po | 8 +- translations/zh_TW.po | 8 +- 16 files changed, 343 insertions(+), 106 deletions(-) diff --git a/advanced.py b/advanced.py index 8248dde..d2ffb8f 100644 --- a/advanced.py +++ b/advanced.py @@ -467,7 +467,8 @@ def layout_filter(self): categories = QComboBox() categories.addItem(_('All'), 'all') - categories.addItem(_('Non-aligned'), 'non_aligned') + if self.merge_enabled: + categories.addItem(_('Non-aligned'), 'non_aligned') categories.addItem(_('Translated'), 'translated') categories.addItem(_('Untranslated'), 'untranslated') diff --git a/components/table.py b/components/table.py index e453cca..d4341db 100644 --- a/components/table.py +++ b/components/table.py @@ -84,7 +84,7 @@ def track_row_data(self, row): paragraph = self.paragraph(row) if paragraph.translation: before_aligned = paragraph.aligned - self.check_row_alignment(paragraph) + self.parent.merge_enabled and self.check_row_alignment(paragraph) # If the alignment of before and after is the same, do nothing. if before_aligned and not paragraph.aligned: self.non_aligned_count += 1 diff --git a/lib/config.py b/lib/config.py index f3b5002..a3babdb 100644 --- a/lib/config.py +++ b/lib/config.py @@ -28,6 +28,7 @@ 'filter_scope': 'text', 'filter_rules': [], 'element_rules': [], + 'reserve_rules': [], 'custom_engines': {}, 'glossary_enabled': False, 'glossary_path': None, diff --git a/lib/element.py b/lib/element.py index 546407b..0a52778 100644 --- a/lib/element.py +++ b/lib/element.py @@ -5,7 +5,8 @@ from lxml import etree from calibre import prepare_string_for_xml as xml_escape -from .utils import ns, css, uid, trim, sorted_mixed_keys, open_file +from .utils import ( + ns, uid, trim, sorted_mixed_keys, open_file, css_to_xpath, create_xpath) from .config import get_config @@ -36,6 +37,9 @@ def __init__(self, element, page_id=None): self.original_color = None self.translation_color = None + self.remove_pattern = None + self.reserve_pattern = None + def _element_copy(self): return copy.deepcopy(self.element) @@ -60,6 +64,12 @@ def set_original_color(self, color): def set_translation_color(self, color): self.translation_color = color + def set_remove_pattern(self, pattern): + self.remove_pattern = pattern + + def set_reserve_pattern(self, pattern): + self.reserve_pattern = pattern + def get_name(self): return None @@ -152,7 +162,7 @@ def add_translation(self, translation=None): self.element.content = '%s %s' % ( translation, self.element.content) else: - self.element.content = '%s %s' %( + self.element.content = '%s %s' % ( self.element.content, translation) @@ -175,11 +185,6 @@ def add_translation(self, translation=None): class PageElement(Element): - def _get_descendents(self, element, tags): - tags = (tags,) if isinstance(tags, str) else tags - xpath = './/*[%s]' % ' or '.join(['self::x:%s' % tag for tag in tags]) - return element.xpath(xpath, namespaces=ns) - def get_name(self): return get_name(self.element) @@ -206,18 +211,16 @@ def _safe_remove(self, element, replacement=''): def get_content(self): element_copy = self._element_copy() - for noise in self._get_descendents(element_copy, ('rt', 'rp')): - self._safe_remove(noise) - # Reserve the
element instead of using a line break to prevent - # conflicts with the mechanism of merge translation. - target_elements = ( - 'img', 'code', 'br', 'hr', 'sub', 'sup', 'kbd', 'abbr', 'wbr', 'var', - 'canvas', 'svg', 'script', 'style') - self.reserve_elements = self._get_descendents( - element_copy, target_elements) + if self.remove_pattern is not None: + for noise in element_copy.xpath( + self.remove_pattern, namespaces=ns): + self._safe_remove(noise) + if self.reserve_pattern is not None: + self.reserve_elements = element_copy.xpath( + self.reserve_pattern, namespaces=ns) for eid, reserve in enumerate(self.reserve_elements): replacement = self.placeholder[0].format(format(eid, '05')) - if get_name(reserve) in ['sub', 'sup']: + if get_name(reserve) in ('sub', 'sup'): parent = reserve.getparent() if parent is not None and get_name(parent) == 'a' and \ parent.text is None and reserve.tail is None and \ @@ -423,6 +426,11 @@ def _create_table(self, translation=None): class Extraction: + priority_elements = ( + 'p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote') + default_filter_rules = ( + r'^[-\d\s\.\'\\"‘’“”,=~!@#$%^&º*|≈<>?/`—…+:–_(){}[\]]+$',) + def __init__( self, pages, rule_mode, filter_scope, filter_rules, element_rules): self.pages = pages @@ -438,9 +446,7 @@ def __init__( self.load_element_patterns() def load_filter_patterns(self): - default_rules = [ - r'^[-\d\s\.\'\\"‘’“”,=~!@#$%^&º*|≈<>?/`—…+:–_(){}[\]]+$'] - patterns = [re.compile(rule) for rule in default_rules] + patterns = [re.compile(rule) for rule in self.default_filter_rules] for rule in self.filter_rules: if self.rule_mode == 'normal': rule = re.compile(re.escape(rule), re.I) @@ -452,13 +458,9 @@ def load_filter_patterns(self): self.filter_patterns = patterns def load_element_patterns(self): - rules = ['pre', 'code'] - rules.extend(self.element_rules) - patterns = [] - for selector in rules: - rule = css(selector) - rule and patterns.append(rule) - self.element_patterns = patterns + default_selectors = ['pre', 'code'] + self.element_patterns = css_to_xpath( + default_selectors + self.element_rules) def get_sorted_pages(self): pages = [] @@ -483,15 +485,20 @@ def need_ignore(self, element): return False def extract_elements(self, page_id, root, elements=[]): - priority_elements = [ - 'p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'] + """If the root matches the pattern, return an empty list; otherwise, + just break the recursion without doing anything. + """ + if self.need_ignore(root): + return [] for element in root.findall('./*'): + if self.need_ignore(element): + continue element_has_content = False if element.text is not None and trim(element.text) != '': element_has_content = True else: children = element.findall('./*') - if children and get_name(element) in priority_elements: + if children and get_name(element) in self.priority_elements: element_has_content = True else: for child in children: @@ -540,6 +547,9 @@ def __init__(self, placeholder, separator, position, merge_length=0): self.translation_color = None self.column_gap = None + self.remove_pattern = None + self.reserve_pattern = None + self.elements = {} self.originals = [] @@ -559,6 +569,18 @@ def set_column_gap(self, values): if isinstance(values, tuple) and len(values) == 2: self.column_gap = values + def load_remove_rules(self, rules=[]): + default_rules = ('rt', 'rp') + self.remove_pattern = create_xpath(default_rules + tuple(rules)) + + def load_reserve_rules(self, rules=[]): + # Reserve the
element instead of using a line break to prevent + # conflicts with the mechanism of merge translation. + default_rules = ( + 'img', 'code', 'br', 'hr', 'sub', 'sup', 'kbd', 'abbr', 'wbr', + 'var', 'canvas', 'svg', 'script', 'style') + self.reserve_pattern = create_xpath(default_rules + tuple(rules)) + def prepare_original(self, elements): count = 0 for oid, element in enumerate(elements): @@ -569,6 +591,8 @@ def prepare_original(self, elements): element.set_translation_color(self.translation_color) if self.column_gap is not None: element.set_column_gap(self.column_gap) + element.set_remove_pattern(self.remove_pattern) + element.set_reserve_pattern(self.reserve_pattern) raw = element.get_raw() content = element.get_content() md5 = uid('%s%s' % (oid, content)) @@ -618,6 +642,8 @@ def prepare_original(self, elements): element.set_translation_color(self.translation_color) if self.column_gap is not None: element.set_column_gap(self.column_gap) + element.set_remove_pattern(self.remove_pattern) + element.set_reserve_pattern(self.reserve_pattern) code = element.get_raw() content = element.get_content() content += self.separator @@ -725,7 +751,7 @@ def get_page_elements(pages): config = get_config() rule_mode = config.get('rule_mode') filter_scope = config.get('filter_scope') - filter_rules = config.get('filter_rules') + filter_rules = config.get('filter_rules', []) element_rules = config.get('element_rules', []) extraction = Extraction( pages, rule_mode, filter_scope, filter_rules, element_rules) @@ -747,4 +773,6 @@ def get_element_handler(placeholder, separator): handler.set_column_gap((gap_type, column_gap.get(gap_type))) handler.set_original_color(config.get('original_color')) handler.set_translation_color(config.get('translation_color')) + handler.load_remove_rules(config.get('element_rules', [])) + handler.load_reserve_rules(config.get('reserve_rules', [])) return handler diff --git a/lib/utils.py b/lib/utils.py index 9c971d0..d170f22 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -25,6 +25,19 @@ def css(seletor): return None +def css_to_xpath(selectors): + patterns = [] + for selector in selectors: + rule = css(selector) + rule and patterns.append(rule) + return patterns + + +def create_xpath(selectors): + selectors = (selectors,) if isinstance(selectors, str) else selectors + return './/*[%s]' % ' or '.join(css_to_xpath(selectors)) + + def uid(*args): md5 = hashlib.md5() for arg in args: diff --git a/setting.py b/setting.py index a00d996..7427289 100644 --- a/setting.py +++ b/setting.py @@ -1072,12 +1072,25 @@ def choose_filter_mode(btn_id): self.element_rules.setMinimumHeight(100) self.element_rules.insertPlainText( '\n'.join(self.config.get('element_rules'))) - element_layout.addWidget(QLabel( _('CSS selectors to exclude elements. One rule per line:'))) element_layout.addWidget(self.element_rules) layout.addWidget(element_group) + # Reserve element + reserve_group = QGroupBox(_('Reserve Element')) + reserve_layout = QVBoxLayout(reserve_group) + self.reserve_rules = QPlainTextEdit() + self.reserve_rules.setPlaceholderText( + '%s %s' % (_('e.g.,'), 'span.footnote, a#footnote')) + self.reserve_rules.setMinimumHeight(100) + self.reserve_rules.insertPlainText( + '\n'.join(self.config.get('reserve_rules'))) + reserve_layout.addWidget(QLabel( + _('CSS selectors to reserve elements. One rule per line:'))) + reserve_layout.addWidget(self.reserve_rules) + layout.addWidget(reserve_group) + # Ebook Metadata metadata_group = QGroupBox(_('Ebook Metadata')) metadata_layout = QFormLayout(metadata_group) @@ -1280,6 +1293,18 @@ def update_content_config(self): self.config.delete('element_rules') element_rules and self.config.update(element_rules=element_rules) + # Reserve rules + rule_content = self.reserve_rules.toPlainText() + reserve_rules = [r for r in rule_content.split('\n') if r.strip()] + for rule in reserve_rules: + if css(rule) is None: + self.alert.pop( + _('{} is not a valid CSS seletor.') + .format(rule), 'warning') + return False + self.config.delete('reserve_rules') + reserve_rules and self.config.update(reserve_rules=reserve_rules) + # Ebook metadata ebook_metadata = self.config.get('ebook_metadata').copy() ebook_metadata.clear() diff --git a/tests/test_config.py b/tests/test_config.py index 50271d1..11bc8be 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -33,6 +33,7 @@ def test_default(self): 'filter_scope': 'text', 'filter_rules': [], 'element_rules': [], + 'reserve_rules': [], 'custom_engines': {}, 'glossary_enabled': False, 'glossary_path': None, diff --git a/tests/test_element.py b/tests/test_element.py index 9daccbe..f1d8ed8 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -1,3 +1,4 @@ +import re import unittest from unittest.mock import patch, Mock @@ -5,7 +6,7 @@ from calibre.ebooks.oeb.base import TOC, Metadata -from ..lib.utils import ns +from ..lib.utils import ns, create_xpath from ..lib.cache import Paragraph from ..lib.element import ( get_string, get_name, Extraction, ElementHandler, ElementHandlerMerge, @@ -20,7 +21,7 @@ class TestFunction(unittest.TestCase): def test_get_string(self): markup = '
' \ '

abc

def
' - element = etree.XML(markup).find('x:p', namespaces=ns) + element = etree.XML(markup).find('.//x:p', namespaces=ns) self.assertEqual( '

abc

', get_string(element, False)) @@ -115,6 +116,8 @@ def test_create_element(self): self.assertIsNone(self.element.translation_lang) self.assertIsNone(self.element.original_color) self.assertIsNone(self.element.translation_color) + self.assertIsNone(self.element.remove_pattern) + self.assertIsNone(self.element.reserve_pattern) def test_set_ignored(self): self.element.set_ignored(True) @@ -144,6 +147,14 @@ def test_set_translation_color(self): self.element.set_translation_color('green') self.assertEqual('green', self.element.translation_color) + def test_set_remove_pattern(self): + self.element.set_remove_pattern('.//*[self::x:sup]') + self.assertEqual('.//*[self::x:sup]', self.element.remove_pattern) + + def test_set_reserve_pattern(self): + self.element.set_reserve_pattern('.//*[self::x:sup]') + self.assertEqual('.//*[self::x:sup]', self.element.reserve_pattern) + def test_get_name(self): self.assertIsNone(self.element.get_name()) @@ -363,6 +374,9 @@ def setUp(self): """) self.paragraph = self.xhtml.find('.//x:p', namespaces=ns) self.element = PageElement(self.paragraph, 'p1') + self.element.remove_pattern = create_xpath(('rt',)) + self.element.reserve_pattern = create_xpath( + ('img', 'sup', 'br', 'code')) self.element.placeholder = Base.placeholder self.element.position = 'below' @@ -407,15 +421,16 @@ def test_get_content_with_sup_sub(self):

""") - self.element = PageElement(xhtml.find('.//x:p', namespaces=ns), 'p1') - self.element.placeholder = Base.placeholder + element = PageElement(xhtml.find('.//x:p', namespaces=ns), 'p1') + element.reserve_pattern = create_xpath(('sup',)) + element.placeholder = Base.placeholder content = ( 'a{{id_00000}} b{{id_00001}} x cx {{id_00002}} d{{id_00003}} x') - self.assertEqual(content, self.element.get_content()) - self.assertEqual('a', get_name(self.element.reserve_elements[0])) - self.assertEqual('sup', get_name(self.element.reserve_elements[1])) - self.assertEqual('sup', get_name(self.element.reserve_elements[2])) - self.assertEqual('sup', get_name(self.element.reserve_elements[3])) + self.assertEqual(content, element.get_content()) + self.assertEqual('a', get_name(element.reserve_elements[0])) + self.assertEqual('sup', get_name(element.reserve_elements[1])) + self.assertEqual('sup', get_name(element.reserve_elements[2])) + self.assertEqual('sup', get_name(element.reserve_elements[3])) def test_get_attributes(self): self.assertEqual('{"class": "abc"}', self.element.get_attributes()) @@ -634,6 +649,7 @@ def test_add_translation_line_break_below(self): """) element = PageElement(xhtml.find('.//x:p', namespaces=ns), 'p1') + element.reserve_pattern = create_xpath(('sup', 'img', 'br')) element.placeholder = Base.placeholder element.position = 'below' element.get_content() @@ -665,6 +681,7 @@ def test_add_translation_line_break_above(self): """) element = PageElement(xhtml.find('.//x:p', namespaces=ns), 'p1') + element.reserve_pattern = create_xpath(('sup', 'img', 'br')) element.placeholder = Base.placeholder element.position = 'above' element.get_content() @@ -725,6 +742,26 @@ def setUp(self): self.extraction = Extraction( [self.page_3, self.page_2, self.page_1], 'normal', 'text', [], []) + def test_create_extraction(self): + self.assertEqual( + ('p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'), + Extraction.priority_elements) + self.assertEqual( + (r'^[-\d\s\.\'\\"‘’“”,=~!@#$%^&º*|≈<>?/`—…+:–_(){}[\]]+$',), + Extraction.default_filter_rules) + self.assertIsInstance(self.extraction, Extraction) + self.assertEqual( + [self.page_3, self.page_2, self.page_1], self.extraction.pages) + self.assertEqual('normal', self.extraction.rule_mode) + self.assertEqual('text', self.extraction.filter_scope) + self.assertEqual([], self.extraction.filter_rules) + self.assertEqual([], self.extraction.element_rules) + self.assertEqual( + [re.compile(Extraction.default_filter_rules[0])], + self.extraction.filter_patterns) + self.assertEqual( + ['self::x:pre', 'self::x:code'], self.extraction.element_patterns) + def test_get_sorted_pages(self): self.assertEqual( [self.page_1, self.page_2], self.extraction.get_sorted_pages()) @@ -804,27 +841,64 @@ def test_extract_elements(self): """) - root = xhtml.find('x:body', namespaces=ns) + root = xhtml.find('.//x:body', namespaces=ns) elements = self.extraction.extract_elements('p1', root, []) - self.assertEqual(9, len(elements)) + self.assertEqual(8, len(elements)) self.assertEqual('h2', elements[0].get_name()) self.assertFalse(elements[0].ignored) - self.assertEqual('pre', elements[-2].get_name()) - self.assertTrue(elements[-2].ignored) self.assertEqual('p', elements[-1].get_name()) self.assertFalse(elements[-1].ignored) + def test_extract_elements_root_wihout_elements(self): xhtml = etree.XML(b""" Document 123456789 """) - root = xhtml.find('x:body', namespaces=ns) + root = xhtml.find('.//x:body', namespaces=ns) + elements = self.extraction.extract_elements('test', root, []) self.assertEqual(1, len(elements)) self.assertEqual('123456789', elements[0].element.text) + def test_extract_elements_ignore_root(self): + xhtml = etree.XML(b""" + + +Document +

a

+""") + root = xhtml.find('.//x:body', namespaces=ns) + self.extraction.element_rules = ['.test'] + self.extraction.load_element_patterns() + + self.assertEqual( + [], self.extraction.extract_elements('test', root, [])) + + def test_extract_elements_ignore_certain_elements(self): + xhtml = etree.XML(b""" + + +Document + +

a

+

b

c

+

d

+

e

+ +""") + root = xhtml.find('.//x:body', namespaces=ns) + self.extraction.element_rules = ['.test', '#test'] + self.extraction.load_element_patterns() + + elements = self.extraction.extract_elements('test', root, []) + self.assertEqual(2, len(elements)) + self.assertEqual( + xhtml.find('.//x:div[1]/x:p', namespaces=ns), elements[0].element) + self.assertEqual( + xhtml.find('.//x:div[3]/x:p', namespaces=ns), elements[1].element) + def test_filter_content(self): def elements(markups): return [ @@ -935,6 +1009,8 @@ def test_create_element_handler_merge(self): self.assertIsNone(self.handler.original_color) self.assertIsNone(self.handler.translation_color) self.assertIsNone(self.handler.column_gap) + self.assertIsNone(self.handler.remove_pattern) + self.assertIsNone(self.handler.reserve_pattern) self.assertEqual({}, self.handler.elements) self.assertEqual([], self.handler.originals) @@ -963,8 +1039,24 @@ def test_set_column_gap(self): self.handler.set_column_gap(('percentage', 2)) self.assertEqual(('percentage', 2), self.handler.column_gap) + def test_load_remove_rules(self): + self.handler.load_remove_rules() + self.assertEqual( + './/*[self::x:rt or self::x:rp]', self.handler.remove_pattern) + + def test_load_reserve_rules(self): + self.handler.load_reserve_rules() + self.assertRegex( + self.handler.reserve_pattern, r'^\.//\*\[self::x:img.*style\]$') + @patch('calibre_plugins.ebook_translator.lib.element.uid') def test_prepare_original(self, mock_uid): + self.handler.translation_lang = 'en' + self.handler.original_color = 'red' + self.handler.translation_color = 'green' + self.handler.column_gap = ('percentage', 20) + self.handler.load_remove_rules() + self.handler.load_reserve_rules() mock_uid.side_effect = ['m1', 'm2', 'm3', 'm4', 'm5'] self.assertEqual([ (0, 'm1', '

a

', 'a', False, '{"id": "a"}', 'p1'), @@ -975,6 +1067,19 @@ def test_prepare_original(self, mock_uid): '{"id": "c", "class": "c"}', 'p1'), (4, 'm5', '

', '', True, None, 'p1')], self.handler.prepare_original(self.elements)) + for element in self.elements: + with self.subTest(element=element): + self.assertEqual(Base.placeholder, element.placeholder) + self.assertEqual('below', element.position) + self.assertEqual('en', element.translation_lang) + self.assertEqual('red', element.original_color) + self.assertEqual('green', element.translation_color) + self.assertEqual(('percentage', 20), element.column_gap) + self.assertEqual( + './/*[self::x:rt or self::x:rp]', + self.handler.remove_pattern) + self.assertRegex( + element.reserve_pattern, r'^\.//\*\[self::x:img.*style\]$') def test_prepare_translation(self): pass @@ -1146,17 +1251,36 @@ def test_align_paragraph(self): self.handler.position = 'above' self.assertEqual( - [('a', 'A\n\nB'),('b', None), ('c', None)], + [('a', 'A\n\nB'), ('b', None), ('c', None)], self.handler.align_paragraph(paragraph)) @patch('calibre_plugins.ebook_translator.lib.element.uid') def test_prepare_original_merge_separator(self, mock_uid): mock_uid.return_value = 'm1' self.handler.separator = Base.separator + self.handler.translation_lang = 'en' + self.handler.original_color = 'red' + self.handler.translation_color = 'green' + self.handler.column_gap = ('percentage', 20) + self.handler.load_remove_rules() + self.handler.load_reserve_rules() self.assertEqual([( 0, 'm1', '

a

\n\n

b

\n\n

c

\n\n', 'a\n\nb\n\nc\n\n', False)], self.handler.prepare_original(self.elements)) + for element in [e for e in self.elements if not e.ignored]: + with self.subTest(element=element): + self.assertEqual(Base.placeholder, element.placeholder) + self.assertEqual('below', element.position) + self.assertEqual('en', element.translation_lang) + self.assertEqual('red', element.original_color) + self.assertEqual('green', element.translation_color) + self.assertEqual(('percentage', 20), element.column_gap) + self.assertEqual( + './/*[self::x:rt or self::x:rp]', + self.handler.remove_pattern) + self.assertRegex( + element.reserve_pattern, r'^\.//\*\[self::x:img.*style\]$') @patch('calibre_plugins.ebook_translator.lib.element.uid') def test_prepare_original_merge_separator_multiple(self, mock_uid): diff --git a/translations/es.po b/translations/es.po index 2d6c325..81bac4d 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: 2024-04-08 00:18+0800\n" +"POT-Creation-Date: 2024-04-08 23:03+0800\n" "PO-Revision-Date: 2023-04-17 14:17+0800\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -709,6 +709,12 @@ msgstr "" msgid "CSS selectors to exclude elements. One rule per line:" msgstr "" +msgid "Reserve Element" +msgstr "" + +msgid "CSS selectors to reserve elements. One rule per line:" +msgstr "" + msgid "Ebook Metadata" msgstr "" diff --git a/translations/fr.po b/translations/fr.po index 0849d3b..226ce62 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: 2024-04-08 00:18+0800\n" +"POT-Creation-Date: 2024-04-08 23:03+0800\n" "PO-Revision-Date: 2023-10-01 15:35-0400\n" "Last-Translator: PoP\n" @@ -720,6 +720,12 @@ msgstr "Ignorer l'élément" msgid "CSS selectors to exclude elements. One rule per line:" msgstr "Sélecteurs CSS pour exclure des éléments. Une règle par ligne:" +msgid "Reserve Element" +msgstr "" + +msgid "CSS selectors to reserve elements. One rule per line:" +msgstr "" + msgid "Ebook Metadata" msgstr "Metadata du ebook" diff --git a/translations/message.pot b/translations/message.pot index baf9fa9..d426b21 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: 2024-04-08 00:18+0800\n" +"POT-Creation-Date: 2024-04-08 23:03+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -51,7 +51,7 @@ msgstr "" msgid "Input Format" msgstr "" -#: advanced.py:236 advanced.py:683 batch.py:58 setting.py:369 +#: advanced.py:236 advanced.py:684 batch.py:58 setting.py:369 msgid "Target Language" msgstr "" @@ -91,127 +91,127 @@ msgstr "" msgid "All" msgstr "" -#: advanced.py:470 +#: advanced.py:471 msgid "Non-aligned" msgstr "" -#: advanced.py:471 advanced.py:773 components/table.py:94 +#: advanced.py:472 advanced.py:774 components/table.py:94 msgid "Translated" msgstr "" -#: advanced.py:472 components/table.py:83 +#: advanced.py:473 components/table.py:83 msgid "Untranslated" msgstr "" -#: advanced.py:475 +#: advanced.py:476 msgid "keyword for filtering" msgstr "" -#: advanced.py:554 +#: advanced.py:555 msgid "Total items: {}" msgstr "" -#: advanced.py:555 lib/translation.py:233 +#: advanced.py:556 lib/translation.py:233 msgid "Character count: {}" msgstr "" -#: advanced.py:564 +#: advanced.py:565 msgid "Non-aligned items: {}" msgstr "" -#: advanced.py:598 cache.py:92 cache.py:182 components/engine.py:202 +#: advanced.py:599 cache.py:92 cache.py:182 components/engine.py:202 #: components/table.py:139 msgid "Delete" msgstr "" -#: advanced.py:599 +#: advanced.py:600 msgid "Translate All" msgstr "" -#: advanced.py:600 +#: advanced.py:601 msgid "Translate Selected" msgstr "" -#: advanced.py:625 advanced.py:636 +#: advanced.py:626 advanced.py:637 msgid "Stop" msgstr "" -#: advanced.py:631 +#: advanced.py:632 msgid "Stopping..." msgstr "" -#: advanced.py:661 +#: advanced.py:662 msgid "Cache Status" msgstr "" -#: advanced.py:664 +#: advanced.py:665 msgid "Disabled" msgstr "" -#: advanced.py:664 +#: advanced.py:665 msgid "Enabled" msgstr "" -#: advanced.py:671 setting.py:328 +#: advanced.py:672 setting.py:328 msgid "Translation Engine" msgstr "" -#: advanced.py:677 batch.py:58 setting.py:368 +#: advanced.py:678 batch.py:58 setting.py:368 msgid "Source Language" msgstr "" -#: advanced.py:689 +#: advanced.py:690 msgid "Custom Ebook Title" msgstr "" -#: advanced.py:694 +#: advanced.py:695 msgid "By default, title metadata will be translated." msgstr "" -#: advanced.py:723 +#: advanced.py:724 msgid "Output Ebook" msgstr "" -#: advanced.py:725 +#: advanced.py:726 msgid "Output" msgstr "" -#: advanced.py:774 +#: advanced.py:775 msgid "The ebook has not been translated yet." msgstr "" -#: advanced.py:778 +#: advanced.py:779 msgid "" "The number of lines in some translation units differs between the original " "text and the translated text. Are you sure you want to output without " "checking alignment?" msgstr "" -#: advanced.py:813 +#: advanced.py:814 msgid "No translation yet" msgstr "" -#: advanced.py:866 components/engine.py:208 setting.py:98 +#: advanced.py:867 components/engine.py:208 setting.py:98 msgid "Save" msgstr "" -#: advanced.py:922 +#: advanced.py:923 msgid "Your changes have been saved." msgstr "" -#: advanced.py:935 +#: advanced.py:936 msgid "Translation log" msgstr "" -#: advanced.py:946 +#: advanced.py:947 msgid "Error log" msgstr "" -#: advanced.py:966 +#: advanced.py:967 msgid "Are you sure you want to translate all {:n} paragraphs?" msgstr "" -#: advanced.py:992 +#: advanced.py:993 msgid "Are you sure you want to stop the translation progress?" msgstr "" @@ -235,7 +235,7 @@ msgstr "" msgid "Unknown" msgstr "" -#: batch.py:160 cache.py:130 setting.py:1136 +#: batch.py:160 cache.py:130 setting.py:1149 msgid "The specified path does not exist." msgstr "" @@ -374,7 +374,7 @@ msgstr "" msgid "Feedback" msgstr "" -#: components/lang.py:34 engines/base.py:70 setting.py:1198 +#: components/lang.py:34 engines/base.py:70 setting.py:1211 msgid "Auto detect" msgstr "" @@ -871,7 +871,7 @@ msgstr "" msgid "Original Text Color" msgstr "" -#: setting.py:894 setting.py:912 setting.py:1071 +#: setting.py:894 setting.py:912 setting.py:1071 setting.py:1085 msgid "e.g.," msgstr "" @@ -935,67 +935,75 @@ msgstr "" msgid "Ignore Element" msgstr "" -#: setting.py:1077 +#: setting.py:1076 msgid "CSS selectors to exclude elements. One rule per line:" msgstr "" -#: setting.py:1082 +#: setting.py:1081 +msgid "Reserve Element" +msgstr "" + +#: setting.py:1090 +msgid "CSS selectors to reserve elements. One rule per line:" +msgstr "" + +#: setting.py:1095 msgid "Ebook Metadata" msgstr "" -#: setting.py:1086 +#: setting.py:1099 msgid "Append target language to title metadata" msgstr "" -#: setting.py:1088 +#: setting.py:1101 msgid "Set target language code to language metadata" msgstr "" -#: setting.py:1091 +#: setting.py:1104 msgid "Subjects of ebook (one subject per line)" msgstr "" -#: setting.py:1092 +#: setting.py:1105 msgid "Language Mark" msgstr "" -#: setting.py:1093 +#: setting.py:1106 msgid "Language Code" msgstr "" -#: setting.py:1094 +#: setting.py:1107 msgid "Append Subjects" msgstr "" -#: setting.py:1115 setting.py:1150 +#: setting.py:1128 setting.py:1163 msgid "Proxy host or port is incorrect." msgstr "" -#: setting.py:1117 +#: setting.py:1130 msgid "The proxy is available." msgstr "" -#: setting.py:1118 +#: setting.py:1131 msgid "The proxy is not available." msgstr "" -#: setting.py:1208 +#: setting.py:1221 msgid "the prompt must include {}." msgstr "" -#: setting.py:1237 setting.py:1244 +#: setting.py:1250 setting.py:1257 msgid "Invalid color value." msgstr "" -#: setting.py:1253 +#: setting.py:1266 msgid "The specified glossary file does not exist." msgstr "" -#: setting.py:1265 +#: setting.py:1278 msgid "{} is not a valid regular expression." msgstr "" -#: setting.py:1277 +#: setting.py:1290 setting.py:1302 msgid "{} is not a valid CSS seletor." msgstr "" diff --git a/translations/pt.po b/translations/pt.po index 8c7ef31..bd0647e 100644 --- a/translations/pt.po +++ b/translations/pt.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: 2024-04-08 00:18+0800\n" +"POT-Creation-Date: 2024-04-08 23:03+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: none\n" @@ -723,6 +723,12 @@ msgstr "Ignorar Elemento" msgid "CSS selectors to exclude elements. One rule per line:" msgstr "Seletores CSS para excluir elementos. Uma regra por linha:" +msgid "Reserve Element" +msgstr "" + +msgid "CSS selectors to reserve elements. One rule per line:" +msgstr "" + msgid "Ebook Metadata" msgstr "Metadados dos ebooks" diff --git a/translations/tr.po b/translations/tr.po index bdcf188..5a984a1 100644 --- a/translations/tr.po +++ b/translations/tr.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: 2024-04-08 00:18+0800\n" +"POT-Creation-Date: 2024-04-08 23:03+0800\n" "PO-Revision-Date: 2024-03-23 15:12+0300\n" "Last-Translator: DogancanYr \n" "Language-Team: Turkish \n" @@ -722,6 +722,12 @@ msgstr "Öğeyi Yoksay" msgid "CSS selectors to exclude elements. One rule per line:" msgstr "Öğeleri dışlamak için CSS seçicileri. " +msgid "Reserve Element" +msgstr "" + +msgid "CSS selectors to reserve elements. One rule per line:" +msgstr "" + msgid "Ebook Metadata" msgstr "E-kitap Meta Verisi" diff --git a/translations/zh_CN.mo b/translations/zh_CN.mo index a5f578a7a2fc22eaa82d38adde3ea5cb80ce1493..190abe4c425906a8a19248abb0c2e1559ce22c47 100644 GIT binary patch delta 5479 zcmYk=30PKD9>?*cf`Fg_F5r&jh8v2cxGQ36Gg5|IYU*g1qY?=WGdk* zmb+s~sAHLF+EkWHu49-sjti4Zj=M(N_xIlaJkQ*xAD?s1z3;i_o^$T|Rwoyfo4u%< z?_6ZSGRILy>X17@&h;Rfnr=PF_Zj>Bk7z!}&Ww_z}z!D@IB6Y(K-!B#c+ zEKbBgoQ_p+Hdb)X=kh32rlG)VaBHy-^^KTrwun`_W-S7sgqx+^F zQ`d!{9v~cUS6F`hz|Su@p`quY(~x_5N4j-P6dz zP#r~}W+Do?#x+EB)Eo7_k3!vGG3rTPL+$SMr~wsWQ~Ve;V^>i9-oqID(X88$nd1Jg zD+T^>&+!z+>DlJYzg~}fc0pJR@BOZCCL+H-t}p7kQC81Iy)}!hz8W>qo#y8l zM*TGEhL_Bn7)1Ri48uSlJ4#O$i5htwtH-0h!#iL_Ohaw9p;n)Odj_ zXHZjFfh`|_^-u$8jgiKUkkVWi`6k7eLAs;f;!lZdcsdo9bB{ad*)AO0ACt< zl2FuM=!|-zL8u$dN6kb5s^5(`7`I|)3}(Y5W3TqCKO54`qe1WE4rDdl5!B`?&mT=) zC)B_Oqt?)e>Ua}sphXysyR7{L>is{1di^e&x9t4SsDTD{VE$EzWGl-0SRGrK$rw$2 z2S8o&~(7no~M11_@qA=Gsxs0TQ2^^lHUd$^B+)-np!aT7DqF6d$QLA~!! zqLwTJb^S}I8CZnXumE+#zo7rbI(fQRB?tAI=AmY4C+hm6s2iR(FPXPcGyDtEpU+iiM`&v5qXyC&wZ?<7HqJzK zvW5Jsp0xT^^A_rJ53OF2Uo?HbvRMtoxWB7Sp$;dSqNcVNYEuqDeP9l% zqeZA6n-x~yhbff3ycuPKSQ-2N6koyS&CZgJ65lh;&l{->bL=FiQ1q(-yNf{x1Aq}>MzskQ&W6i z!$Lc;9QEYu%=gWMcK%!Q8fu^qQ8x_i<^6hAGhhsV0C}@qc zQ6E@}dXnW<--sH}R@9U4M|Jo$R=^9W>q}8ne;?zp>f_$hb-@PIM#G+Moe-;f#P6H7E&ytQoWQ4!ut;<=;|0dej?~#o}$7deyAE+;l zqhzrWE%M$Sx0o#B!wiEY#};Q^mu0|Jnyga1BT12tc|ZDm?V<= zs&M?1+oMc$rPHaU=$ZOX25Qkg&8pd0>Gxv9E$w0DzWD^~)d$?`pc-(4b z!SAO3c#^*}$zigOgp;x4f5#3A*U1{Plthw#B#s;;zPkL;@f3N9L=YYCdbmBf*~%io z%589yl{?{AR$gJ&!v17ESxEj$3dmk^iX0)MHBo^S{zyg<9dD8fB$OnO2ILWPfsFBQ z+1ZrN6CHo@@c)hC?^|SvwH2G<2P<#K{Z=l;VWfzBOxlrMWGLxQo+XFKpUDC8cfyhC z|F1}u(#ZpIhx~ywCf&#o^8d$WDtV*|`I4L??~oTrS@HF1B^7Ffm7blRm0P?%dTB^#R&IJsuZigy=`Z9IC&s3Shvsj%ymIZuJu6F>EiWFIkRKA} Sl`p+Kt909&#bupJLVgAB%|kr^ delta 5387 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 Ho(}sTf8`=& diff --git a/translations/zh_CN.po b/translations/zh_CN.po index c365b62..fa361a5 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: 2024-04-08 00:18+0800\n" +"POT-Creation-Date: 2024-04-08 23:03+0800\n" "PO-Revision-Date: 2023-04-17 14:17+0800\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -709,6 +709,12 @@ msgstr "忽略元素" msgid "CSS selectors to exclude elements. One rule per line:" msgstr "用来排除元素的 CSS 选择器。一行一条规则:" +msgid "Reserve Element" +msgstr "保留元素" + +msgid "CSS selectors to reserve elements. One rule per line:" +msgstr "用来保留元素的 CSS 选择器。一行一条规则:" + msgid "Ebook Metadata" msgstr "电子书元数据" diff --git a/translations/zh_TW.po b/translations/zh_TW.po index 372ff8d..7ad5f3b 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: 2024-04-08 00:18+0800\n" +"POT-Creation-Date: 2024-04-08 23:03+0800\n" "PO-Revision-Date: 2023-04-25 15:36+0800\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -705,6 +705,12 @@ msgstr "" msgid "CSS selectors to exclude elements. One rule per line:" msgstr "" +msgid "Reserve Element" +msgstr "" + +msgid "CSS selectors to reserve elements. One rule per line:" +msgstr "" + msgid "Ebook Metadata" msgstr ""