diff --git a/README.md b/README.md index ac98a3d..68671ec 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # nvda-translate Make NVDA translate any spoken text to the desired language. ## Download -- Stable: [translate-2019.09.3](http://www.mtyp.fr/nvda/translate/translate-2019.09.3.nvda-addon). -- Stable (NVDA 2019.3+): [translate-2020.06](http://www.mtyp.fr/nvda/translate/translate-2020.06.nvda-addon). +- Stable (NVDA 2019.3+): [translate-2021.01](http://www.mtyp.fr/nvda/translate/translate-2021.01.nvda-addon). ## Installation @@ -40,3 +39,8 @@ Of course, the more bandwidth you have, the faster the translation will happen. - Of course, Pull Requests are also welcomed if you want to extend the add-on or fix any issue. - To contact me, you can use the address: [contact author](mailto:podcastcecitek@gmail.com) + +## Contributors +Thanks to everyone who made this extension a reality, including all of you who spent some time testing and reporting bugs. + +Among others, I'd like give a special thank you to Hxebolax, who found and fixed the bug that prevented the add-on to work for months in 2020. diff --git a/addon/doc/en/readme.md b/addon/doc/en/readme.md index f8fe95c..68671ec 100644 --- a/addon/doc/en/readme.md +++ b/addon/doc/en/readme.md @@ -1,8 +1,7 @@ # nvda-translate Make NVDA translate any spoken text to the desired language. ## Download -- Stable: [translate-2019.09.3](http://www.mtyp.fr/nvda/translate-2019.09.3.nvda-addon). -- Stable (NVDA 2019.3+): [translate-2020.01](http://www.mtyp.fr/nvda/translate-2020.05.nvda-addon). +- Stable (NVDA 2019.3+): [translate-2021.01](http://www.mtyp.fr/nvda/translate/translate-2021.01.nvda-addon). ## Installation @@ -40,3 +39,8 @@ Of course, the more bandwidth you have, the faster the translation will happen. - Of course, Pull Requests are also welcomed if you want to extend the add-on or fix any issue. - To contact me, you can use the address: [contact author](mailto:podcastcecitek@gmail.com) + +## Contributors +Thanks to everyone who made this extension a reality, including all of you who spent some time testing and reporting bugs. + +Among others, I'd like give a special thank you to Hxebolax, who found and fixed the bug that prevented the add-on to work for months in 2020. diff --git a/addon/globalPlugins/translate/__init__.py b/addon/globalPlugins/translate/__init__.py index 6a534ac..48dfd38 100644 --- a/addon/globalPlugins/translate/__init__.py +++ b/addon/globalPlugins/translate/__init__.py @@ -17,22 +17,25 @@ import globalVars import globalPluginHandler, logHandler, scriptHandler try: - import api, controlTypes - import ui, wx, gui - import core, config - import speech - from speech import * - import json - curDir = os.path.abspath(os.path.dirname(__file__)) - - sys.path.insert(0, curDir) - sys.path.insert(0, os.path.join(curDir, "html")) - import markupbase - import mtranslate - import addonHandler, languageHandler + import api, controlTypes + import ui, wx, gui + import core, config + import wx + import speech + from speech import * + import json + import queue + curDir = os.path.abspath(os.path.dirname(__file__)) + + sys.path.insert(0, curDir) + sys.path.insert(0, os.path.join(curDir, "html")) + import markupbase + import mtranslate + import updater + import addonHandler, languageHandler except Exception as e: - logHanaler.log.exception("Failed to initialize translate addon", e) - raise e + logHandler.log.exception("Failed to initialize translate addon", e) + raise e addonHandler.initTranslation() # @@ -50,34 +53,34 @@ def translate(text): - """translates the given text to the desired language. + """translates the given text to the desired language. Stores the result into the cache so that the same translation does not asks Google servers too often. - """ - global _translationCache, _enableTranslation, _gpObject - - try: - appName = globalVars.focusObject.appModule.appName - except: - appName = "__global__" - - if _gpObject is None or _enableTranslation is False: - return text - appTable = _translationCache.get(appName, None) - if appTable is None: - _translationCache[appName] = {} - translated = _translationCache[appName].get(text, None) - if translated is not None and translated != text: - return translated - try: - prepared = text.encode('utf8', ':/') - translated = mtranslate.translate(prepared, _gpObject.language) - except Exception as e: - return text - if translated is None or len(translated) == 0: - translated = text - else: - _translationCache[appName][text] = translated - return translated + """ + global _translationCache, _enableTranslation, _gpObject + + try: + appName = globalVars.focusObject.appModule.appName + except: + appName = "__global__" + + if _gpObject is None or _enableTranslation is False: + return text + appTable = _translationCache.get(appName, None) + if appTable is None: + _translationCache[appName] = {} + translated = _translationCache[appName].get(text, None) + if translated is not None and translated != text: + return translated + try: + prepared = text.encode('utf8', ':/') + translated = mtranslate.translate(prepared, _gpObject.language) + except Exception as e: + return text + if translated is None or len(translated) == 0: + translated = text + else: + _translationCache[appName][text] = translated + return translated # @@ -85,20 +88,20 @@ def translate(text): # def speak(speechSequence: SpeechSequence, - priority: Optional[Spri] = None): - global _enableTranslation, _lastTranslatedText - - if _enableTranslation is False: - return _nvdaSpeak(speechSequence=speechSequence, priority=priority) - newSpeechSequence = [] - for val in speechSequence: - if isinstance(val, str): - v = translate(val) - newSpeechSequence.append(v if v is not None else val) - else: - newSpeechSequence.append(val) - _nvdaSpeak(speechSequence=newSpeechSequence, priority=priority) - _lastTranslatedText = " ".join(x if isinstance(x, str) else "" for x in newSpeechSequence) + priority: Optional[Spri] = None): + global _enableTranslation, _lastTranslatedText + + if _enableTranslation is False: + return _nvdaSpeak(speechSequence=speechSequence, priority=priority) + newSpeechSequence = [] + for val in speechSequence: + if isinstance(val, str): + v = translate(val) + newSpeechSequence.append(v if v is not None else val) + else: + newSpeechSequence.append(val) + _nvdaSpeak(speechSequence=newSpeechSequence, priority=priority) + _lastTranslatedText = " ".join(x if isinstance(x, str) else "" for x in newSpeechSequence) # ## This is overloaded as well because the generated text may contain already translated text by @@ -106,183 +109,183 @@ def speak(speechSequence: SpeechSequence, ## already localized, such as object's name, value, or description # -def getPropertiesSpeech( # noqa: C901 - reason = controlTypes.REASON_QUERY, - **propertyValues +def getPropertiesSpeech( # noqa: C901 + reason = controlTypes.REASON_QUERY, + **propertyValues ): - global oldTreeLevel, oldTableID, oldRowNumber, oldRowSpan, oldColumnNumber, oldColumnSpan - textList: List[str] = [] - name: Optional[str] = propertyValues.get('name') - if name: - textList.append(translate(name)) - if 'role' in propertyValues: - role=propertyValues['role'] - speakRole=True - elif '_role' in propertyValues: - speakRole=False - role=propertyValues['_role'] - else: - speakRole=False - role=controlTypes.ROLE_UNKNOWN - value: Optional[str] = propertyValues.get('value') if role not in controlTypes.silentValuesForRoles else None - cellCoordsText: Optional[str] = propertyValues.get('cellCoordsText') - rowNumber = propertyValues.get('rowNumber') - columnNumber = propertyValues.get('columnNumber') - includeTableCellCoords = propertyValues.get('includeTableCellCoords', True) - - if role == controlTypes.ROLE_CHARTELEMENT: - speakRole = False - roleText: Optional[str] = propertyValues.get('roleText') - if ( - speakRole - and ( - roleText - or reason not in ( - controlTypes.REASON_SAYALL, - controlTypes.REASON_CARET, - controlTypes.REASON_FOCUS - ) - or not ( - name - or value - or cellCoordsText - or rowNumber - or columnNumber - ) - or role not in controlTypes.silentRolesOnFocus - ) - and ( - role != controlTypes.ROLE_MATH - or reason not in ( - controlTypes.REASON_CARET, - controlTypes.REASON_SAYALL - ) - )): - textList.append(translate(roleText) if roleText else controlTypes.roleLabels[role]) - if value: - textList.append(translate(value)) - states=propertyValues.get('states',set()) - realStates=propertyValues.get('_states',states) - negativeStates=propertyValues.get('negativeStates',set()) - if states or negativeStates: - labelStates = controlTypes.processAndLabelStates(role, realStates, reason, states, negativeStates) - textList.extend(labelStates) - # sometimes description key is present but value is None - description: Optional[str] = propertyValues.get('description') - if description: - textList.append(translate(description)) - # sometimes keyboardShortcut key is present but value is None - keyboardShortcut: Optional[str] = propertyValues.get('keyboardShortcut') - if keyboardShortcut: - textList.append(keyboardShortcut) - if includeTableCellCoords and cellCoordsText: - textList.append(cellCoordsText) - if cellCoordsText or rowNumber or columnNumber: - tableID = propertyValues.get("_tableID") - # Always treat the table as different if there is no tableID. - sameTable = (tableID and tableID == oldTableID) - # Don't update the oldTableID if no tableID was given. - if tableID and not sameTable: - oldTableID = tableID - # When fetching row and column span - # default the values to 1 to make further checks a lot simpler. - # After all, a table cell that has no rowspan implemented is assumed to span one row. - rowSpan = propertyValues.get("rowSpan") or 1 - columnSpan = propertyValues.get("columnSpan") or 1 - if rowNumber and (not sameTable or rowNumber != oldRowNumber or rowSpan != oldRowSpan): - rowHeaderText: Optional[str] = propertyValues.get("rowHeaderText") - if rowHeaderText: - textList.append(rowHeaderText) - if includeTableCellCoords and not cellCoordsText: - # Translators: Speaks current row number (example output: row 3). - rowNumberTranslation: str = _("row %s") % rowNumber - textList.append(rowNumberTranslation) - if rowSpan>1 and columnSpan<=1: - # Translators: Speaks the row span added to the current row number (example output: through 5). - rowSpanAddedTranslation: str = _("through %s") % (rowNumber + rowSpan - 1) - textList.append(rowSpanAddedTranslation) - oldRowNumber = rowNumber - oldRowSpan = rowSpan - if columnNumber and (not sameTable or columnNumber != oldColumnNumber or columnSpan != oldColumnSpan): - columnHeaderText: Optional[str] = propertyValues.get("columnHeaderText") - if columnHeaderText: - textList.append(translate(columnHeaderText)) - if includeTableCellCoords and not cellCoordsText: - # Translators: Speaks current column number (example output: column 3). - colNumberTranslation: str = _("column %s") % columnNumber - textList.append(colNumberTranslation) - if columnSpan>1 and rowSpan<=1: - # Translators: Speaks the column span added to the current column number (example output: through 5). - colSpanAddedTranslation: str = _("through %s") % (columnNumber + columnSpan - 1) - textList.append(colSpanAddedTranslation) - oldColumnNumber = columnNumber - oldColumnSpan = columnSpan - if includeTableCellCoords and not cellCoordsText and rowSpan>1 and columnSpan>1: - # Translators: Speaks the row and column span added to the current row and column numbers - # (example output: through row 5 column 3). - rowColSpanTranslation: str = _("through row {row} column {column}").format( - row=rowNumber + rowSpan - 1, - column=columnNumber + columnSpan - 1 - ) - textList.append(rowColSpanTranslation) - rowCount=propertyValues.get('rowCount',0) - columnCount=propertyValues.get('columnCount',0) - if rowCount and columnCount: - # Translators: Speaks number of columns and rows in a table (example output: with 3 rows and 2 columns). - rowAndColCountTranslation: str = _("with {rowCount} rows and {columnCount} columns").format( - rowCount=rowCount, - columnCount=columnCount - ) - textList.append(rowAndColCountTranslation) - elif columnCount and not rowCount: - # Translators: Speaks number of columns (example output: with 4 columns). - columnCountTransation: str = _("with %s columns") % columnCount - textList.append(columnCountTransation) - elif rowCount and not columnCount: - # Translators: Speaks number of rows (example output: with 2 rows). - rowCountTranslation: str = _("with %s rows") % rowCount - textList.append(rowCountTranslation) - if rowCount or columnCount: - # The caller is entering a table, so ensure that it is treated as a new table, even if the previous table was the same. - oldTableID = None - ariaCurrent = propertyValues.get('current', False) - if ariaCurrent: - try: - ariaCurrentLabel = controlTypes.isCurrentLabels[ariaCurrent] - textList.append(ariaCurrentLabel) - except KeyError: - log.debugWarning("Aria-current value not handled: %s"%ariaCurrent) - ariaCurrentLabel = controlTypes.isCurrentLabels[True] - textList.append(ariaCurrentLabel) - placeholder: Optional[str] = propertyValues.get('placeholder', None) - if placeholder: - textList.append(translate(placeholder)) - indexInGroup=propertyValues.get('positionInfo_indexInGroup',0) - similarItemsInGroup=propertyValues.get('positionInfo_similarItemsInGroup',0) - if 01 and columnSpan<=1: + # Translators: Speaks the row span added to the current row number (example output: through 5). + rowSpanAddedTranslation: str = _("through %s") % (rowNumber + rowSpan - 1) + textList.append(rowSpanAddedTranslation) + oldRowNumber = rowNumber + oldRowSpan = rowSpan + if columnNumber and (not sameTable or columnNumber != oldColumnNumber or columnSpan != oldColumnSpan): + columnHeaderText: Optional[str] = propertyValues.get("columnHeaderText") + if columnHeaderText: + textList.append(translate(columnHeaderText)) + if includeTableCellCoords and not cellCoordsText: + # Translators: Speaks current column number (example output: column 3). + colNumberTranslation: str = _("column %s") % columnNumber + textList.append(colNumberTranslation) + if columnSpan>1 and rowSpan<=1: + # Translators: Speaks the column span added to the current column number (example output: through 5). + colSpanAddedTranslation: str = _("through %s") % (columnNumber + columnSpan - 1) + textList.append(colSpanAddedTranslation) + oldColumnNumber = columnNumber + oldColumnSpan = columnSpan + if includeTableCellCoords and not cellCoordsText and rowSpan>1 and columnSpan>1: + # Translators: Speaks the row and column span added to the current row and column numbers + # (example output: through row 5 column 3). + rowColSpanTranslation: str = _("through row {row} column {column}").format( + row=rowNumber + rowSpan - 1, + column=columnNumber + columnSpan - 1 + ) + textList.append(rowColSpanTranslation) + rowCount=propertyValues.get('rowCount',0) + columnCount=propertyValues.get('columnCount',0) + if rowCount and columnCount: + # Translators: Speaks number of columns and rows in a table (example output: with 3 rows and 2 columns). + rowAndColCountTranslation: str = _("with {rowCount} rows and {columnCount} columns").format( + rowCount=rowCount, + columnCount=columnCount + ) + textList.append(rowAndColCountTranslation) + elif columnCount and not rowCount: + # Translators: Speaks number of columns (example output: with 4 columns). + columnCountTransation: str = _("with %s columns") % columnCount + textList.append(columnCountTransation) + elif rowCount and not columnCount: + # Translators: Speaks number of rows (example output: with 2 rows). + rowCountTranslation: str = _("with %s rows") % rowCount + textList.append(rowCountTranslation) + if rowCount or columnCount: + # The caller is entering a table, so ensure that it is treated as a new table, even if the previous table was the same. + oldTableID = None + ariaCurrent = propertyValues.get('current', False) + if ariaCurrent: + try: + ariaCurrentLabel = controlTypes.isCurrentLabels[ariaCurrent] + textList.append(ariaCurrentLabel) + except KeyError: + log.debugWarning("Aria-current value not handled: %s"%ariaCurrent) + ariaCurrentLabel = controlTypes.isCurrentLabels[True] + textList.append(ariaCurrentLabel) + placeholder: Optional[str] = propertyValues.get('placeholder', None) + if placeholder: + textList.append(translate(placeholder)) + indexInGroup=propertyValues.get('positionInfo_indexInGroup',0) + similarItemsInGroup=propertyValues.get('positionInfo_similarItemsInGroup',0) + if 0 0: - api.copyToClip(_lastTranslatedText) - ui.message(_("translation {text} ¨copied to clipboard".format(text=_lastTranslatedText))) - else: - ui.message(_("No translation to copy")) - script_copyLastTranslation.__doc__ = _("Copy the latest translated text to the clipboard.") - - def script_flushAllCache(self, gesture): - if scriptHandler.getLastScriptRepeatCount() == 0: - ui.message(_("Press twice to delete all cached translations for all applications.")) - return - global _translationCache - _translationCache = {} - path = os.path.join(globalVars.appArgs.configPath, "translation-cache") - error = False - for entry in os.listdir(path): - try: - os.unlink(os.path.join(path, entry)) - except Exception as e: - logHandler.log.error("Failed to remove {entry}".format(entry=entry)) - error = True - if not error: - ui.message(_("All translations have been deleted.")) - else: - ui.message(_("Some caches failed to be removed.")) - script_flushAllCache.__doc__ = _("Remove all cached translations for all applications.") - - def script_flushCurrentAppCache(self, gesture): - try: - appName = globalVars.focusObject.appModule.appName - except: - ui.message(_("No focused application")) - return - if scriptHandler.getLastScriptRepeatCount() == 0: - ui.message(_("Press twice to delete all translations for {app}".format(app=appName))) - return - - global _translationCache - - _translationCache[appName] = {} - fullPath = os.path.join(globalVars.appArgs.configPath, "translation-cache", "{app}.json".format(app=appName)) - if os.path.exists(fullPath): - try: - os.unlink(fullPath) - except Exception as e: - logHandler.log.error("Failed to remove cache for {appName}: {e}".format(appName=appName, e=e)) - ui.message(_("Error while deleting application's translation cache.")) - return - ui.message(_("Translation cache for {app} has been deleted.".format(app=appName))) - else: - ui.message(_("No saved translations for {app}".format(app=appName))) - - script_flushCurrentAppCache.__doc__ = _("Remove translation cache for the currently focused application") - - - - __gestures = { - "kb:nvda+shift+control+t": "toggleTranslate", - "kb:nvda+shift+c": "copyLastTranslation", - "kb:nvda+shift+control+f": "flushAllCache", - "kb:nvda+shift+f": "flushCurrentAppCache", - } + scriptCategory = _("Translate") + language = None + + def __init__(self): + """Initializes the global plugin object.""" + super(globalPluginHandler.GlobalPlugin, self).__init__() + global _nvdaGetPropertiesSpeech, _nvdaSpeak, _gpObject + + # if on a secure Desktop, disable the Add-on + if globalVars.appArgs.secure: return + _gpObject = self + try: + self.language = config.conf["general"]["language"] + except: + self.language = None + pass + if self.language is None or self.language == 'Windows': + try: + self.language = languageHandler.getWindowsLanguage()[:2] + except: + self.language = 'en' + self.updater = updater.TranslateUpdater() + self.updater.start() + self.inTimer = False + self.hasBeenUpdated = False + wx.CallLater(1000, self.onTimer) + logHandler.log.info("Translate module initialized, translating to %s" %(self.language)) + + _nvdaSpeak = speech._manager.speak + _nvdaGetPropertiesSpeech = speech.getPropertiesSpeech + speech._manager.speak = speak + speech.getPropertiesSpeech = _nvdaGetPropertiesSpeech + self.loadLocalCache() + + + def terminate(self): + """Called when this plugin is terminated, restoring all NVDA's methods.""" + global _nvdaGetPropertiesSpeech, _nvdaSpeak + speech._manager.speak = _nvdaSpeak + speech.getPropertiesSpeech = _nvdaGetPropertiesSpeech + self.saveLocalCache() + def onTimer(self): + if self.inTimer is True or self.hasBeenUpdated is True: + return + self.inTimer = True + try: + evt = self.updater.queue.get_nowait() + except queue.Empty: + evt = None + if evt is not None: + filepath = evt.get("download", None) + if filepath is not None: + import addonHandler + bundle = addonHandler.AddonBundle(filepath) + addonHandler.installAddonBundle(bundle) + logHandler.log.info("Installed version %s, restart NVDA to make the changes permanent" %(evt["version"])) + self.hasBeenUpdated = True + self.inTimer = False + wx.CallLater(1000, self.onTimer) + + def loadLocalCache(self): + global _translationCache + + path = os.path.join(globalVars.appArgs.configPath, "translation-cache") + # Checks that the storage path exists or create it. + if os.path.exists(path) is False: + try: + os.mkdir(path) + except Exception as e: + logHandler.log.error("Failed to create storage path: {path} ({error})".format(path=path, error=e)) + return + + # Scan stored files and load them. + + for entry in os.listdir(path): + m = re.match("(.*)\.json$", entry) + if m is not None: + appName = m.group(1) + try: + cacheFile = codecs.open(os.path.join(path, entry), "r", "utf-8") + except: + continue + try: + values = json.load(cacheFile) + cacheFile.close() + except Exception as e: + logHandler.log.error("Cannot read or decode data from {path}: {e}".format(path=path, e=e)) + cacheFile.close() + continue + _translationCache[appName] = values + cacheFile.close() + def saveLocalCache(self): + global _translationCache + + path = os.path.join(globalVars.appArgs.configPath, "translation-cache") + for appName in _translationCache: + file = os.path.join(path, "%s.json" %(appName)) + try: + cacheFile = codecs.open(file, "w", "utf-8") + json.dump(_translationCache[appName], cacheFile) + cacheFile.close() + except Exception as e: + logHandler.log.error("Failed to save translation cache for {app} to {file}: {error}".format(apap=appName, file=file, error=e)) + continue + + def script_toggleTranslate(self, gesture): + global _enableTranslation + + _enableTranslation = not _enableTranslation + if _enableTranslation: + ui.message(_("Translation enabled.")) + else: + ui.message(_("Translation disabled.")) + + script_toggleTranslate.__doc__ = _("Enables translation to the desired language.") + + def script_copyLastTranslation(self, gesture): + global _lastTranslatedText + + if _lastTranslatedText is not None and len(_lastTranslatedText) > 0: + api.copyToClip(_lastTranslatedText) + ui.message(_("translation {text} ¨copied to clipboard".format(text=_lastTranslatedText))) + else: + ui.message(_("No translation to copy")) + script_copyLastTranslation.__doc__ = _("Copy the latest translated text to the clipboard.") + + def script_flushAllCache(self, gesture): + if scriptHandler.getLastScriptRepeatCount() == 0: + ui.message(_("Press twice to delete all cached translations for all applications.")) + return + global _translationCache + _translationCache = {} + path = os.path.join(globalVars.appArgs.configPath, "translation-cache") + error = False + for entry in os.listdir(path): + try: + os.unlink(os.path.join(path, entry)) + except Exception as e: + logHandler.log.error("Failed to remove {entry}".format(entry=entry)) + error = True + if not error: + ui.message(_("All translations have been deleted.")) + else: + ui.message(_("Some caches failed to be removed.")) + script_flushAllCache.__doc__ = _("Remove all cached translations for all applications.") + + def script_flushCurrentAppCache(self, gesture): + try: + appName = globalVars.focusObject.appModule.appName + except: + ui.message(_("No focused application")) + return + if scriptHandler.getLastScriptRepeatCount() == 0: + ui.message(_("Press twice to delete all translations for {app}".format(app=appName))) + return + + global _translationCache + + _translationCache[appName] = {} + fullPath = os.path.join(globalVars.appArgs.configPath, "translation-cache", "{app}.json".format(app=appName)) + if os.path.exists(fullPath): + try: + os.unlink(fullPath) + except Exception as e: + logHandler.log.error("Failed to remove cache for {appName}: {e}".format(appName=appName, e=e)) + ui.message(_("Error while deleting application's translation cache.")) + return + ui.message(_("Translation cache for {app} has been deleted.".format(app=appName))) + else: + ui.message(_("No saved translations for {app}".format(app=appName))) + + script_flushCurrentAppCache.__doc__ = _("Remove translation cache for the currently focused application") + + + + __gestures = { + "kb:nvda+shift+control+t": "toggleTranslate", + "kb:nvda+shift+c": "copyLastTranslation", + "kb:nvda+shift+control+f": "flushAllCache", + "kb:nvda+shift+f": "flushCurrentAppCache", + } diff --git a/addon/globalPlugins/translate/mtranslate/core.py b/addon/globalPlugins/translate/mtranslate/core.py index 3d87572..3200659 100644 --- a/addon/globalPlugins/translate/mtranslate/core.py +++ b/addon/globalPlugins/translate/mtranslate/core.py @@ -79,7 +79,7 @@ def translate(to_translate, to_language="auto", from_language="auto"): request = urllib.request.Request(link, headers=agent) raw_data = urllib.request.urlopen(request).read() data = raw_data.decode("utf-8") - expr = r'class="t0">(.*?)<' + expr = r'class="result-container">(.*?)<' re_result = re.findall(expr, data) if (len(re_result) == 0): result = "" diff --git a/addon/globalPlugins/translate/updater.py b/addon/globalPlugins/translate/updater.py new file mode 100644 index 0000000..0e53adb --- /dev/null +++ b/addon/globalPlugins/translate/updater.py @@ -0,0 +1,82 @@ +# *-* coding: utf-8 *-* +import logHandler +import config +ADDON_NAME = "translate" +UPDATE_CHECK_INTERVAL = 60 + +import threading +import time +import queue +import urllib +import json +import os + +class TranslateUpdater(threading.Thread): + quit = False + quitLock = threading.RLock() + queue = queue.Queue() + + def run(self): + self.lastCheck = 0 + while self.quit is False: + time.sleep(1) + if time.time() - self.lastCheck < UPDATE_CHECK_INTERVAL: + continue + logHandler.log.info("translate: Checking for update...") + try: + res = urllib.request.urlopen("http://www.mtyp.fr/nvda") + data = res.read() + packet = json.loads(data) + mod = packet.get(ADDON_NAME, None) + if mod is not None: + new_version = self.getLatestVersion(mod) + if new_version is not None: + logHandler.log.info("Translate update available: %s" %(new_version["version"])) + self.queue.put({"update": new_version}) + self.download(new_version) + except Exception as ex: + logHandler.log.exception("Failed to retrieve update list: %s" %(ex)) + self.lastCheck = time.time() + + + + + + logHandler.log.ingo("Translate: exiting update thred...") + def getLatestVersion(self, mod): + import addonHandler + actual = None + for addon in addonHandler.getAvailableAddons(): + if addon.name == ADDON_NAME: + actual = addon + if actual is None: + return None + + + + + + + + + target = None + for version in mod["versions"]: + if version["version"] > actual.version: + target = version + return target + + def download(self, mod): + tmp = os.path.join(config.getUserDefaultConfigPath(), ADDON_NAME + ".nvda-addon") + try: + f = open(tmp, "wb") + res = urllib.request.urlopen(mod["url"]) + f.write(res.read()) + f.close() + except Exception as ex: + logHandler.log.error("Translate: failed to download %s: %s" %(mod["url"], ex)) + return False + self.queue.put({"download": tmp, + "version": mod["version"]}) + return True + + diff --git a/buildVars.py b/buildVars.py index 3f28fb8..e756da3 100644 --- a/buildVars.py +++ b/buildVars.py @@ -20,9 +20,9 @@ "addon_description" : _("""Uses the Google Translate API to translate each spoken text to the desired language, on the fly. This add-on requires an internet connection."""), # version - "addon_version" : "2020.06", + "addon_version" : "2021.01", # Author(s) - "addon_author" : u"Yannick PLASSIARD ", + "addon_author" : u"Yannick PLASSIARD , Hxebolax", # URL for the add-on documentation support "addon_url" : None, # Documentation file name @@ -30,7 +30,7 @@ # Minimum NVDA version supported (e.g. "2018.3") "addon_minimumNVDAVersion" : "2019.3", # Last NVDA version supported/tested (e.g. "2018.4", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion" : "2020.1", + "addon_lastTestedNVDAVersion" : "2020.3", # Add-on update channel (default is stable or None) "addon_updateChannel" : "stable", }