From 4a8f9640c15cd2e5819d27c95736d159a17e3195 Mon Sep 17 00:00:00 2001 From: bookfere Date: Mon, 26 Feb 2024 23:06:15 +0800 Subject: [PATCH] feat: Highlights inconsistent paragraphs between the original and translation lines. resolved #82 --- advanced.py | 44 ++++++++++++++++-------------- components/table.py | 27 ++++++++++++++++--- lib/cache.py | 7 +++++ lib/translation.py | 1 + test.py | 11 ++++---- tests/test_cache.py | 20 ++++++++++++++ translations/es.po | 5 +++- translations/fr.po | 5 +++- translations/message.pot | 56 +++++++++++++++++++++------------------ translations/pt.po | 5 +++- translations/zh_CN.mo | Bin 15754 -> 15866 bytes translations/zh_CN.po | 5 +++- translations/zh_TW.po | 5 +++- 13 files changed, 131 insertions(+), 60 deletions(-) create mode 100644 tests/test_cache.py diff --git a/advanced.py b/advanced.py index 4b33dfd..3c94ab9 100644 --- a/advanced.py +++ b/advanced.py @@ -420,8 +420,30 @@ def write_progress(): progress_bar.setValue(value) self.progress_bar.connect(write_progress) + paragraph_count = QLabel() + + def get_paragraph_count(select_all=True): + item_count = char_count = 0 + paragraphs = self.table.get_selected_items(select_all=select_all) + for paragraph in paragraphs: + item_count += 1 + char_count += len(paragraph.original) + return (item_count, char_count) + all_item_count, all_char_count = get_paragraph_count(True) + + def item_selection_changed(): + item_count, char_count = get_paragraph_count(False) + total = '%s/%s' % (item_count, all_item_count) + parts = '%s/%s' % (char_count, all_char_count) + paragraph_count.setText( + _('Total items: {}').format(total) + ' · ' + + _('Character count: {}').format(parts)) + item_selection_changed() + self.table.itemSelectionChanged.connect(item_selection_changed) + layout.addWidget(self.table, 1) layout.addWidget(progress_bar) + layout.addWidget(paragraph_count) layout.addWidget(self.layout_table_control()) def working_start(): @@ -441,8 +463,6 @@ def layout_table_control(self): action_layout.setContentsMargins(0, 0, 0, 0) delete_button = QPushButton(_('Delete')) - paragraph_count = QLabel() - paragraph_count.setAlignment(Qt.AlignCenter) translate_all = QPushButton(' %s ' % _('Translate All')) translate_selected = QPushButton(' %s ' % _('Translate Selected')) @@ -453,30 +473,15 @@ def layout_table_control(self): delete_button.setDisabled(True) translate_selected.setDisabled(True) - def get_paragraph_count(select_all=True): - item_count = char_count = 0 - paragraphs = self.table.get_selected_items(select_all=select_all) - for paragraph in paragraphs: - item_count += 1 - char_count += len(paragraph.original) - return (item_count, char_count) - all_item_count, all_char_count = get_paragraph_count(True) - def item_selection_changed(): disabled = self.table.selected_count() < 1 delete_button.setDisabled(disabled) translate_selected.setDisabled(disabled) - item_count, char_count = get_paragraph_count(False) - total = '%s/%s' % (item_count, all_item_count) - parts = '%s/%s' % (char_count, all_char_count) - paragraph_count.setText( - _('Total items: {}').format(total) + ' · ' + - _('Character count: {}').format(parts)) item_selection_changed() self.table.itemSelectionChanged.connect(item_selection_changed) action_layout.addWidget(delete_button) - action_layout.addWidget(paragraph_count, 1) + action_layout.addStretch(1) action_layout.addWidget(translate_all) action_layout.addWidget(translate_selected) @@ -708,8 +713,7 @@ def change_selected_item(): change_selected_item() def translation_callback(paragraph): - row = paragraph.row - self.table.row.emit(row) + self.table.row.emit(paragraph.row) self.raw_text.emit(paragraph.raw) self.original_text.emit(paragraph.original) self.translation_text[str].emit(paragraph.translation) diff --git a/components/table.py b/components/table.py index f5442dc..33e7c74 100644 --- a/components/table.py +++ b/components/table.py @@ -1,4 +1,5 @@ from ..lib.utils import group +from ..lib.translation import get_engine_class from .alert import AlertMessage @@ -6,13 +7,11 @@ try: from qt.core import ( Qt, QTableWidget, QHeaderView, QMenu, QAbstractItemView, QCursor, - QAbstractItemView, QTableWidgetItem, pyqtSignal, - QTableWidgetSelectionRange) + QBrush, QTableWidgetItem, pyqtSignal, QTableWidgetSelectionRange) except ImportError: from PyQt5.Qt import ( Qt, QTableWidget, QHeaderView, QMenu, QAbstractItemView, QCursor, - QAbstractItemView, QTableWidgetItem, pyqtSignal, - QTableWidgetSelectionRange) + QBrush, QTableWidgetItem, pyqtSignal, QTableWidgetSelectionRange) load_translations() @@ -42,6 +41,8 @@ def layout(self): self.setEditTriggers(triggers) self.setAlternatingRowColors(True) + self.setVerticalHeaderLabels( + map(str, range(1, len(self.paragraphs) + 1))) for row, paragraph in enumerate(self.paragraphs): original = QTableWidgetItem(paragraph.original) original.setData(Qt.UserRole, paragraph) @@ -70,6 +71,24 @@ def track_row_data(self, row): paragraph.engine_name, paragraph.target_lang, _('Translated')] for column, text in enumerate(items, 1): self.item(row, column).setText(text) + if paragraph.translation: + engine_class = get_engine_class(paragraph.engine_name) + if engine_class is not None and \ + not paragraph.is_alignment(engine_class.separator): + self.mark_row_as_problematic(row) + + def mark_row_as_problematic(self, row): + tip = _('The translation line count does not match the original.') + item = self.verticalHeaderItem(row) + if item is None: + return + item.setBackground(QBrush(Qt.yellow)) + item.setForeground(QBrush(Qt.white)) + item.setToolTip(tip) + for column in range(self.columnCount()): + item = self.item(row, column) + item.setBackground(QBrush(Qt.yellow)) + item.setToolTip(tip) def contextMenuEvent(self, event): if self.parent.on_working: diff --git a/lib/cache.py b/lib/cache.py index 0f11e9f..9cf38d2 100644 --- a/lib/cache.py +++ b/lib/cache.py @@ -1,4 +1,5 @@ import os +import re import json import shutil import sqlite3 @@ -36,6 +37,12 @@ def get_attributes(self): return json.loads(self.attributes) return {} + def is_alignment(self, seperator): + pattern = re.compile(seperator) + count_original = len(pattern.split(self.original.strip())) + count_translation = len(pattern.split(self.translation.strip())) + return count_original == count_translation + def default_cache_path(): path = os.path.join( diff --git a/lib/translation.py b/lib/translation.py index cb79519..d5a3d18 100644 --- a/lib/translation.py +++ b/lib/translation.py @@ -191,6 +191,7 @@ def translate_paragraph(self, paragraph): paragraph.translation = translation.strip() paragraph.engine_name = self.translator.name paragraph.target_lang = self.translator.get_target_lang() + paragraph.seperator = self.translator.separator paragraph.is_cache = False def process_translation(self, paragraph): diff --git a/test.py b/test.py index c480fa8..9916343 100644 --- a/test.py +++ b/test.py @@ -1,9 +1,9 @@ import sys import unittest +from pkgutil import iter_modules +from importlib import import_module # from calibre.utils.run_tests import run_cli -from calibre_plugins.ebook_translator.tests import ( - test_utils, test_config, test_engine, test_element, test_translation) def get_tests(module): @@ -12,9 +12,10 @@ def get_tests(module): def get_test_suite(): suite = unittest.TestSuite() - klasses = [ - test_utils, test_config, test_engine, test_element, test_translation] - suite.addTests(get_tests(klass) for klass in klasses) + for module in iter_modules(['tests']): + module = import_module( + 'calibre_plugins.ebook_translator.tests.%s' % module.name) + suite.addTests(get_tests(module)) return suite diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..1217808 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,20 @@ +import unittest + +from ..lib.cache import Paragraph + + +class TestParagraph(unittest.TestCase): + def setUp(self): + self.paragraph = Paragraph( + 1, 'TEST', 'a\n\nb\n\nc', 'a\n\nb\n\nc\n\n', + translation='A\n\nB\n\nC', attributes='{"class": "test"}') + + def test_get_attributes(self): + self.assertEqual({'class': 'test'}, self.paragraph.get_attributes()) + + def test_check_translation(self): + self.assertTrue(self.paragraph.is_alignment('\n\n')) + + self.paragraph.original = 'a\n\nb\n\nc' + self.paragraph.translation = 'A\n\nB\nC\n\n' + self.assertFalse(self.paragraph.is_alignment('\n\n')) diff --git a/translations/es.po b/translations/es.po index 2809520..9a91290 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-02-17 16:48+0800\n" +"POT-Creation-Date: 2024-02-26 22:51+0800\n" "PO-Revision-Date: 2023-04-17 14:17+0800\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -276,6 +276,9 @@ msgstr "" msgid "Untranslated" msgstr "" +msgid "The translation line count does not match the original." +msgstr "" + msgid "Select the whole chapter" msgstr "" diff --git a/translations/fr.po b/translations/fr.po index 78afc8f..25a5931 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-02-17 16:48+0800\n" +"POT-Creation-Date: 2024-02-26 22:51+0800\n" "PO-Revision-Date: 2023-10-01 15:35-0400\n" "Last-Translator: PoP\n" @@ -284,6 +284,9 @@ msgstr "Statut" msgid "Untranslated" msgstr "Non-traduit" +msgid "The translation line count does not match the original." +msgstr "" + msgid "Select the whole chapter" msgstr "Choisissez le chapitre en entier" diff --git a/translations/message.pot b/translations/message.pot index b9d36f8..5fdc82c 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-02-17 16:48+0800\n" +"POT-Creation-Date: 2024-02-26 22:51+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -87,7 +87,7 @@ msgstr "" msgid "Character count: {}" msgstr "" -#: advanced.py:465 cache.py:92 components/engine.py:200 components/table.py:92 +#: advanced.py:465 cache.py:92 components/engine.py:200 components/table.py:100 msgid "Delete" msgstr "" @@ -139,7 +139,7 @@ msgstr "" msgid "Title" msgstr "" -#: advanced.py:613 components/table.py:81 +#: advanced.py:613 components/table.py:71 msgid "Translated" msgstr "" @@ -151,23 +151,23 @@ msgstr "" msgid "Save" msgstr "" -#: advanced.py:752 +#: advanced.py:751 msgid "Your changes have been saved." msgstr "" -#: advanced.py:765 +#: advanced.py:764 msgid "Translation log" msgstr "" -#: advanced.py:776 +#: advanced.py:775 msgid "Error log" msgstr "" -#: advanced.py:796 +#: advanced.py:795 msgid "Are you sure you want to translate all {:n} paragraphs?" msgstr "" -#: advanced.py:822 +#: advanced.py:821 msgid "Are you sure you want to stop the translation progress?" msgstr "" @@ -175,7 +175,7 @@ msgstr "" msgid "Output Format" msgstr "" -#: batch.py:126 components/engine.py:137 components/table.py:90 +#: batch.py:126 components/engine.py:137 components/table.py:98 msgid "Translate" msgstr "" @@ -241,11 +241,11 @@ msgstr "" msgid "Total: {}" msgstr "" -#: cache.py:190 components/table.py:36 setting.py:54 +#: cache.py:190 components/table.py:35 setting.py:54 msgid "Engine" msgstr "" -#: cache.py:190 components/table.py:36 setting.py:884 +#: cache.py:190 components/table.py:35 setting.py:884 msgid "Language" msgstr "" @@ -261,8 +261,8 @@ msgstr "" msgid "Size (MB)" msgstr "" -#: components/engine.py:64 lib/translation.py:173 tests/test_translation.py:109 -#: tests/test_translation.py:129 tests/test_translation.py:142 +#: components/engine.py:64 lib/translation.py:172 tests/test_translation.py:108 +#: tests/test_translation.py:128 tests/test_translation.py:141 msgid "Translating..." msgstr "" @@ -346,27 +346,31 @@ msgid "" "the translation process and saving time." msgstr "" -#: components/table.py:36 +#: components/table.py:35 msgid "Original" msgstr "" -#: components/table.py:36 +#: components/table.py:35 msgid "Status" msgstr "" -#: components/table.py:77 +#: components/table.py:67 msgid "Untranslated" msgstr "" -#: components/table.py:96 +#: components/table.py:81 +msgid "The translation line count does not match the original." +msgstr "" + +#: components/table.py:104 msgid "Select the whole chapter" msgstr "" -#: components/table.py:102 +#: components/table.py:110 msgid "Select similar paragraphs: {}=\"{}\"" msgstr "" -#: components/table.py:149 +#: components/table.py:157 msgid "Retain at least one row." msgstr "" @@ -508,31 +512,31 @@ msgstr "" msgid "The translation of \"{}\" was completed. Do you want to open the book?" msgstr "" -#: lib/translation.py:133 lib/translation.py:141 lib/translation.py:168 +#: lib/translation.py:132 lib/translation.py:140 lib/translation.py:167 msgid "Translation canceled." msgstr "" -#: lib/translation.py:145 +#: lib/translation.py:144 msgid "No available API key." msgstr "" -#: lib/translation.py:147 +#: lib/translation.py:146 msgid "API key was Changed due to previous one unavailable." msgstr "" -#: lib/translation.py:151 +#: lib/translation.py:150 msgid "Failed to retrieve data from translate engine API." msgstr "" -#: lib/translation.py:159 lib/translation.py:209 lib/translation.py:217 +#: lib/translation.py:158 lib/translation.py:209 lib/translation.py:217 msgid "Original: {}" msgstr "" -#: lib/translation.py:160 +#: lib/translation.py:159 msgid "Status: Failed {} times / Sleeping for {} seconds" msgstr "" -#: lib/translation.py:161 lib/translation.py:219 +#: lib/translation.py:160 lib/translation.py:219 msgid "Error: {}" msgstr "" diff --git a/translations/pt.po b/translations/pt.po index c187e65..1de960e 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-02-17 16:48+0800\n" +"POT-Creation-Date: 2024-02-26 22:51+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: none\n" @@ -281,6 +281,9 @@ msgstr "Status" msgid "Untranslated" msgstr "Não traduzido" +msgid "The translation line count does not match the original." +msgstr "" + msgid "Select the whole chapter" msgstr "Selecionar o capítulo todo" diff --git a/translations/zh_CN.mo b/translations/zh_CN.mo index 033f2899c81d7d62e297d78ce9a7250fa4be8485..af11a2ebf8dbe1e6c7d13554c0c509360f22496a 100644 GIT binary patch delta 4165 zcmXxm3slzi9mnzCQ(Phh9zaBf$)j@33o2Jjjvmom#_VwHEZK5_#)LD4G&qM%^U*EL zSTx;V&B{yaXyt4f=)%i#QCXOn=1kL?ZbRnDdA2!Qon>S156{>C;rqOPzyIa;`~EKf zzqTy<@3P>Z62dk(j`PGY;(CO089wKZ#ptYal?l#q%2iI`IAF4 zl#cwl(fn}9mEtJ;9_HW*EW(#j30%Z@`geU4BJnHKRJ)Y6*7YKq9F7j-sCb3|U>*i|Y6Ws=e?aJ4-htpiZQqc5^0bH|L`| znu=PI88{rPF@R54y$yBWJE#P@Q3;$w&Cpe3@a_hxzo9e|z+f5$-8dDqaE@JAh8o#s ztM5R~(A%iJ@&T%$Uh@iSWYctpsue*_1B8odjB_2P)Bc~9ypF# zt5c{%uAnmi0>|M{dN0A5sQa2xQ@h37kJ|O8QP27KDZ+G|jt^lzZpTdecOO$2kKf=@ z9LHCqUN>NckB>w;LH$PzQwctfnu(vH5?_N#bd$LQwTE`2*8Tu0(QedI{~7f*UB{qq z=(h{8nW42Af$H!s)UGZ=Jvbi+W24z(*SDd*c>7Ra#A8;!h)VEFGcqf*7m`s+lAgu< zYc0mxg<@1kGf*R%YtBc$Ep8D;;A*oK+0br1>bud7daegGQ)f_1^C_|)-B-w1T{0U= z_m^Zd|6`cqxt!3HHRm{2jvngEb_w}&DR+h%E=48s9W2I$sI`3&mDpMH0&3)a=6}ox zHiq7YAvg&~1+6d-S!VYvlB7Fi^7?jowaXl_!138?3WVT9iQ0t&a|WQ@j{<{aHX{b5vsKQtdRSE6R5)!d0{c)wl$ z2=)90)Kc|X{T7C=1fwvh8zxYY#i)*^m}Pdp!mLESZVNF1n^E^~vg>WAj&@u9ebj(H zwEB6Bqy87vbJy~ie---ehCzH61Jn~y?`uBlt(b!vSraPJbvV$m*=Dw*X5=txs!yXv zdI8mOWIjIx0-F{ZnMKYfJ*2sJCxAz8+|E;{-y1|cXFktnuRxd$~c$PWOeAKT0%v_5~a0{xV zS1<-UQ3E-GN~i~i;u(zA`~Oc0>i7m~%A)w+g&xQ@b5RKtqDFWxs-cHb|4FUIR9uWX z_!MesI#6H6v&aMw%;HYeahBLYX*dF_qXwj3HhnRuQv_KYHcyi6w7QT0*=>+(YQauOyBUI!+K9iKmEdL=&-q z_?X!5ZI3Ss{|%+R-e>X286A}L-{U!i7Qc?r@pg!T-;X*{5~BT?i)!nt0}JXK>ly-& z)K@3=(X@bYWxE^mLjZ^NG6&Sx$? Zv#oFAlQ*lay`3vBKe@MKa`Ei&{{t}9);9nE delta 4073 zcmXxm3v|wP9LMqB!^qn9?0GP|xNI)F$kvvbZL~OU(aj~H%TBF0jYe_kLe9fEXhNw! zkxIp>b6j$p44Ji*q8KG*LYE^PbF7Y~lvD4|o^SuN@ALZoe*gdP{`>#AHtWS%q0_A+ zmOHLGq61Oi(z#Thb9-V`>fDId&Qat>VkwTpo>+rvcm-Qw`!>$C#ctRO3$X~NU<=%c z&2bx2#qB`;Lhb;+s(1?3=kGWaRX7NS;Z67w_QqY9j}2xrpH93o=&c;=?P{7Ws4S`Qet! z#jbb50k1J6FID$d?cgIQGfag(5-GGUB)$+lF@DeAW?srB%W|;ZN9NZ8L;6teA zCSxj2N438SwN-2E{ss(bWZOva=YHTP9~&_lvlvAr-ffOWHC%!NuoQD~0}jD6sP@v5 zSOpw`J+KUK!Ih}9a}aelt|zhn8c|=KmV=Oc?rzkGOHo_%6lyPLTfQ74$S+5.Ox z*V+Bus6+NSa!g#Ec^TDi1pBA{qC2qu+RHe8bfYJB!9J+dT8vFIMh&DC)$k0|0LoE& zxdgQZ9ySdG_1tb${X@uZx?`x0>rw4BhU|f6oG=wcqTYgbsMFjH)lm*=OY*Q2-iIkT z-SW#(&uvEyU>9lt2T?0@3YomCM-8YIjig{GmV_S6!E_vGH=aPvY_a8EMXgW;>a2W( zYUr4G5;e1*Q3JV%s^5T`*dM5sZO$xIKMgzU{qILY4VR!sJ`r{3W@0SP!wg)8YOo5a z==Na>p2bvbNk#+8L>;zV)IcVnR&o;R{?n-bW@85ZyZI#4(I!-dy{Ns~hZ@L9)X2}` z&De_Gi!dMc+ziyxE-}}kPW@-7dcWZyY)hsW-j8Yc66Vmq`;f#m{1u%dbQYaI;yBIs>(+t@r`81?SUPf8A)HKpkC2 z%_xu_HVOHrxNg`IZ!-rY2ie_)`d&;z)tir6sh3e(vJ5$QZVNIeSBrYS5i?ogKnClt zB`fOAcOLIVebL@R{@hW1BJpq3Kw9uGQ2{1nK8`~TY^k{dHS^WxCUY-pqKB~%PnvNd zPD(clh9iS=(=GodW|6N!RlIEegX%CcGh7}(o%W8{7l)(nFG3CERgA_p=4Q+$zX#P{ zsGfulNn3hTK_+U6Mx)Nclc)ygqPD0UwX|^!P|2VN09KM14oPRox&t>nX~ zvokSV9&#bOu^?RFmZCaZfofnqszRleA3}BTBl2dsbEt+RnU>1im`SJsc1CU8DAYj9 zP-o&DjMn>KO+qvM3e~`A9E)f1RxF~G0XP$xq^m-`Ef=vTMzH=m6ZxnWcnaBGw-VLf zKGXmYqUs&9@=JK5-v28kqVQicidI!I9<>6U%pPU|YDET{526~LYWH76)n9?ys@0a? zjT&H$picM_0`V)=A|-GXeFwC1YzWLOnmi?iZsv8gKbIs0qDbc@JaBufnD+ zH7j#je|7XJ1u6I?YH#XMZ$%W}OU*0?_1rLQ>ewtcC!tp48PrlQLCtgps^e2TedT;>p&wv|-8dw>s;c~nG9;T7sX!&nY4cDP&e$L8&L#@D{c0Yx$mDJT;S8!q>wW*TR9K@Bk5@`F(`8im^Phfqsh zhVeKHRo}Dw?=Gl=(zgG3#nYd7(k*XU2q`-s$hVgXS}=$aVDZN?Lp7S%*H@r5@m zHaqbMDJ{<;B0&6q9q?X>%}v~5*$DG_TtXc7PQ`XjJw{4vy_DERJWAX}{C{mC@u`;@ z$o4t!zCf~XgEu44!}q=SZXnmU)%%w89?us{FWk#>vk1LX#W5#mciS1s`xv5)=1(DXeG;bW_h?1;)=`0qvMwFkRctA^qs4z;Ev